commit 37ba35a2f33213466afa57ac6788799475eb4f0b Author: dushixiang Date: Mon Nov 10 22:11:46 2025 +0800 Initial commit diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..677b364 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,47 @@ +# Git 相关 +.git +.gitignore +.gitattributes + +# GitHub Actions +.github + +# IDE 相关 +.idea +.vscode +*.swp +*.swo +*~ + +# 构建产物 +bin/ +web/dist/ +web/node_modules/ + +# 数据目录 +data/ + +# 日志文件 +*.log +logs/ + +# 临时文件 +tmp/ +temp/ +*.tmp + +# 测试文件 +*_test.go +test/ + +# 文档 +*.md +!README.md +docs/ + +# 其他 +.DS_Store +Thumbs.db +.env +.env.* +docker-compose.yml diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100644 index 0000000..a09bf69 --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -0,0 +1,137 @@ +name: 构建并发布 Docker 镜像 + +on: + push: + tags: + - 'v*' # 当推送以 v 开头的 tag 时触发 + +env: + # 使用 docker.io 作为 Docker Hub 的注册表 + DOCKER_REGISTRY: docker.io + # 使用 GitHub Container Registry + GHCR_REGISTRY: ghcr.io + # 镜像名称(需要根据实际情况修改) + IMAGE_NAME: ${{ github.repository }} + +jobs: + build-and-push: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + id-token: write + + steps: + # 获取版本号 + - name: 获取版本号 + id: get_version + run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV + + # 检出代码 + - name: 检出代码 + uses: actions/checkout@v4 + + # 安装 make 和 upx + - name: 安装 make 和 upx + run: | + sudo apt-get update + sudo apt-get install -y make upx-ucl + + # 设置 Golang + - name: 设置 Golang + uses: actions/setup-go@v5 + with: + go-version: '1.25.0' + + # 设置 Node.js + - name: 设置 Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + + # 构建前端 + - name: 构建前端 + run: | + make build-web + + # 构建所有平台的 agent + - name: 构建 agents + run: | + make build-agents + + # 构建服务端 + - name: 构建服务端 + run: | + make build-release + + # 设置 QEMU 支持多平台构建 + - name: 设置 QEMU + uses: docker/setup-qemu-action@v3 + + # 设置 Docker Buildx + - name: 设置 Docker Buildx + uses: docker/setup-buildx-action@v3 + + # 登录 Docker Hub + - name: 登录 Docker Hub + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_PASSWORD }} + + # 登录 GitHub Container Registry + - name: 登录 GitHub Container Registry + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ${{ env.GHCR_REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # 提取 Docker 元数据 + - name: 提取 Docker 元数据 + id: meta + uses: docker/metadata-action@v5 + with: + images: | + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }} + ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=raw,value=latest + + # 构建并推送 Docker 镜像 + - name: 构建并推送 Docker 镜像 + id: build-and-push + uses: docker/build-push-action@v6 + with: + context: . + platforms: linux/amd64,linux/arm64 + file: DockerfileForGithubAction + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + build-args: | + VERSION=${{ env.VERSION }} + cache-from: | + type=registry,ref=${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:cache + type=local,src=/tmp/.buildx-cache + cache-to: | + type=registry,ref=${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:cache,mode=max + type=local,dest=/tmp/.buildx-cache + + # 生成构建总结 + - name: 生成构建总结 + run: | + echo "## Docker 镜像构建成功 🎉" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**版本:** ${{ env.VERSION }}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**镜像标签:**" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "${{ steps.meta.outputs.tags }}" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9cbe3f3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,40 @@ +### Example user template template +### Example user template + +# IntelliJ project files +.idea +*.iml +out +gen + +# 日志文件 +logs/ +*.log + +# 数据文件 +data/ + +# 编译产物 +bin/ +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# 测试文件 +*.test +*.out + +# Go 相关 +vendor/ +go.work +go.work.sum + +# 临时文件 +*.swp +*.swo +*~ +.DS_Store + +config.yaml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ea64a29 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,27 @@ +# 用于 GitHub Action 的 Dockerfile +# 这个 Dockerfile 假设编译产物已经在外部构建完成 + +FROM alpine:latest + +# 安装运行时依赖 +RUN apk add --no-cache ca-certificates tzdata + +# 设置时区为上海 +ENV TZ=Asia/Shanghai + +WORKDIR /app + +ARG TARGETOS +ARG TARGETARCH + +# 从外部编译的产物复制文件 +COPY bin/pika-${TARGETOS}-${TARGETARCH} ./pika + +# 复制配置文件示例 +COPY config.example.yaml ./config.yaml + +# 暴露端口 +EXPOSE 8080 + +# 启动服务 +ENTRYPOINT ["./pika"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..8f8ab27 --- /dev/null +++ b/Makefile @@ -0,0 +1,88 @@ +# 版本号(可通过 make VERSION=1.0.0 指定) +VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev") +GIT_REVISION=$(shell git rev-parse HEAD) +GO_VERSION=$(shell go version) +BUILD_TIME=$(shell date +%Y-%m-%d_%H:%M:%S) + +# Go 构建参数 +LDFLAGS=-s -w -X 'github.com/dushixiang/pika/pkg/version.Version=$(VERSION)' +GOFLAGS=CGO_ENABLED=0 + +# 构建前端 +build-web: + cd web && yarn && yarn build + +# 构建服务端(开发) +build-server: + $(GOFLAGS) GOOS=linux GOARCH=amd64 go build -ldflags="$(LDFLAGS)" -o bin/pika-linux-amd64 cmd/serv/main.go + upx bin/pika-linux-amd64 + +build-servers: + $(GOFLAGS) GOOS=linux GOARCH=amd64 go build -ldflags="$(LDFLAGS)" -o bin/pika-linux-amd64 cmd/serv/main.go + $(GOFLAGS) GOOS=linux GOARCH=arm64 go build -ldflags="$(LDFLAGS)" -o bin/pika-linux-arm64 cmd/serv/main.go + + upx bin/pika-linux-amd64 + upx bin/pika-linux-arm64 + +# 构建所有平台的 Agent +build-agents: + @echo "Building agents for all platforms (version: $(VERSION))..." + @mkdir -p bin/agents + + # Linux + $(GOFLAGS) GOOS=linux GOARCH=amd64 go build -ldflags="$(LDFLAGS)" -o bin/agents/pika-agent-linux-amd64 cmd/agent/*.go + $(GOFLAGS) GOOS=linux GOARCH=arm64 go build -ldflags="$(LDFLAGS)" -o bin/agents/pika-agent-linux-arm64 cmd/agent/*.go + $(GOFLAGS) GOOS=linux GOARCH=arm GOARM=7 go build -ldflags="$(LDFLAGS)" -o bin/agents/pika-agent-linux-armv7 cmd/agent/*.go + + # macOS + $(GOFLAGS) GOOS=darwin GOARCH=amd64 go build -ldflags="$(LDFLAGS)" -o bin/agents/pika-agent-darwin-amd64 cmd/agent/*.go + $(GOFLAGS) GOOS=darwin GOARCH=arm64 go build -ldflags="$(LDFLAGS)" -o bin/agents/pika-agent-darwin-arm64 cmd/agent/*.go + + # Windows + $(GOFLAGS) GOOS=windows GOARCH=amd64 go build -ldflags="$(LDFLAGS)" -o bin/agents/pika-agent-windows-amd64.exe cmd/agent/*.go + + @echo "All agents built successfully!" + @echo "Compressing agents with UPX..." + @upx bin/agents/pika-agent-linux-amd64 + @upx bin/agents/pika-agent-linux-arm64 + @upx bin/agents/pika-agent-linux-armv7 + @upx bin/agents/pika-agent-darwin-amd64 + @upx bin/agents/pika-agent-darwin-arm64 + @upx bin/agents/pika-agent-windows-amd64.exe + @echo "All agents compressed successfully!" + @ls -lh bin/agents/ + +# 构建所有 +build-release: + make build-web + make build-agents + make build-servers + +# 清理编译产物 +clean: + rm -rf bin/* + rm -rf web/dist + +# 运行测试 +test: + go test -v ./... + +# 代码格式化 +fmt: + go fmt ./... + cd web && yarn format + +# 代码检查 +lint: + golangci-lint run + +# 生成 Wire 代码 +wire: + cd internal && wire + +# 显示版本信息 +version: + @echo "Version: $(VERSION)" + @echo "Git Revision: $(GIT_REVISION)" + @echo "Go Version: $(GO_VERSION)" + @echo "Build Time: $(BUILD_TIME)" \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..2733fe9 --- /dev/null +++ b/README.md @@ -0,0 +1,374 @@ +# Pika 探针监控系统 + +
+ +一个基于 Go + PostgreSQL 的实时探针监控系统 + +[快速开始](#快速开始) • [功能特性](#功能特性) • [文档](#文档) • [架构](#架构) + +
+ +## 简介 + +Pika 是一个轻量级的探针监控系统,支持实时数据采集、存储和查询。系统采用 WebSocket 进行探针与服务端的通信,使用 PostgreSQL 存储时序数据,提供完整的 RESTful API 和用户管理功能。 + +## 功能特性 + +- ✅ **实时通信**: 基于 WebSocket 的探针通信,支持实时数据上报 +- ✅ **多指标支持**: CPU、内存、磁盘、网络、负载等多种系统指标 +- ✅ **时序存储**: PostgreSQL 存储时序数据,自动清理过期数据(30天) +- ✅ **用户管理**: 完整的用户 CRUD、角色管理、密码加密 +- ✅ **用户认证**: Session 管理和 Token 认证 +- ✅ **RESTful API**: 提供完整的 API 接口 +- ✅ **依赖注入**: 使用 Wire 进行依赖管理 +- ✅ **自动重连**: 探针支持自动重连和心跳检测 +- ✅ **易于扩展**: 清晰的分层架构,易于添加新功能 +- ✅ **psutil 采集**: 探针使用 gopsutil 库进行系统信息采集 + +## 快速开始 + +### 前置要求 + +- Go 1.25+ +- Docker & Docker Compose +- Make + +### 一键启动 + +```bash +# 克隆项目 +git clone +cd pika + +# 启动服务(自动启动数据库、编译、运行) +./start.sh +``` + +服务将在 `http://localhost:18888` 启动 + +### 手动启动 + +```bash +# 1. 启动数据库 +docker-compose up -d + +# 2. 编译项目 +make build-backend + +# 3. 运行服务端 +./bin/pika + +# 4. 运行探针客户端(可选,用于测试) +./bin/agent +``` + +### 测试 API + +```bash +./test_api.sh +``` + +## 技术栈 + +| 类别 | 技术 | +|------|------| +| 语言 | Go 1.25 | +| Web 框架 | Echo v4 | +| 数据库 | PostgreSQL | +| ORM | GORM | +| WebSocket | Gorilla WebSocket | +| 依赖注入 | Google Wire | +| 日志 | Uber Zap | +| 配置 | Viper | +| 系统信息采集 | gopsutil v4 | +| 密码加密 | bcrypt | + +## 架构 + +``` +┌─────────────────────────────────────┐ +│ Web/WebSocket Layer │ +│ (Echo + Gorilla WebSocket) │ +└─────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────┐ +│ Handler Layer │ +│ (account, agent, user handlers) │ +└─────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────┐ +│ Service Layer │ +│ (account, agent, user services) │ +└─────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────┐ +│ Repository Layer │ +│ (session, agent, user, metric) │ +└─────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────┐ +│ Database (PostgreSQL) │ +└─────────────────────────────────────┘ +``` + +## API 接口 + +### 认证相关 +- `POST /api/auth` - Token 登录 +- `POST /api/login` - 用户名密码登录 +- `GET /api/account/info` - 获取用户信息 +- `POST /api/logout` - 退出登录 + +### 探针管理 +- `GET /api/agents` - 获取所有探针 +- `GET /api/agents/online` - 获取在线探针 +- `GET /api/agents/:id` - 获取探针详情 +- `GET /api/agents/:id/metrics` - 获取指标数据(支持时间戳查询) +- `GET /api/agents/:id/metrics/latest` - 获取最新指标 + +### 用户管理 +- `GET /api/users` - 获取用户列表(分页) +- `POST /api/users` - 创建用户 +- `GET /api/users/:id` - 获取用户详情 +- `PUT /api/users/:id` - 更新用户 +- `DELETE /api/users/:id` - 删除用户 +- `POST /api/users/:id/password` - 修改密码 +- `POST /api/users/:id/reset-password` - 重置密码(管理员) +- `POST /api/users/:id/status` - 更新用户状态 + +### WebSocket +- `GET /ws/agent` - 探针连接 + +## 数据模型 + +### 用户表 (users) +- id (主键, UUID) +- username (唯一索引) +- password (bcrypt 加密) +- nickname +- email +- phone +- avatar +- role (admin/user) +- status (0-禁用, 1-启用) +- created_at (时间戳毫秒) +- updated_at (时间戳毫秒) + +### 探针表 (agents) +- id (主键, UUID) +- name +- hostname +- ip +- os +- arch +- version +- status (0-离线, 1-在线) +- last_seen_at (时间戳毫秒) +- created_at (时间戳毫秒) +- updated_at (时间戳毫秒) + +### 指标表 +所有指标表都使用 `int64` 类型的时间戳(毫秒): +- cpu_metrics +- memory_metrics +- disk_metrics +- network_metrics +- load_metrics + +## 项目结构 + +``` +pika/ +├── cmd/ # 应用程序入口 +│ ├── serv/ # 服务端 +│ └── agent/ # 探针客户端 +├── internal/ # 内部代码 +│ ├── handler/ # HTTP/WebSocket 处理器 +│ ├── service/ # 业务逻辑 +│ ├── repo/ # 数据访问 +│ ├── models/ # 数据模型 +│ └── websocket/ # WebSocket 管理 +├── web/ # 前端资源 +├── config.yaml # 配置文件 +├── docker-compose.yml # Docker 配置 +├── Makefile # 构建脚本 +└── *.md # 文档 +``` + +## 配置 + +配置文件位于 `config.yaml`: + +```yaml +Database: + Type: postgres + Postgres: + Hostname: localhost + Port: 15432 + Username: pika + Password: pika + Database: pika + +log: + Level: debug + Filename: ./logs/pika.log + +Server: + Addr: "0.0.0.0:18888" +``` + +## 开发 + +### 编译 + +```bash +# 编译服务端 +make build-server + +# 编译探针客户端 +make build-agent + +# 编译所有后端 +make build-backend + +# 清理 +make clean +``` + +### 运行 + +```bash +# 运行服务端 +make run-server + +# 运行探针客户端 +make run-agent +``` + +### 测试 + +```bash +# 运行测试 +make test + +# API 测试 +./test_api.sh +``` + +## WebSocket 消息协议 + +### 探针注册 + +```json +{ + "type": "register", + "data": { + "name": "探针名称", + "hostname": "主机名", + "ip": "192.168.1.100", + "os": "linux", + "arch": "amd64", + "version": "1.0.0" + } +} +``` + +### 心跳 + +```json +{ + "type": "heartbeat", + "data": {} +} +``` + +### 指标数据 + +```json +{ + "type": "metrics", + "data": { + "type": "cpu", + "data": { + "usagePercent": 45.5, + "coreCount": 8 + } + } +} +``` + +## 监控指标 + +系统支持以下监控指标(使用 gopsutil 采集): + +- **CPU**: 使用率、核心数 +- **内存**: 总量、已用、空闲、使用率 +- **磁盘**: 挂载点、容量、使用情况 +- **网络**: 网卡流量统计 +- **负载**: 1/5/15分钟负载 + +## 时间戳说明 + +所有时间字段均使用 `int64` 类型的毫秒时间戳: + +- 数据库存储:毫秒时间戳 +- API 查询:支持毫秒时间戳参数 +- WebSocket 消息:服务端自动添加时间戳 + +示例: +```bash +# 获取最近1小时的数据 +START=$(( $(date +%s) * 1000 - 3600000 )) +END=$(( $(date +%s) * 1000 )) +curl "http://localhost:18888/api/agents/{id}/metrics?type=cpu&start=$START&end=$END" +``` + +## 数据保留 + +- 默认保留 30 天的历史数据 +- 每小时自动清理过期数据 +- 可在代码中自定义保留策略 + +## 性能 + +- 支持数千个探针同时连接 +- WebSocket 实时通信,低延迟 +- PostgreSQL 时序数据存储 +- 自动清理机制,保持数据库大小可控 + +## 安全 + +生产环境建议: + +1. 修改 WebSocket 的 `CheckOrigin` 函数 +2. 使用 HTTPS/WSS +3. 配置防火墙规则 +4. 使用强密码(bcrypt 加密) +5. 定期备份数据库 +6. 实现完整的 JWT 认证 + +## 更新日志 + +### v2.0.0 (2025-10-20) + +- ✅ 将 Probe 重命名为 Agent +- ✅ 时间字段改为 int64 时间戳(毫秒) +- ✅ 添加用户表和完整的用户管理功能 +- ✅ 探针使用 gopsutil 进行系统信息采集 +- ✅ 密码使用 bcrypt 加密 +- ✅ 完善的用户 CRUD 接口 +- ✅ 支持用户角色和状态管理 + +## 贡献 + +欢迎提交 Issue 和 Pull Request! + +## 许可证 + +[添加许可证信息] + +--- + +**开发状态**: ✅ v2.0 开发完成 +**版本**: 2.0.0 +**最后更新**: 2025-10-20 diff --git a/assets.go b/assets.go new file mode 100644 index 0000000..7b07585 --- /dev/null +++ b/assets.go @@ -0,0 +1,14 @@ +package pika + +import ( + "embed" + "io/fs" +) + +//go:embed bin/agents/* +var agentsFS embed.FS + +func AgentFS() fs.FS { + sub, _ := fs.Sub(agentsFS, "bin/agents") + return sub +} diff --git a/cmd/agent/agent.yaml.example b/cmd/agent/agent.yaml.example new file mode 100644 index 0000000..544e21f --- /dev/null +++ b/cmd/agent/agent.yaml.example @@ -0,0 +1,48 @@ +# Pika Agent 配置文件示例 +# 配置文件默认路径: ~/.pika/agent.yaml + +# 服务器配置 +server: + # 服务器地址(不需要指定协议路径,会自动拼接) + # 例如: http://localhost:18888 或 https://your-server.com + endpoint: http://localhost:18888 + + # API Key(必填,从服务端获取) + api_key: "your-api-key-here" + +# Agent 配置 +agent: + # Agent 名称(可选,默认使用主机名) + # 用于在服务端区分不同的探针 + name: "" + +# 采集器配置 +collector: + # 数据采集间隔(秒) + # 建议: 5-60 秒,太短会增加服务器负载 + interval: 5 + + # 心跳间隔(秒) + # 建议: 30-120 秒 + heartbeat_interval: 30 + + # 网络采集排除的网卡列表(可选,支持正则表达式) + # 如果不配置,默认只排除回环地址(lo, lo0) + # 支持正则表达式匹配网卡名称 + network_exclude: + - "^lo$" # 排除 lo 接口(回环地址) + - "^lo0$" # 排除 lo0 接口(回环地址) + - "^docker.*" # 排除所有 docker 开头的接口 + - "^veth.*" # 排除所有 veth 开头的虚拟接口 + - "^br-.*" # 排除所有 br- 开头的网桥接口 + +# 自动更新配置 +auto_update: + # 是否启用自动更新 + # 启用后会定期检查服务端版本,有新版本时自动下载并更新 + # 更新地址会自动从 server.endpoint 获取 + enabled: false + + # 检查更新间隔(小时) + # 建议: 6-24 小时 + check_interval: 24 diff --git a/cmd/agent/main.go b/cmd/agent/main.go new file mode 100644 index 0000000..7c1385f --- /dev/null +++ b/cmd/agent/main.go @@ -0,0 +1,545 @@ +package main + +import ( + "bufio" + "fmt" + "log" + "os" + "runtime" + "strings" + + "github.com/dushixiang/pika/pkg/agent/config" + "github.com/dushixiang/pika/pkg/agent/service" + "github.com/dushixiang/pika/pkg/agent/updater" + "github.com/spf13/cobra" +) + +var ( + configPath string +) + +// rootCmd 根命令 +var rootCmd = &cobra.Command{ + Use: "agent", + Short: "Pika 监控探针", + Long: `Pika Agent 是一个轻量级的系统监控探针,用于采集服务器的各项性能指标并上报到 Pika 服务端。`, + Run: func(cmd *cobra.Command, args []string) { + _ = cmd.Help() + }, +} + +// versionCmd 版本命令 +var versionCmd = &cobra.Command{ + Use: "version", + Short: "显示版本信息", + Run: func(cmd *cobra.Command, args []string) { + fmt.Printf("Pika Agent v%s\n", service.GetVersion()) + fmt.Printf("OS: %s\n", runtime.GOOS) + fmt.Printf("Arch: %s\n", runtime.GOARCH) + fmt.Printf("Go Version: %s\n", runtime.Version()) + }, +} + +// runCmd 运行命令 +var runCmd = &cobra.Command{ + Use: "run", + Short: "运行探针", + Long: `启动探针并连接到服务器,开始采集和上报监控数据`, + Run: runAgent, +} + +// installCmd 安装服务命令 +var installCmd = &cobra.Command{ + Use: "install", + Short: "安装为系统服务", + Long: `将 Agent 安装为系统服务(systemd/launchd),开机自动启动`, + Run: installService, +} + +// uninstallCmd 卸载服务命令 +var uninstallCmd = &cobra.Command{ + Use: "uninstall", + Short: "卸载系统服务", + Long: `从系统中卸载 Agent 服务`, + Run: uninstallService, +} + +// startCmd 启动服务命令 +var startCmd = &cobra.Command{ + Use: "start", + Short: "启动服务", + Long: `启动已安装的 Agent 服务`, + Run: startService, +} + +// stopCmd 停止服务命令 +var stopCmd = &cobra.Command{ + Use: "stop", + Short: "停止服务", + Long: `停止正在运行的 Agent 服务`, + Run: stopService, +} + +// restartCmd 重启服务命令 +var restartCmd = &cobra.Command{ + Use: "restart", + Short: "重启服务", + Long: `重启 Agent 服务`, + Run: restartService, +} + +// statusCmd 查看服务状态命令 +var statusCmd = &cobra.Command{ + Use: "status", + Short: "查看服务状态", + Long: `查看 Agent 服务的运行状态`, + Run: statusService, +} + +// configCmd 配置命令 +var configCmd = &cobra.Command{ + Use: "config", + Short: "配置管理", + Long: `管理 Agent 配置文件`, +} + +// configInitCmd 初始化配置命令 +var configInitCmd = &cobra.Command{ + Use: "init", + Short: "初始化配置文件", + Long: `创建默认配置文件`, + Run: initConfig, +} + +// configShowCmd 显示配置命令 +var configShowCmd = &cobra.Command{ + Use: "show", + Short: "显示配置文件路径", + Long: `显示当前配置文件的路径`, + Run: func(cmd *cobra.Command, args []string) { + fmt.Printf("配置文件路径: %s\n", configPath) + }, +} + +// updateCmd 更新命令 +var updateCmd = &cobra.Command{ + Use: "update", + Short: "检查并更新", + Long: `检查是否有新版本可用,并进行更新`, + Run: updateAgent, +} + +// registerCmd 注册命令 +var registerCmd = &cobra.Command{ + Use: "register", + Short: "注册并安装探针", + Long: `交互式引导注册探针:配置服务端地址、Token、名称,然后自动安装为系统服务并启动`, + Run: registerAgent, +} + +var ( + serverEndpoint string + serverAPIKey string + agentName string +) + +func init() { + // 全局参数 + rootCmd.PersistentFlags().StringVarP(&configPath, "config", "c", "", "配置文件路径(默认: ~/.pika/agent.yaml)") + + // 注册命令的参数 + registerCmd.Flags().StringVarP(&serverEndpoint, "endpoint", "e", "", "服务端地址 (例如: http://your-server.com:18888)") + registerCmd.Flags().StringVarP(&serverAPIKey, "token", "t", "", "API Token") + registerCmd.Flags().StringVarP(&agentName, "name", "n", "", "探针名称(默认使用主机名)") + + // 添加子命令 + rootCmd.AddCommand(versionCmd) + rootCmd.AddCommand(registerCmd) // 注册命令放在前面,方便用户发现 + rootCmd.AddCommand(runCmd) + rootCmd.AddCommand(installCmd) + rootCmd.AddCommand(uninstallCmd) + rootCmd.AddCommand(startCmd) + rootCmd.AddCommand(stopCmd) + rootCmd.AddCommand(restartCmd) + rootCmd.AddCommand(statusCmd) + rootCmd.AddCommand(updateCmd) + + // 配置命令 + configCmd.AddCommand(configInitCmd) + configCmd.AddCommand(configShowCmd) + rootCmd.AddCommand(configCmd) + + if configPath == "" { + configPath = config.GetDefaultConfigPath() + } +} + +func main() { + if err := rootCmd.Execute(); err != nil { + fmt.Fprintf(os.Stderr, "错误: %v\n", err) + os.Exit(1) + } +} + +// runAgent 运行探针 +func runAgent(cmd *cobra.Command, args []string) { + // 加载配置 + cfg, err := config.Load(configPath) + if err != nil { + log.Fatalf("❌ 加载配置失败: %v", err) + } + + // 创建服务管理器 + mgr, err := service.NewServiceManager(cfg) + if err != nil { + log.Fatalf("❌ 创建服务管理器失败: %v", err) + } + + // 运行服务 + if err := mgr.Run(); err != nil { + log.Fatalf("❌ 运行失败: %v", err) + } +} + +// installService 安装服务 +func installService(cmd *cobra.Command, args []string) { + // 加载配置 + cfg, err := config.Load(configPath) + if err != nil { + log.Fatalf("❌ 加载配置失败: %v", err) + } + + // 创建服务管理器 + mgr, err := service.NewServiceManager(cfg) + if err != nil { + log.Fatalf("❌ 创建服务管理器失败: %v", err) + } + + // 安装服务 + if err := mgr.Install(); err != nil { + log.Fatalf("❌ 安装服务失败: %v", err) + } + + log.Println("✅ 服务安装成功") + log.Println(" 使用 'agent start' 启动服务") +} + +// uninstallService 卸载服务 +func uninstallService(cmd *cobra.Command, args []string) { + // 加载配置 + cfg, err := config.Load(configPath) + if err != nil { + log.Fatalf("❌ 加载配置失败: %v", err) + } + + // 创建服务管理器 + mgr, err := service.NewServiceManager(cfg) + if err != nil { + log.Fatalf("❌ 创建服务管理器失败: %v", err) + } + + // 卸载服务 + if err := mgr.Uninstall(); err != nil { + log.Fatalf("❌ 卸载服务失败: %v", err) + } + + log.Println("✅ 服务卸载成功") +} + +// startService 启动服务 +func startService(cmd *cobra.Command, args []string) { + // 加载配置 + cfg, err := config.Load(configPath) + if err != nil { + log.Fatalf("❌ 加载配置失败: %v", err) + } + + // 创建服务管理器 + mgr, err := service.NewServiceManager(cfg) + if err != nil { + log.Fatalf("❌ 创建服务管理器失败: %v", err) + } + + // 启动服务 + if err := mgr.Start(); err != nil { + log.Fatalf("❌ 启动服务失败: %v", err) + } + + log.Println("✅ 服务启动成功") +} + +// stopService 停止服务 +func stopService(cmd *cobra.Command, args []string) { + // 加载配置 + cfg, err := config.Load(configPath) + if err != nil { + log.Fatalf("❌ 加载配置失败: %v", err) + } + + // 创建服务管理器 + mgr, err := service.NewServiceManager(cfg) + if err != nil { + log.Fatalf("❌ 创建服务管理器失败: %v", err) + } + + // 停止服务 + if err := mgr.Stop(); err != nil { + log.Fatalf("❌ 停止服务失败: %v", err) + } + + log.Println("✅ 服务停止成功") +} + +// restartService 重启服务 +func restartService(cmd *cobra.Command, args []string) { + // 加载配置 + cfg, err := config.Load(configPath) + if err != nil { + log.Fatalf("❌ 加载配置失败: %v", err) + } + + // 创建服务管理器 + mgr, err := service.NewServiceManager(cfg) + if err != nil { + log.Fatalf("❌ 创建服务管理器失败: %v", err) + } + + // 重启服务 + if err := mgr.Restart(); err != nil { + log.Fatalf("❌ 重启服务失败: %v", err) + } + + log.Println("✅ 服务重启成功") +} + +// statusService 查看服务状态 +func statusService(cmd *cobra.Command, args []string) { + // 加载配置 + cfg, err := config.Load(configPath) + if err != nil { + log.Fatalf("❌ 加载配置失败: %v", err) + } + + // 创建服务管理器 + mgr, err := service.NewServiceManager(cfg) + if err != nil { + log.Fatalf("❌ 创建服务管理器失败: %v", err) + } + + // 查看服务状态 + status, err := mgr.Status() + if err != nil { + log.Printf("⚠️ 获取服务状态失败: %v", err) + } + + fmt.Println(status) +} + +// initConfig 初始化配置文件 +func initConfig(cmd *cobra.Command, args []string) { + if configPath == "" { + configPath = config.GetDefaultConfigPath() + } + + // 创建默认配置 + cfg := config.DefaultConfig() + + // 保存配置 + if err := cfg.Save(configPath); err != nil { + log.Fatalf("❌ 保存配置文件失败: %v", err) + } + + log.Printf("✅ 配置文件已创建: %s", configPath) + log.Println(" 请编辑配置文件,设置 server.api_key 等必要参数") +} + +// updateAgent 检查并更新 +func updateAgent(cmd *cobra.Command, args []string) { + // 加载配置 + cfg, err := config.Load(configPath) + if err != nil { + log.Fatalf("❌ 加载配置失败: %v", err) + } + + log.Println("🔍 检查更新...") + + // 检查更新 + versionInfo, err := updater.CheckUpdate(cfg.GetUpdateURL(), service.GetVersion()) + if err != nil { + log.Fatalf("❌ 检查更新失败: %v", err) + } + + // 比较版本 + if versionInfo.Version == service.GetVersion() { + log.Printf("✅ 当前已是最新版本: %s", service.GetVersion()) + return + } + + log.Printf("🆕 发现新版本: %s (当前版本: %s)", versionInfo.Version, service.GetVersion()) + + // 下载并更新 + downloadURL := cfg.GetDownloadURL() + if err := updater.Update(downloadURL); err != nil { + log.Fatalf("❌ 更新失败: %v", err) + } + + log.Println("✅ 更新成功") + log.Println(" 请重启服务以使用新版本") +} + +// registerAgent 注册探针 +func registerAgent(cmd *cobra.Command, args []string) { + reader := bufio.NewReader(os.Stdin) + + log.Println("═══════════════════════════════════════") + log.Println(" 🚀 Pika Agent 注册向导") + log.Println("═══════════════════════════════════════") + log.Println() + + // 1. 获取服务端地址(优先使用命令行参数) + var endpoint string + if serverEndpoint != "" { + endpoint = serverEndpoint + log.Printf("📡 服务端地址: %s (来自命令行参数)", endpoint) + } else { + for { + fmt.Print("📡 请输入服务端地址 (例如: http://your-server.com:18888): ") + input, _ := reader.ReadString('\n') + endpoint = strings.TrimSpace(input) + if endpoint != "" { + break + } + log.Println(" ❌ 服务端地址不能为空,请重新输入") + } + } + + // 2. 获取 API Token(优先使用命令行参数) + var apiKey string + if serverAPIKey != "" { + apiKey = serverAPIKey + log.Printf("🔑 API Token: %s (来自命令行参数)", maskToken(apiKey)) + } else { + for { + fmt.Print("🔑 请输入 API Token: ") + input, _ := reader.ReadString('\n') + apiKey = strings.TrimSpace(input) + if apiKey != "" { + break + } + log.Println(" ❌ API Token 不能为空,请重新输入") + } + } + + // 3. 获取探针名称(优先使用命令行参数,否则询问用户,默认使用主机名) + hostname, _ := os.Hostname() + var name string + if agentName != "" { + name = agentName + log.Printf("📝 探针名称: %s (来自命令行参数)", name) + } else { + fmt.Printf("📝 请输入探针名称 (留空使用主机名 '%s'): ", hostname) + nameInput, _ := reader.ReadString('\n') + name = strings.TrimSpace(nameInput) + if name == "" { + name = hostname + } + } + + log.Println() + log.Println("─────────────────────────────────────") + log.Println("📋 配置信息:") + log.Printf(" 服务端地址: %s", endpoint) + log.Printf(" API Token: %s", maskToken(apiKey)) + log.Printf(" 探针名称: %s", name) + log.Println("─────────────────────────────────────") + log.Println() + + // 4. 确认 + fmt.Print("❓ 确认以上配置并继续安装? (y/N): ") + confirmInput, _ := reader.ReadString('\n') + confirm := strings.ToLower(strings.TrimSpace(confirmInput)) + if confirm != "y" && confirm != "yes" { + log.Println("❌ 已取消注册") + return + } + + log.Println() + log.Println("🔧 开始配置...") + + // 5. 创建配置 + if configPath == "" { + configPath = config.GetDefaultConfigPath() + } + + cfg := &config.Config{ + Path: configPath, + Server: config.ServerConfig{ + Endpoint: endpoint, + APIKey: apiKey, + }, + Agent: config.AgentConfig{ + Name: name, + }, + Collector: config.CollectorConfig{ + Interval: 5, + HeartbeatInterval: 30, + NetworkExclude: []string{ + "^lo$", + "^lo0$", + "^docker.*", + "^veth.*", + "^br-.*", + }, + }, + AutoUpdate: config.AutoUpdateConfig{ + Enabled: true, + CheckInterval: "10m", + }, + } + + // 6. 保存配置 + if err := cfg.Save(configPath); err != nil { + log.Fatalf("❌ 保存配置文件失败: %v", err) + } + log.Printf("✅ 配置文件已保存: %s", configPath) + + // 7. 安装为系统服务 + log.Println("📦 安装系统服务...") + mgr, err := service.NewServiceManager(cfg) + if err != nil { + log.Fatalf("❌ 创建服务管理器失败: %v", err) + } + + if err := mgr.Install(); err != nil { + log.Fatalf("❌ 安装服务失败: %v", err) + } + log.Println("✅ 系统服务安装成功") + + // 8. 启动服务 + log.Println("🚀 启动服务...") + if err := mgr.Start(); err != nil { + log.Fatalf("❌ 启动服务失败: %v", err) + } + log.Println("✅ 服务启动成功") + + log.Println() + log.Println("═══════════════════════════════════════") + log.Println(" 🎉 探针注册完成!") + log.Println("═══════════════════════════════════════") + log.Println() + log.Println("📌 常用命令:") + log.Println(" 查看状态: agent status") + log.Println(" 停止服务: agent stop") + log.Println(" 启动服务: agent start") + log.Println(" 重启服务: agent restart") + log.Println(" 卸载服务: agent uninstall") + log.Println() +} + +// maskToken 对 Token 进行部分遮蔽显示 +func maskToken(token string) string { + if len(token) <= 8 { + return "****" + } + return token[:4] + "****" + token[len(token)-4:] +} diff --git a/cmd/serv/main.go b/cmd/serv/main.go new file mode 100644 index 0000000..a9724c9 --- /dev/null +++ b/cmd/serv/main.go @@ -0,0 +1,7 @@ +package main + +import "github.com/dushixiang/pika/internal" + +func main() { + internal.Run("./config.yaml") +} diff --git a/config.example.yaml b/config.example.yaml new file mode 100644 index 0000000..62f66be --- /dev/null +++ b/config.example.yaml @@ -0,0 +1,22 @@ +Database: + Enabled: true + Type: postgres + Postgres: + Hostname: localhost + Port: 5432 + Username: pika + Password: pika + Database: pika + ShowSql: false +log: + Level: debug # 日志等级 debug,info,waring,error + Filename: ./logs/pika.log + +Server: + Addr: "0.0.0.0:8080" + IPExtractor: "X-Forwarded-For" + +App: + JWT: + Secret: "you_must_change_me" # 替换为任意 UUID 字符串 + ExpiresHours: 168 # 7天 \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..28ece21 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,28 @@ +services: + postgresql: + image: postgres:17 + environment: + POSTGRES_DB: pika + POSTGRES_USER: pika + POSTGRES_PASSWORD: pika + volumes: + - ./data/postgresql:/var/lib/postgresql/data + ports: + - "127.0.0.1:5432:5432" + healthcheck: + test: [ "CMD-SHELL", "pg_isready -U pika" ] + interval: 10s + timeout: 5s + retries: 5 + pika: + image: dushixiang/pika:latest + environment: + DATABASE_POSTGRES_HOSTNAME: postgresql + DATABASE_POSTGRES_PORT: 5432 + DATABASE_POSTGRES_USERNAME: pika + DATABASE_POSTGRES_PASSWORD: pika + DATABASE_POSTGRES_DATABASE: pika + SERVER_ADDR: "0.0.0.0:8080" + SERVER_IP_EXTRACTOR: "x-forwarded-for" + App_JWT_Secret: "" + diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..8e515d8 --- /dev/null +++ b/go.mod @@ -0,0 +1,80 @@ +module github.com/dushixiang/pika + +go 1.25.0 + +require ( + github.com/go-errors/errors v1.5.1 + github.com/go-orz/orz v0.2.9 + github.com/go-playground/locales v0.14.1 + github.com/go-playground/universal-translator v0.18.1 + github.com/go-playground/validator/v10 v10.28.0 + github.com/golang-jwt/jwt/v5 v5.3.0 + github.com/google/uuid v1.6.0 + github.com/google/wire v0.7.0 + github.com/gorilla/websocket v1.5.3 + github.com/jpillora/backoff v1.0.0 + github.com/kardianos/service v1.2.4 + github.com/labstack/echo/v4 v4.13.4 + github.com/shirou/gopsutil/v4 v4.25.10 + github.com/spf13/cobra v1.10.1 + go.uber.org/zap v1.27.0 + golang.org/x/crypto v0.43.0 + gopkg.in/yaml.v3 v3.0.1 + gorm.io/gorm v1.31.1 +) + +require ( + filippo.io/edwards25519 v1.1.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/ebitengine/purego v0.9.0 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.11 // indirect + github.com/glebarez/go-sqlite v1.22.0 // indirect + github.com/glebarez/sqlite v1.11.0 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/go-sql-driver/mysql v1.9.3 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.7.6 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/labstack/gommon v0.4.2 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/ncruces/go-strftime v1.0.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/sagikazarmark/locafero v0.12.0 // indirect + github.com/spf13/afero v1.15.0 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/spf13/viper v1.21.0 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/tklauser/go-sysconf v0.3.15 // indirect + github.com/tklauser/numcpus v0.10.0 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.2 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect + golang.org/x/net v0.46.0 // indirect + golang.org/x/sync v0.18.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/text v0.30.0 // indirect + golang.org/x/time v0.11.0 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect + gorm.io/datatypes v1.2.7 // indirect + gorm.io/driver/mysql v1.6.0 // indirect + gorm.io/driver/postgres v1.6.0 // indirect + modernc.org/libc v1.66.10 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect + modernc.org/sqlite v1.40.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..0dbad42 --- /dev/null +++ b/go.sum @@ -0,0 +1,214 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/ebitengine/purego v0.9.0 h1:mh0zpKBIXDceC63hpvPuGLiJ8ZAa3DfrFTudmfi8A4k= +github.com/ebitengine/purego v0.9.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik= +github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ= +github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc= +github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= +github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= +github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= +github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-orz/orz v0.2.9 h1:9n3yl+N450nDxEfbfE6OX8muQmWorNp9E7YIpvx0SYQ= +github.com/go-orz/orz v0.2.9/go.mod h1:KMrLiZ5R2ZyTho4VHWCJ7z5ff2dGYXCFOu6+lCNm++4= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688= +github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU= +github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= +github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= +github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/wire v0.7.0 h1:JxUKI6+CVBgCO2WToKy/nQk0sS+amI9z9EjVmdaocj4= +github.com/google/wire v0.7.0/go.mod h1:n6YbUQD9cPKTnHXEBN2DXlOp/mVADhVErcMFb0v3J18= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk= +github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/kardianos/service v1.2.4 h1:XNlGtZOYNx2u91urOdg/Kfmc+gfmuIo1Dd3rEi2OgBk= +github.com/kardianos/service v1.2.4/go.mod h1:E4V9ufUuY82F7Ztlu1eN9VXWIQxg8NoLQlmFe0MtrXc= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/labstack/echo/v4 v4.13.4 h1:oTZZW+T3s9gAu5L8vmzihV7/lkXGZuITzTQkTEhcXEA= +github.com/labstack/echo/v4 v4.13.4/go.mod h1:g63b33BZ5vZzcIUF8AtRH40DrTlXnx4UMC8rBdndmjQ= +github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= +github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= +github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP3b/PA= +github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA= +github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= +github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4= +github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI= +github.com/shirou/gopsutil/v4 v4.25.10 h1:at8lk/5T1OgtuCp+AwrDofFRjnvosn0nkN2OLQ6g8tA= +github.com/shirou/gopsutil/v4 v4.25.10/go.mod h1:+kSwyC8DRUD9XXEHCAFjK+0nuArFJM0lva+StQAcskM= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= +github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= +github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= +github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= +github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= +github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= +golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= +golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/datatypes v1.2.7 h1:ww9GAhF1aGXZY3EB3cJPJ7//JiuQo7DlQA7NNlVaTdk= +gorm.io/datatypes v1.2.7/go.mod h1:M2iO+6S3hhi4nAyYe444Pcb0dcIiOMJ7QHaUXxyiNZY= +gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg= +gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo= +gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= +gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo= +gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= +gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= +gorm.io/driver/sqlserver v1.6.0 h1:VZOBQVsVhkHU/NzNhRJKoANt5pZGQAS1Bwc6m6dgfnc= +gorm.io/driver/sqlserver v1.6.0/go.mod h1:WQzt4IJo/WHKnckU9jXBLMJIVNMVeTu25dnOzehntWw= +gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= +gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= +modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4= +modernc.org/cc/v4 v4.26.5/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A= +modernc.org/ccgo/v4 v4.28.1/go.mod h1:uD+4RnfrVgE6ec9NGguUNdhqzNIeeomeXf6CL0GTE5Q= +modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA= +modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= +modernc.org/libc v1.66.10 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A= +modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.40.0 h1:bNWEDlYhNPAUdUdBzjAvn8icAs/2gaKlj4vM+tQ6KdQ= +modernc.org/sqlite v1.40.0/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/internal/app.go b/internal/app.go new file mode 100644 index 0000000..6c343b9 --- /dev/null +++ b/internal/app.go @@ -0,0 +1,352 @@ +package internal + +import ( + "context" + "log" + "net/http" + "strings" + "time" + + "github.com/dushixiang/pika/internal/config" + "github.com/dushixiang/pika/internal/handler" + "github.com/dushixiang/pika/internal/models" + "github.com/dushixiang/pika/internal/service" + "github.com/dushixiang/pika/web" + "github.com/google/uuid" + + "github.com/go-errors/errors" + "github.com/go-orz/orz" + "github.com/go-playground/validator/v10" + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" + "go.uber.org/zap" + "gorm.io/gorm" +) + +func Run(configPath string) { + err := orz.Quick(configPath, setup) + if err != nil { + log.Fatal(err) + } +} + +func setup(app *orz.App) error { + // 数据库迁移 + if err := autoMigrate(app.GetDatabase()); err != nil { + return err + } + + // 读取应用配置 + var appConfig config.AppConfig + if err := app.GetConfig().App.Unmarshal(&appConfig); err != nil { + app.Logger().Error("读取配置失败", zap.Error(err)) + return err + } + + // 设置默认值 + if appConfig.JWT.Secret == "" { + appConfig.JWT.Secret = uuid.NewString() + app.Logger().Warn("未配置JWT密钥,使用随机UUID") + } + if appConfig.JWT.ExpiresHours == 0 { + appConfig.JWT.ExpiresHours = 168 // 7天 + } + + // 初始化应用组件 + components, err := InitializeApp(app.Logger(), app.GetDatabase(), &appConfig) + if err != nil { + return err + } + + // 创建默认管理员用户 + ctx := context.Background() + if err := createDefaultUser(ctx, components, app.Logger()); err != nil { + app.Logger().Error("创建默认用户失败", zap.Error(err)) + // 不返回错误,因为可能用户已存在 + } + + // 启动WebSocket管理器 + go components.WSManager.Run(ctx) + + // 启动数据清理任务 + go components.AgentService.StartCleanupTask(ctx) + + // 启动指标监控任务(用于告警检测) + go startMetricsMonitoring(ctx, components, app.Logger()) + + // 设置API + setupApi(app, components) + + return nil +} + +func setupApi(app *orz.App, components *AppComponents) { + logger := app.Logger() + e := app.GetEcho() + + e.Use(middleware.Recover()) + e.Use(ErrorHandler(logger)) + + // 静态文件服务 + e.Use(middleware.StaticWithConfig(middleware.StaticConfig{ + Skipper: func(c echo.Context) bool { + // 不处理接口 + if strings.HasPrefix(c.Request().RequestURI, "/api") { + return true + } + // 不处理WebSocket + if strings.HasPrefix(c.Request().RequestURI, "/ws") { + return true + } + return false + }, + Index: "index.html", + HTML5: true, + Browse: false, + IgnoreBase: false, + Filesystem: http.FS(web.Assets()), + })) + + customValidator := CustomValidator{Validator: validator.New()} + if err := customValidator.TransInit(); err != nil { + logger.Fatal("failed to init custom validator", zap.Error(err)) + } + e.Validator = &customValidator + e.IPExtractor = echo.ExtractIPDirect() + + // 公开接口(无需认证) + publicApi := e.Group("/api") + { + // 认证相关 + publicApi.POST("/login", components.AccountHandler.Login) + + // 探针信息(公开访问)- 用于公共展示页面 + publicApi.GET("/agents", components.AgentHandler.GetAgents) + publicApi.GET("/agents/:id", components.AgentHandler.Get) + publicApi.GET("/agents/:id/metrics", components.AgentHandler.GetMetrics) + publicApi.GET("/agents/:id/metrics/latest", components.AgentHandler.GetLatestMetrics) + + // Agent 版本和下载 + publicApi.GET("/agent/version", components.AgentHandler.GetAgentVersion) + publicApi.GET("/agent/downloads/:filename", components.AgentHandler.DownloadAgent) + } + + // WebSocket 路由(探针连接) + e.GET("/ws/agent", components.AgentHandler.HandleWebSocket) + + // 管理员 API 路由(需要认证) + adminApi := e.Group("/api/admin") + adminApi.Use(JWTAuthMiddleware(components.AccountHandler)) + { + // 账户相关 + //adminApi.GET("/account/info", components.AccountHandler.Account) + adminApi.POST("/logout", components.AccountHandler.Logout) + + // API密钥管理 + adminApi.GET("/api-keys", components.ApiKeyHandler.Paging) + adminApi.POST("/api-keys", components.ApiKeyHandler.Create) + adminApi.GET("/api-keys/:id", components.ApiKeyHandler.Get) + adminApi.PUT("/api-keys/:id", components.ApiKeyHandler.Update) + adminApi.DELETE("/api-keys/:id", components.ApiKeyHandler.Delete) + adminApi.POST("/api-keys/:id/enable", components.ApiKeyHandler.Enable) + adminApi.POST("/api-keys/:id/disable", components.ApiKeyHandler.Disable) + + // 探针管理(管理员功能) + adminApi.GET("/agents", components.AgentHandler.Paging) + adminApi.GET("/agents/statistics", components.AgentHandler.GetStatistics) + adminApi.GET("/agents/:id", components.AgentHandler.GetForAdmin) + adminApi.PUT("/agents/:id/name", components.AgentHandler.UpdateName) + adminApi.PUT("/agents/:id", components.AgentHandler.UpdateInfo) + adminApi.POST("/agents/:id/command", components.AgentHandler.SendCommand) + + // VPS审计结果(管理员访问) + adminApi.GET("/agents/:id/audit/result", components.AgentHandler.GetAuditResult) + adminApi.GET("/agents/:id/audit/results", components.AgentHandler.ListAuditResults) + + // 告警配置管理 + adminApi.GET("/agents/:agentId/alert-configs", components.AlertHandler.ListAlertConfigsByAgent) + adminApi.POST("/alert-configs", components.AlertHandler.CreateAlertConfig) + adminApi.GET("/alert-configs/:id", components.AlertHandler.GetAlertConfig) + adminApi.PUT("/alert-configs/:id", components.AlertHandler.UpdateAlertConfig) + adminApi.DELETE("/alert-configs/:id", components.AlertHandler.DeleteAlertConfig) + adminApi.POST("/alert-configs/:id/test", components.AlertHandler.TestNotification) + + // 告警记录查询 + adminApi.GET("/alert-records", components.AlertHandler.ListAlertRecords) + + // 用户管理 + adminApi.GET("/users", components.UserHandler.Paging) + adminApi.POST("/users", components.UserHandler.Create) + adminApi.GET("/users/:id", components.UserHandler.Get) + adminApi.PUT("/users/:id", components.UserHandler.Update) + adminApi.DELETE("/users/:id", components.UserHandler.Delete) + adminApi.POST("/users/:id/password", components.UserHandler.ChangePassword) + adminApi.POST("/users/:id/reset-password", components.UserHandler.ResetPassword) + adminApi.POST("/users/:id/status", components.UserHandler.UpdateStatus) + } +} + +func autoMigrate(database *gorm.DB) error { + // 自动迁移数据库表 + return database.AutoMigrate( + &models.User{}, + &models.Agent{}, + &models.ApiKey{}, + &models.CPUMetric{}, + &models.MemoryMetric{}, + &models.DiskMetric{}, + &models.NetworkMetric{}, + &models.LoadMetric{}, + &models.DiskIOMetric{}, + &models.GPUMetric{}, + &models.TemperatureMetric{}, + &models.HostMetric{}, + &models.AuditResult{}, + &models.AlertConfig{}, + &models.AlertRecord{}, + ) +} + +// createDefaultUser 创建默认管理员用户 +func createDefaultUser(ctx context.Context, components *AppComponents, logger *zap.Logger) error { + // 检查是否已存在管理员用户 + _, total, err := components.UserService.ListUsers(ctx, 1, 1) + if err != nil { + return err + } + + // 如果已有用户,则不创建 + if total > 0 { + logger.Info("已存在用户,跳过创建默认用户") + return nil + } + + // 创建默认管理员用户 + defaultUser := &service.CreateUserRequest{ + Username: "admin", + Password: "admin123", + Nickname: "管理员", + } + + user, err := components.UserService.CreateUser(ctx, defaultUser) + if err != nil { + return err + } + + logger.Info("默认管理员用户创建成功", + zap.String("username", user.Username), + zap.String("password", "admin123"), + zap.String("提示", "请及时修改默认密码")) + + return nil +} + +func ErrorHandler(logger *zap.Logger) func(next echo.HandlerFunc) echo.HandlerFunc { + var a = func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + if err := next(c); err != nil { + var he *echo.HTTPError + if errors.As(err, &he) { + return c.JSON(he.Code, orz.Map{ + "code": he.Code, + "message": err.Error(), + }) + } + + logger.Sugar().Errorf("[ERROR] %s", err.Error()) + + return c.JSON(500, orz.Map{ + "code": 500, + "message": "Internal Server Error", + }) + } + return nil + } + } + return a +} + +// startMetricsMonitoring 启动指标监控任务(用于告警检测) +func startMetricsMonitoring(ctx context.Context, components *AppComponents, logger *zap.Logger) { + logger.Info("启动指标监控任务") + + ticker := time.NewTicker(30 * time.Second) // 每30秒检查一次 + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + logger.Info("指标监控任务已停止") + return + case <-ticker.C: + // 检查所有在线探针的最新指标 + agents, err := components.AgentService.ListOnlineAgents(ctx) + if err != nil { + logger.Error("获取在线探针失败", zap.Error(err)) + continue + } + + for _, agent := range agents { + // 获取最新指标 + latest, err := components.AgentService.GetLatestMetrics(ctx, agent.ID) + if err != nil { + logger.Debug("获取探针最新指标失败", zap.String("agentId", agent.ID), zap.Error(err)) + continue + } + + // 提取 CPU、内存、磁盘使用率 + var cpuUsage, memoryUsage, diskUsage float64 + + if latest.CPU != nil { + cpuUsage = latest.CPU.UsagePercent + } + + if latest.Memory != nil { + memoryUsage = latest.Memory.UsagePercent + } + + if latest.Disk != nil { + diskUsage = latest.Disk.AvgUsagePercent + } + + // 检查告警规则 + if err := components.AlertService.CheckMetrics(ctx, agent.ID, cpuUsage, memoryUsage, diskUsage); err != nil { + logger.Error("检查告警规则失败", zap.String("agentId", agent.ID), zap.Error(err)) + } + } + } + } +} + +// JWTAuthMiddleware JWT 认证中间件 +func JWTAuthMiddleware(accountHandler *handler.AccountHandler) echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + // 从 Authorization header 获取 token + authHeader := c.Request().Header.Get("Authorization") + if authHeader == "" { + return echo.NewHTTPError(http.StatusUnauthorized, "未提供认证令牌") + } + + // 检查 Bearer 前缀 + const bearerPrefix = "Bearer " + if len(authHeader) < len(bearerPrefix) || authHeader[:len(bearerPrefix)] != bearerPrefix { + return echo.NewHTTPError(http.StatusUnauthorized, "认证令牌格式错误") + } + + tokenString := authHeader[len(bearerPrefix):] + + // 验证 token + claims, err := accountHandler.ValidateToken(tokenString) + if err != nil { + return echo.NewHTTPError(http.StatusUnauthorized, "认证令牌无效: "+err.Error()) + } + + // 将用户信息存入 context + c.Set("userID", claims.UserID) + c.Set("username", claims.Username) + + return next(c) + } + } +} diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..13b8dbe --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,12 @@ +package config + +// AppConfig 应用配置 +type AppConfig struct { + JWT JWTConfig `mapstructure:"JWT"` +} + +// JWTConfig JWT配置 +type JWTConfig struct { + Secret string `mapstructure:"Secret"` + ExpiresHours int `mapstructure:"ExpiresHours"` +} diff --git a/internal/handler/account_handler.go b/internal/handler/account_handler.go new file mode 100644 index 0000000..591d6e5 --- /dev/null +++ b/internal/handler/account_handler.go @@ -0,0 +1,64 @@ +package handler + +import ( + "github.com/dushixiang/pika/internal/service" + "github.com/go-orz/orz" + "github.com/labstack/echo/v4" +) + +type AccountHandler struct { + accountService *service.AccountService +} + +func NewAccountHandler(accountService *service.AccountService) *AccountHandler { + return &AccountHandler{ + accountService: accountService, + } +} + +// LoginRequest 登录请求 +type LoginRequest struct { + Username string `json:"username" validate:"required"` + Password string `json:"password" validate:"required"` +} + +// Login 用户登录 +func (r AccountHandler) Login(c echo.Context) error { + var req LoginRequest + if err := c.Bind(&req); err != nil { + return err + } + if err := c.Validate(&req); err != nil { + return err + } + + ctx := c.Request().Context() + loginResp, err := r.accountService.Login(ctx, req.Username, req.Password) + if err != nil { + return err + } + + return orz.Ok(c, loginResp) +} + +// Logout 用户登出 +func (r AccountHandler) Logout(c echo.Context) error { + userID := c.Get("userID") + if userID == nil { + return orz.NewError(401, "未登录") + } + + ctx := c.Request().Context() + if err := r.accountService.Logout(ctx, userID.(string)); err != nil { + return err + } + + return orz.Ok(c, orz.Map{ + "message": "登出成功", + }) +} + +// ValidateToken 验证 token(供中间件使用) +func (r AccountHandler) ValidateToken(tokenString string) (*service.JWTClaims, error) { + return r.accountService.ValidateToken(tokenString) +} diff --git a/internal/handler/agent_handler.go b/internal/handler/agent_handler.go new file mode 100644 index 0000000..e3da8e8 --- /dev/null +++ b/internal/handler/agent_handler.go @@ -0,0 +1,568 @@ +package handler + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "slices" + "strconv" + "strings" + "time" + + "github.com/dushixiang/pika" + "github.com/dushixiang/pika/internal/models" + "github.com/dushixiang/pika/internal/protocol" + "github.com/dushixiang/pika/internal/service" + ws "github.com/dushixiang/pika/internal/websocket" + "github.com/dushixiang/pika/pkg/version" + "github.com/go-orz/orz" + "github.com/gorilla/websocket" + "github.com/labstack/echo/v4" + "go.uber.org/zap" +) + +type AgentHandler struct { + logger *zap.Logger + agentService *service.AgentService + wsManager *ws.Manager + upgrader websocket.Upgrader +} + +func NewAgentHandler(logger *zap.Logger, agentService *service.AgentService, wsManager *ws.Manager) *AgentHandler { + + h := &AgentHandler{ + logger: logger, + agentService: agentService, + wsManager: wsManager, + } + + // 初始化upgrader,需要在创建handler之后因为需要引用h.checkOrigin + h.upgrader = websocket.Upgrader{ + ReadBufferSize: 1024 * 32, + WriteBufferSize: 1024 * 32, + } + + // 设置WebSocket消息处理器 + wsManager.SetMessageHandler(h.handleWebSocketMessage) + + return h +} + +// HandleWebSocket 处理WebSocket连接 +func (h *AgentHandler) HandleWebSocket(c echo.Context) error { + conn, err := h.upgrader.Upgrade(c.Response(), c.Request(), nil) + if err != nil { + h.logger.Error("failed to upgrade websocket", zap.Error(err)) + return err + } + + // 等待探针发送注册信息 + conn.SetReadDeadline(time.Now().Add(10 * time.Second)) + _, message, err := conn.ReadMessage() + if err != nil { + h.logger.Error("failed to read register message", zap.Error(err)) + conn.Close() + return err + } + + // 清除读取超时,后续由心跳机制控制 + conn.SetReadDeadline(time.Time{}) + + // 解析注册消息 + var msg protocol.Message + if err := json.Unmarshal(message, &msg); err != nil { + h.logger.Error("failed to parse register message", zap.Error(err)) + conn.Close() + return err + } + + if msg.Type != protocol.MessageTypeRegister { + h.logger.Error("first message must be register", zap.String("type", string(msg.Type))) + conn.Close() + return echo.NewHTTPError(http.StatusBadRequest, "首条消息必须是注册消息") + } + + // 解析探针注册信息 + var registerReq protocol.RegisterRequest + if err := json.Unmarshal(msg.Data, ®isterReq); err != nil { + h.logger.Error("failed to parse register request", zap.Error(err)) + conn.Close() + return err + } + + // 注册探针 - 使用独立的context,不依赖HTTP请求的context + ctx := context.Background() + agent, err := h.agentService.RegisterAgent(ctx, c.RealIP(), ®isterReq.AgentInfo, registerReq.ApiKey) + if err != nil { + h.logger.Error("failed to register agent", zap.Error(err)) + + // 发送注册失败响应 + h.sendRegisterError(conn, err.Error()) + conn.Close() + return err + } + + defer func() { + // 设置探针状态为离线 + _ = h.agentService.UpdateAgentStatus(ctx, agent.ID, 0) + }() + + // 发送注册成功响应 + if err := h.sendRegisterSuccess(conn, agent.ID); err != nil { + h.logger.Error("failed to send register ack", zap.Error(err)) + conn.Close() + return err + } + + // 创建客户端并注册到管理器 + client := &ws.Client{ + ID: agent.ID, + Conn: conn, + Send: make(chan []byte, 256), + Manager: h.wsManager, + LastActive: time.Now(), + } + + h.wsManager.Register(client) + + // 启动读写协程 + go client.WritePump() + go client.ReadPump(ctx) + + return nil +} + +// handleWebSocketMessage 处理WebSocket消息 +func (h *AgentHandler) handleWebSocketMessage(ctx context.Context, agentID string, messageType string, data json.RawMessage) error { + switch protocol.MessageType(messageType) { + case protocol.MessageTypeHeartbeat: + // 心跳消息,更新探针状态 + return h.agentService.UpdateAgentStatus(ctx, agentID, 1) + + case protocol.MessageTypeMetrics: + // 指标数据 + var metricsWrapper protocol.MetricsWrapper + if err := json.Unmarshal(data, &metricsWrapper); err != nil { + return err + } + return h.agentService.HandleMetricData(ctx, agentID, string(metricsWrapper.Type), metricsWrapper.Data) + + case protocol.MessageTypeCommandResp: + // 指令响应 + var cmdResp protocol.CommandResponse + if err := json.Unmarshal(data, &cmdResp); err != nil { + return err + } + return h.agentService.HandleCommandResponse(ctx, agentID, &cmdResp) + + default: + h.logger.Warn("unknown message type", zap.String("type", messageType)) + return nil + } +} + +// sendRegisterSuccess 发送注册成功响应 +func (h *AgentHandler) sendRegisterSuccess(conn *websocket.Conn, agentID string) error { + resp := protocol.RegisterResponse{ + AgentID: agentID, + Status: "success", + } + respData, err := json.Marshal(resp) + if err != nil { + return err + } + + msg := protocol.Message{ + Type: protocol.MessageTypeRegisterAck, + Data: respData, + } + msgData, err := json.Marshal(msg) + if err != nil { + return err + } + + return conn.WriteMessage(websocket.TextMessage, msgData) +} + +// sendRegisterError 发送注册失败响应 +func (h *AgentHandler) sendRegisterError(conn *websocket.Conn, errMsg string) error { + resp := protocol.RegisterResponse{ + Status: "error", + Message: errMsg, + } + respData, _ := json.Marshal(resp) + + msg := protocol.Message{ + Type: protocol.MessageTypeRegisterErr, + Data: respData, + } + msgData, _ := json.Marshal(msg) + + return conn.WriteMessage(websocket.TextMessage, msgData) +} + +// Paging 探针分页查询 +func (h *AgentHandler) Paging(c echo.Context) error { + hostname := c.QueryParam("hostname") + ip := c.QueryParam("ip") + status := c.QueryParam("status") + + pr := orz.GetPageRequest(c, "last_seen_at", "hostname") + + builder := orz.NewPageBuilder(h.agentService.Repository). + PageRequest(pr). + Contains("hostname", hostname). + Contains("ip", ip) + + // 处理状态筛选 + if status == "online" { + builder.Equal("status", "1") + } else if status == "offline" { + builder.Equal("status", "0") + } + + ctx := c.Request().Context() + page, err := builder.Execute(ctx) + if err != nil { + return err + } + return orz.Ok(c, page) +} + +// Get 获取探针详情(公开接口,隐藏敏感信息) +func (h *AgentHandler) Get(c echo.Context) error { + id := c.Param("id") + ctx := c.Request().Context() + + agent, err := h.agentService.GetAgent(ctx, id) + if err != nil { + return err + } + agent.IP = "" + agent.Hostname = "" + + return orz.Ok(c, agent) +} + +// GetForAdmin 获取探针详情(管理员接口,显示完整信息) +func (h *AgentHandler) GetForAdmin(c echo.Context) error { + id := c.Param("id") + ctx := c.Request().Context() + + agent, err := h.agentService.GetAgent(ctx, id) + if err != nil { + return err + } + + return orz.Ok(c, agent) +} + +// GetMetrics 获取探针聚合指标 +func (h *AgentHandler) GetMetrics(c echo.Context) error { + agentID := c.Param("id") + metricType := c.QueryParam("type") + rangeParam := c.QueryParam("range") + + // 验证指标类型 + validTypes := map[string]bool{ + "cpu": true, "memory": true, "disk": true, "network": true, "load": true, + } + if metricType == "" { + return orz.NewError(400, "指标类型不能为空") + } + if !validTypes[metricType] { + return orz.NewError(400, "无效的指标类型") + } + + // 根据 range 参数自动计算时间范围 + var start, end int64 + end = time.Now().UnixMilli() + + if rangeParam == "" { + rangeParam = "1h" // 默认1小时 + } + + switch rangeParam { + case "1h": + start = end - 1*60*60*1000 + case "6h": + start = end - 6*60*60*1000 + case "12h": + start = end - 12*60*60*1000 + case "24h", "1d": + start = end - 24*60*60*1000 + case "3d": + start = end - 3*24*60*60*1000 + case "7d", "1w": + start = end - 7*24*60*60*1000 + case "30d", "1M": + start = end - 30*24*60*60*1000 + default: + return orz.NewError(400, "无效的时间范围,支持: 1h, 6h, 12h, 24h, 3d, 7d, 30d, 90d") + } + + // 服务端自动计算最优聚合间隔 + interval := service.CalculateInterval(start, end) + + ctx := c.Request().Context() + metrics, err := h.agentService.GetMetrics(ctx, agentID, metricType, start, end, interval) + if err != nil { + return err + } + + return orz.Ok(c, orz.Map{ + "agentId": agentID, + "type": metricType, + "range": rangeParam, + "start": start, + "end": end, + "interval": interval, + "metrics": metrics, + }) +} + +// GetLatestMetrics 获取探针最新指标 +func (h *AgentHandler) GetLatestMetrics(c echo.Context) error { + id := c.Param("id") + ctx := c.Request().Context() + + metrics, err := h.agentService.GetLatestMetrics(ctx, id) + if err != nil { + return err + } + + return orz.Ok(c, metrics) +} + +// GetAgents 获取探针列表 +func (h *AgentHandler) GetAgents(c echo.Context) error { + ctx := c.Request().Context() + agents, err := h.agentService.ListAgents(ctx) + if err != nil { + return err + } + + slices.SortFunc(agents, func(a, b models.Agent) int { + if a.Status == b.Status { + return strings.Compare(a.Name, b.Name) + } + return strings.Compare(strconv.Itoa(a.Status), strconv.Itoa(b.Status)) + }) + + // 添加连接状态和最新指标 + result := make([]map[string]interface{}, 0, len(agents)) + for _, agent := range agents { + item := map[string]interface{}{ + "id": agent.ID, + "name": agent.Name, + "hostname": agent.Hostname, + "ip": agent.IP, + "os": agent.OS, + "arch": agent.Arch, + "version": agent.Version, + "platform": agent.Platform, + "location": agent.Location, + "expireTime": agent.ExpireTime, + "status": agent.Status, + "lastSeenAt": agent.LastSeenAt, + } + + // 获取最新指标数据 + metrics, err := h.agentService.GetLatestMetrics(ctx, agent.ID) + if err == nil && metrics != nil { + item["metrics"] = metrics + } + + result = append(result, item) + } + + return orz.Ok(c, orz.Map{ + "items": result, + "total": len(result), + }) +} + +// GetAgentVersion 获取 Agent 版本信息 +func (h *AgentHandler) GetAgentVersion(c echo.Context) error { + return orz.Ok(c, orz.Map{ + "version": version.GetVersion(), + }) +} + +// DownloadAgent 下载 Agent 二进制文件 +func (h *AgentHandler) DownloadAgent(c echo.Context) error { + filename := c.Param("filename") + + // 从嵌入的文件系统读取 + agentFile, err := pika.AgentFS().Open(fmt.Sprintf("pika-%s", filename)) + if err != nil { + h.logger.Error("agent binary not found", zap.String("filename", filename), zap.Error(err)) + return orz.NewError(404, "未找到对应平台的 Agent 二进制文件") + } + + // 设置响应头 + c.Response().Header().Set("Content-Type", "application/octet-stream") + c.Response().Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename)) + + return c.Stream(http.StatusOK, "application/octet-stream", agentFile) +} + +// SendCommand 向探针发送指令 +func (h *AgentHandler) SendCommand(c echo.Context) error { + agentID := c.Param("id") + cmdType := c.QueryParam("type") + + if cmdType == "" { + return orz.NewError(400, "指令类型不能为空") + } + + // 检查agent是否在线 + _, exists := h.wsManager.GetClient(agentID) + if !exists { + return orz.NewError(400, "探针未连接") + } + + // 生成指令ID + cmdID := fmt.Sprintf("%s_%d", cmdType, time.Now().UnixMilli()) + + // 构建指令请求 + cmdReq := protocol.CommandRequest{ + ID: cmdID, + Type: cmdType, + } + + reqData, err := json.Marshal(cmdReq) + if err != nil { + return err + } + + msg := protocol.Message{ + Type: protocol.MessageTypeCommand, + Data: reqData, + } + + msgData, err := json.Marshal(msg) + if err != nil { + return err + } + + // 发送指令 + if err := h.wsManager.SendToClient(agentID, msgData); err != nil { + return orz.NewError(500, "发送指令失败") + } + + h.logger.Info("command sent", zap.String("agentID", agentID), zap.String("cmdID", cmdID), zap.String("type", cmdType)) + + return orz.Ok(c, orz.Map{ + "commandId": cmdID, + "status": "sent", + }) +} + +// GetAuditResult 获取审计结果 +func (h *AgentHandler) GetAuditResult(c echo.Context) error { + agentID := c.Param("id") + ctx := c.Request().Context() + + result, err := h.agentService.GetAuditResult(ctx, agentID) + if err != nil { + return err + } + + return orz.Ok(c, result) +} + +// ListAuditResults 获取审计结果列表 +func (h *AgentHandler) ListAuditResults(c echo.Context) error { + agentID := c.Param("id") + ctx := c.Request().Context() + + results, err := h.agentService.ListAuditResults(ctx, agentID) + if err != nil { + return err + } + + return orz.Ok(c, orz.Map{ + "items": results, + "total": len(results), + }) +} + +// UpdateName 更新探针名称 +func (h *AgentHandler) UpdateName(c echo.Context) error { + agentID := c.Param("id") + + var req struct { + Name string `json:"name" validate:"required"` + } + if err := c.Bind(&req); err != nil { + return orz.NewError(400, "请求参数错误") + } + if err := c.Validate(&req); err != nil { + return orz.NewError(400, "名称不能为空") + } + + ctx := c.Request().Context() + if err := h.agentService.UpdateAgentName(ctx, agentID, req.Name); err != nil { + return err + } + + return orz.Ok(c, orz.Map{ + "message": "更新成功", + }) +} + +// UpdateInfo 更新探针信息(名称、平台、位置、到期时间) +func (h *AgentHandler) UpdateInfo(c echo.Context) error { + agentID := c.Param("id") + + var req struct { + Name *string `json:"name"` + Platform *string `json:"platform"` + Location *string `json:"location"` + ExpireTime *int64 `json:"expireTime"` + } + if err := c.Bind(&req); err != nil { + return orz.NewError(400, "请求参数错误") + } + + // 构建更新字段 + updates := make(map[string]interface{}) + if req.Name != nil { + updates["name"] = *req.Name + } + if req.Platform != nil { + updates["platform"] = *req.Platform + } + if req.Location != nil { + updates["location"] = *req.Location + } + if req.ExpireTime != nil { + updates["expire_time"] = *req.ExpireTime + } + + if len(updates) == 0 { + return orz.NewError(400, "没有需要更新的字段") + } + + ctx := c.Request().Context() + if err := h.agentService.UpdateAgentInfo(ctx, agentID, updates); err != nil { + return err + } + + return orz.Ok(c, orz.Map{ + "message": "更新成功", + }) +} + +// GetStatistics 获取探针统计数据 +func (h *AgentHandler) GetStatistics(c echo.Context) error { + ctx := c.Request().Context() + stats, err := h.agentService.GetStatistics(ctx) + if err != nil { + return err + } + + return orz.Ok(c, stats) +} diff --git a/internal/handler/alert_handler.go b/internal/handler/alert_handler.go new file mode 100644 index 0000000..c28ff9f --- /dev/null +++ b/internal/handler/alert_handler.go @@ -0,0 +1,163 @@ +package handler + +import ( + "net/http" + "strconv" + + "github.com/dushixiang/pika/internal/models" + "github.com/dushixiang/pika/internal/service" + "github.com/labstack/echo/v4" + "go.uber.org/zap" +) + +type AlertHandler struct { + logger *zap.Logger + alertService *service.AlertService +} + +func NewAlertHandler(logger *zap.Logger, alertService *service.AlertService) *AlertHandler { + return &AlertHandler{ + logger: logger, + alertService: alertService, + } +} + +// CreateAlertConfig 创建告警配置 +func (h *AlertHandler) CreateAlertConfig(c echo.Context) error { + var config models.AlertConfig + if err := c.Bind(&config); err != nil { + return c.JSON(http.StatusBadRequest, map[string]string{ + "error": "无效的请求参数", + }) + } + + if err := h.alertService.CreateAlertConfig(c.Request().Context(), &config); err != nil { + h.logger.Error("创建告警配置失败", zap.Error(err)) + return c.JSON(http.StatusInternalServerError, map[string]string{ + "error": "创建告警配置失败", + }) + } + + return c.JSON(http.StatusOK, config) +} + +// UpdateAlertConfig 更新告警配置 +func (h *AlertHandler) UpdateAlertConfig(c echo.Context) error { + id := c.Param("id") + + var config models.AlertConfig + if err := c.Bind(&config); err != nil { + return c.JSON(http.StatusBadRequest, map[string]string{ + "error": "无效的请求参数", + }) + } + + config.ID = id + + if err := h.alertService.UpdateAlertConfig(c.Request().Context(), &config); err != nil { + h.logger.Error("更新告警配置失败", zap.Error(err)) + return c.JSON(http.StatusInternalServerError, map[string]string{ + "error": "更新告警配置失败", + }) + } + + return c.JSON(http.StatusOK, config) +} + +// DeleteAlertConfig 删除告警配置 +func (h *AlertHandler) DeleteAlertConfig(c echo.Context) error { + id := c.Param("id") + + if err := h.alertService.DeleteAlertConfig(c.Request().Context(), id); err != nil { + h.logger.Error("删除告警配置失败", zap.Error(err)) + return c.JSON(http.StatusInternalServerError, map[string]string{ + "error": "删除告警配置失败", + }) + } + + return c.JSON(http.StatusOK, map[string]string{ + "message": "删除成功", + }) +} + +// GetAlertConfig 获取告警配置 +func (h *AlertHandler) GetAlertConfig(c echo.Context) error { + id := c.Param("id") + + config, err := h.alertService.GetAlertConfig(c.Request().Context(), id) + if err != nil { + h.logger.Error("获取告警配置失败", zap.Error(err)) + return c.JSON(http.StatusInternalServerError, map[string]string{ + "error": "获取告警配置失败", + }) + } + + return c.JSON(http.StatusOK, config) +} + +// ListAlertConfigsByAgent 列出探针的告警配置 +func (h *AlertHandler) ListAlertConfigsByAgent(c echo.Context) error { + agentID := c.Param("agentId") + + configs, err := h.alertService.ListAlertConfigsByAgent(c.Request().Context(), agentID) + if err != nil { + h.logger.Error("获取告警配置列表失败", zap.Error(err)) + return c.JSON(http.StatusInternalServerError, map[string]string{ + "error": "获取告警配置列表失败", + }) + } + + return c.JSON(http.StatusOK, configs) +} + +// ListAlertRecords 列出告警记录 +func (h *AlertHandler) ListAlertRecords(c echo.Context) error { + agentID := c.QueryParam("agentId") + + // 解析分页参数 + limit := 20 + offset := 0 + + if l := c.QueryParam("limit"); l != "" { + if parsedLimit, err := strconv.Atoi(l); err == nil && parsedLimit > 0 { + limit = parsedLimit + } + } + + if o := c.QueryParam("offset"); o != "" { + if parsedOffset, err := strconv.Atoi(o); err == nil && parsedOffset >= 0 { + offset = parsedOffset + } + } + + records, total, err := h.alertService.ListAlertRecords(c.Request().Context(), agentID, limit, offset) + if err != nil { + h.logger.Error("获取告警记录失败", zap.Error(err)) + return c.JSON(http.StatusInternalServerError, map[string]string{ + "error": "获取告警记录失败", + }) + } + + return c.JSON(http.StatusOK, map[string]interface{}{ + "records": records, + "total": total, + "limit": limit, + "offset": offset, + }) +} + +// TestNotification 测试告警通知 +func (h *AlertHandler) TestNotification(c echo.Context) error { + id := c.Param("id") + + if err := h.alertService.TestNotification(c.Request().Context(), id); err != nil { + h.logger.Error("测试告警通知失败", zap.Error(err)) + return c.JSON(http.StatusInternalServerError, map[string]string{ + "error": "测试告警通知失败: " + err.Error(), + }) + } + + return c.JSON(http.StatusOK, map[string]string{ + "message": "测试通知已发送", + }) +} diff --git a/internal/handler/api_key_handler.go b/internal/handler/api_key_handler.go new file mode 100644 index 0000000..d75ca63 --- /dev/null +++ b/internal/handler/api_key_handler.go @@ -0,0 +1,159 @@ +package handler + +import ( + "github.com/dushixiang/pika/internal/service" + "github.com/go-orz/orz" + "github.com/labstack/echo/v4" + "go.uber.org/zap" +) + +type ApiKeyHandler struct { + logger *zap.Logger + apiKeyService *service.ApiKeyService +} + +func NewApiKeyHandler(logger *zap.Logger, apiKeyService *service.ApiKeyService) *ApiKeyHandler { + return &ApiKeyHandler{ + logger: logger, + apiKeyService: apiKeyService, + } +} + +// GenerateApiKeyRequest 生成API密钥请求 +type GenerateApiKeyRequest struct { + Name string `json:"name" validate:"required"` +} + +// UpdateApiKeyNameRequest 更新API密钥名称请求 +type UpdateApiKeyNameRequest struct { + Name string `json:"name" validate:"required"` +} + +// Paging API密钥分页查询 +func (r ApiKeyHandler) Paging(c echo.Context) error { + name := c.QueryParam("name") + + pr := orz.GetPageRequest(c, "created_at", "name") + + builder := orz.NewPageBuilder(r.apiKeyService.Repository). + PageRequest(pr). + Contains("name", name) + + ctx := c.Request().Context() + page, err := builder.Execute(ctx) + if err != nil { + return err + } + + // 返回完整密钥,由前端控制显示/隐藏 + return orz.Ok(c, orz.Map{ + "items": page.Items, + "total": page.Total, + }) +} + +// Create 生成API密钥 +func (r ApiKeyHandler) Create(c echo.Context) error { + var req GenerateApiKeyRequest + if err := c.Bind(&req); err != nil { + return err + } + if err := c.Validate(&req); err != nil { + return err + } + + // 从上下文获取用户ID + userID := c.Get("userID").(string) + + ctx := c.Request().Context() + apiKey, err := r.apiKeyService.GenerateApiKey(ctx, req.Name, userID) + if err != nil { + r.logger.Error("failed to generate api key", zap.Error(err)) + return err + } + + return orz.Ok(c, apiKey) +} + +// Get 获取API密钥详情 +func (r ApiKeyHandler) Get(c echo.Context) error { + id := c.Param("id") + ctx := c.Request().Context() + + apiKey, err := r.apiKeyService.GetApiKey(ctx, id) + if err != nil { + r.logger.Error("failed to get api key", zap.Error(err)) + return err + } + + // 返回完整密钥,由前端控制显示/隐藏 + return orz.Ok(c, apiKey) +} + +// Update 更新API密钥名称 +func (r ApiKeyHandler) Update(c echo.Context) error { + id := c.Param("id") + + var req UpdateApiKeyNameRequest + if err := c.Bind(&req); err != nil { + return err + } + if err := c.Validate(&req); err != nil { + return err + } + + ctx := c.Request().Context() + if err := r.apiKeyService.UpdateApiKeyName(ctx, id, req.Name); err != nil { + r.logger.Error("failed to update api key name", zap.Error(err)) + return err + } + + return orz.Ok(c, orz.Map{ + "message": "API密钥名称更新成功", + }) +} + +// Delete 删除API密钥 +func (r ApiKeyHandler) Delete(c echo.Context) error { + id := c.Param("id") + ctx := c.Request().Context() + + if err := r.apiKeyService.DeleteApiKey(ctx, id); err != nil { + r.logger.Error("failed to delete api key", zap.Error(err)) + return err + } + + return orz.Ok(c, orz.Map{ + "message": "API密钥删除成功", + }) +} + +// Enable 启用API密钥 +func (r ApiKeyHandler) Enable(c echo.Context) error { + id := c.Param("id") + ctx := c.Request().Context() + + if err := r.apiKeyService.EnableApiKey(ctx, id); err != nil { + r.logger.Error("failed to enable api key", zap.Error(err)) + return err + } + + return orz.Ok(c, orz.Map{ + "message": "API密钥启用成功", + }) +} + +// Disable 禁用API密钥 +func (r ApiKeyHandler) Disable(c echo.Context) error { + id := c.Param("id") + ctx := c.Request().Context() + + if err := r.apiKeyService.DisableApiKey(ctx, id); err != nil { + r.logger.Error("failed to disable api key", zap.Error(err)) + return err + } + + return orz.Ok(c, orz.Map{ + "message": "API密钥禁用成功", + }) +} diff --git a/internal/handler/user_handler.go b/internal/handler/user_handler.go new file mode 100644 index 0000000..0d336ca --- /dev/null +++ b/internal/handler/user_handler.go @@ -0,0 +1,182 @@ +package handler + +import ( + "github.com/dushixiang/pika/internal/service" + "github.com/go-orz/orz" + "github.com/labstack/echo/v4" +) + +type UserHandler struct { + userService *service.UserService +} + +func NewUserHandler(userService *service.UserService) *UserHandler { + return &UserHandler{ + userService: userService, + } +} + +// ResetPasswordRequest 重置密码请求 +type ResetPasswordRequest struct { + NewPassword string `json:"newPassword" validate:"required,min=6"` +} + +// UpdateUserStatusRequest 更新用户状态请求 +type UpdateUserStatusRequest struct { + Status int `json:"status" validate:"required,oneof=0 1"` +} + +// Paging 用户分页查询 +func (r UserHandler) Paging(c echo.Context) error { + username := c.QueryParam("username") + + pr := orz.GetPageRequest(c, "created_at", "username") + + builder := orz.NewPageBuilder(r.userService.Repository). + PageRequest(pr). + Contains("username", username) + + ctx := c.Request().Context() + page, err := builder.Execute(ctx) + if err != nil { + return err + } + + return orz.Ok(c, orz.Map{ + "items": page.Items, + "total": page.Total, + }) +} + +// Create 创建用户 +func (r UserHandler) Create(c echo.Context) error { + var req service.CreateUserRequest + if err := c.Bind(&req); err != nil { + return err + } + if err := c.Validate(&req); err != nil { + return err + } + + ctx := c.Request().Context() + user, err := r.userService.CreateUser(ctx, &req) + if err != nil { + return err + } + + return orz.Ok(c, user) +} + +// Get 获取用户详情 +func (r UserHandler) Get(c echo.Context) error { + id := c.Param("id") + ctx := c.Request().Context() + + user, err := r.userService.GetUser(ctx, id) + if err != nil { + return err + } + + return orz.Ok(c, user) +} + +// Update 更新用户 +func (r UserHandler) Update(c echo.Context) error { + id := c.Param("id") + + var req service.UpdateUserRequest + if err := c.Bind(&req); err != nil { + return err + } + if err := c.Validate(&req); err != nil { + return err + } + + ctx := c.Request().Context() + user, err := r.userService.UpdateUser(ctx, id, &req) + if err != nil { + return err + } + + return orz.Ok(c, user) +} + +// Delete 删除用户 +func (r UserHandler) Delete(c echo.Context) error { + id := c.Param("id") + ctx := c.Request().Context() + + if err := r.userService.DeleteUser(ctx, id); err != nil { + return err + } + + return orz.Ok(c, orz.Map{ + "message": "用户删除成功", + }) +} + +// ChangePassword 修改密码 +func (r UserHandler) ChangePassword(c echo.Context) error { + id := c.Param("id") + + var req service.ChangePasswordRequest + if err := c.Bind(&req); err != nil { + return err + } + if err := c.Validate(&req); err != nil { + return err + } + + ctx := c.Request().Context() + if err := r.userService.ChangePassword(ctx, id, req.OldPassword, req.NewPassword); err != nil { + return err + } + + return orz.Ok(c, orz.Map{ + "message": "密码修改成功", + }) +} + +// ResetPassword 重置密码(管理员操作) +func (r UserHandler) ResetPassword(c echo.Context) error { + id := c.Param("id") + + var req ResetPasswordRequest + if err := c.Bind(&req); err != nil { + return err + } + if err := c.Validate(&req); err != nil { + return err + } + + ctx := c.Request().Context() + if err := r.userService.ResetPassword(ctx, id, req.NewPassword); err != nil { + return err + } + + return orz.Ok(c, orz.Map{ + "message": "密码重置成功", + }) +} + +// UpdateStatus 更新用户状态 +func (r UserHandler) UpdateStatus(c echo.Context) error { + id := c.Param("id") + + var req UpdateUserStatusRequest + if err := c.Bind(&req); err != nil { + return err + } + if err := c.Validate(&req); err != nil { + return err + } + + ctx := c.Request().Context() + if err := r.userService.UpdateUserStatus(ctx, id, req.Status); err != nil { + return err + } + + return orz.Ok(c, orz.Map{ + "message": "用户状态更新成功", + }) +} diff --git a/internal/models/agent.go b/internal/models/agent.go new file mode 100644 index 0000000..b545f71 --- /dev/null +++ b/internal/models/agent.go @@ -0,0 +1,23 @@ +package models + +// Agent 探针信息 +type Agent struct { + ID string `gorm:"primaryKey" json:"id"` // 探针ID (UUID) + Name string `gorm:"index" json:"name"` // 探针名称 + Hostname string `gorm:"index" json:"hostname,omitempty"` // 主机名 + IP string `gorm:"index" json:"ip,omitempty"` // IP地址 + OS string `json:"os"` // 操作系统 + Arch string `json:"arch"` // 架构 + Version string `json:"version"` // 探针版本 + Platform string `json:"platform"` // 平台 + Location string `json:"location"` // 位置 + ExpireTime int64 `json:"expireTime"` // 到期时间(时间戳毫秒) + Status int `json:"status"` // 状态: 0-离线, 1-在线 + LastSeenAt int64 `gorm:"index" json:"lastSeenAt"` // 最后上线时间(时间戳毫秒) + CreatedAt int64 `json:"createdAt"` // 创建时间(时间戳毫秒) + UpdatedAt int64 `json:"updatedAt" gorm:"autoUpdateTime:milli"` // 更新时间(时间戳毫秒) +} + +func (Agent) TableName() string { + return "agents" +} diff --git a/internal/models/alert.go b/internal/models/alert.go new file mode 100644 index 0000000..36852a2 --- /dev/null +++ b/internal/models/alert.go @@ -0,0 +1,106 @@ +package models + +// AlertConfig 告警配置 +type AlertConfig struct { + ID string `gorm:"primaryKey" json:"id"` // 告警配置ID (UUID) + AgentID string `gorm:"index" json:"agentId"` // 探针ID(全局配置使用"global") + AgentIDs []string `gorm:"-" json:"agentIds"` // 监控的探针ID列表(前端使用) + AgentIDsStr string `json:"-" gorm:"column:agent_ids"` // 数据库存储(JSON序列化) + Name string `json:"name"` // 告警配置名称 + Enabled bool `json:"enabled"` // 是否启用 + CreatedAt int64 `json:"createdAt"` // 创建时间(时间戳毫秒) + UpdatedAt int64 `json:"updatedAt" gorm:"autoUpdateTime:milli"` // 更新时间(时间戳毫秒) + + // 告警规则 + Rules AlertRules `gorm:"embedded;embeddedPrefix:rule_" json:"rules"` + + // 告警通知配置 + Notification NotificationConfig `gorm:"embedded;embeddedPrefix:notify_" json:"notification"` +} + +// AlertRules 告警规则 +type AlertRules struct { + // CPU 告警配置 + CPUEnabled bool `json:"cpuEnabled"` // 是否启用CPU告警 + CPUThreshold float64 `json:"cpuThreshold"` // CPU使用率阈值(0-100) + CPUDuration int `json:"cpuDuration"` // 持续时间(秒) + + // 内存告警配置 + MemoryEnabled bool `json:"memoryEnabled"` // 是否启用内存告警 + MemoryThreshold float64 `json:"memoryThreshold"` // 内存使用率阈值(0-100) + MemoryDuration int `json:"memoryDuration"` // 持续时间(秒) + + // 磁盘告警配置 + DiskEnabled bool `json:"diskEnabled"` // 是否启用磁盘告警 + DiskThreshold float64 `json:"diskThreshold"` // 磁盘使用率阈值(0-100) + DiskDuration int `json:"diskDuration"` // 持续时间(秒) + + // 网络断开告警配置 + NetworkEnabled bool `json:"networkEnabled"` // 是否启用网络断开告警 + NetworkDuration int `json:"networkDuration"` // 持续时间(秒) +} + +// NotificationConfig 告警通知配置 +type NotificationConfig struct { + // 钉钉配置 + DingTalkEnabled bool `json:"dingTalkEnabled"` // 是否启用钉钉通知 + DingTalkWebhook string `json:"dingTalkWebhook"` // 钉钉Webhook URL + DingTalkSecret string `json:"dingTalkSecret"` // 钉钉加签密钥(可选) + + // 企业微信配置 + WeComEnabled bool `json:"weComEnabled"` // 是否启用企业微信通知 + WeComWebhook string `json:"weComWebhook"` // 企业微信Webhook URL + + // 飞书配置 + FeishuEnabled bool `json:"feishuEnabled"` // 是否启用飞书通知 + FeishuWebhook string `json:"feishuWebhook"` // 飞书Webhook URL + + // 邮件配置 + EmailEnabled bool `json:"emailEnabled"` // 是否启用邮件通知 + EmailAddresses []string `gorm:"-" json:"emailAddresses"` // 接收邮件地址列表 + EmailAddressesStr string `json:"-" gorm:"column:email_addresses"` // 数据库存储(JSON序列化) + + // 自定义Webhook配置 + CustomWebhookEnabled bool `json:"customWebhookEnabled"` // 是否启用自定义Webhook + CustomWebhookURL string `json:"customWebhookUrl"` // 自定义Webhook URL +} + +func (AlertConfig) TableName() string { + return "alert_configs" +} + +// AlertRecord 告警记录 +type AlertRecord struct { + ID int64 `gorm:"primaryKey;autoIncrement" json:"id"` // 记录ID + AgentID string `gorm:"index" json:"agentId"` // 探针ID + ConfigID string `gorm:"index" json:"configId"` // 告警配置ID + ConfigName string `json:"configName"` // 告警配置名称 + AlertType string `json:"alertType"` // 告警类型: cpu, memory, disk, network + Message string `json:"message"` // 告警消息 + Threshold float64 `json:"threshold"` // 告警阈值 + ActualValue float64 `json:"actualValue"` // 实际值 + Level string `json:"level"` // 告警级别: info, warning, critical + Status string `json:"status"` // 状态: firing(告警中), resolved(已恢复) + FiredAt int64 `gorm:"index" json:"firedAt"` // 触发时间(时间戳毫秒) + ResolvedAt int64 `json:"resolvedAt,omitempty"` // 恢复时间(时间戳毫秒) + CreatedAt int64 `json:"createdAt"` // 创建时间(时间戳毫秒) + UpdatedAt int64 `json:"updatedAt" gorm:"autoUpdateTime:milli"` // 更新时间(时间戳毫秒) +} + +func (AlertRecord) TableName() string { + return "alert_records" +} + +// AlertState 告警状态(内存中保存,用于判断是否持续超过阈值) +type AlertState struct { + AgentID string // 探针ID + ConfigID string // 告警配置ID + AlertType string // 告警类型 + Value float64 // 当前值 + Threshold float64 // 阈值 + StartTime int64 // 开始超过阈值的时间 + Duration int // 需要持续的时间(秒) + LastCheckTime int64 // 上次检查时间 + IsFiring bool // 是否正在告警 + LastRecordID int64 // 最后一条告警记录ID +} diff --git a/internal/models/api_key.go b/internal/models/api_key.go new file mode 100644 index 0000000..537a2f6 --- /dev/null +++ b/internal/models/api_key.go @@ -0,0 +1,16 @@ +package models + +// ApiKey API密钥信息 +type ApiKey struct { + ID string `gorm:"primaryKey" json:"id"` // 密钥ID (UUID) + Name string `gorm:"index" json:"name"` // 密钥名称/备注 + Key string `gorm:"uniqueIndex" json:"key"` // API密钥 + Enabled bool `gorm:"index;default:true" json:"enabled"` // 是否启用 + CreatedBy string `gorm:"index" json:"createdBy"` // 创建人ID + CreatedAt int64 `json:"createdAt"` // 创建时间(时间戳毫秒) + UpdatedAt int64 `json:"updatedAt" gorm:"autoUpdateTime:milli"` // 更新时间(时间戳毫秒) +} + +func (ApiKey) TableName() string { + return "api_keys" +} diff --git a/internal/models/audit.go b/internal/models/audit.go new file mode 100644 index 0000000..28affe4 --- /dev/null +++ b/internal/models/audit.go @@ -0,0 +1,17 @@ +package models + +// AuditResult 审计结果模型 +type AuditResult struct { + ID int64 `gorm:"primaryKey;autoIncrement" json:"id"` + AgentID string `gorm:"type:varchar(64);not null;index" json:"agentId"` + Type string `gorm:"type:varchar(32);not null" json:"type"` // vps_audit + Result string `gorm:"type:text;not null" json:"result"` // JSON格式的审计结果 + StartTime int64 `gorm:"not null" json:"startTime"` + EndTime int64 `gorm:"not null" json:"endTime"` + CreatedAt int64 `gorm:"not null" json:"createdAt"` +} + +// TableName 表名 +func (AuditResult) TableName() string { + return "audit_results" +} diff --git a/internal/models/metric.go b/internal/models/metric.go new file mode 100644 index 0000000..68a8e2c --- /dev/null +++ b/internal/models/metric.go @@ -0,0 +1,151 @@ +package models + +// CPUMetric CPU指标 +type CPUMetric struct { + ID uint `gorm:"primaryKey;autoIncrement" json:"id"` + AgentID string `gorm:"index:idx_agent_time" json:"agentId"` // 探针ID + UsagePercent float64 `json:"usagePercent"` // CPU使用率 + LogicalCores int `json:"logicalCores"` // 逻辑核心数 + PhysicalCores int `json:"physicalCores"` // 物理核心数 + ModelName string `json:"modelName"` // CPU型号 + Timestamp int64 `gorm:"index:idx_agent_time;index:idx_time" json:"timestamp"` // 时间戳(毫秒) +} + +func (CPUMetric) TableName() string { + return "cpu_metrics" +} + +// MemoryMetric 内存指标 +type MemoryMetric struct { + ID uint `gorm:"primaryKey;autoIncrement" json:"id"` + AgentID string `gorm:"index:idx_agent_time" json:"agentId"` // 探针ID + Total uint64 `json:"total"` // 总内存(字节) + Used uint64 `json:"used"` // 已使用(字节) + Free uint64 `json:"free"` // 空闲(字节) + UsagePercent float64 `json:"usagePercent"` // 使用率 + Timestamp int64 `gorm:"index:idx_agent_time;index:idx_time" json:"timestamp"` // 时间戳(毫秒) +} + +func (MemoryMetric) TableName() string { + return "memory_metrics" +} + +// DiskMetric 磁盘指标 +type DiskMetric struct { + ID uint `gorm:"primaryKey;autoIncrement" json:"id"` + AgentID string `gorm:"index:idx_agent_time" json:"agentId"` // 探针ID + MountPoint string `json:"mountPoint"` // 挂载点 + Total uint64 `json:"total"` // 总容量(字节) + Used uint64 `json:"used"` // 已使用(字节) + Free uint64 `json:"free"` // 空闲(字节) + UsagePercent float64 `json:"usagePercent"` // 使用率 + Timestamp int64 `gorm:"index:idx_agent_time;index:idx_time" json:"timestamp"` // 时间戳(毫秒) +} + +func (DiskMetric) TableName() string { + return "disk_metrics" +} + +// NetworkMetric 网络指标 +type NetworkMetric struct { + ID uint `gorm:"primaryKey;autoIncrement" json:"id"` + AgentID string `gorm:"index:idx_agent_time" json:"agentId"` // 探针ID + Interface string `json:"interface"` // 网卡名称 + BytesSentRate uint64 `json:"bytesSentRate"` // 发送速率(字节/秒) + BytesRecvRate uint64 `json:"bytesRecvRate"` // 接收速率(字节/秒) + BytesSentTotal uint64 `json:"bytesSentTotal"` // 累计发送字节数 + BytesRecvTotal uint64 `json:"bytesRecvTotal"` // 累计接收字节数 + Timestamp int64 `gorm:"index:idx_agent_time;index:idx_time" json:"timestamp"` // 时间戳(毫秒) +} + +func (NetworkMetric) TableName() string { + return "network_metrics" +} + +// LoadMetric 系统负载指标 +type LoadMetric struct { + ID uint `gorm:"primaryKey;autoIncrement" json:"id"` + AgentID string `gorm:"index:idx_agent_time" json:"agentId"` // 探针ID + Load1 float64 `json:"load1"` // 1分钟负载 + Load5 float64 `json:"load5"` // 5分钟负载 + Load15 float64 `json:"load15"` // 15分钟负载 + Timestamp int64 `gorm:"index:idx_agent_time;index:idx_time" json:"timestamp"` // 时间戳(毫秒) +} + +func (LoadMetric) TableName() string { + return "load_metrics" +} + +// DiskIOMetric 磁盘IO指标 +type DiskIOMetric struct { + ID uint `gorm:"primaryKey;autoIncrement" json:"id"` + AgentID string `gorm:"index:idx_agent_time" json:"agentId"` // 探针ID + Device string `json:"device"` // 设备名称 + ReadCount uint64 `json:"readCount"` // 读取次数 + WriteCount uint64 `json:"writeCount"` // 写入次数 + ReadBytes uint64 `json:"readBytes"` // 读取字节数 + WriteBytes uint64 `json:"writeBytes"` // 写入字节数 + ReadTime uint64 `json:"readTime"` // 读取时间(毫秒) + WriteTime uint64 `json:"writeTime"` // 写入时间(毫秒) + IoTime uint64 `json:"ioTime"` // IO时间(毫秒) + IopsInProgress uint64 `json:"iopsInProgress"` // 正在进行的IO操作数 + Timestamp int64 `gorm:"index:idx_agent_time;index:idx_time" json:"timestamp"` // 时间戳(毫秒) +} + +func (DiskIOMetric) TableName() string { + return "disk_io_metrics" +} + +// GPUMetric GPU指标 +type GPUMetric struct { + ID uint `gorm:"primaryKey;autoIncrement" json:"id"` + AgentID string `gorm:"index:idx_agent_time" json:"agentId"` // 探针ID + Index int `json:"index"` // GPU索引 + Name string `json:"name"` // GPU名称 + Utilization float64 `json:"utilization"` // GPU使用率(%) + MemoryTotal uint64 `json:"memoryTotal"` // 总显存(字节) + MemoryUsed uint64 `json:"memoryUsed"` // 已使用显存(字节) + MemoryFree uint64 `json:"memoryFree"` // 空闲显存(字节) + Temperature float64 `json:"temperature"` // 温度(℃) + PowerDraw float64 `json:"powerDraw"` // 功耗(瓦) + FanSpeed float64 `json:"fanSpeed"` // 风扇转速(%) + PerformanceState string `json:"performanceState"` // 性能状态 + Timestamp int64 `gorm:"index:idx_agent_time;index:idx_time" json:"timestamp"` // 时间戳(毫秒) +} + +func (GPUMetric) TableName() string { + return "gpu_metrics" +} + +// TemperatureMetric 温度指标 +type TemperatureMetric struct { + ID uint `gorm:"primaryKey;autoIncrement" json:"id"` + AgentID string `gorm:"index:idx_agent_time" json:"agentId"` // 探针ID + SensorKey string `json:"sensorKey"` // 传感器标识 + SensorLabel string `json:"sensorLabel"` // 传感器标签 + Temperature float64 `json:"temperature"` // 温度(℃) + Timestamp int64 `gorm:"index:idx_agent_time;index:idx_time" json:"timestamp"` // 时间戳(毫秒) +} + +func (TemperatureMetric) TableName() string { + return "temperature_metrics" +} + +// HostMetric 主机信息指标 +type HostMetric struct { + ID uint `gorm:"primaryKey;autoIncrement" json:"id"` + AgentID string `gorm:"index:idx_agent_time" json:"agentId"` // 探针ID + OS string `json:"os"` // 操作系统 + Platform string `json:"platform"` // 平台 + PlatformVersion string `json:"platformVersion"` // 平台版本 + KernelVersion string `json:"kernelVersion"` // 内核版本 + KernelArch string `json:"kernelArch"` // 内核架构 + Uptime uint64 `json:"uptime"` // 运行时间(秒) + BootTime uint64 `json:"bootTime"` // 启动时间(Unix时间戳-秒) + Procs uint64 `json:"procs"` // 进程数 + Timestamp int64 `gorm:"index:idx_agent_time;index:idx_time" json:"timestamp"` // 时间戳(毫秒) +} + +func (HostMetric) TableName() string { + return "host_metrics" +} diff --git a/internal/models/user.go b/internal/models/user.go new file mode 100644 index 0000000..900bdfb --- /dev/null +++ b/internal/models/user.go @@ -0,0 +1,15 @@ +package models + +// User 用户信息 +type User struct { + ID string `gorm:"primaryKey" json:"id"` // 用户ID (UUID) + Username string `gorm:"uniqueIndex" json:"username"` // 用户名 + Password string `json:"-"` // 密码(加密存储,不返回给前端) + Nickname string `json:"nickname"` // 昵称 + CreatedAt int64 `json:"createdAt"` // 创建时间(时间戳毫秒) + UpdatedAt int64 `json:"updatedAt" gorm:"autoUpdateTime:milli"` // 更新时间(时间戳毫秒) +} + +func (User) TableName() string { + return "users" +} diff --git a/internal/protocol/message.go b/internal/protocol/message.go new file mode 100644 index 0000000..1ab12f2 --- /dev/null +++ b/internal/protocol/message.go @@ -0,0 +1,243 @@ +package protocol + +import "encoding/json" + +// Message WebSocket消息结构 +type Message struct { + Type MessageType `json:"type"` + Data json.RawMessage `json:"data"` +} + +// RegisterRequest 注册请求 +type RegisterRequest struct { + AgentInfo AgentInfo `json:"agentInfo"` + ApiKey string `json:"apiKey"` +} + +// RegisterResponse 注册响应 +type RegisterResponse struct { + AgentID string `json:"agentId"` + Status string `json:"status"` + Message string `json:"message,omitempty"` +} + +// AgentInfo 探针信息 +type AgentInfo struct { + ID string `json:"id"` // 探针唯一标识(持久化) + Name string `json:"name"` // 探针名称 + Hostname string `json:"hostname"` // 主机名 + OS string `json:"os"` // 操作系统 + Arch string `json:"arch"` // 架构 + Version string `json:"version"` // 版本号 +} + +// MetricsWrapper 指标数据包装 +type MetricsWrapper struct { + Type MetricType `json:"type"` + Data json.RawMessage `json:"data"` +} + +type MessageType string + +// 控制消息 +const ( + MessageTypeRegister MessageType = "register" + MessageTypeRegisterAck MessageType = "register_ack" + MessageTypeRegisterErr MessageType = "register_error" + MessageTypeHeartbeat MessageType = "heartbeat" + MessageTypeCommand MessageType = "command" + MessageTypeCommandResp MessageType = "command_response" + // 指标消息 + MessageTypeMetrics MessageType = "metrics" +) + +type MetricType string + +// 消息类型常量 +const ( + MetricTypeCPU MetricType = "cpu" + MetricTypeMemory MetricType = "memory" + MetricTypeDisk MetricType = "disk" + MetricTypeDiskIO MetricType = "disk_io" + MetricTypeNetwork MetricType = "network" + MetricTypeLoad MetricType = "load" + MetricTypeHost MetricType = "host" + MetricTypeGPU MetricType = "gpu" + MetricTypeTemperature MetricType = "temperature" +) + +// CPUData CPU数据 +type CPUData struct { + // 静态信息(不常变化,但每次都发送) + LogicalCores int `json:"logicalCores"` + PhysicalCores int `json:"physicalCores"` + ModelName string `json:"modelName"` + // 动态信息 + UsagePercent float64 `json:"usagePercent"` + PerCore []float64 `json:"perCore,omitempty"` +} + +// MemoryData 内存数据 +type MemoryData struct { + // 静态信息(不常变化,但每次都发送) + Total uint64 `json:"total"` + SwapTotal uint64 `json:"swapTotal,omitempty"` + // 动态信息 + Used uint64 `json:"used"` + Free uint64 `json:"free"` + Available uint64 `json:"available"` + UsagePercent float64 `json:"usagePercent"` + Cached uint64 `json:"cached,omitempty"` + Buffers uint64 `json:"buffers,omitempty"` + SwapUsed uint64 `json:"swapUsed,omitempty"` + SwapFree uint64 `json:"swapFree,omitempty"` +} + +// DiskData 磁盘数据 +type DiskData struct { + MountPoint string `json:"mountPoint"` + Device string `json:"device"` + Fstype string `json:"fstype"` + Total uint64 `json:"total"` + Used uint64 `json:"used"` + Free uint64 `json:"free"` + UsagePercent float64 `json:"usagePercent"` +} + +// DiskIOData 磁盘IO数据 +type DiskIOData struct { + Device string `json:"device"` + ReadCount uint64 `json:"readCount"` + WriteCount uint64 `json:"writeCount"` + ReadBytes uint64 `json:"readBytes"` + WriteBytes uint64 `json:"writeBytes"` + ReadTime uint64 `json:"readTime"` + WriteTime uint64 `json:"writeTime"` + IoTime uint64 `json:"ioTime"` + IopsInProgress uint64 `json:"iopsInProgress"` +} + +// NetworkData 网络数据 +type NetworkData struct { + Interface string `json:"interface"` + MacAddress string `json:"macAddress,omitempty"` + Addrs []string `json:"addrs,omitempty"` + BytesSentRate uint64 `json:"bytesSentRate"` // 发送速率(字节/秒) + BytesRecvRate uint64 `json:"bytesRecvRate"` // 接收速率(字节/秒) + BytesSentTotal uint64 `json:"bytesSentTotal"` // 累计发送字节数 + BytesRecvTotal uint64 `json:"bytesRecvTotal"` // 累计接收字节数 +} + +// LoadData 系统负载数据 +type LoadData struct { + Load1 float64 `json:"load1"` + Load5 float64 `json:"load5"` + Load15 float64 `json:"load15"` +} + +// HostInfoData 主机信息 +type HostInfoData struct { + Hostname string `json:"hostname"` + Uptime uint64 `json:"uptime"` + BootTime uint64 `json:"bootTime"` + Procs uint64 `json:"procs"` + OS string `json:"os"` + Platform string `json:"platform"` + PlatformFamily string `json:"platformFamily"` + PlatformVersion string `json:"platformVersion"` + KernelVersion string `json:"kernelVersion"` + KernelArch string `json:"kernelArch"` + VirtualizationSystem string `json:"virtualizationSystem,omitempty"` + VirtualizationRole string `json:"virtualizationRole,omitempty"` +} + +// GPUData GPU数据 +type GPUData struct { + Index int `json:"index"` + Name string `json:"name"` + UUID string `json:"uuid,omitempty"` + Temperature float64 `json:"temperature,omitempty"` + Utilization float64 `json:"utilization,omitempty"` + MemoryTotal uint64 `json:"memoryTotal,omitempty"` + MemoryUsed uint64 `json:"memoryUsed,omitempty"` + MemoryFree uint64 `json:"memoryFree,omitempty"` + PowerUsage float64 `json:"powerUsage,omitempty"` + FanSpeed float64 `json:"fanSpeed,omitempty"` +} + +// TemperatureData 温度数据 +type TemperatureData struct { + SensorKey string `json:"sensorKey"` + Temperature float64 `json:"temperature"` + High float64 `json:"high,omitempty"` + Critical float64 `json:"critical,omitempty"` +} + +// CommandRequest 指令请求 +type CommandRequest struct { + ID string `json:"id"` // 指令ID + Type string `json:"type"` // 指令类型: vps_audit + Args string `json:"args,omitempty"` +} + +// CommandResponse 指令响应 +type CommandResponse struct { + ID string `json:"id"` // 指令ID + Type string `json:"type"` // 指令类型 + Status string `json:"status"` // running/success/error + Error string `json:"error,omitempty"` // 错误信息 + Result string `json:"result,omitempty"` // 结果数据(JSON字符串) +} + +// VPSAuditResult VPS安全审计结果 +type VPSAuditResult struct { + // 系统信息 + SystemInfo SystemInfo `json:"systemInfo"` + // 安全检查结果 + SecurityChecks []SecurityCheck `json:"securityChecks"` + // 审计开始时间 + StartTime int64 `json:"startTime"` + // 审计结束时间 + EndTime int64 `json:"endTime"` + // 风险评分 (0-100) + RiskScore int `json:"riskScore"` + // 威胁等级: low/medium/high/critical + ThreatLevel string `json:"threatLevel"` + // 修复建议 + Recommendations []string `json:"recommendations,omitempty"` +} + +// SystemInfo 系统信息 +type SystemInfo struct { + Hostname string `json:"hostname"` + OS string `json:"os"` + KernelVersion string `json:"kernelVersion"` + Uptime uint64 `json:"uptime"` + PublicIP string `json:"publicIP,omitempty"` +} + +// SecurityCheck 安全检查项 +type SecurityCheck struct { + Category string `json:"category"` // 检查类别 + Status string `json:"status"` // pass/fail/warn/skip + Message string `json:"message"` // 检查消息 + Details []SecurityCheckSub `json:"details,omitempty"` +} + +// SecurityCheckSub 安全检查子项 +type SecurityCheckSub struct { + Name string `json:"name"` // 子检查名称 + Status string `json:"status"` // pass/fail/warn/skip + Message string `json:"message"` // 检查消息 + Evidence *Evidence `json:"evidence,omitempty"` // 证据信息 +} + +// Evidence 安全事件证据 +type Evidence struct { + FileHash string `json:"fileHash,omitempty"` // 文件SHA256哈希 + ProcessTree []string `json:"processTree,omitempty"` // 进程树 + FilePath string `json:"filePath,omitempty"` // 文件路径 + Timestamp int64 `json:"timestamp,omitempty"` // 时间戳(毫秒) + NetworkConn string `json:"networkConn,omitempty"` // 网络连接信息 + RiskLevel string `json:"riskLevel,omitempty"` // 风险等级: low/medium/high +} diff --git a/internal/providers.go b/internal/providers.go new file mode 100644 index 0000000..a4adc44 --- /dev/null +++ b/internal/providers.go @@ -0,0 +1,65 @@ +package internal + +import ( + "github.com/dushixiang/pika/internal/config" + "github.com/dushixiang/pika/internal/handler" + "github.com/dushixiang/pika/internal/repo" + "github.com/dushixiang/pika/internal/service" + "github.com/dushixiang/pika/internal/websocket" + "go.uber.org/zap" + "gorm.io/gorm" +) + +// provideAccountService 提供AccountService +func provideAccountService(logger *zap.Logger, userService *service.UserService, cfg *config.AppConfig) *service.AccountService { + return service.NewAccountService(logger, userService, cfg.JWT.Secret, cfg.JWT.ExpiresHours) +} + +// provideApiKeyRepo 提供ApiKeyRepo +func provideApiKeyRepo(db *gorm.DB) *repo.ApiKeyRepo { + return repo.NewApiKeyRepo(db) +} + +// provideApiKeyService 提供ApiKeyService +func provideApiKeyService(logger *zap.Logger, apiKeyRepo *repo.ApiKeyRepo) *service.ApiKeyService { + return service.NewApiKeyService(logger, apiKeyRepo) +} + +// provideApiKeyHandler 提供ApiKeyHandler +func provideApiKeyHandler(logger *zap.Logger, apiKeyService *service.ApiKeyService) *handler.ApiKeyHandler { + return handler.NewApiKeyHandler(logger, apiKeyService) +} + +// provideAgentService 提供AgentService +func provideAgentService(logger *zap.Logger, agentRepo *repo.AgentRepo, metricRepo *repo.MetricRepo, apiKeyService *service.ApiKeyService) *service.AgentService { + return service.NewAgentService(logger, agentRepo, metricRepo, apiKeyService) +} + +// provideAgentHandler 提供AgentHandler +func provideAgentHandler(logger *zap.Logger, agentService *service.AgentService, wsManager *websocket.Manager, cfg *config.AppConfig) *handler.AgentHandler { + return handler.NewAgentHandler( + logger, + agentService, + wsManager, + ) +} + +// provideAlertRepo 提供AlertRepo +func provideAlertRepo(db *gorm.DB) *repo.AlertRepo { + return repo.NewAlertRepo(db) +} + +// provideNotifier 提供Notifier +func provideNotifier(logger *zap.Logger) *service.Notifier { + return service.NewNotifier(logger) +} + +// provideAlertService 提供AlertService +func provideAlertService(alertRepo *repo.AlertRepo, agentRepo *repo.AgentRepo, notifier *service.Notifier, logger *zap.Logger) *service.AlertService { + return service.NewAlertService(alertRepo, agentRepo, notifier, logger) +} + +// provideAlertHandler 提供AlertHandler +func provideAlertHandler(logger *zap.Logger, alertService *service.AlertService) *handler.AlertHandler { + return handler.NewAlertHandler(logger, alertService) +} diff --git a/internal/repo/agent_repo.go b/internal/repo/agent_repo.go new file mode 100644 index 0000000..cff9e1a --- /dev/null +++ b/internal/repo/agent_repo.go @@ -0,0 +1,142 @@ +package repo + +import ( + "context" + "time" + + "github.com/dushixiang/pika/internal/models" + "github.com/go-orz/orz" + "gorm.io/gorm" +) + +type AgentRepo struct { + orz.Repository[models.Agent, string] + db *gorm.DB +} + +func NewAgentRepo(db *gorm.DB) *AgentRepo { + return &AgentRepo{ + Repository: orz.NewRepository[models.Agent, string](db), + db: db, + } +} + +// UpdateStatus 更新探针状态 +func (r *AgentRepo) UpdateStatus(ctx context.Context, agentID string, status int) error { + return r.db.WithContext(ctx). + Model(&models.Agent{}). + Where("id = ?", agentID). + Updates(map[string]interface{}{ + "status": status, + "last_seen_at": time.Now().UnixMilli(), + }).Error +} + +// FindOnlineAgents 查找所有在线探针 +func (r *AgentRepo) FindOnlineAgents(ctx context.Context) ([]models.Agent, error) { + var agents []models.Agent + err := r.db.WithContext(ctx). + Where("status = ?", 1). + Find(&agents).Error + return agents, err +} + +// FindByIP 根据IP查找探针 +func (r *AgentRepo) FindByIP(ctx context.Context, ip string) (*models.Agent, error) { + var agent models.Agent + err := r.db.WithContext(ctx). + Where("ip = ?", ip). + First(&agent).Error + if err != nil { + return nil, err + } + return &agent, nil +} + +// FindByHostname 根据主机名查找探针 +func (r *AgentRepo) FindByHostname(ctx context.Context, hostname string) (*models.Agent, error) { + var agent models.Agent + err := r.db.WithContext(ctx). + Where("hostname = ?", hostname). + First(&agent).Error + if err != nil { + return nil, err + } + return &agent, nil +} + +// FindByHostnameAndIP 根据主机名和IP查找探针 +func (r *AgentRepo) FindByHostnameAndIP(ctx context.Context, hostname, ip string) (*models.Agent, error) { + var agent models.Agent + err := r.db.WithContext(ctx). + Where("hostname = ? AND ip = ?", hostname, ip). + First(&agent).Error + if err != nil { + return nil, err + } + return &agent, nil +} + +// SaveAuditResult 保存审计结果 +func (r *AgentRepo) SaveAuditResult(ctx context.Context, audit *models.AuditResult) error { + return r.db.WithContext(ctx).Create(audit).Error +} + +// GetLatestAuditResult 获取最新的审计结果 +func (r *AgentRepo) GetLatestAuditResult(ctx context.Context, agentID string) (*models.AuditResult, error) { + var audit models.AuditResult + err := r.db.WithContext(ctx). + Where("agent_id = ?", agentID). + Order("created_at DESC"). + First(&audit).Error + if err != nil { + return nil, err + } + return &audit, nil +} + +// ListAuditResults 获取审计结果列表 +func (r *AgentRepo) ListAuditResults(ctx context.Context, agentID string) ([]models.AuditResult, error) { + var audits []models.AuditResult + err := r.db.WithContext(ctx). + Where("agent_id = ?", agentID). + Order("created_at DESC"). + Limit(50). + Find(&audits).Error + return audits, err +} + +// UpdateName 更新探针名称 +func (r *AgentRepo) UpdateName(ctx context.Context, agentID string, name string) error { + return r.db.WithContext(ctx). + Model(&models.Agent{}). + Where("id = ?", agentID). + Update("name", name).Error +} + +// UpdateInfo 更新探针信息(名称、平台、位置、到期时间) +func (r *AgentRepo) UpdateInfo(ctx context.Context, agentID string, updates map[string]interface{}) error { + return r.db.WithContext(ctx). + Model(&models.Agent{}). + Where("id = ?", agentID). + Updates(updates).Error +} + +// GetStatistics 获取探针统计数据 +func (r *AgentRepo) GetStatistics(ctx context.Context) (total int64, online int64, err error) { + // 获取总数 + err = r.db.WithContext(ctx). + Model(&models.Agent{}). + Count(&total).Error + if err != nil { + return 0, 0, err + } + + // 获取在线数量 + err = r.db.WithContext(ctx). + Model(&models.Agent{}). + Where("status = ?", 1). + Count(&online).Error + + return total, online, err +} diff --git a/internal/repo/alert_repo.go b/internal/repo/alert_repo.go new file mode 100644 index 0000000..bcfd0ef --- /dev/null +++ b/internal/repo/alert_repo.go @@ -0,0 +1,183 @@ +package repo + +import ( + "context" + "encoding/json" + + "github.com/dushixiang/pika/internal/models" + "github.com/go-orz/orz" + "gorm.io/gorm" +) + +type AlertRepo struct { + orz.Repository[models.AlertConfig, string] + db *gorm.DB +} + +func NewAlertRepo(db *gorm.DB) *AlertRepo { + return &AlertRepo{ + Repository: orz.NewRepository[models.AlertConfig, string](db), + db: db, + } +} + +// FindByAgentID 根据探针ID查找告警配置 +func (r *AlertRepo) FindByAgentID(ctx context.Context, agentID string) ([]models.AlertConfig, error) { + var configs []models.AlertConfig + err := r.db.WithContext(ctx). + Where("agent_id = ?", agentID). + Find(&configs).Error + + // 处理 EmailAddresses 和 AgentIDs 的反序列化 + for i := range configs { + if configs[i].Notification.EmailAddressesStr != "" { + _ = json.Unmarshal([]byte(configs[i].Notification.EmailAddressesStr), &configs[i].Notification.EmailAddresses) + } + if configs[i].AgentIDsStr != "" { + _ = json.Unmarshal([]byte(configs[i].AgentIDsStr), &configs[i].AgentIDs) + } + } + + return configs, err +} + +// FindEnabledByAgentID 根据探针ID查找已启用的告警配置 +func (r *AlertRepo) FindEnabledByAgentID(ctx context.Context, agentID string) ([]models.AlertConfig, error) { + var configs []models.AlertConfig + err := r.db.WithContext(ctx). + Where("agent_id = ? AND enabled = ?", agentID, true). + Find(&configs).Error + + // 处理 EmailAddresses 和 AgentIDs 的反序列化 + for i := range configs { + if configs[i].Notification.EmailAddressesStr != "" { + _ = json.Unmarshal([]byte(configs[i].Notification.EmailAddressesStr), &configs[i].Notification.EmailAddresses) + } + if configs[i].AgentIDsStr != "" { + _ = json.Unmarshal([]byte(configs[i].AgentIDsStr), &configs[i].AgentIDs) + } + } + + return configs, err +} + +// FindAllEnabled 查找所有已启用的告警配置 +func (r *AlertRepo) FindAllEnabled(ctx context.Context) ([]models.AlertConfig, error) { + var configs []models.AlertConfig + err := r.db.WithContext(ctx). + Where("enabled = ?", true). + Find(&configs).Error + + // 处理 EmailAddresses 和 AgentIDs 的反序列化 + for i := range configs { + if configs[i].Notification.EmailAddressesStr != "" { + _ = json.Unmarshal([]byte(configs[i].Notification.EmailAddressesStr), &configs[i].Notification.EmailAddresses) + } + if configs[i].AgentIDsStr != "" { + _ = json.Unmarshal([]byte(configs[i].AgentIDsStr), &configs[i].AgentIDs) + } + } + + return configs, err +} + +// CreateAlertConfig 创建告警配置 +func (r *AlertRepo) CreateAlertConfig(ctx context.Context, config *models.AlertConfig) error { + // 序列化 EmailAddresses 和 AgentIDs + if len(config.Notification.EmailAddresses) > 0 { + data, _ := json.Marshal(config.Notification.EmailAddresses) + config.Notification.EmailAddressesStr = string(data) + } + if len(config.AgentIDs) > 0 { + data, _ := json.Marshal(config.AgentIDs) + config.AgentIDsStr = string(data) + } + + return r.db.WithContext(ctx).Create(config).Error +} + +// UpdateAlertConfig 更新告警配置 +func (r *AlertRepo) UpdateAlertConfig(ctx context.Context, config *models.AlertConfig) error { + // 序列化 EmailAddresses 和 AgentIDs + if len(config.Notification.EmailAddresses) > 0 { + data, _ := json.Marshal(config.Notification.EmailAddresses) + config.Notification.EmailAddressesStr = string(data) + } + if len(config.AgentIDs) > 0 { + data, _ := json.Marshal(config.AgentIDs) + config.AgentIDsStr = string(data) + } + + return r.db.WithContext(ctx).Save(config).Error +} + +// DeleteAlertConfig 删除告警配置 +func (r *AlertRepo) DeleteAlertConfig(ctx context.Context, id string) error { + return r.db.WithContext(ctx).Delete(&models.AlertConfig{}, "id = ?", id).Error +} + +// GetAlertConfig 获取告警配置 +func (r *AlertRepo) GetAlertConfig(ctx context.Context, id string) (*models.AlertConfig, error) { + var config models.AlertConfig + err := r.db.WithContext(ctx).Where("id = ?", id).First(&config).Error + if err != nil { + return nil, err + } + + // 处理 EmailAddresses 和 AgentIDs 的反序列化 + if config.Notification.EmailAddressesStr != "" { + _ = json.Unmarshal([]byte(config.Notification.EmailAddressesStr), &config.Notification.EmailAddresses) + } + if config.AgentIDsStr != "" { + _ = json.Unmarshal([]byte(config.AgentIDsStr), &config.AgentIDs) + } + + return &config, nil +} + +// CreateAlertRecord 创建告警记录 +func (r *AlertRepo) CreateAlertRecord(ctx context.Context, record *models.AlertRecord) error { + return r.db.WithContext(ctx).Create(record).Error +} + +// UpdateAlertRecord 更新告警记录 +func (r *AlertRepo) UpdateAlertRecord(ctx context.Context, record *models.AlertRecord) error { + return r.db.WithContext(ctx).Save(record).Error +} + +// ListAlertRecords 列出告警记录 +func (r *AlertRepo) ListAlertRecords(ctx context.Context, agentID string, limit int, offset int) ([]models.AlertRecord, int64, error) { + var records []models.AlertRecord + var total int64 + + query := r.db.WithContext(ctx).Model(&models.AlertRecord{}) + + if agentID != "" { + query = query.Where("agent_id = ?", agentID) + } + + err := query.Count(&total).Error + if err != nil { + return nil, 0, err + } + + err = query.Order("fired_at DESC"). + Limit(limit). + Offset(offset). + Find(&records).Error + + return records, total, err +} + +// GetLatestAlertRecord 获取最新的告警记录 +func (r *AlertRepo) GetLatestAlertRecord(ctx context.Context, configID string, alertType string) (*models.AlertRecord, error) { + var record models.AlertRecord + err := r.db.WithContext(ctx). + Where("config_id = ? AND alert_type = ? AND status = ?", configID, alertType, "firing"). + Order("fired_at DESC"). + First(&record).Error + if err != nil { + return nil, err + } + return &record, nil +} diff --git a/internal/repo/api_key_repo.go b/internal/repo/api_key_repo.go new file mode 100644 index 0000000..ea7a807 --- /dev/null +++ b/internal/repo/api_key_repo.go @@ -0,0 +1,109 @@ +package repo + +import ( + "context" + + "github.com/dushixiang/pika/internal/models" + "github.com/go-orz/orz" + "gorm.io/gorm" +) + +type ApiKeyRepo struct { + orz.Repository[models.ApiKey, string] + db *gorm.DB +} + +func NewApiKeyRepo(db *gorm.DB) *ApiKeyRepo { + return &ApiKeyRepo{ + Repository: orz.NewRepository[models.ApiKey, string](db), + db: db, + } +} + +// FindByKey 根据密钥查找 +func (r *ApiKeyRepo) FindByKey(ctx context.Context, key string) (*models.ApiKey, error) { + var apiKey models.ApiKey + err := r.db.WithContext(ctx). + Where("key = ?", key). + First(&apiKey).Error + if err != nil { + return nil, err + } + return &apiKey, nil +} + +// FindEnabledByKey 根据密钥查找启用的密钥 +func (r *ApiKeyRepo) FindEnabledByKey(ctx context.Context, key string) (*models.ApiKey, error) { + var apiKey models.ApiKey + err := r.db.WithContext(ctx). + Where("key = ? AND enabled = ?", key, true). + First(&apiKey).Error + if err != nil { + return nil, err + } + return &apiKey, nil +} + +// ListByUser 列出用户创建的所有密钥 +func (r *ApiKeyRepo) ListByUser(ctx context.Context, userID string, page, pageSize int) ([]models.ApiKey, int64, error) { + var apiKeys []models.ApiKey + var total int64 + + offset := (page - 1) * pageSize + + err := r.db.WithContext(ctx). + Model(&models.ApiKey{}). + Where("created_by = ?", userID). + Count(&total).Error + if err != nil { + return nil, 0, err + } + + err = r.db.WithContext(ctx). + Where("created_by = ?", userID). + Order("created_at DESC"). + Limit(pageSize). + Offset(offset). + Find(&apiKeys).Error + + return apiKeys, total, err +} + +// ListAll 列出所有密钥 +func (r *ApiKeyRepo) ListAll(ctx context.Context, page, pageSize int) ([]models.ApiKey, int64, error) { + var apiKeys []models.ApiKey + var total int64 + + offset := (page - 1) * pageSize + + err := r.db.WithContext(ctx). + Model(&models.ApiKey{}). + Count(&total).Error + if err != nil { + return nil, 0, err + } + + err = r.db.WithContext(ctx). + Order("created_at DESC"). + Limit(pageSize). + Offset(offset). + Find(&apiKeys).Error + + return apiKeys, total, err +} + +// UpdateName 更新密钥名称 +func (r *ApiKeyRepo) UpdateName(ctx context.Context, id, name string) error { + return r.db.WithContext(ctx). + Model(&models.ApiKey{}). + Where("id = ?", id). + Update("name", name).Error +} + +// UpdateEnabled 更新密钥启用状态 +func (r *ApiKeyRepo) UpdateEnabled(ctx context.Context, id string, enabled bool) error { + return r.db.WithContext(ctx). + Model(&models.ApiKey{}). + Where("id = ?", id). + Update("enabled", enabled).Error +} diff --git a/internal/repo/metric_repo.go b/internal/repo/metric_repo.go new file mode 100644 index 0000000..0f71c39 --- /dev/null +++ b/internal/repo/metric_repo.go @@ -0,0 +1,386 @@ +package repo + +import ( + "context" + + "github.com/dushixiang/pika/internal/models" + "gorm.io/gorm" +) + +type MetricRepo struct { + db *gorm.DB +} + +func NewMetricRepo(db *gorm.DB) *MetricRepo { + return &MetricRepo{ + db: db, + } +} + +// SaveCPUMetric 保存CPU指标 +func (r *MetricRepo) SaveCPUMetric(ctx context.Context, metric *models.CPUMetric) error { + return r.db.WithContext(ctx).Create(metric).Error +} + +// SaveMemoryMetric 保存内存指标 +func (r *MetricRepo) SaveMemoryMetric(ctx context.Context, metric *models.MemoryMetric) error { + return r.db.WithContext(ctx).Create(metric).Error +} + +// SaveDiskMetric 保存磁盘指标 +func (r *MetricRepo) SaveDiskMetric(ctx context.Context, metric *models.DiskMetric) error { + return r.db.WithContext(ctx).Create(metric).Error +} + +// SaveNetworkMetric 保存网络指标 +func (r *MetricRepo) SaveNetworkMetric(ctx context.Context, metric *models.NetworkMetric) error { + return r.db.WithContext(ctx).Create(metric).Error +} + +// SaveLoadMetric 保存负载指标 +func (r *MetricRepo) SaveLoadMetric(ctx context.Context, metric *models.LoadMetric) error { + return r.db.WithContext(ctx).Create(metric).Error +} + +// SaveDiskIOMetric 保存磁盘IO指标 +func (r *MetricRepo) SaveDiskIOMetric(ctx context.Context, metric *models.DiskIOMetric) error { + return r.db.WithContext(ctx).Create(metric).Error +} + +// SaveGPUMetric 保存GPU指标 +func (r *MetricRepo) SaveGPUMetric(ctx context.Context, metric *models.GPUMetric) error { + return r.db.WithContext(ctx).Create(metric).Error +} + +// SaveTemperatureMetric 保存温度指标 +func (r *MetricRepo) SaveTemperatureMetric(ctx context.Context, metric *models.TemperatureMetric) error { + return r.db.WithContext(ctx).Create(metric).Error +} + +// SaveHostMetric 保存主机信息指标(只保留最新的一条记录) +func (r *MetricRepo) SaveHostMetric(ctx context.Context, metric *models.HostMetric) error { + // 使用事务确保原子性 + return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + // 先删除该 agent 的所有旧记录 + if err := tx.Where("agent_id = ?", metric.AgentID).Delete(&models.HostMetric{}).Error; err != nil { + return err + } + // 插入新记录 + return tx.Create(metric).Error + }) +} + +// GetLatestHostMetric 获取最新的主机信息 +func (r *MetricRepo) GetLatestHostMetric(ctx context.Context, agentID string) (*models.HostMetric, error) { + var metric models.HostMetric + err := r.db.WithContext(ctx). + Where("agent_id = ?", agentID). + Order("timestamp DESC"). + First(&metric).Error + if err != nil { + return nil, err + } + return &metric, nil +} + +// DeleteOldMetrics 删除指定时间之前的所有指标数据 +func (r *MetricRepo) DeleteOldMetrics(ctx context.Context, beforeTimestamp int64) error { + // 批量大小 + batchSize := 1000 + + // 定义要清理的表(Host 信息只保留最新的,不需要清理) + tables := []interface{}{ + &models.CPUMetric{}, + &models.MemoryMetric{}, + &models.DiskMetric{}, + &models.NetworkMetric{}, + &models.LoadMetric{}, + &models.DiskIOMetric{}, + &models.GPUMetric{}, + &models.TemperatureMetric{}, + } + + // 对每个表进行分批删除 + for _, table := range tables { + for { + // 分批删除,避免长事务 + result := r.db.WithContext(ctx). + Where("timestamp < ?", beforeTimestamp). + Limit(batchSize). + Delete(table) + + if result.Error != nil { + return result.Error + } + + // 如果删除的行数少于批量大小,说明已经删除完毕 + if result.RowsAffected < int64(batchSize) { + break + } + } + } + + return nil +} + +// AggregatedCPUMetric CPU聚合指标 +type AggregatedCPUMetric struct { + Timestamp int64 `json:"timestamp"` + AvgUsage float64 `json:"avgUsage"` + MinUsage float64 `json:"minUsage"` + MaxUsage float64 `json:"maxUsage"` + LogicalCores int `json:"logicalCores"` +} + +// GetCPUMetrics 获取聚合后的CPU指标(始终返回聚合数据) +// interval: 聚合间隔,单位秒(如:60表示1分钟,3600表示1小时) +func (r *MetricRepo) GetCPUMetrics(ctx context.Context, agentID string, start, end int64, interval int) ([]AggregatedCPUMetric, error) { + var metrics []AggregatedCPUMetric + + query := ` + SELECT + CAST(FLOOR(timestamp / ?) * ? AS BIGINT) as timestamp, + AVG(usage_percent) as avg_usage, + MIN(usage_percent) as min_usage, + MAX(usage_percent) as max_usage, + MAX(logical_cores) as logical_cores + FROM cpu_metrics + WHERE agent_id = ? AND timestamp >= ? AND timestamp <= ? + GROUP BY 1 + ORDER BY timestamp ASC + ` + + intervalMs := int64(interval * 1000) + err := r.db.WithContext(ctx). + Raw(query, intervalMs, intervalMs, agentID, start, end). + Scan(&metrics).Error + + return metrics, err +} + +// AggregatedMemoryMetric 内存聚合指标 +type AggregatedMemoryMetric struct { + Timestamp int64 `json:"timestamp"` + AvgUsage float64 `json:"avgUsage"` + MinUsage float64 `json:"minUsage"` + MaxUsage float64 `json:"maxUsage"` + Total uint64 `json:"total"` +} + +// GetMemoryMetrics 获取聚合后的内存指标(始终返回聚合数据) +func (r *MetricRepo) GetMemoryMetrics(ctx context.Context, agentID string, start, end int64, interval int) ([]AggregatedMemoryMetric, error) { + var metrics []AggregatedMemoryMetric + + query := ` + SELECT + CAST(FLOOR(timestamp / ?) * ? AS BIGINT) as timestamp, + AVG(usage_percent) as avg_usage, + MIN(usage_percent) as min_usage, + MAX(usage_percent) as max_usage, + MAX(total) as total + FROM memory_metrics + WHERE agent_id = ? AND timestamp >= ? AND timestamp <= ? + GROUP BY 1 + ORDER BY timestamp ASC + ` + + intervalMs := int64(interval * 1000) + err := r.db.WithContext(ctx). + Raw(query, intervalMs, intervalMs, agentID, start, end). + Scan(&metrics).Error + + return metrics, err +} + +// AggregatedDiskMetric 磁盘聚合指标 +type AggregatedDiskMetric struct { + Timestamp int64 `json:"timestamp"` + MountPoint string `json:"mountPoint"` + AvgUsage float64 `json:"avgUsage"` + MinUsage float64 `json:"minUsage"` + MaxUsage float64 `json:"maxUsage"` + Total uint64 `json:"total"` +} + +// GetDiskMetrics 获取聚合后的磁盘指标(始终返回聚合数据) +func (r *MetricRepo) GetDiskMetrics(ctx context.Context, agentID string, start, end int64, interval int) ([]AggregatedDiskMetric, error) { + var metrics []AggregatedDiskMetric + + query := ` + SELECT + CAST(FLOOR(timestamp / ?) * ? AS BIGINT) as timestamp, + mount_point, + AVG(usage_percent) as avg_usage, + MIN(usage_percent) as min_usage, + MAX(usage_percent) as max_usage, + MAX(total) as total + FROM disk_metrics + WHERE agent_id = ? AND timestamp >= ? AND timestamp <= ? + GROUP BY 1, mount_point + ORDER BY timestamp ASC, mount_point + ` + + intervalMs := int64(interval * 1000) + err := r.db.WithContext(ctx). + Raw(query, intervalMs, intervalMs, agentID, start, end). + Scan(&metrics).Error + + return metrics, err +} + +// AggregatedNetworkMetric 网络聚合指标 +type AggregatedNetworkMetric struct { + Timestamp int64 `json:"timestamp"` + AvgSentRate float64 `json:"avgSentRate"` + AvgRecvRate float64 `json:"avgRecvRate"` + MaxSentRate uint64 `json:"maxSentRate"` + MaxRecvRate uint64 `json:"maxRecvRate"` +} + +// GetNetworkMetrics 获取聚合后的网络指标(合并所有网卡接口) +func (r *MetricRepo) GetNetworkMetrics(ctx context.Context, agentID string, start, end int64, interval int) ([]AggregatedNetworkMetric, error) { + var metrics []AggregatedNetworkMetric + + query := ` + SELECT + CAST(FLOOR(timestamp / ?) * ? AS BIGINT) as timestamp, + AVG(bytes_sent_rate) as avg_sent_rate, + AVG(bytes_recv_rate) as avg_recv_rate, + MAX(bytes_sent_rate) as max_sent_rate, + MAX(bytes_recv_rate) as max_recv_rate + FROM network_metrics + WHERE agent_id = ? AND timestamp >= ? AND timestamp <= ? + GROUP BY 1 + ORDER BY timestamp ASC + ` + + intervalMs := int64(interval * 1000) + err := r.db.WithContext(ctx). + Raw(query, intervalMs, intervalMs, agentID, start, end). + Scan(&metrics).Error + + return metrics, err +} + +// AggregatedLoadMetric 负载聚合指标 +type AggregatedLoadMetric struct { + Timestamp int64 `json:"timestamp"` + AvgLoad1 float64 `json:"avgLoad1"` + AvgLoad5 float64 `json:"avgLoad5"` + AvgLoad15 float64 `json:"avgLoad15"` + MaxLoad1 float64 `json:"maxLoad1"` + MaxLoad5 float64 `json:"maxLoad5"` + MaxLoad15 float64 `json:"maxLoad15"` +} + +// GetLoadMetrics 获取聚合后的负载指标(始终返回聚合数据) +func (r *MetricRepo) GetLoadMetrics(ctx context.Context, agentID string, start, end int64, interval int) ([]AggregatedLoadMetric, error) { + var metrics []AggregatedLoadMetric + + query := ` + SELECT + CAST(FLOOR(timestamp / ?) * ? AS BIGINT) as timestamp, + AVG(load1) as avg_load1, + AVG(load5) as avg_load5, + AVG(load15) as avg_load15, + MAX(load1) as max_load1, + MAX(load5) as max_load5, + MAX(load15) as max_load15 + FROM load_metrics + WHERE agent_id = ? AND timestamp >= ? AND timestamp <= ? + GROUP BY 1 + ORDER BY timestamp ASC + ` + + intervalMs := int64(interval * 1000) + err := r.db.WithContext(ctx). + Raw(query, intervalMs, intervalMs, agentID, start, end). + Scan(&metrics).Error + + return metrics, err +} + +// GetLatestCPUMetric 获取最新的CPU指标 +func (r *MetricRepo) GetLatestCPUMetric(ctx context.Context, agentID string) (*models.CPUMetric, error) { + var metric models.CPUMetric + err := r.db.WithContext(ctx). + Where("agent_id = ?", agentID). + Order("timestamp DESC"). + First(&metric).Error + if err != nil { + return nil, err + } + return &metric, nil +} + +// GetLatestMemoryMetric 获取最新的内存指标 +func (r *MetricRepo) GetLatestMemoryMetric(ctx context.Context, agentID string) (*models.MemoryMetric, error) { + var metric models.MemoryMetric + err := r.db.WithContext(ctx). + Where("agent_id = ?", agentID). + Order("timestamp DESC"). + First(&metric).Error + if err != nil { + return nil, err + } + return &metric, nil +} + +// GetLatestDiskMetrics 获取最新的磁盘指标(所有挂载点) +func (r *MetricRepo) GetLatestDiskMetrics(ctx context.Context, agentID string) ([]models.DiskMetric, error) { + // 先获取最新时间戳 + var latestTimestamp int64 + err := r.db.WithContext(ctx). + Model(&models.DiskMetric{}). + Where("agent_id = ?", agentID). + Select("MAX(timestamp)"). + Scan(&latestTimestamp).Error + + if err != nil { + return nil, err + } + + // 获取该时间戳的所有磁盘数据 + var metrics []models.DiskMetric + err = r.db.WithContext(ctx). + Where("agent_id = ? AND timestamp = ?", agentID, latestTimestamp). + Find(&metrics).Error + + return metrics, err +} + +// GetLatestNetworkMetrics 获取最新的网络指标(所有网卡) +func (r *MetricRepo) GetLatestNetworkMetrics(ctx context.Context, agentID string) ([]models.NetworkMetric, error) { + // 先获取最新时间戳 + var latestTimestamp int64 + err := r.db.WithContext(ctx). + Model(&models.NetworkMetric{}). + Where("agent_id = ?", agentID). + Select("MAX(timestamp)"). + Scan(&latestTimestamp).Error + + if err != nil { + return nil, err + } + + // 获取该时间戳的所有网络数据 + var metrics []models.NetworkMetric + err = r.db.WithContext(ctx). + Where("agent_id = ? AND timestamp = ?", agentID, latestTimestamp). + Find(&metrics).Error + + return metrics, err +} + +// GetLatestLoadMetric 获取最新的负载指标 +func (r *MetricRepo) GetLatestLoadMetric(ctx context.Context, agentID string) (*models.LoadMetric, error) { + var metric models.LoadMetric + err := r.db.WithContext(ctx). + Where("agent_id = ?", agentID). + Order("timestamp DESC"). + First(&metric).Error + if err != nil { + return nil, err + } + return &metric, nil +} diff --git a/internal/repo/user_repo.go b/internal/repo/user_repo.go new file mode 100644 index 0000000..29ee49c --- /dev/null +++ b/internal/repo/user_repo.go @@ -0,0 +1,81 @@ +package repo + +import ( + "context" + + "github.com/dushixiang/pika/internal/models" + "github.com/go-orz/orz" + "gorm.io/gorm" +) + +type UserRepo struct { + orz.Repository[models.User, string] + db *gorm.DB +} + +func NewUserRepo(db *gorm.DB) *UserRepo { + return &UserRepo{ + Repository: orz.NewRepository[models.User, string](db), + db: db, + } +} + +// FindByUsername 根据用户名查找用户 +func (r *UserRepo) FindByUsername(ctx context.Context, username string) (*models.User, error) { + var user models.User + err := r.db.WithContext(ctx). + Where("username = ?", username). + First(&user).Error + if err != nil { + return nil, err + } + return &user, nil +} + +// FindByEmail 根据邮箱查找用户 +func (r *UserRepo) FindByEmail(ctx context.Context, email string) (*models.User, error) { + var user models.User + err := r.db.WithContext(ctx). + Where("email = ?", email). + First(&user).Error + if err != nil { + return nil, err + } + return &user, nil +} + +// UpdatePassword 更新密码 +func (r *UserRepo) UpdatePassword(ctx context.Context, userID string, password string) error { + return r.db.WithContext(ctx). + Model(&models.User{}). + Where("id = ?", userID). + Update("password", password).Error +} + +// UpdateStatus 更新状态 +func (r *UserRepo) UpdateStatus(ctx context.Context, userID string, status int) error { + return r.db.WithContext(ctx). + Model(&models.User{}). + Where("id = ?", userID). + Update("status", status).Error +} + +// ListUsers 列出用户(分页) +func (r *UserRepo) ListUsers(ctx context.Context, offset, limit int) ([]models.User, int64, error) { + var users []models.User + var total int64 + + // 获取总数 + if err := r.db.WithContext(ctx).Model(&models.User{}).Count(&total).Error; err != nil { + return nil, 0, err + } + + // 获取分页数据 + err := r.db.WithContext(ctx). + Offset(offset). + Limit(limit). + Order("created_at DESC"). + Find(&users).Error + + return users, total, err +} diff --git a/internal/service/account_service.go b/internal/service/account_service.go new file mode 100644 index 0000000..a28942a --- /dev/null +++ b/internal/service/account_service.go @@ -0,0 +1,118 @@ +package service + +import ( + "context" + "time" + + "github.com/dushixiang/pika/internal/models" + "github.com/go-errors/errors" + "github.com/golang-jwt/jwt/v5" + "go.uber.org/zap" +) + +func NewAccountService(logger *zap.Logger, userService *UserService, jwtSecret string, tokenExpireHours int) *AccountService { + if jwtSecret == "" { + logger.Fatal("JWT secret cannot be empty") + } + if len(jwtSecret) < 32 { + logger.Warn("JWT secret is too short, should be at least 32 characters for security") + } + if tokenExpireHours <= 0 { + tokenExpireHours = 168 // 默认7天 + } + + service := &AccountService{ + logger: logger, + userService: userService, + jwtSecret: jwtSecret, + tokenExpireHours: tokenExpireHours, + } + return service +} + +type AccountService struct { + logger *zap.Logger + userService *UserService + jwtSecret string + tokenExpireHours int +} + +// JWTClaims JWT 声明 +type JWTClaims struct { + UserID string `json:"userId"` + Username string `json:"username"` + jwt.RegisteredClaims +} + +// LoginResponse 登录响应 +type LoginResponse struct { + Token string `json:"token"` + ExpiresAt int64 `json:"expiresAt"` + User *models.User `json:"user"` +} + +// Login 用户登录 +func (s *AccountService) Login(ctx context.Context, username, password string) (*LoginResponse, error) { + // 验证用户名密码 + user, err := s.userService.ValidatePassword(ctx, username, password) + if err != nil { + return nil, err + } + + // 生成 JWT token + expiresAt := time.Now().Add(time.Duration(s.tokenExpireHours) * time.Hour) + claims := &JWTClaims{ + UserID: user.ID, + Username: user.Username, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(expiresAt), + IssuedAt: jwt.NewNumericDate(time.Now()), + NotBefore: jwt.NewNumericDate(time.Now()), + Issuer: "pika", + Subject: user.ID, + }, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + tokenString, err := token.SignedString([]byte(s.jwtSecret)) + if err != nil { + s.logger.Error("生成token失败", zap.Error(err)) + return nil, errors.New("生成token失败") + } + + s.logger.Info("用户登录成功", zap.String("userID", user.ID), zap.String("username", user.Username)) + + return &LoginResponse{ + Token: tokenString, + ExpiresAt: expiresAt.UnixMilli(), + User: user, + }, nil +} + +// Logout 用户登出 +func (s *AccountService) Logout(ctx context.Context, userID string) error { + + s.logger.Info("用户登出成功", zap.String("userID", userID)) + return nil +} + +// ValidateToken 验证 JWT token +func (s *AccountService) ValidateToken(tokenString string) (*JWTClaims, error) { + token, err := jwt.ParseWithClaims(tokenString, &JWTClaims{}, func(token *jwt.Token) (interface{}, error) { + // 验证签名方法 + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, errors.New("无效的签名方法") + } + return []byte(s.jwtSecret), nil + }) + + if err != nil { + return nil, err + } + + if claims, ok := token.Claims.(*JWTClaims); ok && token.Valid { + return claims, nil + } + + return nil, errors.New("无效的token") +} diff --git a/internal/service/agent_service.go b/internal/service/agent_service.go new file mode 100644 index 0000000..26c1cab --- /dev/null +++ b/internal/service/agent_service.go @@ -0,0 +1,670 @@ +package service + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "github.com/dushixiang/pika/internal/models" + "github.com/dushixiang/pika/internal/protocol" + "github.com/dushixiang/pika/internal/repo" + "go.uber.org/zap" +) + +type AgentService struct { + logger *zap.Logger + agentRepo *repo.AgentRepo + metricRepo *repo.MetricRepo + apiKeyService *ApiKeyService + Repository *repo.AgentRepo +} + +func NewAgentService(logger *zap.Logger, agentRepo *repo.AgentRepo, metricRepo *repo.MetricRepo, apiKeyService *ApiKeyService) *AgentService { + return &AgentService{ + logger: logger, + agentRepo: agentRepo, + metricRepo: metricRepo, + apiKeyService: apiKeyService, + Repository: agentRepo, + } +} + +// RegisterAgent 注册探针 +func (s *AgentService) RegisterAgent(ctx context.Context, ip string, info *protocol.AgentInfo, apiKey string) (*models.Agent, error) { + // 验证API密钥 + if _, err := s.apiKeyService.ValidateApiKey(ctx, apiKey); err != nil { + s.logger.Warn("agent registration failed: invalid api key", + zap.String("agentID", info.ID), + zap.String("hostname", info.Hostname), + ) + return nil, err + } + + // 验证探针 ID + if info.ID == "" { + return nil, fmt.Errorf("agent ID 不能为空") + } + + // 使用探针的持久化 ID 来识别同一个探针 + // 这样即使主机名或 IP 变化,也能正确识别 + existingAgent, err := s.agentRepo.FindById(ctx, info.ID) + if err == nil { + // 更新现有探针信息(允许主机名、IP、名称等变化) + now := time.Now().UnixMilli() + existingAgent.Hostname = info.Hostname + existingAgent.IP = ip + existingAgent.OS = info.OS + existingAgent.Arch = info.Arch + existingAgent.Version = info.Version + existingAgent.Status = 1 + existingAgent.LastSeenAt = now + existingAgent.UpdatedAt = now + + if err := s.agentRepo.UpdateById(ctx, &existingAgent); err != nil { + return nil, err + } + s.logger.Info("agent re-registered", + zap.String("agentID", existingAgent.ID), + zap.String("name", info.Name), + zap.String("hostname", info.Hostname), + zap.String("ip", ip), + zap.String("version", info.Version)) + return &existingAgent, nil + } + + // 创建新探针(使用客户端提供的持久化 ID) + now := time.Now().UnixMilli() + agent := &models.Agent{ + ID: info.ID, // 使用客户端持久化的 ID + Name: info.Name, + Hostname: info.Hostname, + IP: ip, + OS: info.OS, + Arch: info.Arch, + Version: info.Version, + Status: 1, + LastSeenAt: now, + CreatedAt: now, + UpdatedAt: now, + } + + if err := s.agentRepo.Create(ctx, agent); err != nil { + return nil, err + } + + s.logger.Info("agent registered successfully", + zap.String("agentID", agent.ID), + zap.String("name", info.Name), + zap.String("hostname", info.Hostname), + zap.String("ip", ip), + zap.String("version", info.Version)) + return agent, nil +} + +// UpdateAgentStatus 更新探针状态 +func (s *AgentService) UpdateAgentStatus(ctx context.Context, agentID string, status int) error { + return s.agentRepo.UpdateStatus(ctx, agentID, status) +} + +// GetAgent 获取探针信息 +func (s *AgentService) GetAgent(ctx context.Context, agentID string) (*models.Agent, error) { + agent, err := s.agentRepo.FindById(ctx, agentID) + if err != nil { + return nil, err + } + return &agent, nil +} + +// ListAgents 列出所有探针 +func (s *AgentService) ListAgents(ctx context.Context) ([]models.Agent, error) { + return s.agentRepo.FindAll(ctx) +} + +// ListOnlineAgents 列出所有在线探针 +func (s *AgentService) ListOnlineAgents(ctx context.Context) ([]models.Agent, error) { + return s.agentRepo.FindOnlineAgents(ctx) +} + +// HandleMetricData 处理指标数据 +func (s *AgentService) HandleMetricData(ctx context.Context, agentID string, metricType string, data json.RawMessage) error { + now := time.Now().UnixMilli() + + switch protocol.MetricType(metricType) { + case protocol.MetricTypeCPU: + // CPU数据现在包含静态和动态信息 + var cpuData protocol.CPUData + if err := json.Unmarshal(data, &cpuData); err != nil { + return err + } + metric := &models.CPUMetric{ + AgentID: agentID, + UsagePercent: cpuData.UsagePercent, + LogicalCores: cpuData.LogicalCores, + PhysicalCores: cpuData.PhysicalCores, + ModelName: cpuData.ModelName, + Timestamp: now, + } + return s.metricRepo.SaveCPUMetric(ctx, metric) + + case protocol.MetricTypeMemory: + // Memory数据现在包含静态和动态信息 + var memData protocol.MemoryData + if err := json.Unmarshal(data, &memData); err != nil { + return err + } + metric := &models.MemoryMetric{ + AgentID: agentID, + Total: memData.Total, // 现在可以从合并后的数据获取 + Used: memData.Used, + Free: memData.Free, + UsagePercent: memData.UsagePercent, + Timestamp: now, + } + return s.metricRepo.SaveMemoryMetric(ctx, metric) + + case protocol.MetricTypeDisk: + // Disk现在是数组,需要批量处理 + var diskDataList []protocol.DiskData + if err := json.Unmarshal(data, &diskDataList); err != nil { + return err + } + // 保存每个磁盘的数据 + for _, diskData := range diskDataList { + metric := &models.DiskMetric{ + AgentID: agentID, + MountPoint: diskData.MountPoint, + Total: diskData.Total, + Used: diskData.Used, + Free: diskData.Free, + UsagePercent: diskData.UsagePercent, + Timestamp: now, + } + if err := s.metricRepo.SaveDiskMetric(ctx, metric); err != nil { + s.logger.Error("failed to save disk metric", + zap.Error(err), + zap.String("agentID", agentID), + zap.String("mountPoint", diskData.MountPoint)) + } + } + return nil + + case protocol.MetricTypeNetwork: + // Network现在是数组,需要批量处理 + var networkDataList []protocol.NetworkData + if err := json.Unmarshal(data, &networkDataList); err != nil { + return err + } + // 保存每个网卡的数据 + for _, netData := range networkDataList { + metric := &models.NetworkMetric{ + AgentID: agentID, + Interface: netData.Interface, + BytesSentRate: netData.BytesSentRate, + BytesRecvRate: netData.BytesRecvRate, + BytesSentTotal: netData.BytesSentTotal, + BytesRecvTotal: netData.BytesRecvTotal, + Timestamp: now, + } + if err := s.metricRepo.SaveNetworkMetric(ctx, metric); err != nil { + s.logger.Error("failed to save network metric", + zap.Error(err), + zap.String("agentID", agentID), + zap.String("interface", netData.Interface)) + } + } + return nil + + case protocol.MetricTypeLoad: + var loadData protocol.LoadData + if err := json.Unmarshal(data, &loadData); err != nil { + return err + } + metric := &models.LoadMetric{ + AgentID: agentID, + Load1: loadData.Load1, + Load5: loadData.Load5, + Load15: loadData.Load15, + Timestamp: now, + } + return s.metricRepo.SaveLoadMetric(ctx, metric) + + case protocol.MetricTypeDiskIO: + // DiskIO现在是数组,需要批量处理 + var diskIODataList []*protocol.DiskIOData + if err := json.Unmarshal(data, &diskIODataList); err != nil { + return err + } + // 保存每个磁盘的IO数据 + for _, diskIOData := range diskIODataList { + metric := &models.DiskIOMetric{ + AgentID: agentID, + Device: diskIOData.Device, + ReadCount: diskIOData.ReadCount, + WriteCount: diskIOData.WriteCount, + ReadBytes: diskIOData.ReadBytes, + WriteBytes: diskIOData.WriteBytes, + ReadTime: diskIOData.ReadTime, + WriteTime: diskIOData.WriteTime, + IoTime: diskIOData.IoTime, + IopsInProgress: diskIOData.IopsInProgress, + Timestamp: now, + } + if err := s.metricRepo.SaveDiskIOMetric(ctx, metric); err != nil { + s.logger.Error("failed to save disk io metric", + zap.Error(err), + zap.String("agentID", agentID), + zap.String("device", diskIOData.Device)) + } + } + return nil + + case protocol.MetricTypeHost: + var hostData protocol.HostInfoData + if err := json.Unmarshal(data, &hostData); err != nil { + return err + } + // 保存主机信息 + metric := &models.HostMetric{ + AgentID: agentID, + OS: hostData.OS, + Platform: hostData.Platform, + PlatformVersion: hostData.PlatformVersion, + KernelVersion: hostData.KernelVersion, + KernelArch: hostData.KernelArch, + Uptime: hostData.Uptime, + BootTime: hostData.BootTime, + Procs: hostData.Procs, + Timestamp: now, + } + return s.metricRepo.SaveHostMetric(ctx, metric) + + case protocol.MetricTypeGPU: + // GPU现在是数组,需要批量处理 + var gpuDataList []protocol.GPUData + if err := json.Unmarshal(data, &gpuDataList); err != nil { + return err + } + // 保存每个GPU的数据 + for _, gpuData := range gpuDataList { + metric := &models.GPUMetric{ + AgentID: agentID, + Index: gpuData.Index, + Name: gpuData.Name, + Utilization: gpuData.Utilization, + MemoryTotal: gpuData.MemoryTotal, + MemoryUsed: gpuData.MemoryUsed, + MemoryFree: gpuData.MemoryFree, + Temperature: gpuData.Temperature, + PowerDraw: gpuData.PowerUsage, // protocol 中是 PowerUsage + FanSpeed: gpuData.FanSpeed, + PerformanceState: "", // protocol 中没有这个字段,留空 + Timestamp: now, + } + if err := s.metricRepo.SaveGPUMetric(ctx, metric); err != nil { + s.logger.Error("failed to save gpu metric", + zap.Error(err), + zap.String("agentID", agentID), + zap.Int("index", gpuData.Index)) + } + } + return nil + + case protocol.MetricTypeTemperature: + // Temperature现在是数组,需要批量处理 + var tempDataList []protocol.TemperatureData + if err := json.Unmarshal(data, &tempDataList); err != nil { + return err + } + // 保存每个温度传感器的数据 + for _, tempData := range tempDataList { + metric := &models.TemperatureMetric{ + AgentID: agentID, + SensorKey: tempData.SensorKey, + SensorLabel: tempData.SensorKey, // protocol 中没有 SensorLabel,使用 SensorKey + Temperature: tempData.Temperature, + Timestamp: now, + } + if err := s.metricRepo.SaveTemperatureMetric(ctx, metric); err != nil { + s.logger.Error("failed to save temperature metric", + zap.Error(err), + zap.String("agentID", agentID), + zap.String("sensor", tempData.SensorKey)) + } + } + return nil + + default: + s.logger.Warn("unknown metric type", zap.String("type", metricType)) + return nil + } +} + +// CalculateInterval 根据时间范围计算合适的聚合间隔(秒) +// 目标是返回大约200-500个数据点 +func CalculateInterval(start, end int64) int { + duration := (end - start) / 1000 // 转换为秒 + + switch { + case duration <= 3600: // 1小时内: 10秒聚合 + return 10 + case duration <= 21600: // 6小时内: 1分钟聚合 + return 60 + case duration <= 86400: // 1天内: 5分钟聚合 + return 300 + case duration <= 259200: // 3天内: 15分钟聚合 + return 900 + case duration <= 604800: // 7天内: 30分钟聚合 + return 1800 + case duration <= 2592000: // 30天内: 2小时聚合 + return 7200 + default: // 30天以上: 6小时聚合 + return 21600 + } +} + +// GetMetrics 获取聚合指标数据 +func (s *AgentService) GetMetrics(ctx context.Context, agentID, metricType string, start, end int64, interval int) (interface{}, error) { + switch metricType { + case "cpu": + return s.metricRepo.GetCPUMetrics(ctx, agentID, start, end, interval) + case "memory": + return s.metricRepo.GetMemoryMetrics(ctx, agentID, start, end, interval) + case "disk": + return s.metricRepo.GetDiskMetrics(ctx, agentID, start, end, interval) + case "network": + return s.metricRepo.GetNetworkMetrics(ctx, agentID, start, end, interval) + case "load": + return s.metricRepo.GetLoadMetrics(ctx, agentID, start, end, interval) + default: + return nil, nil + } +} + +// GetLatestMetrics 获取最新指标 +func (s *AgentService) GetLatestMetrics(ctx context.Context, agentID string) (*LatestMetrics, error) { + result := &LatestMetrics{} + + // 获取最新CPU指标 + if cpu, err := s.metricRepo.GetLatestCPUMetric(ctx, agentID); err == nil { + result.CPU = cpu + } + + // 获取最新内存指标 + if memory, err := s.metricRepo.GetLatestMemoryMetric(ctx, agentID); err == nil { + result.Memory = memory + } + + // 获取最新磁盘指标并计算平均使用率和总容量 + if disks, err := s.metricRepo.GetLatestDiskMetrics(ctx, agentID); err == nil && len(disks) > 0 { + var totalUsage float64 + var totalSpace, usedSpace, freeSpace uint64 + for _, disk := range disks { + totalUsage += disk.UsagePercent + totalSpace += disk.Total + usedSpace += disk.Used + freeSpace += disk.Free + } + result.Disk = &DiskSummary{ + AvgUsagePercent: totalUsage / float64(len(disks)), + TotalDisks: len(disks), + Total: totalSpace, + Used: usedSpace, + Free: freeSpace, + } + } + + // 获取最新网络指标并汇总速率和累计流量 + // 注意: 采集器已经计算好了每秒速率,这里直接汇总所有网卡的速率和累计流量 + if networks, err := s.metricRepo.GetLatestNetworkMetrics(ctx, agentID); err == nil && len(networks) > 0 { + var totalSentRate, totalRecvRate, totalSentTotal, totalRecvTotal uint64 + for _, net := range networks { + totalSentRate += net.BytesSentRate // 累加每个网卡的发送速率 + totalRecvRate += net.BytesRecvRate // 累加每个网卡的接收速率 + totalSentTotal += net.BytesSentTotal // 累加每个网卡的累计发送流量 + totalRecvTotal += net.BytesRecvTotal // 累加每个网卡的累计接收流量 + } + result.Network = &NetworkSummary{ + TotalBytesSentRate: totalSentRate, // 所有网卡的总发送速率(字节/秒) + TotalBytesRecvRate: totalRecvRate, // 所有网卡的总接收速率(字节/秒) + TotalBytesSentTotal: totalSentTotal, // 所有网卡的累计发送流量 + TotalBytesRecvTotal: totalRecvTotal, // 所有网卡的累计接收流量 + TotalInterfaces: len(networks), + } + } + + // 获取最新负载信息 + if load, err := s.metricRepo.GetLatestLoadMetric(ctx, agentID); err == nil { + result.Load = load + } + + // 获取最新主机信息 + if host, err := s.metricRepo.GetLatestHostMetric(ctx, agentID); err == nil { + result.Host = host + } + + return result, nil +} + +// StartCleanupTask 启动数据清理任务 +func (s *AgentService) StartCleanupTask(ctx context.Context) { + ticker := time.NewTicker(1 * time.Hour) + defer ticker.Stop() + + s.logger.Info("cleanup task started") + + for { + select { + case <-ctx.Done(): + s.logger.Info("cleanup task stopped") + return + case <-ticker.C: + s.cleanupOldMetrics(ctx) + } + } +} + +// cleanupOldMetrics 清理旧数据 +func (s *AgentService) cleanupOldMetrics(ctx context.Context) { + // 删除30天前的数据 + before := time.Now().AddDate(0, 0, -30).UnixMilli() + + s.logger.Info("starting to clean old metrics", zap.Int64("beforeTimestamp", before)) + + if err := s.metricRepo.DeleteOldMetrics(ctx, before); err != nil { + s.logger.Error("failed to clean old metrics", zap.Error(err)) + return + } + + s.logger.Info("old metrics cleaned successfully") +} + +// DiskSummary 磁盘汇总数据 +type DiskSummary struct { + AvgUsagePercent float64 `json:"avgUsagePercent"` // 平均使用率 + TotalDisks int `json:"totalDisks"` // 磁盘数量 + Total uint64 `json:"total"` // 总容量(字节) + Used uint64 `json:"used"` // 已使用(字节) + Free uint64 `json:"free"` // 空闲(字节) +} + +// NetworkSummary 网络汇总数据 +type NetworkSummary struct { + TotalBytesSentRate uint64 `json:"totalBytesSentRate"` // 总发送速率(字节/秒) + TotalBytesRecvRate uint64 `json:"totalBytesRecvRate"` // 总接收速率(字节/秒) + TotalBytesSentTotal uint64 `json:"totalBytesSentTotal"` // 累计总发送流量 + TotalBytesRecvTotal uint64 `json:"totalBytesRecvTotal"` // 累计总接收流量 + TotalInterfaces int `json:"totalInterfaces"` // 网卡数量 +} + +// LatestMetrics 最新指标数据(用于API响应) +type LatestMetrics struct { + CPU *models.CPUMetric `json:"cpu,omitempty"` + Memory *models.MemoryMetric `json:"memory,omitempty"` + Disk *DiskSummary `json:"disk,omitempty"` + Network *NetworkSummary `json:"network,omitempty"` + Load *models.LoadMetric `json:"load,omitempty"` + Host *models.HostMetric `json:"host,omitempty"` +} + +// HandleCommandResponse 处理指令响应 +func (s *AgentService) HandleCommandResponse(ctx context.Context, agentID string, resp *protocol.CommandResponse) error { + s.logger.Info("command response received", + zap.String("agentID", agentID), + zap.String("cmdID", resp.ID), + zap.String("type", resp.Type), + zap.String("status", resp.Status)) + + // 根据指令类型处理响应 + switch resp.Type { + case "vps_audit": + return s.handleVPSAuditResponse(ctx, agentID, resp) + default: + s.logger.Warn("unknown command type", zap.String("type", resp.Type)) + return nil + } +} + +// handleVPSAuditResponse 处理VPS审计响应 +func (s *AgentService) handleVPSAuditResponse(ctx context.Context, agentID string, resp *protocol.CommandResponse) error { + if resp.Status == "error" { + s.logger.Error("vps audit failed", + zap.String("agentID", agentID), + zap.String("error", resp.Error)) + return nil + } + + if resp.Status == "running" { + s.logger.Info("vps audit is running", zap.String("agentID", agentID)) + return nil + } + + if resp.Status == "success" { + // 解析审计结果 + var auditResult protocol.VPSAuditResult + if err := json.Unmarshal([]byte(resp.Result), &auditResult); err != nil { + s.logger.Error("failed to parse audit result", zap.Error(err)) + return err + } + + // 存储审计结果 + return s.SaveAuditResult(ctx, agentID, &auditResult) + } + + return nil +} + +// SaveAuditResult 保存审计结果 +func (s *AgentService) SaveAuditResult(ctx context.Context, agentID string, result *protocol.VPSAuditResult) error { + // 将结果序列化为JSON存储 + resultJSON, err := json.Marshal(result) + if err != nil { + return err + } + + auditRecord := &models.AuditResult{ + AgentID: agentID, + Type: "vps_audit", + Result: string(resultJSON), + StartTime: result.StartTime, + EndTime: result.EndTime, + CreatedAt: time.Now().UnixMilli(), + } + + // 保存到数据库 + return s.agentRepo.SaveAuditResult(ctx, auditRecord) +} + +// GetAuditResult 获取最新的审计结果 +func (s *AgentService) GetAuditResult(ctx context.Context, agentID string) (*protocol.VPSAuditResult, error) { + record, err := s.agentRepo.GetLatestAuditResult(ctx, agentID) + if err != nil { + return nil, err + } + + var result protocol.VPSAuditResult + if err := json.Unmarshal([]byte(record.Result), &result); err != nil { + return nil, err + } + + return &result, nil +} + +// ListAuditResults 获取审计结果列表 +func (s *AgentService) ListAuditResults(ctx context.Context, agentID string) ([]map[string]interface{}, error) { + records, err := s.agentRepo.ListAuditResults(ctx, agentID) + if err != nil { + return nil, err + } + + results := make([]map[string]interface{}, 0, len(records)) + for _, record := range records { + var auditResult protocol.VPSAuditResult + if err := json.Unmarshal([]byte(record.Result), &auditResult); err != nil { + s.logger.Error("failed to parse audit result", zap.Error(err)) + continue + } + + // 统计检查结果 + passCount := 0 + failCount := 0 + warnCount := 0 + for _, check := range auditResult.SecurityChecks { + switch check.Status { + case "pass": + passCount++ + case "fail": + failCount++ + case "warn": + warnCount++ + } + } + + results = append(results, map[string]interface{}{ + "id": record.ID, + "agentId": record.AgentID, + "type": record.Type, + "startTime": record.StartTime, + "endTime": record.EndTime, + "createdAt": record.CreatedAt, + "passCount": passCount, + "failCount": failCount, + "warnCount": warnCount, + "totalCount": len(auditResult.SecurityChecks), + "systemInfo": auditResult.SystemInfo, + }) + } + + return results, nil +} + +// UpdateAgentName 更新探针名称 +func (s *AgentService) UpdateAgentName(ctx context.Context, agentID string, name string) error { + return s.agentRepo.UpdateName(ctx, agentID, name) +} + +// UpdateAgentInfo 更新探针信息(名称、平台、位置、到期时间) +func (s *AgentService) UpdateAgentInfo(ctx context.Context, agentID string, updates map[string]interface{}) error { + return s.agentRepo.UpdateInfo(ctx, agentID, updates) +} + +// GetStatistics 获取探针统计数据 +func (s *AgentService) GetStatistics(ctx context.Context) (map[string]interface{}, error) { + total, online, err := s.agentRepo.GetStatistics(ctx) + if err != nil { + return nil, err + } + + offline := total - online + onlineRate := 0.0 + if total > 0 { + onlineRate = float64(online) / float64(total) * 100 + } + + return map[string]interface{}{ + "total": total, + "online": online, + "offline": offline, + "onlineRate": onlineRate, + }, nil +} diff --git a/internal/service/alert_service.go b/internal/service/alert_service.go new file mode 100644 index 0000000..621d585 --- /dev/null +++ b/internal/service/alert_service.go @@ -0,0 +1,341 @@ +package service + +import ( + "context" + "fmt" + "sync" + "time" + + "github.com/dushixiang/pika/internal/models" + "github.com/dushixiang/pika/internal/repo" + "github.com/google/uuid" + "go.uber.org/zap" +) + +// AlertService 告警服务 +type AlertService struct { + alertRepo *repo.AlertRepo + agentRepo *repo.AgentRepo + notifier *Notifier + logger *zap.Logger + + // 告警状态缓存(内存中维护) + states map[string]*models.AlertState + mu sync.RWMutex +} + +func NewAlertService(alertRepo *repo.AlertRepo, agentRepo *repo.AgentRepo, notifier *Notifier, logger *zap.Logger) *AlertService { + return &AlertService{ + alertRepo: alertRepo, + agentRepo: agentRepo, + notifier: notifier, + logger: logger, + states: make(map[string]*models.AlertState), + } +} + +// CreateAlertConfig 创建告警配置 +func (s *AlertService) CreateAlertConfig(ctx context.Context, config *models.AlertConfig) error { + config.ID = uuid.New().String() + config.CreatedAt = time.Now().UnixMilli() + config.UpdatedAt = time.Now().UnixMilli() + + return s.alertRepo.CreateAlertConfig(ctx, config) +} + +// UpdateAlertConfig 更新告警配置 +func (s *AlertService) UpdateAlertConfig(ctx context.Context, config *models.AlertConfig) error { + config.UpdatedAt = time.Now().UnixMilli() + return s.alertRepo.UpdateAlertConfig(ctx, config) +} + +// DeleteAlertConfig 删除告警配置 +func (s *AlertService) DeleteAlertConfig(ctx context.Context, id string) error { + // 清理该配置相关的状态 + s.mu.Lock() + for key, state := range s.states { + if state.ConfigID == id { + delete(s.states, key) + } + } + s.mu.Unlock() + + return s.alertRepo.DeleteAlertConfig(ctx, id) +} + +// GetAlertConfig 获取告警配置 +func (s *AlertService) GetAlertConfig(ctx context.Context, id string) (*models.AlertConfig, error) { + return s.alertRepo.GetAlertConfig(ctx, id) +} + +// ListAlertConfigsByAgent 列出探针的告警配置 +func (s *AlertService) ListAlertConfigsByAgent(ctx context.Context, agentID string) ([]models.AlertConfig, error) { + return s.alertRepo.FindByAgentID(ctx, agentID) +} + +// ListAlertRecords 列出告警记录 +func (s *AlertService) ListAlertRecords(ctx context.Context, agentID string, limit int, offset int) ([]models.AlertRecord, int64, error) { + return s.alertRepo.ListAlertRecords(ctx, agentID, limit, offset) +} + +// CheckMetrics 检查指标并触发告警 +func (s *AlertService) CheckMetrics(ctx context.Context, agentID string, cpu, memory, disk float64) error { + // 获取全局告警配置 + globalConfigs, err := s.alertRepo.FindEnabledByAgentID(ctx, "global") + if err != nil { + s.logger.Error("获取全局告警配置失败", zap.Error(err)) + return err + } + + // 过滤出适用于该探针的配置 + var configs []models.AlertConfig + for _, config := range globalConfigs { + // 如果 AgentIDs 为空,表示监控所有探针 + if len(config.AgentIDs) == 0 { + configs = append(configs, config) + continue + } + // 如果 AgentIDs 不为空,检查当前探针是否在列表中 + for _, id := range config.AgentIDs { + if id == agentID { + configs = append(configs, config) + break + } + } + } + + if len(configs) == 0 { + return nil + } + + // 获取探针信息(用于发送通知) + agent, err := s.agentRepo.FindById(ctx, agentID) + if err != nil { + s.logger.Error("获取探针信息失败", zap.Error(err)) + return err + } + + now := time.Now().UnixMilli() + + // 检查每个配置的告警规则 + for _, config := range configs { + // 检查 CPU 告警 + if config.Rules.CPUEnabled { + s.checkAlert(ctx, &config, &agent, "cpu", cpu, config.Rules.CPUThreshold, config.Rules.CPUDuration, now) + } + + // 检查内存告警 + if config.Rules.MemoryEnabled { + s.checkAlert(ctx, &config, &agent, "memory", memory, config.Rules.MemoryThreshold, config.Rules.MemoryDuration, now) + } + + // 检查磁盘告警 + if config.Rules.DiskEnabled { + s.checkAlert(ctx, &config, &agent, "disk", disk, config.Rules.DiskThreshold, config.Rules.DiskDuration, now) + } + } + + return nil +} + +// checkAlert 检查单个告警规则 +func (s *AlertService) checkAlert(ctx context.Context, config *models.AlertConfig, agent *models.Agent, alertType string, currentValue, threshold float64, duration int, now int64) { + stateKey := fmt.Sprintf("%s:%s:%s", config.AgentID, config.ID, alertType) + + s.mu.Lock() + state, exists := s.states[stateKey] + if !exists { + state = &models.AlertState{ + AgentID: config.AgentID, + ConfigID: config.ID, + AlertType: alertType, + Threshold: threshold, + Duration: duration, + } + s.states[stateKey] = state + } + s.mu.Unlock() + + // 更新当前值和检查时间 + state.Value = currentValue + state.LastCheckTime = now + + // 判断是否超过阈值 + if currentValue >= threshold { + // 超过阈值 + if state.StartTime == 0 { + // 首次超过阈值,记录开始时间 + state.StartTime = now + } + + // 计算已持续时间(秒) + elapsedSeconds := (now - state.StartTime) / 1000 + + // 判断是否达到持续时间要求 + if elapsedSeconds >= int64(duration) { + // 达到持续时间要求,触发告警 + if !state.IsFiring { + // 从未触发状态变为触发状态 + s.fireAlert(ctx, config, agent, state) + } + } + } else { + // 未超过阈值 + if state.IsFiring { + // 从触发状态变为恢复状态 + s.resolveAlert(ctx, config, agent, state) + } + + // 重置开始时间 + state.StartTime = 0 + } +} + +// fireAlert 触发告警 +func (s *AlertService) fireAlert(ctx context.Context, config *models.AlertConfig, agent *models.Agent, state *models.AlertState) { + s.logger.Info("触发告警", + zap.String("agentId", agent.ID), + zap.String("agentName", agent.Name), + zap.String("configId", config.ID), + zap.String("alertType", state.AlertType), + zap.Float64("value", state.Value), + zap.Float64("threshold", state.Threshold), + ) + + // 创建告警记录 + record := &models.AlertRecord{ + AgentID: agent.ID, + ConfigID: config.ID, + ConfigName: config.Name, + AlertType: state.AlertType, + Message: s.buildAlertMessage(state), + Threshold: state.Threshold, + ActualValue: state.Value, + Level: s.calculateLevel(state.Value, state.Threshold), + Status: "firing", + FiredAt: time.Now().UnixMilli(), + CreatedAt: time.Now().UnixMilli(), + } + + err := s.alertRepo.CreateAlertRecord(ctx, record) + if err != nil { + s.logger.Error("创建告警记录失败", zap.Error(err)) + return + } + + // 更新状态 + state.IsFiring = true + state.LastRecordID = record.ID + + // 发送通知 + go func() { + if err := s.notifier.SendNotification(context.Background(), config, record, agent); err != nil { + s.logger.Error("发送告警通知失败", zap.Error(err)) + } + }() +} + +// resolveAlert 恢复告警 +func (s *AlertService) resolveAlert(ctx context.Context, config *models.AlertConfig, agent *models.Agent, state *models.AlertState) { + s.logger.Info("告警恢复", + zap.String("agentId", agent.ID), + zap.String("agentName", agent.Name), + zap.String("configId", config.ID), + zap.String("alertType", state.AlertType), + zap.Float64("value", state.Value), + ) + + // 更新告警记录状态 + if state.LastRecordID > 0 { + // 先查询完整记录 + existingRecord, err := s.alertRepo.GetLatestAlertRecord(ctx, config.ID, state.AlertType) + if err == nil && existingRecord != nil { + existingRecord.Status = "resolved" + existingRecord.ActualValue = state.Value + existingRecord.ResolvedAt = time.Now().UnixMilli() + existingRecord.UpdatedAt = time.Now().UnixMilli() + + err = s.alertRepo.UpdateAlertRecord(ctx, existingRecord) + if err != nil { + s.logger.Error("更新告警记录失败", zap.Error(err)) + } else { + // 发送恢复通知 + go func() { + if err := s.notifier.SendNotification(context.Background(), config, existingRecord, agent); err != nil { + s.logger.Error("发送恢复通知失败", zap.Error(err)) + } + }() + } + } + } + + // 更新状态 + state.IsFiring = false + state.LastRecordID = 0 +} + +// buildAlertMessage 构建告警消息 +func (s *AlertService) buildAlertMessage(state *models.AlertState) string { + var alertTypeName string + switch state.AlertType { + case "cpu": + alertTypeName = "CPU使用率" + case "memory": + alertTypeName = "内存使用率" + case "disk": + alertTypeName = "磁盘使用率" + case "network": + alertTypeName = "网络连接" + default: + alertTypeName = state.AlertType + } + + return fmt.Sprintf("%s持续%d秒超过%.2f%%,当前值%.2f%%", + alertTypeName, + state.Duration, + state.Threshold, + state.Value, + ) +} + +// calculateLevel 计算告警级别 +func (s *AlertService) calculateLevel(value, threshold float64) string { + diff := value - threshold + + if diff < 20 { + return "info" + } else if diff < 50 { + return "warning" + } else { + return "critical" + } +} + +// TestNotification 测试告警通知 +func (s *AlertService) TestNotification(ctx context.Context, configID string) error { + config, err := s.alertRepo.GetAlertConfig(ctx, configID) + if err != nil { + return fmt.Errorf("获取告警配置失败: %w", err) + } + + agent, err := s.agentRepo.FindById(ctx, config.AgentID) + if err != nil { + return fmt.Errorf("获取探针信息失败: %w", err) + } + + // 构造测试告警记录 + testRecord := &models.AlertRecord{ + AgentID: agent.ID, + ConfigID: config.ID, + ConfigName: config.Name, + AlertType: "cpu", + Message: "这是一条测试告警消息", + Threshold: 70.0, + ActualValue: 75.0, + Level: "warning", + Status: "firing", + FiredAt: time.Now().UnixMilli(), + } + + return s.notifier.SendNotification(ctx, config, testRecord, &agent) +} diff --git a/internal/service/api_key_service.go b/internal/service/api_key_service.go new file mode 100644 index 0000000..c3d4827 --- /dev/null +++ b/internal/service/api_key_service.go @@ -0,0 +1,145 @@ +package service + +import ( + "context" + "crypto/rand" + "encoding/base64" + "errors" + "time" + + "github.com/dushixiang/pika/internal/models" + "github.com/dushixiang/pika/internal/repo" + "github.com/google/uuid" + "go.uber.org/zap" +) + +type ApiKeyService struct { + logger *zap.Logger + apiKeyRepo *repo.ApiKeyRepo + Repository *repo.ApiKeyRepo +} + +func NewApiKeyService(logger *zap.Logger, apiKeyRepo *repo.ApiKeyRepo) *ApiKeyService { + return &ApiKeyService{ + logger: logger, + apiKeyRepo: apiKeyRepo, + Repository: apiKeyRepo, + } +} + +// GenerateApiKey 生成API密钥 +func (s *ApiKeyService) GenerateApiKey(ctx context.Context, name, userID string) (*models.ApiKey, error) { + // 生成32字节随机密钥 + key, err := s.generateSecureKey(32) + if err != nil { + return nil, err + } + + now := time.Now().UnixMilli() + apiKey := &models.ApiKey{ + ID: uuid.NewString(), + Name: name, + Key: key, + Enabled: true, + CreatedBy: userID, + CreatedAt: now, + UpdatedAt: now, + } + + if err := s.apiKeyRepo.Create(ctx, apiKey); err != nil { + return nil, err + } + + s.logger.Info("api key generated", + zap.String("keyID", apiKey.ID), + zap.String("name", name), + zap.String("userID", userID)) + + return apiKey, nil +} + +// ValidateApiKey 验证API密钥 +func (s *ApiKeyService) ValidateApiKey(ctx context.Context, key string) (*models.ApiKey, error) { + if key == "" { + return nil, errors.New("api key is required") + } + + apiKey, err := s.apiKeyRepo.FindEnabledByKey(ctx, key) + if err != nil { + s.logger.Warn("invalid api key", zap.String("key", key)) + return nil, errors.New("invalid api key") + } + + return apiKey, nil +} + +// GetApiKey 获取API密钥信息 +func (s *ApiKeyService) GetApiKey(ctx context.Context, id string) (*models.ApiKey, error) { + apiKey, err := s.apiKeyRepo.FindById(ctx, id) + if err != nil { + return nil, err + } + return &apiKey, nil +} + +// ListApiKeys 列出所有API密钥 +func (s *ApiKeyService) ListApiKeys(ctx context.Context, page, pageSize int) ([]models.ApiKey, int64, error) { + return s.apiKeyRepo.ListAll(ctx, page, pageSize) +} + +// ListUserApiKeys 列出用户的API密钥 +func (s *ApiKeyService) ListUserApiKeys(ctx context.Context, userID string, page, pageSize int) ([]models.ApiKey, int64, error) { + return s.apiKeyRepo.ListByUser(ctx, userID, page, pageSize) +} + +// UpdateApiKeyName 更新API密钥名称 +func (s *ApiKeyService) UpdateApiKeyName(ctx context.Context, id, name string) error { + if err := s.apiKeyRepo.UpdateName(ctx, id, name); err != nil { + return err + } + + s.logger.Info("api key name updated", + zap.String("keyID", id), + zap.String("name", name)) + + return nil +} + +// EnableApiKey 启用API密钥 +func (s *ApiKeyService) EnableApiKey(ctx context.Context, id string) error { + if err := s.apiKeyRepo.UpdateEnabled(ctx, id, true); err != nil { + return err + } + + s.logger.Info("api key enabled", zap.String("keyID", id)) + return nil +} + +// DisableApiKey 禁用API密钥 +func (s *ApiKeyService) DisableApiKey(ctx context.Context, id string) error { + if err := s.apiKeyRepo.UpdateEnabled(ctx, id, false); err != nil { + return err + } + + s.logger.Info("api key disabled", zap.String("keyID", id)) + return nil +} + +// DeleteApiKey 删除API密钥 +func (s *ApiKeyService) DeleteApiKey(ctx context.Context, id string) error { + if err := s.apiKeyRepo.DeleteById(ctx, id); err != nil { + return err + } + + s.logger.Info("api key deleted", zap.String("keyID", id)) + return nil +} + +// generateSecureKey 生成安全的随机密钥 +func (s *ApiKeyService) generateSecureKey(length int) (string, error) { + bytes := make([]byte, length) + if _, err := rand.Read(bytes); err != nil { + return "", err + } + return base64.URLEncoding.EncodeToString(bytes), nil +} diff --git a/internal/service/notifier.go b/internal/service/notifier.go new file mode 100644 index 0000000..aaff6d4 --- /dev/null +++ b/internal/service/notifier.go @@ -0,0 +1,265 @@ +package service + +import ( + "bytes" + "context" + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "time" + + "github.com/dushixiang/pika/internal/models" + "go.uber.org/zap" +) + +// Notifier 告警通知服务 +type Notifier struct { + logger *zap.Logger +} + +func NewNotifier(logger *zap.Logger) *Notifier { + return &Notifier{ + logger: logger, + } +} + +// SendNotification 发送告警通知 +func (n *Notifier) SendNotification(ctx context.Context, config *models.AlertConfig, record *models.AlertRecord, agent *models.Agent) error { + n.logger.Info("发送告警通知", + zap.String("agentId", agent.ID), + zap.String("agentName", agent.Name), + zap.String("alertType", record.AlertType), + zap.String("status", record.Status), + ) + + notification := config.Notification + + // 构造通知消息内容 + message := n.buildMessage(agent, record) + + var errs []error + + // 发送钉钉通知 + if notification.DingTalkEnabled && notification.DingTalkWebhook != "" { + if err := n.sendDingTalk(ctx, notification.DingTalkWebhook, notification.DingTalkSecret, message); err != nil { + n.logger.Error("发送钉钉通知失败", zap.Error(err)) + errs = append(errs, err) + } + } + + // 发送企业微信通知 + if notification.WeComEnabled && notification.WeComWebhook != "" { + if err := n.sendWeCom(ctx, notification.WeComWebhook, message); err != nil { + n.logger.Error("发送企业微信通知失败", zap.Error(err)) + errs = append(errs, err) + } + } + + // 发送飞书通知 + if notification.FeishuEnabled && notification.FeishuWebhook != "" { + if err := n.sendFeishu(ctx, notification.FeishuWebhook, message); err != nil { + n.logger.Error("发送飞书通知失败", zap.Error(err)) + errs = append(errs, err) + } + } + + // 发送自定义Webhook + if notification.CustomWebhookEnabled && notification.CustomWebhookURL != "" { + if err := n.sendCustomWebhook(ctx, notification.CustomWebhookURL, agent, record); err != nil { + n.logger.Error("发送自定义Webhook失败", zap.Error(err)) + errs = append(errs, err) + } + } + + // TODO: 实现邮件通知 + // if notification.EmailEnabled && len(notification.EmailAddresses) > 0 { + // if err := n.sendEmail(ctx, notification.EmailAddresses, message); err != nil { + // n.logger.Error("发送邮件通知失败", zap.Error(err)) + // errs = append(errs, err) + // } + // } + + if len(errs) > 0 { + return fmt.Errorf("部分通知发送失败: %v", errs) + } + + return nil +} + +// buildMessage 构建告警消息文本 +func (n *Notifier) buildMessage(agent *models.Agent, record *models.AlertRecord) string { + var message string + + // 告警级别图标 + levelIcon := "" + switch record.Level { + case "info": + levelIcon = "ℹ️" + case "warning": + levelIcon = "⚠️" + case "critical": + levelIcon = "🚨" + } + + // 告警类型名称 + alertTypeName := "" + switch record.AlertType { + case "cpu": + alertTypeName = "CPU告警" + case "memory": + alertTypeName = "内存告警" + case "disk": + alertTypeName = "磁盘告警" + case "network": + alertTypeName = "网络断开告警" + } + + if record.Status == "firing" { + // 告警触发消息 + message = fmt.Sprintf( + "%s %s\n\n"+ + "探针: %s (%s)\n"+ + "主机: %s\n"+ + "IP: %s\n"+ + "告警类型: %s\n"+ + "告警消息: %s\n"+ + "阈值: %.2f%%\n"+ + "当前值: %.2f%%\n"+ + "触发时间: %s", + levelIcon, + alertTypeName, + agent.Name, + agent.ID, + agent.Hostname, + agent.IP, + record.AlertType, + record.Message, + record.Threshold, + record.ActualValue, + time.Unix(record.FiredAt/1000, 0).Format("2006-01-02 15:04:05"), + ) + } else if record.Status == "resolved" { + // 告警恢复消息 + message = fmt.Sprintf( + "✅ %s已恢复\n\n"+ + "探针: %s (%s)\n"+ + "主机: %s\n"+ + "IP: %s\n"+ + "告警类型: %s\n"+ + "当前值: %.2f%%\n"+ + "恢复时间: %s", + alertTypeName, + agent.Name, + agent.ID, + agent.Hostname, + agent.IP, + record.AlertType, + record.ActualValue, + time.Unix(record.ResolvedAt/1000, 0).Format("2006-01-02 15:04:05"), + ) + } + + return message +} + +// sendDingTalk 发送钉钉通知 +func (n *Notifier) sendDingTalk(ctx context.Context, webhook, secret, message string) error { + // 构造钉钉消息体 + body := map[string]interface{}{ + "msgtype": "text", + "text": map[string]string{ + "content": message, + }, + } + + // 如果有加签密钥,计算签名 + timestamp := time.Now().UnixMilli() + if secret != "" { + sign := n.calculateDingTalkSign(timestamp, secret) + webhook = fmt.Sprintf("%s×tamp=%d&sign=%s", webhook, timestamp, sign) + } + + return n.sendJSONRequest(ctx, webhook, body) +} + +// calculateDingTalkSign 计算钉钉加签 +func (n *Notifier) calculateDingTalkSign(timestamp int64, secret string) string { + stringToSign := fmt.Sprintf("%d\n%s", timestamp, secret) + h := hmac.New(sha256.New, []byte(secret)) + h.Write([]byte(stringToSign)) + return base64.StdEncoding.EncodeToString(h.Sum(nil)) +} + +// sendWeCom 发送企业微信通知 +func (n *Notifier) sendWeCom(ctx context.Context, webhook, message string) error { + body := map[string]interface{}{ + "msgtype": "text", + "text": map[string]string{ + "content": message, + }, + } + + return n.sendJSONRequest(ctx, webhook, body) +} + +// sendFeishu 发送飞书通知 +func (n *Notifier) sendFeishu(ctx context.Context, webhook, message string) error { + body := map[string]interface{}{ + "msg_type": "text", + "content": map[string]string{ + "text": message, + }, + } + + return n.sendJSONRequest(ctx, webhook, body) +} + +// sendCustomWebhook 发送自定义Webhook +func (n *Notifier) sendCustomWebhook(ctx context.Context, webhook string, agent *models.Agent, record *models.AlertRecord) error { + // 发送完整的告警记录和探针信息 + body := map[string]interface{}{ + "agent": agent, + "record": record, + } + + return n.sendJSONRequest(ctx, webhook, body) +} + +// sendJSONRequest 发送JSON请求 +func (n *Notifier) sendJSONRequest(ctx context.Context, url string, body interface{}) error { + data, err := json.Marshal(body) + if err != nil { + return fmt.Errorf("序列化请求体失败: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(data)) + if err != nil { + return fmt.Errorf("创建请求失败: %w", err) + } + + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{ + Timeout: 10 * time.Second, + } + + resp, err := client.Do(req) + if err != nil { + return fmt.Errorf("发送请求失败: %w", err) + } + defer resp.Body.Close() + + // 读取响应 + respBody, _ := io.ReadAll(resp.Body) + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return fmt.Errorf("请求失败,状态码: %d, 响应: %s", resp.StatusCode, string(respBody)) + } + + n.logger.Info("通知发送成功", zap.String("url", url), zap.String("response", string(respBody))) + return nil +} diff --git a/internal/service/user_service.go b/internal/service/user_service.go new file mode 100644 index 0000000..afe5d25 --- /dev/null +++ b/internal/service/user_service.go @@ -0,0 +1,197 @@ +package service + +import ( + "context" + "errors" + "time" + + "github.com/dushixiang/pika/internal/models" + "github.com/dushixiang/pika/internal/repo" + "github.com/google/uuid" + "go.uber.org/zap" + "golang.org/x/crypto/bcrypt" +) + +type UserService struct { + logger *zap.Logger + userRepo *repo.UserRepo + Repository *repo.UserRepo +} + +func NewUserService(logger *zap.Logger, userRepo *repo.UserRepo) *UserService { + return &UserService{ + logger: logger, + userRepo: userRepo, + Repository: userRepo, + } +} + +// CreateUser 创建用户 +func (s *UserService) CreateUser(ctx context.Context, req *CreateUserRequest) (*models.User, error) { + // 检查用户名是否已存在 + existingUser, err := s.userRepo.FindByUsername(ctx, req.Username) + if err == nil && existingUser != nil { + return nil, errors.New("用户名已存在") + } + // 如果不是"记录不存在"的错误,说明是数据库错误 + if err != nil && err.Error() != "record not found" { + s.logger.Error("查询用户名失败", zap.Error(err), zap.String("username", req.Username)) + return nil, errors.New("系统错误,请稍后重试") + } + + // 加密密码 + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) + if err != nil { + return nil, err + } + + now := time.Now().UnixMilli() + user := &models.User{ + ID: uuid.NewString(), + Username: req.Username, + Password: string(hashedPassword), + Nickname: req.Nickname, + CreatedAt: now, + UpdatedAt: now, + } + + if err := s.userRepo.Create(ctx, user); err != nil { + return nil, err + } + + s.logger.Info("user created successfully", zap.String("userID", user.ID), zap.String("username", user.Username)) + return user, nil +} + +// UpdateUser 更新用户 +func (s *UserService) UpdateUser(ctx context.Context, userID string, req *UpdateUserRequest) (*models.User, error) { + user, err := s.userRepo.FindById(ctx, userID) + if err != nil { + return nil, errors.New("用户不存在") + } + + // 更新字段 + if req.Nickname != "" { + user.Nickname = req.Nickname + } + + if err := s.userRepo.UpdateById(ctx, &user); err != nil { + return nil, err + } + + s.logger.Info("user updated successfully", zap.String("userID", userID)) + return &user, nil +} + +// DeleteUser 删除用户 +func (s *UserService) DeleteUser(ctx context.Context, userID string) error { + if err := s.userRepo.DeleteById(ctx, userID); err != nil { + return err + } + + s.logger.Info("user deleted successfully", zap.String("userID", userID)) + return nil +} + +// GetUser 获取用户信息 +func (s *UserService) GetUser(ctx context.Context, userID string) (*models.User, error) { + user, err := s.userRepo.FindById(ctx, userID) + if err != nil { + return nil, errors.New("用户不存在") + } + return &user, nil +} + +// GetUserByUsername 根据用户名获取用户 +func (s *UserService) GetUserByUsername(ctx context.Context, username string) (*models.User, error) { + return s.userRepo.FindByUsername(ctx, username) +} + +// ListUsers 列出用户 +func (s *UserService) ListUsers(ctx context.Context, page, pageSize int) ([]models.User, int64, error) { + offset := (page - 1) * pageSize + return s.userRepo.ListUsers(ctx, offset, pageSize) +} + +// ChangePassword 修改密码 +func (s *UserService) ChangePassword(ctx context.Context, userID string, oldPassword, newPassword string) error { + user, err := s.userRepo.FindById(ctx, userID) + if err != nil { + return errors.New("用户不存在") + } + + // 验证旧密码 + if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(oldPassword)); err != nil { + return errors.New("旧密码错误") + } + + // 加密新密码 + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost) + if err != nil { + return err + } + + if err := s.userRepo.UpdatePassword(ctx, userID, string(hashedPassword)); err != nil { + return err + } + + s.logger.Info("password changed successfully", zap.String("userID", userID)) + return nil +} + +// ResetPassword 重置密码(管理员操作) +func (s *UserService) ResetPassword(ctx context.Context, userID, newPassword string) error { + // 加密新密码 + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost) + if err != nil { + return err + } + + if err := s.userRepo.UpdatePassword(ctx, userID, string(hashedPassword)); err != nil { + return err + } + + s.logger.Info("password reset successfully", zap.String("userID", userID)) + return nil +} + +// UpdateUserStatus 更新用户状态 +func (s *UserService) UpdateUserStatus(ctx context.Context, userID string, status int) error { + if err := s.userRepo.UpdateStatus(ctx, userID, status); err != nil { + return err + } + + s.logger.Info("user status updated successfully", zap.String("userID", userID), zap.Int("status", status)) + return nil +} + +// ValidatePassword 验证密码 +func (s *UserService) ValidatePassword(ctx context.Context, username, password string) (*models.User, error) { + user, err := s.userRepo.FindByUsername(ctx, username) + if err != nil { + return nil, errors.New("用户名或密码错误") + } + + // 验证密码 + if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil { + return nil, errors.New("用户名或密码错误") + } + + return user, nil +} + +// 数据结构定义 +type CreateUserRequest struct { + Username string `json:"username" validate:"required"` + Password string `json:"password" validate:"required,min=6"` + Nickname string `json:"nickname"` +} + +type UpdateUserRequest struct { + Nickname string `json:"nickname"` +} + +type ChangePasswordRequest struct { + OldPassword string `json:"oldPassword" validate:"required"` + NewPassword string `json:"newPassword" validate:"required,min=6"` +} diff --git a/internal/validator.go b/internal/validator.go new file mode 100644 index 0000000..50a2acb --- /dev/null +++ b/internal/validator.go @@ -0,0 +1,51 @@ +package internal + +import ( + "fmt" + "net/http" + "strings" + + "github.com/go-playground/locales/en" + "github.com/go-playground/locales/zh" + ut "github.com/go-playground/universal-translator" + "github.com/go-playground/validator/v10" + zhTranslations "github.com/go-playground/validator/v10/translations/zh" + "github.com/labstack/echo/v4" +) + +type CustomValidator struct { + Validator *validator.Validate + trans ut.Translator +} + +func (cv *CustomValidator) TransInit() error { + zhT := zh.New() //chinese + enT := en.New() //english + uni := ut.New(enT, zhT, enT) + + trans, ok := uni.GetTranslator("zh") + if !ok { + return fmt.Errorf("uni.GetTranslator zh failed") + } + cv.trans = trans + //register translate + // 注册翻译器 + return zhTranslations.RegisterDefaultTranslations(cv.Validator, trans) +} + +func (cv *CustomValidator) Validate(i interface{}) error { + if err := cv.Validator.Struct(i); err != nil { + errs, ok := err.(validator.ValidationErrors) + if !ok { + return err + } + translate := errs.Translate(cv.trans) + var messages []string + for _, msg := range translate { + messages = append(messages, msg) + } + // Optionally, you could return the error to give each route more control over the status code + return echo.NewHTTPError(http.StatusBadRequest, strings.Join(messages, ",")) + } + return nil +} diff --git a/internal/websocket/manager.go b/internal/websocket/manager.go new file mode 100644 index 0000000..3c4afc2 --- /dev/null +++ b/internal/websocket/manager.go @@ -0,0 +1,286 @@ +package websocket + +import ( + "context" + "encoding/json" + "sync" + "time" + + "github.com/dushixiang/pika/internal/protocol" + "github.com/gorilla/websocket" + "go.uber.org/zap" +) + +// Client WebSocket客户端 +type Client struct { + ID string // 探针ID + Conn *websocket.Conn // WebSocket连接 + Send chan []byte // 发送消息通道 + Manager *Manager // 管理器引用 + LastActive time.Time // 最后活跃时间 + closed bool // 标记channel是否已关闭 + closeMu sync.Mutex // 保护closed字段 +} + +// Manager WebSocket连接管理器 +type Manager struct { + clients map[string]*Client // 客户端映射 probeID -> Client + register chan *Client // 注册通道 + unregister chan *Client // 注销通道 + broadcast chan []byte // 广播通道 + mu sync.RWMutex // 读写锁 + logger *zap.Logger // 日志 + onMessage MessageHandler // 消息处理器 +} + +// MessageHandler 消息处理器接口 +type MessageHandler func(ctx context.Context, probeID string, messageType string, data json.RawMessage) error + +// NewManager 创建新的WebSocket管理器 +func NewManager(logger *zap.Logger) *Manager { + return &Manager{ + clients: make(map[string]*Client), + register: make(chan *Client, 10), + unregister: make(chan *Client, 10), + broadcast: make(chan []byte, 256), + logger: logger, + } +} + +// SetMessageHandler 设置消息处理器 +func (m *Manager) SetMessageHandler(handler MessageHandler) { + m.onMessage = handler +} + +// Run 启动管理器 +func (m *Manager) Run(ctx context.Context) { + ticker := time.NewTicker(30 * time.Second) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + m.logger.Info("websocket manager stopped") + return + case client := <-m.register: + m.registerClient(client) + case client := <-m.unregister: + m.unregisterClient(client) + case message := <-m.broadcast: + m.broadcastMessage(message) + case <-ticker.C: + m.checkInactiveClients() + } + } +} + +// Register 注册客户端(公开方法) +func (m *Manager) Register(client *Client) { + m.register <- client +} + +// registerClient 注册客户端 +func (m *Manager) registerClient(client *Client) { + m.mu.Lock() + defer m.mu.Unlock() + + // 如果已存在该探针的连接,先关闭旧连接 + if oldClient, exists := m.clients[client.ID]; exists { + m.logger.Info("agent reconnected, closing old connection", zap.String("agentID", client.ID)) + oldClient.closeChannel() + oldClient.Conn.Close() + } + + m.clients[client.ID] = client + m.logger.Info("agent connected", zap.String("agentID", client.ID), zap.Int("totalClients", len(m.clients))) +} + +// unregisterClient 注销客户端 +func (m *Manager) unregisterClient(client *Client) { + m.mu.Lock() + defer m.mu.Unlock() + + if _, exists := m.clients[client.ID]; exists { + delete(m.clients, client.ID) + client.closeChannel() + m.logger.Info("agent disconnected", zap.String("agentID", client.ID), zap.Int("totalClients", len(m.clients))) + } +} + +// broadcastMessage 广播消息 +func (m *Manager) broadcastMessage(message []byte) { + m.mu.RLock() + defer m.mu.RUnlock() + + for _, client := range m.clients { + select { + case client.Send <- message: + default: + // 发送失败,客户端可能已断开 + m.logger.Warn("failed to send message, client may be disconnected", zap.String("agentID", client.ID)) + } + } +} + +// checkInactiveClients 检查不活跃的客户端 +func (m *Manager) checkInactiveClients() { + m.mu.RLock() + inactiveClients := make([]*Client, 0) + timeout := 2 * time.Minute + + for _, client := range m.clients { + if time.Since(client.LastActive) > timeout { + inactiveClients = append(inactiveClients, client) + } + } + m.mu.RUnlock() + + // 断开不活跃的客户端 + for _, client := range inactiveClients { + // 再次检查客户端是否仍然存在(避免竞态条件) + m.mu.RLock() + _, exists := m.clients[client.ID] + m.mu.RUnlock() + + if exists { + m.logger.Warn("agent inactive timeout, disconnecting", zap.String("agentID", client.ID)) + client.Conn.Close() + m.unregister <- client + } + } +} + +// SendToClient 发送消息给指定客户端 +func (m *Manager) SendToClient(probeID string, message []byte) error { + m.mu.RLock() + client, exists := m.clients[probeID] + m.mu.RUnlock() + + if !exists { + return ErrClientNotFound + } + + select { + case client.Send <- message: + return nil + default: + return ErrSendTimeout + } +} + +// GetClient 获取客户端 +func (m *Manager) GetClient(probeID string) (*Client, bool) { + m.mu.RLock() + defer m.mu.RUnlock() + client, exists := m.clients[probeID] + return client, exists +} + +// GetAllClients 获取所有客户端ID +func (m *Manager) GetAllClients() []string { + m.mu.RLock() + defer m.mu.RUnlock() + + ids := make([]string, 0, len(m.clients)) + for id := range m.clients { + ids = append(ids, id) + } + return ids +} + +// ClientCount 获取客户端数量 +func (m *Manager) ClientCount() int { + m.mu.RLock() + defer m.mu.RUnlock() + return len(m.clients) +} + +// ReadPump 读取客户端消息 +func (c *Client) ReadPump(ctx context.Context) { + defer func() { + c.Manager.unregister <- c + c.Conn.Close() + }() + + c.Conn.SetReadDeadline(time.Now().Add(60 * time.Second)) + c.Conn.SetPongHandler(func(string) error { + c.Conn.SetReadDeadline(time.Now().Add(60 * time.Second)) + c.LastActive = time.Now() + return nil + }) + + for { + _, message, err := c.Conn.ReadMessage() + if err != nil { + if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { + c.Manager.logger.Error("websocket read error", zap.Error(err), zap.String("agentID", c.ID)) + } + break + } + + c.LastActive = time.Now() + + // 解析消息 + var msg protocol.Message + if err := json.Unmarshal(message, &msg); err != nil { + c.Manager.logger.Error("failed to parse message", zap.Error(err), zap.String("agentID", c.ID)) + continue + } + + // 处理消息 + if c.Manager.onMessage != nil { + if err := c.Manager.onMessage(ctx, c.ID, string(msg.Type), msg.Data); err != nil { + c.Manager.logger.Error("failed to handle message", zap.Error(err), zap.String("agentID", c.ID), zap.String("type", string(msg.Type))) + } + } + } +} + +// WritePump 向客户端写入消息 +func (c *Client) WritePump() { + ticker := time.NewTicker(30 * time.Second) + defer func() { + ticker.Stop() + c.Conn.Close() + }() + + for { + select { + case message, ok := <-c.Send: + c.Conn.SetWriteDeadline(time.Now().Add(10 * time.Second)) + if !ok { + // 通道已关闭 + c.Conn.WriteMessage(websocket.CloseMessage, []byte{}) + return + } + + if err := c.Conn.WriteMessage(websocket.TextMessage, message); err != nil { + c.Manager.logger.Error("failed to write message", zap.Error(err), zap.String("agentID", c.ID)) + return + } + + case <-ticker.C: + c.Conn.SetWriteDeadline(time.Now().Add(10 * time.Second)) + if err := c.Conn.WriteMessage(websocket.PingMessage, nil); err != nil { + return + } + } + } +} + +// closeChannel 安全地关闭发送通道 +func (c *Client) closeChannel() { + c.closeMu.Lock() + defer c.closeMu.Unlock() + + if !c.closed { + close(c.Send) + c.closed = true + } +} + +// 错误定义 +var ( + ErrClientNotFound = &websocket.CloseError{Code: 1000, Text: "client not found"} + ErrSendTimeout = &websocket.CloseError{Code: 1001, Text: "send timeout"} +) diff --git a/internal/wire.go b/internal/wire.go new file mode 100644 index 0000000..a1ee9de --- /dev/null +++ b/internal/wire.go @@ -0,0 +1,64 @@ +//go:build wireinject +// +build wireinject + +package internal + +import ( + "github.com/dushixiang/pika/internal/config" + "github.com/dushixiang/pika/internal/handler" + "github.com/dushixiang/pika/internal/repo" + "github.com/dushixiang/pika/internal/service" + "github.com/dushixiang/pika/internal/websocket" + "github.com/google/wire" + "go.uber.org/zap" + "gorm.io/gorm" +) + +// InitializeApp 初始化应用 +func InitializeApp(logger *zap.Logger, db *gorm.DB, cfg *config.AppConfig) (*AppComponents, error) { + wire.Build( + // Repositories + repo.NewAgentRepo, + repo.NewMetricRepo, + repo.NewUserRepo, + provideApiKeyRepo, + provideAlertRepo, + + // Services + provideApiKeyService, + provideAgentService, + service.NewUserService, + provideNotifier, + provideAlertService, + + // Providers for services with config + provideAccountService, + provideAgentHandler, + provideApiKeyHandler, + provideAlertHandler, + + // WebSocket Manager + websocket.NewManager, + + // Handlers + handler.NewAccountHandler, + handler.NewUserHandler, + + // App Components + wire.Struct(new(AppComponents), "*"), + ) + return nil, nil +} + +// AppComponents 应用组件 +type AppComponents struct { + AccountHandler *handler.AccountHandler + AgentHandler *handler.AgentHandler + UserHandler *handler.UserHandler + ApiKeyHandler *handler.ApiKeyHandler + AlertHandler *handler.AlertHandler + AgentService *service.AgentService + UserService *service.UserService + AlertService *service.AlertService + WSManager *websocket.Manager +} diff --git a/internal/wire_gen.go b/internal/wire_gen.go new file mode 100644 index 0000000..673a8ec --- /dev/null +++ b/internal/wire_gen.go @@ -0,0 +1,67 @@ +// Code generated by Wire. DO NOT EDIT. + +//go:generate go run -mod=mod github.com/google/wire/cmd/wire +//go:build !wireinject +// +build !wireinject + +package internal + +import ( + "github.com/dushixiang/pika/internal/config" + "github.com/dushixiang/pika/internal/handler" + "github.com/dushixiang/pika/internal/repo" + "github.com/dushixiang/pika/internal/service" + "github.com/dushixiang/pika/internal/websocket" + "go.uber.org/zap" + "gorm.io/gorm" +) + +// Injectors from wire.go: + +// InitializeApp 初始化应用 +func InitializeApp(logger *zap.Logger, db *gorm.DB, cfg *config.AppConfig) (*AppComponents, error) { + userRepo := repo.NewUserRepo(db) + userService := service.NewUserService(logger, userRepo) + accountService := provideAccountService(logger, userService, cfg) + accountHandler := handler.NewAccountHandler(accountService) + agentRepo := repo.NewAgentRepo(db) + metricRepo := repo.NewMetricRepo(db) + apiKeyRepo := provideApiKeyRepo(db) + apiKeyService := provideApiKeyService(logger, apiKeyRepo) + agentService := provideAgentService(logger, agentRepo, metricRepo, apiKeyService) + manager := websocket.NewManager(logger) + agentHandler := provideAgentHandler(logger, agentService, manager, cfg) + userHandler := handler.NewUserHandler(userService) + apiKeyHandler := provideApiKeyHandler(logger, apiKeyService) + alertRepo := provideAlertRepo(db) + notifier := provideNotifier(logger) + alertService := provideAlertService(alertRepo, agentRepo, notifier, logger) + alertHandler := provideAlertHandler(logger, alertService) + appComponents := &AppComponents{ + AccountHandler: accountHandler, + AgentHandler: agentHandler, + UserHandler: userHandler, + ApiKeyHandler: apiKeyHandler, + AlertHandler: alertHandler, + AgentService: agentService, + UserService: userService, + AlertService: alertService, + WSManager: manager, + } + return appComponents, nil +} + +// wire.go: + +// AppComponents 应用组件 +type AppComponents struct { + AccountHandler *handler.AccountHandler + AgentHandler *handler.AgentHandler + UserHandler *handler.UserHandler + ApiKeyHandler *handler.ApiKeyHandler + AlertHandler *handler.AlertHandler + AgentService *service.AgentService + UserService *service.UserService + AlertService *service.AlertService + WSManager *websocket.Manager +} diff --git a/pkg/agent/audit/audit.go b/pkg/agent/audit/audit.go new file mode 100644 index 0000000..418f56a --- /dev/null +++ b/pkg/agent/audit/audit.go @@ -0,0 +1,2606 @@ +package audit + +import ( + "bufio" + "bytes" + "crypto/sha256" + "encoding/hex" + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + "runtime" + "strconv" + "strings" + "time" + + "github.com/dushixiang/pika/internal/protocol" + "github.com/shirou/gopsutil/v4/host" + gopsutilNet "github.com/shirou/gopsutil/v4/net" + "github.com/shirou/gopsutil/v4/process" +) + +const ( + StatusPass string = "pass" + StatusFail string = "fail" + StatusWarn string = "warn" + StatusSkip string = "skip" +) + +// RunAudit 执行 VPS 安全审计 +func RunAudit() (*protocol.VPSAuditResult, error) { + startTime := time.Now().UnixMilli() + + // 检查操作系统 + if runtime.GOOS != "linux" { + return nil, fmt.Errorf("只支持 Linux 系统") + } + + // 检查运行权限 - root权限可以执行更完整的审计 + if os.Geteuid() != 0 { + return nil, fmt.Errorf("需要root权限运行完整审计") + } + + // 获取系统信息 + systemInfo, err := getSystemInfo() + if err != nil { + return nil, fmt.Errorf("获取系统信息失败: %w", err) + } + + // 执行安全检查(从应急响应角度) + checks := []SecurityCheckFunc{ + checkRootkit, // rootkit检测 + checkSuspiciousProcesses, // 可疑进程 + checkSSHSecurity, // SSH安全 + checkListeningPorts, // 异常端口 + checkCronJobs, // 定时任务 + checkSuspiciousFiles, // 可疑文件 + checkSystemAccounts, // 系统账户 + checkNetworkConnections, // 网络连接 + checkFileIntegrity, // 系统文件完整性 + checkLoginHistory, // 登录历史 + checkImmutableFiles, // 不可变文件(优化版) + checkSuspiciousEnvVars, // 进程环境变量 + } + + var securityChecks []protocol.SecurityCheck + for _, checkFunc := range checks { + check := checkFunc() + securityChecks = append(securityChecks, check) + } + + endTime := time.Now().UnixMilli() + + // 计算风险评分 + riskScore, threatLevel, recommendations := calculateRiskScore(securityChecks) + + return &protocol.VPSAuditResult{ + SystemInfo: *systemInfo, + SecurityChecks: securityChecks, + StartTime: startTime, + EndTime: endTime, + RiskScore: riskScore, + ThreatLevel: threatLevel, + Recommendations: recommendations, + }, nil +} + +type SecurityCheckFunc func() protocol.SecurityCheck + +// getSystemInfo 获取系统信息 +func getSystemInfo() (*protocol.SystemInfo, error) { + hostname, _ := os.Hostname() + if hostname == "" { + hostname = "unknown" + } + + info, err := host.Info() + if err != nil { + return nil, err + } + + osInfo := fmt.Sprintf("%s %s", info.Platform, info.PlatformVersion) + if info.PlatformFamily != "" { + osInfo = fmt.Sprintf("%s (%s)", osInfo, info.PlatformFamily) + } + + return &protocol.SystemInfo{ + Hostname: hostname, + OS: osInfo, + KernelVersion: info.KernelVersion, + Uptime: info.Uptime, + }, nil +} + +// checkRootkit 检测rootkit和恶意内核模块 +func checkRootkit() protocol.SecurityCheck { + check := protocol.SecurityCheck{ + Category: "rootkit_detection", + Status: StatusPass, + Message: "Rootkit检测", + Details: []protocol.SecurityCheckSub{}, + } + + // 检查加载的内核模块 + suspiciousModules := checkKernelModules() + if len(suspiciousModules) > 0 { + check.Status = StatusWarn + for _, mod := range suspiciousModules { + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "suspicious_module", + Status: StatusWarn, + Message: fmt.Sprintf("可疑内核模块: %s", mod), + }) + } + } else { + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "kernel_modules", + Status: StatusPass, + Message: "未发现可疑内核模块", + }) + } + + // 检查隐藏进程 + hiddenProcs := checkHiddenProcesses() + if len(hiddenProcs) > 0 { + check.Status = StatusFail + for _, pid := range hiddenProcs { + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "hidden_process", + Status: StatusFail, + Message: fmt.Sprintf("检测到隐藏进程: PID %d", pid), + }) + } + } else { + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "hidden_processes", + Status: StatusPass, + Message: "未发现隐藏进程", + }) + } + + if check.Status == StatusPass { + check.Message = "未检测到rootkit" + } else if check.Status == StatusWarn { + check.Message = "发现可疑rootkit特征" + } else { + check.Message = "发现rootkit感染迹象" + } + + return check +} + +// checkSuspiciousEnvVars 检查进程的可疑环境变量 +func checkSuspiciousEnvVars() protocol.SecurityCheck { + check := protocol.SecurityCheck{ + Category: "suspicious_env_vars", + Status: StatusPass, + Message: "进程环境变量检查", + Details: []protocol.SecurityCheckSub{}, + } + + procs, err := process.Processes() + if err != nil { + check.Status = StatusSkip + check.Message = "无法获取进程列表" + return check + } + + suspiciousCount := 0 + for _, p := range procs { + env, err := p.Environ() + if err != nil { + continue + } + + name, _ := p.Name() + exe, _ := p.Exe() + + for _, e := range env { + if strings.HasPrefix(e, "LD_PRELOAD=") || strings.HasPrefix(e, "LD_LIBRARY_PATH=") { + parts := strings.SplitN(e, "=", 2) + if len(parts) != 2 { + continue + } + value := parts[1] + + // 检查是否是合法的库路径 + if isLegitimateLibraryPath(value, name, exe) { + continue + } + + // 评估风险等级 + riskLevel := assessLibraryRisk(value) + status := StatusWarn + if riskLevel == "high" { + status = StatusFail + check.Status = StatusFail + } else if check.Status != StatusFail { + check.Status = StatusWarn + } + + suspiciousCount++ + + // 收集证据 + evidence := collectProcessEvidence(p, riskLevel) + + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "suspicious_ld_env", + Status: status, + Message: fmt.Sprintf("进程 '%s'(PID: %d) 发现可疑环境变量: %s (风险等级: %s)", name, p.Pid, e, riskLevel), + Evidence: evidence, + }) + // 找到一个就够了 + break + } + } + } + + if suspiciousCount == 0 { + check.Message = "未发现可疑的进程环境变量" + } else { + check.Message = fmt.Sprintf("发现 %d 个进程存在可疑环境变量", suspiciousCount) + } + + return check +} + +// checkSuspiciousProcesses 检查可疑进程 +func checkSuspiciousProcesses() protocol.SecurityCheck { + check := protocol.SecurityCheck{ + Category: "suspicious_processes", + Status: StatusPass, + Message: "可疑进程检测", + Details: []protocol.SecurityCheckSub{}, + } + + procs, err := process.Processes() + if err != nil { + check.Status = StatusSkip + check.Message = "无法获取进程列表" + return check + } + + suspiciousCount := 0 + + for _, p := range procs { + name, _ := p.Name() + cmdline, _ := p.Cmdline() + exe, _ := p.Exe() + + // 检查挖矿程序 + if isMinerProcess(name, cmdline) { + suspiciousCount++ + check.Status = StatusFail + + // 收集证据 + evidence := collectProcessEvidence(p, "high") + + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "miner_process", + Status: StatusFail, + Message: fmt.Sprintf("检测到挖矿程序: %s (PID: %d)", name, p.Pid), + Evidence: evidence, + }) + } + + // 检查反弹shell + if isReverseShell(cmdline) { + suspiciousCount++ + check.Status = StatusFail + + // 收集证据 + evidence := collectProcessEvidence(p, "high") + + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "reverse_shell", + Status: StatusFail, + Message: fmt.Sprintf("检测到反弹shell: %s (PID: %d)", cmdline, p.Pid), + Evidence: evidence, + }) + } + + // 检查无文件进程(memfd_create等) + if strings.Contains(exe, "memfd:") || strings.Contains(exe, "(deleted)") { + suspiciousCount++ + check.Status = StatusWarn + + // 收集证据 + evidence := collectProcessEvidence(p, "high") + + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "fileless_process", + Status: StatusWarn, + Message: fmt.Sprintf("检测到无文件进程: %s (PID: %d, 路径: %s)", name, p.Pid, exe), + Evidence: evidence, + }) + } + + // 检查进程名称和可执行文件不匹配(进程伪装) + // 改进的伪装检测逻辑 + if exe != "" && name != "" && !strings.Contains(exe, "(deleted)") { + baseName := filepath.Base(exe) + + // 更精确的匹配逻辑 + nameMatch := (baseName == name) || + strings.HasPrefix(name, baseName) || + strings.Contains(name, ":") // PostgreSQL等进程有冒号后缀 + + if !nameMatch && !strings.Contains(exe, name) { + // 扩展白名单 + whitelistPatterns := []string{ + "python", "java", "node", "ruby", "perl", "php", + "bash", "sh", "zsh", "ksh", "fish", + "systemd", "containerd", "docker", "podman", + "postgres", "mysqld", "redis", // 数据库worker进程 + "nginx", "httpd", "apache2", // Web服务器worker + } + + isWhitelisted := false + nameLower := strings.ToLower(name) + baseNameLower := strings.ToLower(baseName) + + for _, pattern := range whitelistPatterns { + if strings.Contains(nameLower, pattern) || + strings.Contains(baseNameLower, pattern) { + isWhitelisted = true + break + } + } + + // 检查是否是符号链接导致的不匹配(常见于busybox等) + if !isWhitelisted { + // 只有当路径看起来可疑时才报警 + if isSuspiciousPath(exe) { + suspiciousCount++ + check.Status = StatusWarn + + evidence := collectProcessEvidence(p, "medium") + + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "process_masquerading", + Status: StatusWarn, + Message: fmt.Sprintf("可疑的进程名称不匹配: 名称=%s, 路径=%s (PID: %d)", name, exe, p.Pid), + Evidence: evidence, + }) + } + } + } + } + } + + if suspiciousCount == 0 { + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "clean", + Status: StatusPass, + Message: "未发现可疑进程", + }) + check.Message = "进程正常" + } else { + check.Message = fmt.Sprintf("发现 %d 个可疑进程", suspiciousCount) + } + + return check +} + +// checkSSHSecurity 检查SSH后门和安全配置(增强版) +func checkSSHSecurity() protocol.SecurityCheck { + check := protocol.SecurityCheck{ + Category: "ssh_security", + Status: StatusPass, + Message: "SSH安全检查", + Details: []protocol.SecurityCheckSub{}, + } + + // 检查SSH服务是否运行 + if !isProcessRunning("sshd") { + check.Message = "SSH服务未运行" + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "ssh_status", + Status: StatusPass, + Message: "SSH未运行", + }) + return check + } + + // 1. 检查authorized_keys文件内容(新增) + suspiciousKeys := checkAuthorizedKeysContent() + if len(suspiciousKeys) > 0 { + check.Status = StatusWarn + for _, key := range suspiciousKeys { + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "suspicious_authorized_key", + Status: StatusWarn, + Message: key, + }) + } + } + + // 2. 检查SSH配置 + sshConfig := readSSHConfig() + + // 检查是否允许root登录 + permitRoot := sshConfig["permitrootlogin"] // 转小写统一处理 + if permitRoot == "yes" { + check.Status = StatusWarn + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "permit_root_login", + Status: StatusWarn, + Message: "允许root SSH登录", + }) + } else { + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "permit_root_login", + Status: StatusPass, + Message: fmt.Sprintf("Root登录策略: %s", permitRoot), + }) + } + + // 检查空密码登录 + permitEmpty := sshConfig["permitemptypasswords"] + if permitEmpty == "yes" { + check.Status = StatusFail + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "permit_empty_passwords", + Status: StatusFail, + Message: "允许空密码登录", + }) + } + + // 检查密码认证是否启用(建议使用密钥) + passwordAuth := sshConfig["passwordauthentication"] + if passwordAuth == "yes" { + check.Status = StatusWarn + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "password_auth_enabled", + Status: StatusWarn, + Message: "启用了密码认证(建议仅使用密钥认证)", + }) + } + + // 3. 检查SSH后门进程 + suspiciousSshd := checkSuspiciousSSHD() + if len(suspiciousSshd) > 0 { + check.Status = StatusFail + for _, sshd := range suspiciousSshd { + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "backdoor_sshd", + Status: StatusFail, + Message: sshd, + }) + } + } + + // 4. 检查SSH二进制文件完整性(新增) + sshdBinaries := []string{"/usr/sbin/sshd", "/usr/bin/ssh"} + for _, binPath := range sshdBinaries { + info, err := os.Stat(binPath) + if err != nil { + continue + } + + // 检查最近修改 + modTime := info.ModTime() + age := time.Since(modTime) + if age < 30*24*time.Hour { // 最近30天 + fileHash := calculateSHA256(binPath) + check.Status = StatusWarn + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "ssh_binary_modified", + Status: StatusWarn, + Message: fmt.Sprintf("SSH二进制文件最近被修改: %s (%d天前)", binPath, int(age.Hours()/24)), + Evidence: &protocol.Evidence{ + FilePath: binPath, + FileHash: fileHash, + Timestamp: modTime.UnixMilli(), + RiskLevel: "high", + }, + }) + } + } + + // 5. 检查SSH配置文件的软链接(新增,检测配置劫持) + sshConfigPaths := []string{"/etc/ssh/sshd_config"} + for _, configPath := range sshConfigPaths { + linkTarget, err := os.Readlink(configPath) + if err == nil { + // 是软链接,检查目标是否可疑 + if strings.Contains(linkTarget, "/tmp") || strings.Contains(linkTarget, "/dev/shm") { + check.Status = StatusFail + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "ssh_config_symlink", + Status: StatusFail, + Message: fmt.Sprintf("SSH配置文件是指向可疑位置的软链接: %s -> %s", configPath, linkTarget), + }) + } + } + } + + if check.Status == StatusPass { + check.Message = "SSH配置安全" + } else if check.Status == StatusWarn { + check.Message = "SSH配置存在风险" + } else { + check.Message = "检测到SSH后门" + } + + return check +} + +// checkListeningPorts 检查异常监听端口(增强版:进程-端口合理性分析) +func checkListeningPorts() protocol.SecurityCheck { + check := protocol.SecurityCheck{ + Category: "listening_ports", + Status: StatusPass, + Message: "端口监听检查", + Details: []protocol.SecurityCheckSub{}, + } + + // 使用gopsutil获取监听端口及进程信息 + connections, err := gopsutilNet.Connections("all") + if err != nil { + check.Status = StatusSkip + check.Message = "无法获取网络连接信息" + return check + } + + // 反弹shell/恶意工具常用端口(降低优先级,减少误报) + suspiciousPorts := map[uint32]string{ + 4444: "Metasploit默认端口", + 1337: "黑客常用端口", + 31337: "黑客常用端口", + } + + // 常见服务的合法端口范围 + legitimatePortRanges := map[string][]uint32{ + "nginx": {80, 443, 8080, 8443}, + "apache": {80, 443, 8080, 8443}, + "httpd": {80, 443, 8080, 8443}, + "mysql": {3306, 33060}, + "mariadb": {3306, 33060}, + "postgres": {5432, 5433}, + "redis": {6379, 6380}, + "mongodb": {27017, 27018, 27019}, + "ssh": {22, 2222}, + "sshd": {22, 2222}, + "docker": {2375, 2376, 2377}, + "containerd": {2375, 2376}, + } + + suspiciousCount := 0 + totalListening := 0 + publicListening := 0 + + for _, conn := range connections { + if conn.Status != "LISTEN" { + continue + } + + totalListening++ + port := conn.Laddr.Port + listenAddr := conn.Laddr.IP + pid := conn.Pid + + // 本地回环监听的服务一般是安全的,跳过 + if listenAddr == "127.0.0.1" || listenAddr == "::1" { + continue + } + + publicListening++ + + // 获取进程信息 + if pid > 0 { + proc, err := process.NewProcess(int32(pid)) + if err != nil { + continue + } + + name, _ := proc.Name() + exe, _ := proc.Exe() + cmdline, _ := proc.Cmdline() + + // 检查1: 高危端口 + if desc, exists := suspiciousPorts[port]; exists { + suspiciousCount++ + check.Status = StatusWarn + + evidence := collectProcessEvidence(proc, "high") + + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: fmt.Sprintf("suspicious_port_%d", port), + Status: StatusWarn, + Message: fmt.Sprintf("高危端口监听: %s:%d (%s) - 进程: %s (PID: %d)", listenAddr, port, desc, name, pid), + Evidence: evidence, + }) + continue + } + + // 检查2: 挖矿程序监听端口 + if isMinerProcess(name, cmdline) { + suspiciousCount++ + check.Status = StatusFail + + evidence := collectProcessEvidence(proc, "high") + + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: fmt.Sprintf("miner_listener_%d", port), + Status: StatusFail, + Message: fmt.Sprintf("挖矿程序监听端口: %s 监听 %s:%d (PID: %d)", name, listenAddr, port, pid), + Evidence: evidence, + }) + continue + } + + // 检查3: 可疑路径的程序监听端口 + if isSuspiciousPath(exe) { + suspiciousCount++ + check.Status = StatusFail + + evidence := collectProcessEvidence(proc, "high") + + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: fmt.Sprintf("suspicious_exe_listener_%d", port), + Status: StatusFail, + Message: fmt.Sprintf("可疑路径程序监听端口: %s 监听 %s:%d (PID: %d, 路径: %s)", name, listenAddr, port, pid, exe), + Evidence: evidence, + }) + continue + } + + // 检查4: 进程与端口的合理性(新增) + // 例如: MySQL进程监听80端口是异常的 + nameLower := strings.ToLower(name) + isReasonable := true + + // 检查是否是已知服务 + if validPorts, exists := legitimatePortRanges[nameLower]; exists { + isReasonable = false + for _, validPort := range validPorts { + if port == validPort { + isReasonable = true + break + } + } + + // 如果不合理,报告 + if !isReasonable { + suspiciousCount++ + check.Status = StatusWarn + + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: fmt.Sprintf("port_mismatch_%d", port), + Status: StatusWarn, + Message: fmt.Sprintf("进程端口不匹配: %s 监听异常端口 %s:%d (预期端口: %v)", name, listenAddr, port, validPorts), + }) + continue + } + } + + // 检查5: 0.0.0.0监听高危端口(新增) + // 某些服务应该只监听localhost + if listenAddr == "0.0.0.0" || listenAddr == "::" { + dangerousPublicPorts := []uint32{3306, 5432, 6379, 27017, 9200, 5601, 11211} + for _, dangerousPort := range dangerousPublicPorts { + if port == dangerousPort { + suspiciousCount++ + check.Status = StatusWarn + + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: fmt.Sprintf("public_db_port_%d", port), + Status: StatusWarn, + Message: fmt.Sprintf("数据库服务公网监听: %s 在 0.0.0.0:%d 监听 (建议仅监听127.0.0.1)", name, port), + }) + break + } + } + } + } + } + + if suspiciousCount == 0 { + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "safe_ports", + Status: StatusPass, + Message: fmt.Sprintf("监听%d个端口(公网%d个),未发现可疑监听", totalListening, publicListening), + }) + check.Message = "端口监听正常" + } else { + check.Message = fmt.Sprintf("发现 %d 个可疑端口监听", suspiciousCount) + } + + return check +} + +// isSuspiciousPath 检查可执行文件路径是否可疑 +func isSuspiciousPath(path string) bool { + if path == "" { + return false + } + + suspiciousPaths := []string{ + "/tmp/", + "/dev/shm/", + "/var/tmp/", + "memfd:", + "(deleted)", + } + + for _, sp := range suspiciousPaths { + if strings.Contains(path, sp) { + return true + } + } + + return false +} + +// checkCronJobs 检查定时任务(包括systemd timer) +func checkCronJobs() protocol.SecurityCheck { + check := protocol.SecurityCheck{ + Category: "cron_jobs", + Status: StatusPass, + Message: "定时任务检查", + Details: []protocol.SecurityCheckSub{}, + } + + suspiciousCount := 0 + + // 1. 检查传统crontab + systemCrons := []string{ + "/etc/crontab", + "/etc/cron.d/", + "/etc/cron.hourly/", + "/etc/cron.daily/", + "/etc/cron.weekly/", + "/etc/cron.monthly/", + "/var/spool/cron/", + } + + for _, cronPath := range systemCrons { + suspicious := checkCronPath(cronPath) + suspiciousCount += len(suspicious) + for _, cron := range suspicious { + check.Status = StatusWarn + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "suspicious_cron", + Status: StatusWarn, + Message: cron, + }) + } + } + + // 2. 检查systemd timers(现代Linux更常用) + suspiciousTimers := checkSystemdTimers() + if len(suspiciousTimers) > 0 { + suspiciousCount += len(suspiciousTimers) + check.Status = StatusWarn + for _, timer := range suspiciousTimers { + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "suspicious_systemd_timer", + Status: StatusWarn, + Message: timer, + }) + } + } + + if suspiciousCount == 0 { + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "cron_clean", + Status: StatusPass, + Message: "定时任务正常", + }) + check.Message = "定时任务正常" + } else { + check.Message = fmt.Sprintf("发现 %d 个可疑定时任务", suspiciousCount) + } + + return check +} + +// checkSystemdTimers 检查systemd定时器 +func checkSystemdTimers() []string { + var suspicious []string + // 获取所有timer单元的详细信息 + timerListOutput, err := execCommand("systemctl", "list-unit-files", "--type=timer", "--no-pager") + if err != nil { + return suspicious + } + + // 解析timer列表 + lines := strings.Split(timerListOutput, "\n") + for _, line := range lines { + fields := strings.Fields(line) + if len(fields) < 1 || !strings.HasSuffix(fields[0], ".timer") { + continue + } + + timerName := fields[0] + + // 获取timer对应的service内容 + serviceName := strings.TrimSuffix(timerName, ".timer") + ".service" + serviceContent, err := execCommand("systemctl", "cat", serviceName) + if err != nil { + continue + } + + // 检查service内容是否可疑 + if isSuspiciousCron(serviceContent) { + suspicious = append(suspicious, fmt.Sprintf("可疑systemd timer: %s -> %s", timerName, serviceName)) + } + } + + return suspicious +} + +// checkSuspiciousFiles 检查可疑文件(增强版) +func checkSuspiciousFiles() protocol.SecurityCheck { + check := protocol.SecurityCheck{ + Category: "suspicious_files", + Status: StatusPass, + Message: "可疑文件检查", + Details: []protocol.SecurityCheckSub{}, + } + + suspiciousCount := 0 + + // 1. 检查临时目录下的可执行文件 + tmpDirs := []string{"/tmp", "/dev/shm", "/var/tmp"} + for _, dir := range tmpDirs { + files := findSuspiciousExecutables(dir) + if len(files) > 0 { + check.Status = StatusWarn + for _, file := range files { + suspiciousCount++ + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "tmp_executable", + Status: StatusWarn, + Message: file, + }) + } + } + } + + // 2. 检查隐藏的可执行文件(以.开头) + hiddenExecs := findHiddenExecutables() + if len(hiddenExecs) > 0 { + check.Status = StatusWarn + for _, file := range hiddenExecs { + suspiciousCount++ + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "hidden_executable", + Status: StatusWarn, + Message: fmt.Sprintf("隐藏可执行文件: %s", file), + }) + } + } + + // 3. 检查可疑的SUID/SGID文件 + suspiciousSUIDs := findSuspiciousSUIDFiles() + if len(suspiciousSUIDs) > 0 { + check.Status = StatusFail + for _, file := range suspiciousSUIDs { + suspiciousCount++ + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "suspicious_suid", + Status: StatusFail, + Message: file, + }) + } + } + + if suspiciousCount == 0 { + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "files_clean", + Status: StatusPass, + Message: "未发现可疑文件", + }) + check.Message = "文件系统正常" + } else { + check.Message = fmt.Sprintf("发现 %d 个可疑文件", suspiciousCount) + } + + return check +} + +// findSuspiciousExecutables 查找可疑的可执行文件(带时间和大小分析) +func findSuspiciousExecutables(dir string) []string { + suspicious := []string{} + + entries, err := os.ReadDir(dir) + if err != nil { + return suspicious + } + + now := time.Now() + for _, entry := range entries { + if entry.IsDir() { + continue + } + + fullPath := filepath.Join(dir, entry.Name()) + info, err := entry.Info() + if err != nil { + continue + } + + // 检查是否可执行 + if info.Mode()&0111 != 0 { + // 最近24小时创建的可执行文件更可疑 + age := now.Sub(info.ModTime()) + sizeKB := info.Size() / 1024 + + if age < 24*time.Hour { + suspicious = append(suspicious, fmt.Sprintf("%s 下的最近可执行文件: %s (大小: %dKB, 创建时间: %s)", + dir, fullPath, sizeKB, info.ModTime().Format("2006-01-02 15:04:05"))) + } else if sizeKB > 1024 { // 大于1MB的可执行文件 + suspicious = append(suspicious, fmt.Sprintf("%s 下的大型可执行文件: %s (大小: %dKB)", + dir, fullPath, sizeKB)) + } else { + // 其他可执行文件只在特定条件下报告 + if strings.HasPrefix(entry.Name(), ".") { + suspicious = append(suspicious, fmt.Sprintf("%s 下的隐藏可执行文件: %s", dir, fullPath)) + } + } + } + } + + return suspicious +} + +// findSuspiciousSUIDFiles 查找可疑的SUID/SGID文件 +func findSuspiciousSUIDFiles() []string { + suspicious := []string{} + + // 已知的合法SUID文件列表(常见系统工具) + legitimateSUIDs := map[string]bool{ + "/usr/bin/sudo": true, + "/usr/bin/su": true, + "/usr/bin/passwd": true, + "/usr/bin/chsh": true, + "/usr/bin/chfn": true, + "/usr/bin/gpasswd": true, + "/usr/bin/newgrp": true, + "/bin/su": true, + "/bin/mount": true, + "/bin/umount": true, + "/bin/ping": true, + "/usr/bin/pkexec": true, + "/usr/lib/openssh/ssh-keysign": true, + "/usr/lib/dbus-1.0/dbus-daemon-launch-helper": true, + "/usr/sbin/unix_chkpwd": true, + "/sbin/unix_chkpwd": true, + } + + // 使用find命令查找SUID/SGID文件(更高效) + output, err := execCommand("find", "/usr", "/bin", "/sbin", "-type", "f", "(", + "-perm", "-4000", "-o", "-perm", "-2000", ")", "-ls") + if err != nil { + return suspicious + } + + lines := strings.Split(output, "\n") + for _, line := range lines { + if strings.TrimSpace(line) == "" { + continue + } + + // 解析find -ls输出,提取文件路径 + fields := strings.Fields(line) + if len(fields) < 11 { + continue + } + + filePath := fields[len(fields)-1] + + // 跳过合法的SUID文件 + if legitimateSUIDs[filePath] { + continue + } + + // 检查文件是否在可疑路径 + if strings.HasPrefix(filePath, "/tmp/") || strings.HasPrefix(filePath, "/dev/shm/") || + strings.HasPrefix(filePath, "/var/tmp/") { + suspicious = append(suspicious, fmt.Sprintf("可疑位置的SUID文件: %s (高危)", filePath)) + continue + } + + // 报告所有非标准的SUID文件 + suspicious = append(suspicious, fmt.Sprintf("非标准SUID文件: %s", filePath)) + } + + // 限制报告数量 + if len(suspicious) > 15 { + suspicious = suspicious[:15] + } + + return suspicious +} + +// checkSystemAccounts 检查系统账户异常 +func checkSystemAccounts() protocol.SecurityCheck { + check := protocol.SecurityCheck{ + Category: "system_accounts", + Status: StatusPass, + Message: "系统账户检查", + Details: []protocol.SecurityCheckSub{}, + } + + // 检查可登录的系统账户 + suspiciousAccounts := findSuspiciousAccounts() + if len(suspiciousAccounts) > 0 { + check.Status = StatusWarn + for _, acc := range suspiciousAccounts { + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "suspicious_account", + Status: StatusWarn, + Message: acc, + }) + } + } + + // 检查无密码账户 + noPasswordAccounts := findNoPasswordAccounts() + if len(noPasswordAccounts) > 0 { + check.Status = StatusFail + for _, acc := range noPasswordAccounts { + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "no_password_account", + Status: StatusFail, + Message: fmt.Sprintf("无密码账户: %s", acc), + }) + } + } + + // 检查UID为0的非root账户 + rootUIDAccounts := findRootUIDAccounts() + if len(rootUIDAccounts) > 0 { + check.Status = StatusFail + for _, acc := range rootUIDAccounts { + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "uid0_account", + Status: StatusFail, + Message: fmt.Sprintf("UID为0的非root账户: %s", acc), + }) + } + } + + if check.Status == StatusPass { + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "accounts_clean", + Status: StatusPass, + Message: "系统账户正常", + }) + check.Message = "账户配置正常" + } else if check.Status == StatusWarn { + check.Message = "发现可疑账户" + } else { + check.Message = "发现严重账户安全问题" + } + + return check +} + +// checkNetworkConnections 检查异常网络连接(增强版:关联进程) +func checkNetworkConnections() protocol.SecurityCheck { + check := protocol.SecurityCheck{ + Category: "network_connections", + Status: StatusPass, + Message: "网络连接检查", + Details: []protocol.SecurityCheckSub{}, + } + + connections, err := gopsutilNet.Connections("all") + if err != nil { + check.Status = StatusSkip + check.Message = "无法获取网络连接" + return check + } + + suspiciousCount := 0 + establishedCount := 0 + + // 检查可疑的外部连接 + for _, conn := range connections { + if conn.Status != "ESTABLISHED" { + continue + } + + establishedCount++ + + // 获取进程信息 + var processName string + var processPath string + if conn.Pid > 0 { + proc, err := process.NewProcess(int32(conn.Pid)) + if err == nil { + processName, _ = proc.Name() + processPath, _ = proc.Exe() + } + } + + // 检查1: 连接到挖矿池 + if isMiningPool(conn.Raddr.IP, conn.Raddr.Port) { + suspiciousCount++ + check.Status = StatusFail + + msg := fmt.Sprintf("连接到挖矿池: %s:%d", conn.Raddr.IP, conn.Raddr.Port) + if processName != "" { + msg += fmt.Sprintf(" (进程: %s, PID: %d)", processName, conn.Pid) + } + + var evidence *protocol.Evidence + if conn.Pid > 0 { + proc, _ := process.NewProcess(int32(conn.Pid)) + if proc != nil { + evidence = collectProcessEvidence(proc, "high") + } + } + + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "mining_connection", + Status: StatusFail, + Message: msg, + Evidence: evidence, + }) + } + + // 检查2: 系统进程的异常外连 + if processName != "" && isSystemProcess(processName) && !isLocalIP(conn.Raddr.IP) { + // 系统进程不应该连接外部IP + suspiciousCount++ + check.Status = StatusWarn + + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "system_process_external_conn", + Status: StatusWarn, + Message: fmt.Sprintf("系统进程异常外连: %s (PID:%d) -> %s:%d", processName, conn.Pid, conn.Raddr.IP, conn.Raddr.Port), + }) + } + + // 检查3: 可疑路径的进程外连 + if processPath != "" && isSuspiciousPath(processPath) { + suspiciousCount++ + check.Status = StatusFail + + var evidence *protocol.Evidence + if conn.Pid > 0 { + proc, _ := process.NewProcess(int32(conn.Pid)) + if proc != nil { + evidence = collectProcessEvidence(proc, "high") + } + } + + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "suspicious_path_connection", + Status: StatusFail, + Message: fmt.Sprintf("可疑路径进程外连: %s (PID:%d) -> %s:%d", processPath, conn.Pid, conn.Raddr.IP, conn.Raddr.Port), + Evidence: evidence, + }) + } + } + + if suspiciousCount == 0 { + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "connections_clean", + Status: StatusPass, + Message: fmt.Sprintf("共 %d 个活动连接,未发现异常", establishedCount), + }) + check.Message = "网络连接正常" + } else { + check.Message = fmt.Sprintf("发现 %d 个可疑连接", suspiciousCount) + } + + return check +} + +// isSystemProcess 判断是否是系统进程 +func isSystemProcess(name string) bool { + systemProcs := []string{ + "systemd", "init", "sshd", "cron", "rsyslogd", + "dbus-daemon", "systemd-udevd", "systemd-logind", + } + + nameLower := strings.ToLower(name) + for _, sp := range systemProcs { + if nameLower == sp { + return true + } + } + return false +} + +// isLocalIP 判断是否是本地IP +func isLocalIP(ip string) bool { + if ip == "127.0.0.1" || ip == "::1" || ip == "localhost" { + return true + } + // 检查私有IP段 + if strings.HasPrefix(ip, "192.168.") || strings.HasPrefix(ip, "10.") || + strings.HasPrefix(ip, "172.16.") || strings.HasPrefix(ip, "172.17.") || + strings.HasPrefix(ip, "172.18.") || strings.HasPrefix(ip, "172.19.") || + strings.HasPrefix(ip, "172.2") || strings.HasPrefix(ip, "172.30.") || + strings.HasPrefix(ip, "172.31.") { + return true + } + return false +} + +// checkFileIntegrity 检查关键系统文件完整性(增强版) +func checkFileIntegrity() protocol.SecurityCheck { + check := protocol.SecurityCheck{ + Category: "file_integrity", + Status: StatusPass, + Message: "系统文件完整性检查", + Details: []protocol.SecurityCheckSub{}, + } + + suspiciousCount := 0 + + // 1. 检查/etc/ld.so.preload (这个文件通常不应该存在) + if content, err := os.ReadFile("/etc/ld.so.preload"); err == nil { + contentStr := strings.TrimSpace(string(content)) + if contentStr != "" { + suspiciousCount++ + check.Status = StatusWarn + + fileHash := calculateSHA256("/etc/ld.so.preload") + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "ld_preload_exists", + Status: StatusWarn, + Message: fmt.Sprintf("/etc/ld.so.preload 存在: %s", contentStr), + Evidence: &protocol.Evidence{ + FilePath: "/etc/ld.so.preload", + FileHash: fileHash, + RiskLevel: "high", + }, + }) + } + } + + // 2. 检查/etc/ld.so.conf.d/下的可疑配置 + ldConfDir := "/etc/ld.so.conf.d" + if entries, err := os.ReadDir(ldConfDir); err == nil { + for _, entry := range entries { + if entry.IsDir() { + continue + } + fullPath := filepath.Join(ldConfDir, entry.Name()) + content, err := os.ReadFile(fullPath) + if err != nil { + continue + } + contentStr := string(content) + if strings.Contains(contentStr, "/tmp") || strings.Contains(contentStr, "/dev/shm") { + suspiciousCount++ + check.Status = StatusWarn + + fileHash := calculateSHA256(fullPath) + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "suspicious_ld_conf", + Status: StatusWarn, + Message: fmt.Sprintf("可疑的库配置: %s 包含临时目录路径", fullPath), + Evidence: &protocol.Evidence{ + FilePath: fullPath, + FileHash: fileHash, + RiskLevel: "medium", + }, + }) + } + } + } + + // 3. 检查关键系统二进制文件的最近修改(新增) + criticalBinaries := []string{ + "/bin/bash", "/bin/sh", "/bin/ls", "/bin/ps", + "/usr/bin/ssh", "/usr/bin/sudo", "/usr/sbin/sshd", + "/usr/bin/passwd", "/usr/bin/chsh", + } + + now := time.Now() + for _, binPath := range criticalBinaries { + info, err := os.Stat(binPath) + if err != nil { + continue // 文件不存在,跳过 + } + + // 检查最近7天内被修改的关键二进制文件(可能被替换) + modTime := info.ModTime() + age := now.Sub(modTime) + if age < 7*24*time.Hour { + suspiciousCount++ + check.Status = StatusWarn + + fileHash := calculateSHA256(binPath) + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "recently_modified_binary", + Status: StatusWarn, + Message: fmt.Sprintf("关键系统文件最近被修改: %s (修改时间: %s, %d天前)", binPath, modTime.Format("2006-01-02 15:04:05"), int(age.Hours()/24)), + Evidence: &protocol.Evidence{ + FilePath: binPath, + FileHash: fileHash, + Timestamp: modTime.UnixMilli(), + RiskLevel: "medium", + }, + }) + } + } + + // 4. 检查启动脚本的最近修改(新增) + startupScripts := []string{ + "/etc/rc.local", + "/etc/profile", + "/etc/bash.bashrc", + "/etc/environment", + } + + for _, scriptPath := range startupScripts { + info, err := os.Stat(scriptPath) + if err != nil { + continue + } + + modTime := info.ModTime() + age := now.Sub(modTime) + if age < 7*24*time.Hour { + suspiciousCount++ + check.Status = StatusWarn + + fileHash := calculateSHA256(scriptPath) + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "recently_modified_startup", + Status: StatusWarn, + Message: fmt.Sprintf("启动脚本最近被修改: %s (修改时间: %s, %d天前)", scriptPath, modTime.Format("2006-01-02 15:04:05"), int(age.Hours()/24)), + Evidence: &protocol.Evidence{ + FilePath: scriptPath, + FileHash: fileHash, + Timestamp: modTime.UnixMilli(), + RiskLevel: "medium", + }, + }) + } + } + + // 5. 检查PAM配置(新增,用于检测SSH后门) + pamSshdPath := "/etc/pam.d/sshd" + if content, err := os.ReadFile(pamSshdPath); err == nil { + contentStr := string(content) + // 检查是否包含可疑的pam模块 + suspiciousPamModules := []string{"pam_backdoor", "pam_unix_auth"} + for _, module := range suspiciousPamModules { + if strings.Contains(contentStr, module) { + suspiciousCount++ + check.Status = StatusFail + + fileHash := calculateSHA256(pamSshdPath) + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "suspicious_pam_module", + Status: StatusFail, + Message: fmt.Sprintf("PAM配置包含可疑模块: %s in %s", module, pamSshdPath), + Evidence: &protocol.Evidence{ + FilePath: pamSshdPath, + FileHash: fileHash, + RiskLevel: "high", + }, + }) + } + } + } + + if suspiciousCount == 0 { + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "integrity_ok", + Status: StatusPass, + Message: "系统文件完整性正常", + }) + check.Message = "文件完整性正常" + } else { + check.Message = fmt.Sprintf("发现 %d 个文件异常", suspiciousCount) + } + + return check +} + +// checkImmutableFiles 检查不可变文件(优化版:只检查关键文件和批量执行) +func checkImmutableFiles() protocol.SecurityCheck { + check := protocol.SecurityCheck{ + Category: "immutable_files", + Status: StatusPass, + Message: "不可变文件检查", + Details: []protocol.SecurityCheckSub{}, + } + + // 策略1: 只检查最关键的文件(通常被rootkit/后门保护) + criticalFiles := []string{ + "/etc/ld.so.preload", + "/etc/crontab", + "/etc/rc.local", + "/etc/passwd", + "/etc/shadow", + "/etc/sudoers", + "/etc/hosts", + "/etc/ssh/sshd_config", + "/root/.ssh/authorized_keys", + "/etc/systemd/system", + "/lib/systemd/system", + } + + suspiciousCount := 0 + + // 先检查关键文件 + for _, file := range criticalFiles { + if _, err := os.Stat(file); err != nil { + continue // 文件不存在,跳过 + } + + // 执行lsattr检查 + lsattrCmd := exec.Command("lsattr", "-d", file) + lsattrOutput, err := lsattrCmd.Output() + if err != nil { + continue + } + + output := string(lsattrOutput) + // 检查是否有i标志(immutable) + if strings.Contains(output, "----i") || strings.Contains(output, "---i-") { + suspiciousCount++ + check.Status = StatusWarn + + // 计算文件哈希 + fileHash := calculateSHA256(file) + + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "immutable_critical_file", + Status: StatusWarn, + Message: fmt.Sprintf("关键文件被设置为不可变: %s", file), + Evidence: &protocol.Evidence{ + FilePath: file, + FileHash: fileHash, + RiskLevel: "high", // 关键文件不可变是高危信号 + }, + }) + } + } + + // 策略2: 批量检查关键目录(性能优化) + criticalDirs := []string{"/etc", "/bin", "/sbin"} + + for _, dir := range criticalDirs { + // 使用lsattr -R批量递归检查(比逐个文件快得多) + lsattrCmd := exec.Command("lsattr", "-Ra", dir) + lsattrOutput, err := lsattrCmd.Output() + if err != nil { + continue // 可能是权限问题或目录不存在 + } + + // 解析输出 + lines := strings.Split(string(lsattrOutput), "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" { + continue + } + + // lsattr输出格式: "----i-------e------ /path/to/file" + if strings.Contains(line, "----i") || strings.Contains(line, "---i-") { + // 提取文件路径 + parts := strings.Fields(line) + if len(parts) < 2 { + continue + } + filePath := parts[len(parts)-1] + + // 跳过已经检查过的关键文件(避免重复报告) + alreadyReported := false + for _, cf := range criticalFiles { + if filePath == cf { + alreadyReported = true + break + } + } + if alreadyReported { + continue + } + + suspiciousCount++ + check.Status = StatusWarn + + fileHash := calculateSHA256(filePath) + + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "immutable_file", + Status: StatusWarn, + Message: fmt.Sprintf("发现不可变文件: %s", filePath), + Evidence: &protocol.Evidence{ + FilePath: filePath, + FileHash: fileHash, + RiskLevel: "medium", + }, + }) + + // 限制报告数量,避免过多 + if suspiciousCount >= 20 { + break + } + } + } + + if suspiciousCount >= 20 { + break + } + } + + if suspiciousCount > 0 { + if suspiciousCount >= 20 { + check.Message = fmt.Sprintf("发现大量不可变文件(>=20个),可能存在rootkit") + } else { + check.Message = fmt.Sprintf("发现 %d 个不可变文件", suspiciousCount) + } + } else { + check.Message = "未发现可疑的不可变文件" + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "immutable_files_clean", + Status: StatusPass, + Message: "未发现可疑的不可变文件", + }) + } + + return check +} + +// checkCommandHistory 检查命令历史 + +// checkLoginHistory 检查登录历史(智能分析版) +func checkLoginHistory() protocol.SecurityCheck { + check := protocol.SecurityCheck{ + Category: "login_history", + Status: StatusPass, + Message: "登录历史检查", + Details: []protocol.SecurityCheckSub{}, + } + + // 分析最近的成功登录 + lastOutput, err := execCommand("last", "-n", "50") + if err == nil { + suspiciousLogins := analyzeSuccessfulLogins(lastOutput) + if len(suspiciousLogins) > 0 { + check.Status = StatusWarn + for _, login := range suspiciousLogins { + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "suspicious_login", + Status: StatusWarn, + Message: login, + }) + } + } + } + + // 分析失败的登录攻击 + lastbOutput, err := execCommand("lastb", "-n", "100") + if err == nil { + attackAnalysis := analyzeFailedLogins(lastbOutput) + if attackAnalysis != nil { + // 只有高频攻击或成功攻击才报告 + if attackAnalysis.HighFrequencyIPs > 0 { + check.Status = StatusWarn + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "brute_force_attack", + Status: StatusWarn, + Message: fmt.Sprintf("检测到暴力破解攻击: %d个IP高频尝试登录(>10次)", attackAnalysis.HighFrequencyIPs), + }) + + // 列出前5个最活跃的攻击IP + for i, ipStat := range attackAnalysis.TopAttackers { + if i >= 5 { + break + } + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: fmt.Sprintf("top_attacker_%d", i+1), + Status: StatusWarn, + Message: fmt.Sprintf("攻击来源 #%d: %s (失败%d次, 尝试用户: %s)", i+1, ipStat.IP, ipStat.Count, strings.Join(ipStat.Users, ", ")), + }) + } + } + + // 统计信息(仅作参考,不算异常) + if attackAnalysis.TotalFailed > 0 { + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "failed_login_summary", + Status: StatusPass, + Message: fmt.Sprintf("失败登录统计: 共%d次失败尝试, 来自%d个不同IP, 尝试了%d个不同用户名", attackAnalysis.TotalFailed, attackAnalysis.UniqueIPs, attackAnalysis.UniqueUsers), + }) + } + } else { + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "failed_logins", + Status: StatusPass, + Message: "暂无失败登录记录", + }) + } + } else { + check.Details = append(check.Details, protocol.SecurityCheckSub{ + Name: "failed_logins_check", + Status: StatusSkip, + Message: "无法检查失败登录记录(可能需要root权限)", + }) + } + + if check.Status == StatusPass { + check.Message = "未发现可疑登录活动" + } else { + check.Message = "发现可疑登录活动" + } + + return check +} + +// analyzeSuccessfulLogins 分析成功登录,检测异常模式 +func analyzeSuccessfulLogins(output string) []string { + suspicious := []string{} + lines := strings.Split(output, "\n") + + ipCount := make(map[string]int) + var logins []loginRecord + + for _, line := range lines { + trimmed := strings.TrimSpace(line) + if trimmed == "" || strings.Contains(trimmed, "wtmp begins") || strings.Contains(trimmed, "wtmp ends") { + continue + } + + // 解析登录记录: root pts/0 222.212.179.149 Mon Nov 10 13:45 still logged in + fields := strings.Fields(trimmed) + if len(fields) >= 3 { + user := fields[0] + ip := fields[2] + + // 过滤本地登录和系统重启记录 + if ip == "0.0.0.0" || ip == ":0" || ip == ":0.0" || user == "reboot" || user == "shutdown" { + continue + } + + logins = append(logins, loginRecord{user: user, ip: ip}) + ipCount[ip]++ + } + } + + // 检测1: 短时间内来自同一IP的多次成功登录(可能是暴力破解成功后的批量操作) + for ip, count := range ipCount { + if count >= 5 { + suspicious = append(suspicious, fmt.Sprintf("检测到IP %s 在最近50条记录中成功登录%d次(可能异常)", ip, count)) + } + } + + // 检测2: root用户从多个不同IP登录(可能账户被多人使用) + rootIPs := make(map[string]bool) + for _, login := range logins { + if login.user == "root" { + rootIPs[login.ip] = true + } + } + if len(rootIPs) >= 3 { + ips := make([]string, 0, len(rootIPs)) + for ip := range rootIPs { + ips = append(ips, ip) + } + suspicious = append(suspicious, fmt.Sprintf("检测到root用户从%d个不同IP登录: %s", len(rootIPs), strings.Join(ips, ", "))) + } + + return suspicious +} + +// loginRecord 登录记录 +type loginRecord struct { + user string + ip string +} + +// AttackAnalysis 攻击分析结果 +type AttackAnalysis struct { + TotalFailed int + UniqueIPs int + UniqueUsers int + HighFrequencyIPs int + TopAttackers []IPAttackStat +} + +// IPAttackStat IP攻击统计 +type IPAttackStat struct { + IP string + Count int + Users []string +} + +// analyzeFailedLogins 分析失败登录,识别暴力破解攻击 +func analyzeFailedLogins(output string) *AttackAnalysis { + lines := strings.Split(output, "\n") + + ipStats := make(map[string]*IPAttackStat) + userSet := make(map[string]bool) + totalFailed := 0 + + for _, line := range lines { + trimmed := strings.TrimSpace(line) + if trimmed == "" || strings.Contains(trimmed, "btmp begins") || strings.Contains(trimmed, "btmp ends") { + continue + } + + // 解析失败登录: admin ssh:notty 167.71.230.163 Sat Nov 8 06:12 - 06:12 (00:00) + fields := strings.Fields(trimmed) + if len(fields) >= 3 { + user := fields[0] + ip := fields[2] + + totalFailed++ + userSet[user] = true + + if stat, exists := ipStats[ip]; exists { + stat.Count++ + // 记录尝试的用户(去重) + userExists := false + for _, u := range stat.Users { + if u == user { + userExists = true + break + } + } + if !userExists && len(stat.Users) < 5 { + stat.Users = append(stat.Users, user) + } + } else { + ipStats[ip] = &IPAttackStat{ + IP: ip, + Count: 1, + Users: []string{user}, + } + } + } + } + + if totalFailed == 0 { + return nil + } + + // 统计高频攻击IP(失败次数>10次) + highFreqCount := 0 + var topAttackers []IPAttackStat + for _, stat := range ipStats { + if stat.Count > 10 { + highFreqCount++ + topAttackers = append(topAttackers, *stat) + } + } + + // 按攻击次数排序 + for i := 0; i < len(topAttackers); i++ { + for j := i + 1; j < len(topAttackers); j++ { + if topAttackers[j].Count > topAttackers[i].Count { + topAttackers[i], topAttackers[j] = topAttackers[j], topAttackers[i] + } + } + } + + return &AttackAnalysis{ + TotalFailed: totalFailed, + UniqueIPs: len(ipStats), + UniqueUsers: len(userSet), + HighFrequencyIPs: highFreqCount, + TopAttackers: topAttackers, + } +} + +// ========== 辅助检测函数 ========== + +// checkKernelModules 检查可疑内核模块 +func checkKernelModules() []string { + suspicious := []string{} + + content, err := os.ReadFile("/proc/modules") + if err != nil { + return suspicious + } + + // 已知的恶意模块名称特征 + maliciousPatterns := []string{ + "rootkit", "backdoor", "hidden", "stealth", + "diamorphine", "suterusu", "reptile", + } + + lines := strings.Split(string(content), "\n") + for _, line := range lines { + fields := strings.Fields(line) + if len(fields) > 0 { + modName := fields[0] + for _, pattern := range maliciousPatterns { + if strings.Contains(strings.ToLower(modName), pattern) { + suspicious = append(suspicious, modName) + break + } + } + } + } + + return suspicious +} + +// checkHiddenProcesses 检查隐藏进程 +func checkHiddenProcesses() []int32 { + hidden := []int32{} + + // 读取/proc目录 + procDirs, err := os.ReadDir("/proc") + if err != nil { + return hidden + } + + procPIDs := make(map[int32]bool) + for _, entry := range procDirs { + if !entry.IsDir() { + continue + } + pid, err := strconv.Atoi(entry.Name()) + if err != nil { + continue + } + procPIDs[int32(pid)] = true + } + + // 使用gopsutil获取进程列表 + procs, err := process.Processes() + if err != nil { + return hidden + } + + gopsutilPIDs := make(map[int32]bool) + for _, p := range procs { + gopsutilPIDs[p.Pid] = true + } + + // 对比差异(简单检测) + for pid := range procPIDs { + if !gopsutilPIDs[pid] { + hidden = append(hidden, pid) + } + } + + return hidden +} + +// isMinerProcess 检查是否是挖矿进程 +func isMinerProcess(name, cmdline string) bool { + minerKeywords := []string{ + "xmrig", "minergate", "cpuminer", "ccminer", + "ethminer", "claymore", "phoenix", "t-rex", + "minerd", "stratum", "nicehash", "cryptonight", + } + + checkStr := strings.ToLower(name + " " + cmdline) + for _, keyword := range minerKeywords { + if strings.Contains(checkStr, keyword) { + return true + } + } + + // 检查挖矿命令特征 + if strings.Contains(cmdline, "--donate-level") || + strings.Contains(cmdline, "-o stratum+tcp") || + strings.Contains(cmdline, "--pool") && strings.Contains(cmdline, "--user") { + return true + } + + return false +} + +// isReverseShell 检查是否是反弹shell +func isReverseShell(cmdline string) bool { + patterns := []string{ + "bash -i >& /dev/tcp", + "nc -e /bin/bash", + "nc -e /bin/sh", + "python -c 'import socket", + "perl -e 'use Socket", + "/bin/sh -i", + "/bin/bash -i", + } + + for _, pattern := range patterns { + if strings.Contains(cmdline, pattern) { + return true + } + } + + return false +} + +// isProcessRunning 检查进程是否运行 +func isProcessRunning(name string) bool { + procs, err := process.Processes() + if err != nil { + return false + } + + for _, p := range procs { + pName, err := p.Name() + if err == nil && strings.Contains(pName, name) { + return true + } + } + + return false +} + +// checkAuthorizedKeysContent 检查authorized_keys文件内容(增强版) +func checkAuthorizedKeysContent() []string { + suspicious := []string{} + users := getAllUsers() + + for _, user := range users { + keyPath := filepath.Join(user.Home, ".ssh", "authorized_keys") + + // 检查文件是否存在 + info, err := os.Stat(keyPath) + if err != nil { + continue + } + + // 1. 检查最近7天内被修改(从24小时改为7天,减少误报) + if time.Since(info.ModTime()) < 7*24*time.Hour { + suspicious = append(suspicious, fmt.Sprintf("用户 '%s' 的 authorized_keys 最近被修改: %s (%d天前)", + user.Username, info.ModTime().Format("2006-01-02 15:04:05"), int(time.Since(info.ModTime()).Hours()/24))) + } + + // 2. 检查文件内容 + content, err := os.ReadFile(keyPath) + if err != nil { + continue + } + + lines := strings.Split(string(content), "\n") + for lineNum, line := range lines { + line = strings.TrimSpace(line) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + + // 检查可疑的公钥选项 + if strings.Contains(line, "command=") { + // 有command选项可能是强制命令,需要review + suspicious = append(suspicious, fmt.Sprintf("用户 '%s' 的公钥包含command选项(行%d): %s...", + user.Username, lineNum+1, truncateString(line, 60))) + } + + if strings.Contains(line, "from=") { + // 检查from字段是否包含*通配符 + if strings.Contains(line, "from=\"*\"") || strings.Contains(line, "from=*") { + suspicious = append(suspicious, fmt.Sprintf("用户 '%s' 的公钥允许任意来源(from=*): 行%d", user.Username, lineNum+1)) + } + } + + // 检查是否有多个公钥(可能被植入后门公钥) + // 简单统计:超过5个公钥就提示 + } + + keyCount := 0 + for _, line := range lines { + line = strings.TrimSpace(line) + if line != "" && !strings.HasPrefix(line, "#") && + (strings.HasPrefix(line, "ssh-rsa") || strings.HasPrefix(line, "ssh-ed25519") || strings.HasPrefix(line, "ecdsa-")) { + keyCount++ + } + } + + if keyCount > 5 { + suspicious = append(suspicious, fmt.Sprintf("用户 '%s' 拥有大量公钥(%d个), 建议审查", user.Username, keyCount)) + } + + // 3. 检查文件权限(authorized_keys应该是600或644) + mode := info.Mode() + if mode.Perm() != 0600 && mode.Perm() != 0644 { + suspicious = append(suspicious, fmt.Sprintf("用户 '%s' 的 authorized_keys 权限异常: %o (应为600或644)", user.Username, mode.Perm())) + } + } + + return suspicious +} + +// truncateString 截断字符串 +func truncateString(s string, maxLen int) string { + if len(s) <= maxLen { + return s + } + return s[:maxLen] +} + +// readSSHConfig 读取SSH配置(改进版:统一转小写) +func readSSHConfig() map[string]string { + config := make(map[string]string) + + // 优先使用sshd -T + output, err := execCommand("sshd", "-T") + if err == nil { + lines := strings.Split(output, "\n") + for _, line := range lines { + parts := strings.Fields(line) + if len(parts) >= 2 { + // 键名统一转小写 + config[strings.ToLower(parts[0])] = parts[1] + } + } + return config + } + + // 备用:读配置文件 + configPaths := []string{"/etc/ssh/sshd_config", "/etc/sshd_config"} + for _, path := range configPaths { + content, err := os.ReadFile(path) + if err != nil { + continue + } + + lines := strings.Split(string(content), "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + + parts := strings.Fields(line) + if len(parts) >= 2 { + // 键名统一转小写 + config[strings.ToLower(parts[0])] = parts[1] + } + } + break + } + + return config +} + +// checkSuspiciousSSHD 检查可疑的sshd进程 +func checkSuspiciousSSHD() []string { + suspicious := []string{} + + procs, err := process.Processes() + if err != nil { + return suspicious + } + + sshdCount := 0 + for _, p := range procs { + name, _ := p.Name() + if strings.Contains(name, "sshd") { + sshdCount++ + exe, _ := p.Exe() + + // 检查sshd路径是否正常 + if exe != "" && !strings.HasPrefix(exe, "/usr/sbin/sshd") { + suspicious = append(suspicious, fmt.Sprintf("异常sshd路径: %s (PID: %d)", exe, p.Pid)) + } + } + } + + // 如果有多个sshd守护进程(不是子进程),可能异常 + if sshdCount > 10 { + suspicious = append(suspicious, fmt.Sprintf("sshd进程数异常: %d", sshdCount)) + } + + return suspicious +} + +// checkCronPath 检查cron目录 +func checkCronPath(cronPath string) []string { + suspicious := []string{} + + info, err := os.Stat(cronPath) + if err != nil { + return suspicious + } + + if info.IsDir() { + entries, err := os.ReadDir(cronPath) + if err != nil { + return suspicious + } + + for _, entry := range entries { + fullPath := filepath.Join(cronPath, entry.Name()) + content, err := os.ReadFile(fullPath) + if err != nil { + continue + } + + // 检查可疑命令 + contentStr := string(content) + if isSuspiciousCron(contentStr) { + suspicious = append(suspicious, fmt.Sprintf("%s: %s", fullPath, contentStr[:100])) + } + } + } else { + content, err := os.ReadFile(cronPath) + if err != nil { + return suspicious + } + + lines := strings.Split(string(content), "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + + if isSuspiciousCron(line) { + suspicious = append(suspicious, fmt.Sprintf("%s: %s", cronPath, line)) + } + } + } + + return suspicious +} + +// isSuspiciousCron 判断是否是可疑的cron任务(改进版:减少误报) +func isSuspiciousCron(content string) bool { + lowerContent := strings.ToLower(content) + + // 高危模式:下载后立即执行 + highRiskPatterns := []string{ + "curl.*|.*sh", // curl xxx | sh + "wget.*|.*sh", // wget xxx | sh + "curl.*|.*bash", // curl xxx | bash + "wget.*|.*bash", // wget xxx | bash + "curl.*&&.*chmod.*&&", // 下载->赋权->执行 + "wget.*&&.*chmod.*&&", // 下载->赋权->执行 + "base64 -d.*|.*sh", // Base64解码后执行 + "base64 -d.*|.*bash", // Base64解码后执行 + "bash -i", // 交互式shell + "sh -i", // 交互式shell + "/dev/tcp/", // Bash反弹shell + "nc.*-e", // Netcat反弹shell + "ncat.*-e", // Ncat反弹shell + "socat.*exec", // Socat执行命令 + "python.*socket", // Python反弹shell + "perl.*socket", // Perl反弹shell + } + + for _, pattern := range highRiskPatterns { + if strings.Contains(lowerContent, pattern) { + return true + } + } + + // 中危模式:可疑路径+执行 + if (strings.Contains(lowerContent, "/tmp/") || strings.Contains(lowerContent, "/dev/shm/")) && + (strings.Contains(lowerContent, "chmod +x") || strings.Contains(lowerContent, "bash") || strings.Contains(lowerContent, "sh ")) { + return true + } + + // 单独的curl/wget不算可疑(合法备份脚本常用) + return false +} + +// findExecutablesInDir 查找目录下的可执行文件 +func findExecutablesInDir(dir string) []string { + executables := []string{} + + entries, err := os.ReadDir(dir) + if err != nil { + return executables + } + + for _, entry := range entries { + if entry.IsDir() { + continue + } + + fullPath := filepath.Join(dir, entry.Name()) + info, err := entry.Info() + if err != nil { + continue + } + + // 检查是否可执行 + if info.Mode()&0111 != 0 { + executables = append(executables, fullPath) + } + } + + return executables +} + +// findHiddenExecutables 查找隐藏的可执行文件 +func findHiddenExecutables() []string { + hidden := []string{} + + searchDirs := []string{"/tmp", "/dev/shm", "/var/tmp"} + + for _, dir := range searchDirs { + entries, err := os.ReadDir(dir) + if err != nil { + continue + } + + for _, entry := range entries { + name := entry.Name() + if !strings.HasPrefix(name, ".") { + continue + } + + if entry.IsDir() { + continue + } + + fullPath := filepath.Join(dir, name) + info, err := entry.Info() + if err != nil { + continue + } + + if info.Mode()&0111 != 0 { + hidden = append(hidden, fullPath) + } + } + } + + return hidden +} + +// UserInfo 用户信息 +type UserInfo struct { + Username string + UID string + Home string + Shell string +} + +// getAllUsers 获取所有用户 +func getAllUsers() []UserInfo { + users := []UserInfo{} + + file, err := os.Open("/etc/passwd") + if err != nil { + return users + } + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + parts := strings.Split(line, ":") + if len(parts) >= 7 { + users = append(users, UserInfo{ + Username: parts[0], + UID: parts[2], + Home: parts[5], + Shell: parts[6], + }) + } + } + + return users +} + +// findSuspiciousAccounts 查找可疑账户 +func findSuspiciousAccounts() []string { + suspicious := []string{} + + users := getAllUsers() + for _, user := range users { + uid, _ := strconv.Atoi(user.UID) + + // 系统账户(UID<1000)但有可登录shell + if uid < 1000 && user.Username != "root" { + // 合法的系统shell列表 + legitimateSystemShells := []string{ + "/sbin/nologin", + "/bin/false", + "/usr/sbin/nologin", + "/bin/sync", // sync账户使用 + "/sbin/shutdown", // shutdown账户使用 + "/sbin/halt", // halt账户使用 + } + + isLegitimate := false + for _, legitShell := range legitimateSystemShells { + if user.Shell == legitShell { + isLegitimate = true + break + } + } + + // 只有非合法shell才报告为可疑 + if !isLegitimate { + suspicious = append(suspicious, fmt.Sprintf("系统账户有可登录shell: %s (UID: %s, Shell: %s)", user.Username, user.UID, user.Shell)) + } + } + } + + return suspicious +} + +// findNoPasswordAccounts 查找无密码账户 +func findNoPasswordAccounts() []string { + noPassword := []string{} + + file, err := os.Open("/etc/shadow") + if err != nil { + return noPassword + } + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + parts := strings.Split(line, ":") + if len(parts) >= 2 { + username := parts[0] + password := parts[1] + + // 只有密码字段为空才是真正的无密码账户 + // "!" 和 "!!" 表示账户被锁定,这是安全的 + // "*" 表示无法使用密码登录(通常用于系统账户) + if password == "" { + noPassword = append(noPassword, username) + } + } + } + + return noPassword +} + +// findRootUIDAccounts 查找UID为0的非root账户 +func findRootUIDAccounts() []string { + rootUID := []string{} + + users := getAllUsers() + for _, user := range users { + if user.UID == "0" && user.Username != "root" { + rootUID = append(rootUID, user.Username) + } + } + + return rootUID +} + +// isMiningPool 检查是否连接到挖矿池 +func isMiningPool(ip string, port uint32) bool { + // 常见挖矿池端口 + minerPorts := []uint32{3333, 4444, 5555, 7777, 8888, 14444, 45560} + for _, p := range minerPorts { + if port == p { + return true + } + } + + return false +} + +// isTorConnection 检查是否是TOR连接 +func isTorConnection(port uint32) bool { + torPorts := []uint32{9001, 9030, 9050, 9051, 9150} + for _, p := range torPorts { + if port == p { + return true + } + } + return false +} + +// execCommand 执行命令 +func execCommand(name string, args ...string) (string, error) { + cmd := exec.Command(name, args...) + var stdout bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stdout + + err := cmd.Run() + if err != nil { + return "", err + } + + return stdout.String(), nil +} + +// ========== 新增的辅助函数 ========== + +// calculateRiskScore 计算综合风险评分 +func calculateRiskScore(checks []protocol.SecurityCheck) (int, string, []string) { + score := 0 + var recommendations []string + + for _, check := range checks { + for _, detail := range check.Details { + switch detail.Status { + case StatusFail: + score += 20 + recommendations = append(recommendations, + fmt.Sprintf("【紧急】%s: %s", check.Category, detail.Message)) + case StatusWarn: + score += 5 + recommendations = append(recommendations, + fmt.Sprintf("【警告】%s: %s", check.Category, detail.Message)) + } + } + } + + // 限制最大分数为100 + if score > 100 { + score = 100 + } + + // 确定威胁等级 + var level string + switch { + case score >= 80: + level = "critical" + case score >= 50: + level = "high" + case score >= 20: + level = "medium" + default: + level = "low" + } + + // 限制建议数量,优先显示紧急的 + if len(recommendations) > 20 { + recommendations = recommendations[:20] + } + + return score, level, recommendations +} + +// calculateSHA256 计算文件SHA256哈希 +func calculateSHA256(filePath string) string { + file, err := os.Open(filePath) + if err != nil { + return "" + } + defer file.Close() + + hash := sha256.New() + if _, err := io.Copy(hash, file); err != nil { + return "" + } + + return hex.EncodeToString(hash.Sum(nil)) +} + +// buildProcessTree 构建进程树 +func buildProcessTree(p *process.Process) []string { + var tree []string + current := p + + // 向上追溯父进程(最多5层) + for i := 0; i < 5; i++ { + if current == nil { + break + } + + name, _ := current.Name() + exe, _ := current.Exe() + cmdline, _ := current.Cmdline() + + info := fmt.Sprintf("PID:%d Name:%s", current.Pid, name) + if exe != "" && exe != name { + info += fmt.Sprintf(" Exe:%s", exe) + } + if cmdline != "" && len(cmdline) < 100 { + info += fmt.Sprintf(" Cmd:%s", cmdline) + } + + tree = append([]string{info}, tree...) // 前置插入 + + ppid, err := current.Ppid() + if err != nil || ppid == 0 { + break + } + + parent, err := process.NewProcess(ppid) + if err != nil { + break + } + current = parent + } + + return tree +} + +// collectProcessEvidence 收集进程证据 +func collectProcessEvidence(p *process.Process, riskLevel string) *protocol.Evidence { + exe, _ := p.Exe() + createTime, _ := p.CreateTime() + + var fileHash string + if exe != "" && !strings.Contains(exe, "deleted") && !strings.Contains(exe, "memfd:") { + fileHash = calculateSHA256(exe) + } + + processTree := buildProcessTree(p) + + return &protocol.Evidence{ + FileHash: fileHash, + ProcessTree: processTree, + FilePath: exe, + Timestamp: createTime, + RiskLevel: riskLevel, + } +} + +// isLegitimateLibraryPath 检查是否是合法的库路径 +func isLegitimateLibraryPath(path, processName, processExe string) bool { + // 开发工具 + devTools := []string{"jetbrains", "vscode", "idea", "pycharm", "goland"} + for _, tool := range devTools { + if strings.Contains(strings.ToLower(processExe), tool) { + return true + } + } + + // 数据库 + databases := []string{"postgres", "mysql", "mariadb", "mongodb", "redis"} + for _, db := range databases { + if strings.Contains(strings.ToLower(processName), db) { + return true + } + } + + // 容器相关 + containers := []string{"docker", "containerd", "kubelet", "podman", "cri-o"} + for _, container := range containers { + if strings.Contains(strings.ToLower(processName), container) { + return true + } + } + + // Web服务器 + webServers := []string{"nginx", "apache", "httpd", "caddy"} + for _, ws := range webServers { + if strings.Contains(strings.ToLower(processName), ws) { + return true + } + } + + // 系统库路径 + systemLibPaths := []string{ + "/usr/lib", "/lib", "/usr/local/lib", + "/opt/", "/usr/share", + } + for _, sysPath := range systemLibPaths { + if strings.HasPrefix(path, sysPath) { + return true + } + } + + return false +} + +// assessLibraryRisk 评估库路径的风险等级 +func assessLibraryRisk(path string) string { + // 高危路径 + highRiskPaths := []string{"/tmp/", "/dev/shm/", "/var/tmp/"} + for _, hrp := range highRiskPaths { + if strings.Contains(path, hrp) { + return "high" + } + } + + // 隐藏文件 + if strings.Contains(path, "/.") { + return "medium" + } + + return "low" +} + +// execCommandSafe 安全地执行命令(不使用shell) +func execCommandSafe(name string, args ...string) (string, error) { + cmd := exec.Command(name, args...) + var stdout bytes.Buffer + cmd.Stdout = &stdout + // 不捕获stderr,避免污染输出 + + err := cmd.Run() + if err != nil { + return "", err + } + + return stdout.String(), nil +} diff --git a/pkg/agent/collector/cpu.go b/pkg/agent/collector/cpu.go new file mode 100644 index 0000000..0f5be84 --- /dev/null +++ b/pkg/agent/collector/cpu.go @@ -0,0 +1,80 @@ +package collector + +import ( + "runtime" + "sync" + "time" + + "github.com/dushixiang/pika/internal/protocol" + + "github.com/shirou/gopsutil/v4/cpu" +) + +// CPUCollector CPU 监控采集器 +type CPUCollector struct { + // 缓存不常变化的信息 + logicalCores int + physicalCores int + modelName string + initOnce sync.Once +} + +// NewCPUCollector 创建 CPU 采集器 +func NewCPUCollector() *CPUCollector { + return &CPUCollector{} +} + +// init 初始化缓存数据(只执行一次) +func (c *CPUCollector) init() { + c.initOnce.Do(func() { + // 获取逻辑核心数 + logicalCores, err := cpu.Counts(true) + if err != nil { + logicalCores = runtime.NumCPU() + } + c.logicalCores = logicalCores + + // 获取物理核心数 + physicalCores, err := cpu.Counts(false) + if err != nil { + physicalCores = logicalCores / 2 // 粗略估算 + } + c.physicalCores = physicalCores + + // 获取 CPU 信息 + cpuInfos, err := cpu.Info() + if err == nil && len(cpuInfos) > 0 { + c.modelName = cpuInfos[0].ModelName + } + }) +} + +// Collect 采集 CPU 数据(返回完整数据,包括静态和动态信息) +func (c *CPUCollector) Collect() (*protocol.CPUData, error) { + c.init() + + // 获取 CPU 总体使用率 + percentages, err := cpu.Percent(time.Second, false) + if err != nil { + return nil, err + } + + cpuPercent := 0.0 + if len(percentages) > 0 { + cpuPercent = percentages[0] + } + + // 获取每个核心的使用率 + perCorePercent, err := cpu.Percent(time.Second, true) + if err != nil { + perCorePercent = nil // 如果失败,不影响整体数据 + } + + return &protocol.CPUData{ + LogicalCores: c.logicalCores, + PhysicalCores: c.physicalCores, + ModelName: c.modelName, + UsagePercent: cpuPercent, + PerCore: perCorePercent, + }, nil +} diff --git a/pkg/agent/collector/disk.go b/pkg/agent/collector/disk.go new file mode 100644 index 0000000..2cde69b --- /dev/null +++ b/pkg/agent/collector/disk.go @@ -0,0 +1,164 @@ +package collector + +import ( + "runtime" + "strings" + + "github.com/dushixiang/pika/internal/protocol" + "github.com/shirou/gopsutil/v4/disk" +) + +// DiskCollector 磁盘监控采集器 +type DiskCollector struct { +} + +// NewDiskCollector 创建磁盘采集器 +func NewDiskCollector() *DiskCollector { + return &DiskCollector{} +} + +// shouldIgnorePartition 判断是否应该忽略该分区 +func shouldIgnorePartition(partition disk.PartitionStat) bool { + // 在 macOS 上过滤掉特殊文件系统 + if runtime.GOOS == "darwin" { + // 忽略 devfs、com.apple.TimeMachine 等虚拟文件系统 + ignoredFsTypes := []string{ + "devfs", // 设备文件系统 + "autofs", // 自动挂载文件系统 + "mtmfs", // Mobile Time Machine + "com.apple.osxfuse", // FUSE 文件系统 + } + + for _, ignoredType := range ignoredFsTypes { + if partition.Fstype == ignoredType { + return true + } + } + + // 忽略特定的挂载点 + ignoredMountPoints := []string{ + "/dev", + "/System/Volumes/VM", // 虚拟内存 + "/System/Volumes/Preboot", // Preboot 分区 + "/System/Volumes/Update", // 更新分区 + "/System/Volumes/Hardware", // 硬件分区 + "/System/Volumes/xarts", // xART 分区 + "/System/Volumes/iSCPreboot", // iSC Preboot + "/System/Volumes/Data", // 这是系统数据卷,通常会和主卷重复计数 + "/private/var/vm", // 虚拟内存 + } + + for _, ignoredMount := range ignoredMountPoints { + if strings.HasPrefix(partition.Mountpoint, ignoredMount) { + return true + } + } + } + + // 在 Linux 上过滤掉特殊文件系统 + if runtime.GOOS == "linux" { + ignoredFsTypes := []string{ + "tmpfs", + "devtmpfs", + "devfs", + "proc", + "sysfs", + "cgroup", + "cgroup2", + "nsfs", + "overlay", + "squashfs", + "iso9660", + } + + for _, ignoredType := range ignoredFsTypes { + if partition.Fstype == ignoredType { + return true + } + } + } + + // 过滤掉只读的 CD-ROM 等 + for _, opt := range partition.Opts { + if strings.Contains(strings.ToLower(opt), "cdrom") { + return true + } + } + + return false +} + +// Collect 采集磁盘数据(合并静态和动态数据) +func (d *DiskCollector) Collect() ([]protocol.DiskData, error) { + partitions, err := disk.Partitions(false) + if err != nil { + return nil, err + } + var diskDataList []protocol.DiskData + for _, partition := range partitions { + // 跳过应该忽略的分区 + if shouldIgnorePartition(partition) { + continue + } + + // 获取动态使用情况 + usage, err := disk.Usage(partition.Mountpoint) + if err != nil { + continue // 跳过无法访问的分区 + } + + // 跳过容量为 0 的分区(可能是虚拟文件系统) + if usage.Total == 0 { + continue + } + + diskData := protocol.DiskData{ + MountPoint: partition.Mountpoint, + Device: partition.Device, + Fstype: partition.Fstype, + Total: usage.Total, + Used: usage.Used, + Free: usage.Free, + UsagePercent: usage.UsedPercent, + } + + diskDataList = append(diskDataList, diskData) + } + + return diskDataList, nil +} + +// DiskIOCollector 磁盘 IO 监控采集器 +type DiskIOCollector struct{} + +// NewDiskIOCollector 创建磁盘 IO 采集器 +func NewDiskIOCollector() *DiskIOCollector { + return &DiskIOCollector{} +} + +// Collect 采集磁盘 IO 数据(全部是动态数据,无需缓存) +func (d *DiskIOCollector) Collect() ([]*protocol.DiskIOData, error) { + ioCounters, err := disk.IOCounters() + if err != nil { + return nil, err + } + + var diskIODataList []*protocol.DiskIOData + for device, counter := range ioCounters { + diskIOData := &protocol.DiskIOData{ + Device: device, + ReadCount: counter.ReadCount, + WriteCount: counter.WriteCount, + ReadBytes: counter.ReadBytes, + WriteBytes: counter.WriteBytes, + ReadTime: counter.ReadTime, + WriteTime: counter.WriteTime, + IoTime: counter.IoTime, + IopsInProgress: counter.IopsInProgress, + } + + diskIODataList = append(diskIODataList, diskIOData) + } + + return diskIODataList, nil +} diff --git a/pkg/agent/collector/gpu.go b/pkg/agent/collector/gpu.go new file mode 100644 index 0000000..91918fe --- /dev/null +++ b/pkg/agent/collector/gpu.go @@ -0,0 +1,165 @@ +package collector + +import ( + "encoding/csv" + "os/exec" + "strconv" + "strings" + "sync" + + "github.com/dushixiang/pika/internal/protocol" +) + +// gpuStaticInfo GPU 静态信息 +type gpuStaticInfo struct { + Index int + Name string + UUID string + MemoryTotal uint64 +} + +// GPUCollector GPU 监控采集器 +type GPUCollector struct { + staticData map[int]*gpuStaticInfo // key: gpu index + staticInitOnce sync.Once + mu sync.RWMutex +} + +// NewGPUCollector 创建 GPU 采集器 +func NewGPUCollector() *GPUCollector { + return &GPUCollector{ + staticData: make(map[int]*gpuStaticInfo), + } +} + +// initStatic 初始化静态数据(只执行一次) +func (g *GPUCollector) initStatic() { + g.staticInitOnce.Do(func() { + // 检查 nvidia-smi 是否可用 + _, err := exec.LookPath("nvidia-smi") + if err != nil { + return + } + + // 查询静态信息: index, name, uuid, memory.total + cmd := exec.Command("nvidia-smi", + "--query-gpu=index,name,uuid,memory.total", + "--format=csv,noheader,nounits") + + output, err := cmd.Output() + if err != nil { + return + } + + // 解析 CSV 输出 + reader := csv.NewReader(strings.NewReader(string(output))) + reader.TrimLeadingSpace = true + records, err := reader.ReadAll() + if err != nil { + return + } + + g.mu.Lock() + defer g.mu.Unlock() + + for _, record := range records { + if len(record) < 4 { + continue + } + + index, _ := strconv.Atoi(record[0]) + memoryTotal, _ := strconv.ParseUint(record[3], 10, 64) + + g.staticData[index] = &gpuStaticInfo{ + Index: index, + Name: strings.TrimSpace(record[1]), + UUID: strings.TrimSpace(record[2]), + MemoryTotal: memoryTotal * 1024 * 1024, // 转换为字节 + } + } + }) +} + +// Collect 采集 GPU 数据(合并静态和动态数据) +func (g *GPUCollector) Collect() ([]*protocol.GPUData, error) { + g.initStatic() + + // 如果没有检测到 GPU,返回空数组 + g.mu.RLock() + if len(g.staticData) == 0 { + g.mu.RUnlock() + return []*protocol.GPUData{}, nil + } + g.mu.RUnlock() + + // 采集动态数据 + dynamicData, err := g.collectDynamic() + if err != nil { + return []*protocol.GPUData{}, nil + } + + return dynamicData, nil +} + +// collectDynamic 采集 GPU 动态数据 +func (g *GPUCollector) collectDynamic() ([]*protocol.GPUData, error) { + // 使用 nvidia-smi 查询动态数据 + // 输出格式: index, temperature.gpu, utilization.gpu, memory.used, memory.free, power.draw, fan.speed + cmd := exec.Command("nvidia-smi", + "--query-gpu=index,temperature.gpu,utilization.gpu,memory.used,memory.free,power.draw,fan.speed", + "--format=csv,noheader,nounits") + + output, err := cmd.Output() + if err != nil { + return nil, err + } + + // 解析 CSV 输出 + reader := csv.NewReader(strings.NewReader(string(output))) + reader.TrimLeadingSpace = true + records, err := reader.ReadAll() + if err != nil { + return nil, err + } + + g.mu.RLock() + defer g.mu.RUnlock() + + var gpuDataList []*protocol.GPUData + for _, record := range records { + if len(record) < 7 { + continue + } + + index, _ := strconv.Atoi(record[0]) + temperature, _ := strconv.ParseFloat(record[1], 64) + utilization, _ := strconv.ParseFloat(record[2], 64) + memoryUsed, _ := strconv.ParseUint(record[3], 10, 64) + memoryFree, _ := strconv.ParseUint(record[4], 10, 64) + powerUsage, _ := strconv.ParseFloat(record[5], 64) + fanSpeed, _ := strconv.ParseFloat(record[6], 64) + + // 获取静态信息 + staticInfo := g.staticData[index] + if staticInfo == nil { + continue + } + + gpuData := &protocol.GPUData{ + Index: staticInfo.Index, + Name: staticInfo.Name, + UUID: staticInfo.UUID, + MemoryTotal: staticInfo.MemoryTotal, + Temperature: temperature, + Utilization: utilization, + MemoryUsed: memoryUsed * 1024 * 1024, // 转换为字节 + MemoryFree: memoryFree * 1024 * 1024, // 转换为字节 + PowerUsage: powerUsage, + FanSpeed: fanSpeed, + } + + gpuDataList = append(gpuDataList, gpuData) + } + + return gpuDataList, nil +} diff --git a/pkg/agent/collector/manager.go b/pkg/agent/collector/manager.go new file mode 100644 index 0000000..fd197bb --- /dev/null +++ b/pkg/agent/collector/manager.go @@ -0,0 +1,154 @@ +package collector + +import ( + "encoding/json" + + "github.com/dushixiang/pika/internal/protocol" + "github.com/dushixiang/pika/pkg/agent/config" +) + +// WebSocketWriter 定义 WebSocket 写入接口 +type WebSocketWriter interface { + WriteJSON(v interface{}) error +} + +// Manager 采集器管理器 +type Manager struct { + cpuCollector *CPUCollector + memoryCollector *MemoryCollector + diskCollector *DiskCollector + diskIOCollector *DiskIOCollector + networkCollector *NetworkCollector + loadCollector *LoadCollector + hostCollector *HostCollector + temperatureCollector *TemperatureCollector + gpuCollector *GPUCollector +} + +// NewManager 创建采集器管理器 +func NewManager(cfg *config.Config) *Manager { + return &Manager{ + cpuCollector: NewCPUCollector(), + memoryCollector: NewMemoryCollector(), + diskCollector: NewDiskCollector(), + diskIOCollector: NewDiskIOCollector(), + networkCollector: NewNetworkCollector(cfg), + loadCollector: NewLoadCollector(), + hostCollector: NewHostCollector(), + temperatureCollector: NewTemperatureCollector(), + gpuCollector: NewGPUCollector(), + } +} + +// CollectAndSendCPU 采集并发送 CPU 指标 +func (m *Manager) CollectAndSendCPU(conn WebSocketWriter) error { + cpuData, err := m.cpuCollector.Collect() + if err != nil { + return err + } + + return m.sendMetrics(conn, protocol.MetricTypeCPU, cpuData) +} + +// CollectAndSendMemory 采集并发送内存指标 +func (m *Manager) CollectAndSendMemory(conn WebSocketWriter) error { + memData, err := m.memoryCollector.Collect() + if err != nil { + return err + } + + return m.sendMetrics(conn, protocol.MetricTypeMemory, memData) +} + +// CollectAndSendDisk 采集并发送磁盘指标 +func (m *Manager) CollectAndSendDisk(conn WebSocketWriter) error { + diskDataList, err := m.diskCollector.Collect() + if err != nil { + return err + } + return m.sendMetrics(conn, protocol.MetricTypeDisk, diskDataList) +} + +// CollectAndSendDiskIO 采集并发送磁盘 IO 指标 +func (m *Manager) CollectAndSendDiskIO(conn WebSocketWriter) error { + diskIODataList, err := m.diskIOCollector.Collect() + if err != nil { + return err + } + return m.sendMetrics(conn, protocol.MetricTypeDiskIO, diskIODataList) +} + +// CollectAndSendNetwork 采集并发送网络指标 +func (m *Manager) CollectAndSendNetwork(conn WebSocketWriter) error { + networkDataList, err := m.networkCollector.Collect() + if err != nil { + return err + } + return m.sendMetrics(conn, protocol.MetricTypeNetwork, networkDataList) +} + +// CollectAndSendLoad 采集并发送系统负载指标 +func (m *Manager) CollectAndSendLoad(conn WebSocketWriter) error { + loadData, err := m.loadCollector.Collect() + if err != nil { + return err + } + return m.sendMetrics(conn, protocol.MetricTypeLoad, loadData) +} + +// CollectAndSendHost 采集并发送主机信息 +func (m *Manager) CollectAndSendHost(conn WebSocketWriter) error { + hostData, err := m.hostCollector.Collect() + if err != nil { + return err + } + + return m.sendMetrics(conn, protocol.MetricTypeHost, hostData) +} + +// CollectAndSendGPU 采集并发送 GPU 指标 +func (m *Manager) CollectAndSendGPU(conn WebSocketWriter) error { + gpuDataList, err := m.gpuCollector.Collect() + if err != nil || len(gpuDataList) == 0 { + // GPU 监控不是必须的,失败或无数据时直接返回 + return nil + } + + return m.sendMetrics(conn, protocol.MetricTypeGPU, gpuDataList) +} + +// CollectAndSendTemperature 采集并发送温度信息 +func (m *Manager) CollectAndSendTemperature(conn WebSocketWriter) error { + tempDataList, err := m.temperatureCollector.Collect() + if err != nil || len(tempDataList) == 0 { + // 温度监控不是必须的,失败或无数据时直接返回 + return nil + } + + return m.sendMetrics(conn, protocol.MetricTypeTemperature, tempDataList) +} + +// sendMetrics 发送指标数据 +func (m *Manager) sendMetrics(conn WebSocketWriter, metricType protocol.MetricType, data interface{}) error { + dataBytes, err := json.Marshal(data) + if err != nil { + return err + } + + metrics := protocol.MetricsWrapper{ + Type: metricType, + Data: json.RawMessage(dataBytes), + } + + metricsData, err := json.Marshal(metrics) + if err != nil { + return err + } + + msg := protocol.Message{ + Type: protocol.MessageTypeMetrics, + Data: metricsData, + } + + return conn.WriteJSON(msg) +} diff --git a/pkg/agent/collector/memory.go b/pkg/agent/collector/memory.go new file mode 100644 index 0000000..22fa624 --- /dev/null +++ b/pkg/agent/collector/memory.go @@ -0,0 +1,76 @@ +package collector + +import ( + "sync" + + "github.com/dushixiang/pika/internal/protocol" + "github.com/shirou/gopsutil/v4/mem" +) + +// MemoryCollector 内存监控采集器 +type MemoryCollector struct { + // 缓存不常变化的信息 + total uint64 + swapTotal uint64 + initOnce sync.Once +} + +// NewMemoryCollector 创建内存采集器 +func NewMemoryCollector() *MemoryCollector { + return &MemoryCollector{} +} + +// init 初始化缓存数据(只执行一次) +func (m *MemoryCollector) init() { + m.initOnce.Do(func() { + // 获取虚拟内存信息 + vmStat, err := mem.VirtualMemory() + if err != nil { + return + } + m.total = vmStat.Total + + // 获取 Swap 信息 + swapStat, err := mem.SwapMemory() + if err == nil { + m.swapTotal = swapStat.Total + } + }) +} + +// Collect 采集内存数据(返回完整数据,包括静态和动态信息) +func (m *MemoryCollector) Collect() (*protocol.MemoryData, error) { + m.init() + + // 获取虚拟内存信息 + vmStat, err := mem.VirtualMemory() + if err != nil { + return nil, err + } + + memData := &protocol.MemoryData{ + Total: m.total, + SwapTotal: m.swapTotal, + Used: vmStat.Used, + Free: vmStat.Free, + Available: vmStat.Available, + UsagePercent: vmStat.UsedPercent, + } + + // 尝试获取更详细的内存信息(Linux 特有) + if vmStat.Cached > 0 { + memData.Cached = vmStat.Cached + } + if vmStat.Buffers > 0 { + memData.Buffers = vmStat.Buffers + } + + // 获取 Swap 信息 + swapStat, err := mem.SwapMemory() + if err == nil { + memData.SwapUsed = swapStat.Used + memData.SwapFree = swapStat.Free + } + + return memData, nil +} diff --git a/pkg/agent/collector/network.go b/pkg/agent/collector/network.go new file mode 100644 index 0000000..4caf3ab --- /dev/null +++ b/pkg/agent/collector/network.go @@ -0,0 +1,114 @@ +package collector + +import ( + "time" + + "github.com/dushixiang/pika/internal/protocol" + "github.com/dushixiang/pika/pkg/agent/config" + "github.com/shirou/gopsutil/v4/net" +) + +// NetworkCollector 网络监控采集器 +type NetworkCollector struct { + config *config.Config // 配置信息 + lastStats map[string]net.IOCountersStat // 上次采集的统计数据 + lastCollectAt time.Time // 上次采集时间 +} + +// NewNetworkCollector 创建网络采集器 +func NewNetworkCollector(cfg *config.Config) *NetworkCollector { + return &NetworkCollector{ + config: cfg, + lastStats: make(map[string]net.IOCountersStat), + } +} + +// Collect 采集网络数据(计算自上次采集以来的增量) +func (n *NetworkCollector) Collect() ([]protocol.NetworkData, error) { + now := time.Now() + + // 计算距离上次采集的时间间隔(秒) + var intervalSeconds float64 + if !n.lastCollectAt.IsZero() { + intervalSeconds = now.Sub(n.lastCollectAt).Seconds() + } + + // 获取网络接口信息 + interfaces, err := net.Interfaces() + if err != nil { + return nil, err + } + + // 创建接口信息映射 + interfaceMap := make(map[string]*protocol.NetworkData) + for _, iface := range interfaces { + // 使用配置中的排除规则过滤网卡 + if n.config.ShouldExcludeNetworkInterface(iface.Name) { + continue + } + + // 获取 IP 地址列表 + var addrs []string + for _, addr := range iface.Addrs { + addrs = append(addrs, addr.Addr) + } + + interfaceMap[iface.Name] = &protocol.NetworkData{ + Interface: iface.Name, + MacAddress: iface.HardwareAddr, + Addrs: addrs, + } + } + + // 获取网络 IO 统计 + ioCounters, err := net.IOCounters(true) + if err != nil { + return nil, err + } + + var networkDataList []protocol.NetworkData + for _, counter := range ioCounters { + // 使用配置中的排除规则过滤网卡 + if n.config.ShouldExcludeNetworkInterface(counter.Name) { + continue + } + + // 如果已有接口信息,则更新;否则创建新的 + netData := interfaceMap[counter.Name] + if netData == nil { + netData = &protocol.NetworkData{ + Interface: counter.Name, + } + } + + // 计算增量(如果是第一次采集,则使用当前值) + lastStat, exists := n.lastStats[counter.Name] + if exists && intervalSeconds > 0 { + // 计算增量并转换为每秒速率 + bytesSentDelta := counter.BytesSent - lastStat.BytesSent + bytesRecvDelta := counter.BytesRecv - lastStat.BytesRecv + + // 存储每秒的速率(转为整数) + netData.BytesSentRate = uint64(float64(bytesSentDelta) / intervalSeconds) + netData.BytesRecvRate = uint64(float64(bytesRecvDelta) / intervalSeconds) + netData.BytesSentTotal = counter.BytesSent + netData.BytesRecvTotal = counter.BytesRecv + } else { + // 第一次采集,使用0值(避免返回巨大的累计值) + netData.BytesSentRate = 0 + netData.BytesRecvRate = 0 + netData.BytesSentTotal = counter.BytesSent + netData.BytesRecvTotal = counter.BytesRecv + } + + // 保存当前统计数据用于下次计算增量 + n.lastStats[counter.Name] = counter + + networkDataList = append(networkDataList, *netData) + } + + // 更新采集时间 + n.lastCollectAt = now + + return networkDataList, nil +} diff --git a/pkg/agent/collector/system.go b/pkg/agent/collector/system.go new file mode 100644 index 0000000..cc17195 --- /dev/null +++ b/pkg/agent/collector/system.go @@ -0,0 +1,66 @@ +package collector + +import ( + "github.com/dushixiang/pika/internal/protocol" + "github.com/shirou/gopsutil/v4/host" + "github.com/shirou/gopsutil/v4/load" +) + +// LoadCollector 系统负载监控采集器 +type LoadCollector struct{} + +// NewLoadCollector 创建负载采集器 +func NewLoadCollector() *LoadCollector { + return &LoadCollector{} +} + +// Collect 采集系统负载数据 +func (l *LoadCollector) Collect() (*protocol.LoadData, error) { + avgStat, err := load.Avg() + if err != nil { + return nil, err + } + + return &protocol.LoadData{ + Load1: avgStat.Load1, + Load5: avgStat.Load5, + Load15: avgStat.Load15, + }, nil +} + +// HostCollector 主机信息采集器 +type HostCollector struct{} + +// NewHostCollector 创建主机信息采集器 +func NewHostCollector() *HostCollector { + return &HostCollector{} +} + +// Collect 采集主机信息(定期采集以检测主机名等变化) +func (h *HostCollector) Collect() (*protocol.HostInfoData, error) { + hostInfo, err := host.Info() + if err != nil { + return nil, err + } + + hostData := &protocol.HostInfoData{ + Hostname: hostInfo.Hostname, + Uptime: hostInfo.Uptime, + BootTime: hostInfo.BootTime, + Procs: hostInfo.Procs, + OS: hostInfo.OS, + Platform: hostInfo.Platform, + PlatformFamily: hostInfo.PlatformFamily, + PlatformVersion: hostInfo.PlatformVersion, + KernelVersion: hostInfo.KernelVersion, + KernelArch: hostInfo.KernelArch, + } + + // 尝试获取虚拟化信息 + if hostInfo.VirtualizationSystem != "" { + hostData.VirtualizationSystem = hostInfo.VirtualizationSystem + hostData.VirtualizationRole = hostInfo.VirtualizationRole + } + + return hostData, nil +} diff --git a/pkg/agent/collector/temperature.go b/pkg/agent/collector/temperature.go new file mode 100644 index 0000000..936a66f --- /dev/null +++ b/pkg/agent/collector/temperature.go @@ -0,0 +1,125 @@ +package collector + +import ( + "os/exec" + "runtime" + "strconv" + "strings" + + "github.com/dushixiang/pika/internal/protocol" +) + +// TemperatureCollector 温度监控采集器 +type TemperatureCollector struct{} + +// NewTemperatureCollector 创建温度采集器 +func NewTemperatureCollector() *TemperatureCollector { + return &TemperatureCollector{} +} + +// Collect 采集温度数据(某些系统可能不支持) +func (t *TemperatureCollector) Collect() ([]*protocol.TemperatureData, error) { + switch runtime.GOOS { + case "linux": + return t.collectLinux() + case "darwin": + return t.collectDarwin() + default: + // 不支持的系统,返回空数组 + return []*protocol.TemperatureData{}, nil + } +} + +// collectLinux 在 Linux 系统上采集温度数据 +func (t *TemperatureCollector) collectLinux() ([]*protocol.TemperatureData, error) { + // 尝试使用 sensors 命令 + _, err := exec.LookPath("sensors") + if err != nil { + // sensors 命令不可用,返回空数组 + return []*protocol.TemperatureData{}, nil + } + + cmd := exec.Command("sensors", "-A") + output, err := cmd.Output() + if err != nil { + return []*protocol.TemperatureData{}, nil + } + + var tempDataList []*protocol.TemperatureData + lines := strings.Split(string(output), "\n") + currentSensor := "" + + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" { + continue + } + + // 检测传感器名称 + if !strings.Contains(line, ":") { + currentSensor = line + continue + } + + // 解析温度行 + if strings.Contains(line, "°C") { + parts := strings.Split(line, ":") + if len(parts) < 2 { + continue + } + + label := strings.TrimSpace(parts[0]) + valueStr := strings.TrimSpace(parts[1]) + + // 提取温度值 + tempStr := strings.Split(valueStr, "°C")[0] + tempStr = strings.TrimSpace(strings.TrimPrefix(tempStr, "+")) + + temp, err := strconv.ParseFloat(tempStr, 64) + if err != nil { + continue + } + + sensorKey := currentSensor + "_" + label + tempData := &protocol.TemperatureData{ + SensorKey: sensorKey, + Temperature: temp, + } + + tempDataList = append(tempDataList, tempData) + } + } + + return tempDataList, nil +} + +// collectDarwin 在 macOS 系统上采集温度数据 +func (t *TemperatureCollector) collectDarwin() ([]*protocol.TemperatureData, error) { + // macOS 的温度监控需要特殊工具,如 osx-cpu-temp + _, err := exec.LookPath("osx-cpu-temp") + if err != nil { + // 工具不可用,返回空数组 + return []*protocol.TemperatureData{}, nil + } + + cmd := exec.Command("osx-cpu-temp") + output, err := cmd.Output() + if err != nil { + return []*protocol.TemperatureData{}, nil + } + + // 解析输出,格式类似:60.0°C + outputStr := strings.TrimSpace(string(output)) + tempStr := strings.Split(outputStr, "°C")[0] + temp, err := strconv.ParseFloat(tempStr, 64) + if err != nil { + return []*protocol.TemperatureData{}, nil + } + + tempData := &protocol.TemperatureData{ + SensorKey: "CPU", + Temperature: temp, + } + + return []*protocol.TemperatureData{tempData}, nil +} diff --git a/pkg/agent/config/config.go b/pkg/agent/config/config.go new file mode 100644 index 0000000..aae3c31 --- /dev/null +++ b/pkg/agent/config/config.go @@ -0,0 +1,285 @@ +package config + +import ( + "fmt" + "net/url" + "os" + "path/filepath" + "regexp" + "runtime" + "time" + + "gopkg.in/yaml.v3" +) + +// Config Agent 配置 +type Config struct { + // 配置文件路径 + Path string `yaml:"-"` + + // 服务器配置 + Server ServerConfig `yaml:"server"` + + // Agent 配置 + Agent AgentConfig `yaml:"agent"` + + // 采集配置 + Collector CollectorConfig `yaml:"collector"` + + // 自动更新配置 + AutoUpdate AutoUpdateConfig `yaml:"auto_update"` +} + +// ServerConfig 服务器配置 +type ServerConfig struct { + // 服务器地址(如:http://localhost:18888 或 https://your-server.com) + Endpoint string `yaml:"endpoint"` + + // API Key + APIKey string `yaml:"api_key"` +} + +// AgentConfig Agent 配置 +type AgentConfig struct { + // Agent 名称(默认使用主机名) + Name string `yaml:"name"` +} + +// CollectorConfig 采集器配置 +type CollectorConfig struct { + // 数据采集间隔(秒) + Interval int `yaml:"interval"` + + // 心跳间隔(秒) + HeartbeatInterval int `yaml:"heartbeat_interval"` + + // 网络采集排除的网卡列表(支持正则表达式) + // 如果为空,默认只排除回环地址(lo, lo0) + NetworkExclude []string `yaml:"network_exclude"` +} + +// AutoUpdateConfig 自动更新配置 +type AutoUpdateConfig struct { + // 是否启用自动更新 + Enabled bool `yaml:"enabled"` + + // 检查更新间隔 + CheckInterval string `yaml:"check_interval"` +} + +// DefaultConfig 返回默认配置 +func DefaultConfig() *Config { + return &Config{ + Server: ServerConfig{ + Endpoint: "http://localhost:8080", + APIKey: "", + }, + Agent: AgentConfig{ + Name: "", + }, + Collector: CollectorConfig{ + Interval: 5, + HeartbeatInterval: 30, + }, + AutoUpdate: AutoUpdateConfig{ + Enabled: true, + CheckInterval: "10m", + }, + } +} + +// GetDefaultConfigPath 获取默认配置文件路径 +func GetDefaultConfigPath() string { + homeDir, err := os.UserHomeDir() + if err != nil { + homeDir = "." + } + return filepath.Join(homeDir, ".pika", "agent.yaml") +} + +// Load 加载配置文件 +func Load(path string) (*Config, error) { + // 如果路径为空,使用默认路径 + if path == "" { + path = GetDefaultConfigPath() + } + + // 读取配置文件 + data, err := os.ReadFile(path) + if err != nil { + if os.IsNotExist(err) { + // 配置文件不存在,创建默认配置 + cfg := DefaultConfig() + if err := cfg.Save(path); err != nil { + return nil, fmt.Errorf("创建默认配置文件失败: %w", err) + } + return cfg, nil + } + return nil, fmt.Errorf("读取配置文件失败: %w", err) + } + + // 解析配置 + cfg := DefaultConfig() + if err := yaml.Unmarshal(data, cfg); err != nil { + return nil, fmt.Errorf("解析配置文件失败: %w", err) + } + + // 验证配置 + if err := cfg.Validate(); err != nil { + return nil, fmt.Errorf("配置验证失败: %w", err) + } + + cfg.Path = path + return cfg, nil +} + +// Save 保存配置到文件 +func (c *Config) Save(path string) error { + // 如果路径为空,使用默认路径 + if path == "" { + path = GetDefaultConfigPath() + } + + // 确保目录存在 + dir := filepath.Dir(path) + if err := os.MkdirAll(dir, 0755); err != nil { + return fmt.Errorf("创建配置目录失败: %w", err) + } + + // 序列化配置 + data, err := yaml.Marshal(c) + if err != nil { + return fmt.Errorf("序列化配置失败: %w", err) + } + + // 写入文件 + if err := os.WriteFile(path, data, 0644); err != nil { + return fmt.Errorf("写入配置文件失败: %w", err) + } + + return nil +} + +// Validate 验证配置 +func (c *Config) Validate() error { + if c.Server.Endpoint == "" { + return fmt.Errorf("服务器地址不能为空") + } + + if c.Server.APIKey == "" { + return fmt.Errorf("API Key 不能为空") + } + + if c.Collector.Interval <= 0 { + return fmt.Errorf("采集间隔必须大于 0") + } + + if c.Collector.HeartbeatInterval <= 0 { + return fmt.Errorf("心跳间隔必须大于 0") + } + + if c.AutoUpdate.Enabled { + if _, err := time.ParseDuration(c.AutoUpdate.CheckInterval); err != nil { + return fmt.Errorf("更新检查间隔格式错误: %w", err) + } + } + + return nil +} + +// GetCollectorInterval 获取采集间隔时长 +func (c *Config) GetCollectorInterval() time.Duration { + return time.Duration(c.Collector.Interval) * time.Second +} + +// GetHeartbeatInterval 获取心跳间隔时长 +func (c *Config) GetHeartbeatInterval() time.Duration { + return time.Duration(c.Collector.HeartbeatInterval) * time.Second +} + +// GetUpdateCheckInterval 获取更新检查间隔时长 +func (c *Config) GetUpdateCheckInterval() time.Duration { + duration, _ := time.ParseDuration(c.AutoUpdate.CheckInterval) + return duration +} + +// GetWebSocketURL 获取 WebSocket 连接地址 +func (c *Config) GetWebSocketURL() string { + endpoint := c.Server.Endpoint + // 将 http:// 替换为 ws://,https:// 替换为 wss:// + if len(endpoint) >= 7 && endpoint[:7] == "http://" { + return "ws://" + endpoint[7:] + "/ws/agent" + } + if len(endpoint) >= 8 && endpoint[:8] == "https://" { + return "wss://" + endpoint[8:] + "/ws/agent" + } + // 如果没有协议前缀,默认使用 ws:// + return "ws://" + endpoint + "/ws/agent" +} + +// GetUpdateURL 获取更新检查地址 +func (c *Config) GetUpdateURL() string { + return c.Endpoint() + "/api/agent/version" +} + +func (c *Config) GetDownloadURL() string { + var filename = fmt.Sprintf("agent-%s-%s", runtime.GOOS, runtime.GOARCH) + if runtime.GOOS == "windows" { + filename += ".exe" + } + return c.Endpoint() + "/api/agent/downloads/" + filename +} + +func (c *Config) Endpoint() string { + u, err := url.Parse(c.Server.Endpoint) + if err != nil { + return c.Server.Endpoint + } + var endpoint = fmt.Sprintf("%s://%s", u.Scheme, u.Host) + return endpoint +} + +// GetNetworkExcludePatterns 获取网络排除的正则表达式列表 +// 如果配置为空,返回默认排除规则(回环地址和常见虚拟网卡) +func (c *Config) GetNetworkExcludePatterns() ([]*regexp.Regexp, error) { + patterns := c.Collector.NetworkExclude + + // 如果没有配置,使用默认排除规则 + if len(patterns) == 0 { + patterns = []string{ + "^lo$", // 回环地址 + "^lo0$", // 回环地址 + "^docker.*", // Docker 网卡 + "^veth.*", // 虚拟以太网接口 + "^br-.*", // 网桥接口 + } + } + + var regexps []*regexp.Regexp + for _, pattern := range patterns { + re, err := regexp.Compile(pattern) + if err != nil { + return nil, fmt.Errorf("编译网络排除规则 '%s' 失败: %w", pattern, err) + } + regexps = append(regexps, re) + } + + return regexps, nil +} + +// ShouldExcludeNetworkInterface 检查网卡是否应该被排除 +func (c *Config) ShouldExcludeNetworkInterface(interfaceName string) bool { + patterns, err := c.GetNetworkExcludePatterns() + if err != nil { + // 如果正则编译失败,使用默认规则 + return interfaceName == "lo" || interfaceName == "lo0" + } + + for _, pattern := range patterns { + if pattern.MatchString(interfaceName) { + return true + } + } + + return false +} diff --git a/pkg/agent/id/id.go b/pkg/agent/id/id.go new file mode 100644 index 0000000..81ac6fc --- /dev/null +++ b/pkg/agent/id/id.go @@ -0,0 +1,101 @@ +package id + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/google/uuid" +) + +// Manager 管理探针的唯一标识 +type Manager struct { + idFilePath string +} + +// NewManager 创建 ID 管理器 +func NewManager() *Manager { + return &Manager{ + idFilePath: getIDFilePath(), + } +} + +// getIDFilePath 获取 ID 文件路径 +func getIDFilePath() string { + // 获取用户主目录 + homeDir, err := os.UserHomeDir() + if err != nil { + // 如果无法获取主目录,使用当前目录 + homeDir = "." + } + + // 统一使用 ~/.pika/agent.id + return filepath.Join(homeDir, ".pika", "agent.id") +} + +// Load 加载或生成探针 ID +// 如果 ID 文件存在,则读取;否则生成新的 UUID 并保存 +func (m *Manager) Load() (string, error) { + // 尝试读取现有 ID + if id, err := m.read(); err == nil && id != "" { + return id, nil + } + + // 生成新 ID + id := uuid.NewString() + + // 保存 ID + if err := m.save(id); err != nil { + return "", fmt.Errorf("保存 agent ID 失败: %w", err) + } + + return id, nil +} + +// read 读取 ID 文件 +func (m *Manager) read() (string, error) { + data, err := os.ReadFile(m.idFilePath) + if err != nil { + return "", err + } + + id := strings.TrimSpace(string(data)) + if id == "" { + return "", fmt.Errorf("ID 文件为空") + } + + return id, nil +} + +// save 保存 ID 到文件 +func (m *Manager) save(id string) error { + // 确保目录存在 + dir := filepath.Dir(m.idFilePath) + if err := os.MkdirAll(dir, 0755); err != nil { + return fmt.Errorf("创建目录失败: %w", err) + } + + // 写入 ID 文件 + if err := os.WriteFile(m.idFilePath, []byte(id), 0644); err != nil { + return fmt.Errorf("写入文件失败: %w", err) + } + + return nil +} + +// GetPath 获取 ID 文件路径 +func (m *Manager) GetPath() string { + return m.idFilePath +} + +// Exists 检查 ID 文件是否存在 +func (m *Manager) Exists() bool { + _, err := os.Stat(m.idFilePath) + return err == nil +} + +// Delete 删除 ID 文件 +func (m *Manager) Delete() error { + return os.Remove(m.idFilePath) +} diff --git a/pkg/agent/service/agent.go b/pkg/agent/service/agent.go new file mode 100644 index 0000000..a459ae8 --- /dev/null +++ b/pkg/agent/service/agent.go @@ -0,0 +1,499 @@ +package service + +import ( + "context" + "encoding/json" + "fmt" + "log" + "os" + "runtime" + "sync" + "time" + + "github.com/dushixiang/pika/internal/protocol" + "github.com/dushixiang/pika/pkg/agent/audit" + "github.com/dushixiang/pika/pkg/agent/collector" + "github.com/dushixiang/pika/pkg/agent/config" + "github.com/dushixiang/pika/pkg/agent/id" + "github.com/dushixiang/pika/pkg/version" + "github.com/gorilla/websocket" + "github.com/jpillora/backoff" +) + +// safeConn 线程安全的 WebSocket 连接包装器 +type safeConn struct { + conn *websocket.Conn + mu sync.Mutex +} + +// WriteJSON 线程安全地写入 JSON 消息 +func (sc *safeConn) WriteJSON(v interface{}) error { + sc.mu.Lock() + defer sc.mu.Unlock() + return sc.conn.WriteJSON(v) +} + +// WriteMessage 线程安全地写入消息 +func (sc *safeConn) WriteMessage(messageType int, data []byte) error { + sc.mu.Lock() + defer sc.mu.Unlock() + return sc.conn.WriteMessage(messageType, data) +} + +// ReadJSON 读取 JSON 消息(读操作本身是安全的) +func (sc *safeConn) ReadJSON(v interface{}) error { + return sc.conn.ReadJSON(v) +} + +// Close 关闭连接 +func (sc *safeConn) Close() error { + return sc.conn.Close() +} + +// Agent 探针服务 +type Agent struct { + cfg *config.Config + idMgr *id.Manager + cancel context.CancelFunc +} + +// New 创建 Agent 实例 +func New(cfg *config.Config) *Agent { + return &Agent{ + cfg: cfg, + idMgr: id.NewManager(), + } +} + +// Start 启动探针服务 +func (a *Agent) Start(ctx context.Context) error { + // 创建可取消的 context + ctx, cancel := context.WithCancel(ctx) + a.cancel = cancel + + // 启动探针主循环 + b := &backoff.Backoff{ + Min: 5 * time.Second, + Max: 5 * time.Minute, + Factor: 2, + Jitter: true, + } + + for { + select { + case <-ctx.Done(): + return nil + default: + } + + if err := a.runOnce(ctx); err != nil { + retryAfter := b.Duration() + log.Printf("⚠️ 探针运行出错: %v,将在 %v 后重试", err, retryAfter) + + select { + case <-time.After(retryAfter): + continue + case <-ctx.Done(): + return nil + } + } + + // 正常断开,重置退避 + b.Reset() + log.Println("连接已断开,准备重连...") + time.Sleep(3 * time.Second) + } +} + +// Stop 停止探针服务 +func (a *Agent) Stop() { + if a.cancel != nil { + a.cancel() + } +} + +// runOnce 运行一次探针连接 +func (a *Agent) runOnce(ctx context.Context) error { + wsURL := a.cfg.GetWebSocketURL() + log.Printf("🔌 正在连接到服务器: %s", wsURL) + + // 连接到服务器 + rawConn, _, err := websocket.DefaultDialer.Dial(wsURL, nil) + if err != nil { + return fmt.Errorf("连接失败: %w", err) + } + defer rawConn.Close() + + // 创建线程安全的连接包装器 + conn := &safeConn{conn: rawConn} + + // 设置 Ping 处理器,自动响应服务端的 Ping + rawConn.SetPingHandler(func(appData string) error { + // WriteControl 有内置锁,可以安全调用 + err := rawConn.WriteControl(websocket.PongMessage, []byte(appData), time.Now().Add(time.Second)) + if err == nil { + log.Println("💓 收到 Ping,已发送 Pong") + } + return err + }) + + // 发送注册消息 + if err := a.registerAgent(conn); err != nil { + return fmt.Errorf("注册失败: %w", err) + } + + log.Println("✅ 探针注册成功,开始监控...") + + // 创建采集器管理器 + collectorManager := collector.NewManager(a.cfg) + + // 创建完成通道 + done := make(chan struct{}) + errChan := make(chan error, 3) + + // 启动读取循环(处理服务端的 Ping/Pong 等控制消息) + go func() { + if err := a.readLoop(rawConn, done); err != nil { + errChan <- fmt.Errorf("读取失败: %w", err) + } + }() + + // 启动心跳和数据发送 + go func() { + if err := a.heartbeatLoop(ctx, conn, done); err != nil { + errChan <- fmt.Errorf("心跳失败: %w", err) + } + }() + + go func() { + if err := a.metricsLoop(ctx, conn, collectorManager, done); err != nil { + errChan <- fmt.Errorf("数据采集失败: %w", err) + } + }() + + // 等待错误或上下文取消 + select { + case err := <-errChan: + close(done) + return err + case <-ctx.Done(): + close(done) + // 优雅关闭连接 + closeMsg := websocket.FormatCloseMessage(websocket.CloseNormalClosure, "") + if err := conn.WriteMessage(websocket.CloseMessage, closeMsg); err != nil { + log.Printf("⚠️ 关闭连接失败: %v", err) + } + time.Sleep(time.Second) + return nil + } +} + +// readLoop 读取服务端消息(主要用于处理 Ping/Pong 和指令) +func (a *Agent) readLoop(conn *websocket.Conn, done chan struct{}) error { + for { + select { + case <-done: + return nil + default: + } + + // 读取消息(这会触发 PingHandler) + _, message, err := conn.ReadMessage() + if err != nil { + // 检查是否是正常关闭 + if websocket.IsCloseError(err, websocket.CloseNormalClosure, websocket.CloseGoingAway) { + log.Println("服务端正常关闭连接") + return nil + } + // 其他错误 + return fmt.Errorf("读取消息失败: %w", err) + } + + // 解析消息 + var msg protocol.Message + if err := json.Unmarshal(message, &msg); err != nil { + log.Printf("⚠️ 解析消息失败: %v", err) + continue + } + + // 处理指令消息 + if msg.Type == protocol.MessageTypeCommand { + go a.handleCommand(conn, msg.Data) + } + } +} + +// registerAgent 注册探针 +func (a *Agent) registerAgent(conn *safeConn) error { + // 加载或生成探针 ID + agentID, err := a.idMgr.Load() + if err != nil { + return fmt.Errorf("加载 agent ID 失败: %w", err) + } + log.Printf("🆔 Agent ID: %s (存储在: %s)", agentID, a.idMgr.GetPath()) + + // 获取主机信息 + hostname, _ := os.Hostname() + if hostname == "" { + hostname = "unknown" + } + + // 使用配置或默认值 + agentName := a.cfg.Agent.Name + if agentName == "" { + agentName = hostname + } + + // 构建注册请求 + registerReq := protocol.RegisterRequest{ + AgentInfo: protocol.AgentInfo{ + ID: agentID, + Name: agentName, + Hostname: hostname, + OS: runtime.GOOS, + Arch: runtime.GOARCH, + Version: version.GetVersion(), + }, + ApiKey: a.cfg.Server.APIKey, + } + + reqData, err := json.Marshal(registerReq) + if err != nil { + return fmt.Errorf("序列化注册请求失败: %w", err) + } + + msg := protocol.Message{ + Type: protocol.MessageTypeRegister, + Data: reqData, + } + + if err := conn.WriteJSON(msg); err != nil { + return fmt.Errorf("发送注册消息失败: %w", err) + } + + // 读取注册响应 + var response protocol.Message + if err := conn.ReadJSON(&response); err != nil { + return fmt.Errorf("读取注册响应失败: %w", err) + } + + // 检查响应类型 + if response.Type == protocol.MessageTypeRegisterErr { + var errResp protocol.RegisterResponse + if err := json.Unmarshal(response.Data, &errResp); err == nil { + return fmt.Errorf("注册失败: %s", errResp.Message) + } + return fmt.Errorf("注册失败: 未知错误") + } + + if response.Type != protocol.MessageTypeRegisterAck { + return fmt.Errorf("注册失败: 收到未知响应类型 %s", response.Type) + } + + var registerResp protocol.RegisterResponse + if err := json.Unmarshal(response.Data, ®isterResp); err != nil { + return fmt.Errorf("解析注册响应失败: %w", err) + } + + log.Printf("注册成功: AgentID=%s, Status=%s", registerResp.AgentID, registerResp.Status) + return nil +} + +// heartbeatLoop 心跳循环 +func (a *Agent) heartbeatLoop(ctx context.Context, conn *safeConn, done chan struct{}) error { + ticker := time.NewTicker(a.cfg.GetHeartbeatInterval()) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + msg := protocol.Message{ + Type: protocol.MessageTypeHeartbeat, + Data: json.RawMessage(`{}`), + } + if err := conn.WriteJSON(msg); err != nil { + return fmt.Errorf("发送心跳失败: %w", err) + } + log.Println("💓 心跳已发送") + case <-done: + return nil + case <-ctx.Done(): + return nil + } + } +} + +// metricsLoop 指标采集循环 +func (a *Agent) metricsLoop(ctx context.Context, conn *safeConn, manager *collector.Manager, done chan struct{}) error { + // 立即采集一次动态数据 + if err := a.collectAndSendAllMetrics(conn, manager); err != nil { + log.Printf("⚠️ 初始数据采集失败: %v", err) + } + + // 定时采集动态指标 + ticker := time.NewTicker(a.cfg.GetCollectorInterval()) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + // 采集并发送各种动态指标 + if err := a.collectAndSendAllMetrics(conn, manager); err != nil { + return fmt.Errorf("数据采集失败: %w", err) + } + case <-done: + return nil + case <-ctx.Done(): + return nil + } + } +} + +// collectAndSendAllMetrics 采集并发送所有动态指标 +func (a *Agent) collectAndSendAllMetrics(conn *safeConn, manager *collector.Manager) error { + var hasError bool + + // CPU 动态指标 + if err := manager.CollectAndSendCPU(conn); err != nil { + log.Printf("⚠️ 发送CPU指标失败: %v", err) + hasError = true + } + + // 内存动态指标 + if err := manager.CollectAndSendMemory(conn); err != nil { + log.Printf("⚠️ 发送内存指标失败: %v", err) + hasError = true + } + + // 磁盘指标 + if err := manager.CollectAndSendDisk(conn); err != nil { + log.Printf("⚠️ 发送磁盘指标失败: %v", err) + hasError = true + } + + // 磁盘 IO 指标 + if err := manager.CollectAndSendDiskIO(conn); err != nil { + log.Printf("⚠️ 发送磁盘IO指标失败: %v", err) + hasError = true + } + + // 网络指标 + if err := manager.CollectAndSendNetwork(conn); err != nil { + log.Printf("⚠️ 发送网络指标失败: %v", err) + hasError = true + } + + // 系统负载指标 + if err := manager.CollectAndSendLoad(conn); err != nil { + log.Printf("⚠️ 发送负载指标失败: %v", err) + hasError = true + } + + // 主机信息 + if err := manager.CollectAndSendHost(conn); err != nil { + log.Printf("⚠️ 发送主机信息失败: %v", err) + hasError = true + } + + // GPU 信息(可选) + if err := manager.CollectAndSendGPU(conn); err != nil { + log.Printf("ℹ️ 发送GPU信息失败: %v", err) + } + + // 温度信息(可选) + if err := manager.CollectAndSendTemperature(conn); err != nil { + log.Printf("ℹ️ 发送温度信息失败: %v", err) + } + + if hasError { + return fmt.Errorf("部分指标采集失败") + } + + return nil +} + +// handleCommand 处理服务端下发的指令 +func (a *Agent) handleCommand(conn *websocket.Conn, data json.RawMessage) { + var cmdReq protocol.CommandRequest + if err := json.Unmarshal(data, &cmdReq); err != nil { + log.Printf("⚠️ 解析指令失败: %v", err) + return + } + + log.Printf("📥 收到指令: %s (ID: %s)", cmdReq.Type, cmdReq.ID) + + // 发送运行中状态 + a.sendCommandResponse(conn, cmdReq.ID, cmdReq.Type, "running", "", "") + + switch cmdReq.Type { + case "vps_audit": + a.handleVPSAudit(conn, cmdReq.ID) + default: + log.Printf("⚠️ 未知指令类型: %s", cmdReq.Type) + a.sendCommandResponse(conn, cmdReq.ID, cmdReq.Type, "error", "未知指令类型", "") + } +} + +// handleVPSAudit 处理VPS安全审计指令 +func (a *Agent) handleVPSAudit(conn *websocket.Conn, cmdID string) { + // 导入 audit 包 + result, err := a.runVPSAudit() + if err != nil { + log.Printf("❌ VPS安全审计失败: %v", err) + a.sendCommandResponse(conn, cmdID, "vps_audit", "error", err.Error(), "") + return + } + + // 将结果序列化为JSON + resultJSON, err := json.Marshal(result) + if err != nil { + log.Printf("❌ 序列化审计结果失败: %v", err) + a.sendCommandResponse(conn, cmdID, "vps_audit", "error", "序列化结果失败", "") + return + } + + log.Println("✅ VPS安全审计完成") + a.sendCommandResponse(conn, cmdID, "vps_audit", "success", "", string(resultJSON)) +} + +// runVPSAudit 运行VPS安全审计 +func (a *Agent) runVPSAudit() (*protocol.VPSAuditResult, error) { + return audit.RunAudit() +} + +// sendCommandResponse 发送指令响应 +func (a *Agent) sendCommandResponse(conn *websocket.Conn, cmdID, cmdType, status, errMsg, result string) { + resp := protocol.CommandResponse{ + ID: cmdID, + Type: cmdType, + Status: status, + Error: errMsg, + Result: result, + } + + respData, err := json.Marshal(resp) + if err != nil { + log.Printf("⚠️ 序列化指令响应失败: %v", err) + return + } + + msg := protocol.Message{ + Type: protocol.MessageTypeCommandResp, + Data: respData, + } + + msgData, err := json.Marshal(msg) + if err != nil { + log.Printf("⚠️ 序列化消息失败: %v", err) + return + } + + if err := conn.WriteMessage(websocket.TextMessage, msgData); err != nil { + log.Printf("⚠️ 发送指令响应失败: %v", err) + } +} + +// GetVersion 获取版本号 +func GetVersion() string { + return version.GetVersion() +} diff --git a/pkg/agent/service/service.go b/pkg/agent/service/service.go new file mode 100644 index 0000000..c2c4d51 --- /dev/null +++ b/pkg/agent/service/service.go @@ -0,0 +1,230 @@ +package service + +import ( + "context" + "fmt" + "log" + "os" + "os/signal" + "syscall" + + "github.com/dushixiang/pika/pkg/agent/config" + "github.com/dushixiang/pika/pkg/agent/updater" + "github.com/kardianos/service" +) + +// program 实现 service.Interface +type program struct { + cfg *config.Config + agent *Agent + ctx context.Context + cancel context.CancelFunc +} + +// Start 启动服务 +func (p *program) Start(s service.Service) error { + log.Println("✅ Pika Agent 服务启动中...") + + // 创建 context + p.ctx, p.cancel = context.WithCancel(context.Background()) + + // 创建 Agent 实例 + p.agent = New(p.cfg) + + // 启动自动更新(如果启用) + if p.cfg.AutoUpdate.Enabled { + upd, err := updater.New(p.cfg, GetVersion()) + if err != nil { + log.Printf("⚠️ 创建更新器失败: %v", err) + } else { + go upd.Start(p.ctx) + } + } + + // 在后台启动 Agent + go func() { + if err := p.agent.Start(p.ctx); err != nil { + log.Printf("⚠️ 探针运行出错: %v", err) + } + }() + + return nil +} + +// Stop 停止服务 +func (p *program) Stop(s service.Service) error { + log.Println("📴 Pika Agent 服务停止中...") + + if p.cancel != nil { + p.cancel() + } + + if p.agent != nil { + p.agent.Stop() + } + + log.Println("✅ Pika Agent 服务已停止") + return nil +} + +// ServiceManager 服务管理器 +type ServiceManager struct { + cfg *config.Config + service service.Service +} + +// NewServiceManager 创建服务管理器 +func NewServiceManager(cfg *config.Config) (*ServiceManager, error) { + // 获取可执行文件路径 + execPath, err := os.Executable() + if err != nil { + return nil, fmt.Errorf("获取可执行文件路径失败: %w", err) + } + + // 配置服务 + svcConfig := &service.Config{ + Name: "pika-agent", + DisplayName: "Pika Agent", + Description: "Pika 监控探针 - 采集系统性能指标并上报到服务端", + Arguments: []string{"run", "--config", cfg.Path}, + Executable: execPath, + Option: service.KeyValue{ + // Linux systemd 配置 + "Restart": "always", // 总是重启 + "RestartSec": "10", // 重启前等待 10 秒 + "StartLimitInterval": "0", // 无限制重启次数 + "KillMode": "process", // 只杀主进程 + + // Windows 配置 + "OnFailure": "restart", // 失败时重启 + "ResetPeriod": 86400, // 重置失败计数周期 (秒) + "RestartDelay": 10000, // 重启延迟 (毫秒) + + // 其他 Unix 系统 (upstart/launchd) + "KeepAlive": true, // 保持运行 + "RunAtLoad": true, // 启动时运行 + }, + } + + // 创建 program + prg := &program{ + cfg: cfg, + } + + // 创建服务 + s, err := service.New(prg, svcConfig) + if err != nil { + return nil, fmt.Errorf("创建服务失败: %w", err) + } + + return &ServiceManager{ + cfg: cfg, + service: s, + }, nil +} + +// Install 安装服务 +func (m *ServiceManager) Install() error { + return m.service.Install() +} + +// Uninstall 卸载服务 +func (m *ServiceManager) Uninstall() error { + // 先停止服务 + _ = m.service.Stop() + + return m.service.Uninstall() +} + +// Start 启动服务 +func (m *ServiceManager) Start() error { + return m.service.Start() +} + +// Stop 停止服务 +func (m *ServiceManager) Stop() error { + return m.service.Stop() +} + +// Restart 重启服务 +func (m *ServiceManager) Restart() error { + return m.service.Restart() +} + +// Status 查看服务状态 +func (m *ServiceManager) Status() (string, error) { + status, err := m.service.Status() + if err != nil { + return "", err + } + + var statusStr string + switch status { + case service.StatusRunning: + statusStr = "运行中 (Running)" + case service.StatusStopped: + statusStr = "已停止 (Stopped)" + case service.StatusUnknown: + statusStr = "未知 (Unknown)" + default: + statusStr = fmt.Sprintf("状态: %d", status) + } + + return statusStr, nil +} + +// Run 运行服务(用于 service run 命令) +func (m *ServiceManager) Run() error { + // 检查是否在服务模式下运行 + interactive := service.Interactive() + + if !interactive { + // 在服务管理器控制下运行 + return m.service.Run() + } + + // 交互模式(前台运行) + log.Printf("✅ 配置加载成功") + log.Printf(" 服务器地址: %s", m.cfg.Server.Endpoint) + log.Printf(" 采集间隔: %v", m.cfg.GetCollectorInterval()) + log.Printf(" 心跳间隔: %v", m.cfg.GetHeartbeatInterval()) + + // 创建 context + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // 监听系统信号 + interrupt := make(chan os.Signal, 1) + signal.Notify(interrupt, os.Interrupt, syscall.SIGTERM) + + // 创建 Agent 实例 + agent := New(m.cfg) + + // 启动自动更新(如果启用) + if m.cfg.AutoUpdate.Enabled { + upd, err := updater.New(m.cfg, GetVersion()) + if err != nil { + log.Printf("⚠️ 创建更新器失败: %v", err) + } else { + go upd.Start(ctx) + } + } + + // 启动 Agent + go func() { + if err := agent.Start(ctx); err != nil { + log.Printf("⚠️ 探针运行出错: %v", err) + } + }() + + // 等待中断信号 + <-interrupt + log.Println("📴 收到中断信号,正在关闭...") + cancel() + + // 等待 Agent 停止 + agent.Stop() + log.Println("✅ 探针已停止") + + return nil +} diff --git a/pkg/agent/updater/updater.go b/pkg/agent/updater/updater.go new file mode 100644 index 0000000..925f16e --- /dev/null +++ b/pkg/agent/updater/updater.go @@ -0,0 +1,268 @@ +package updater + +import ( + "context" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "os" + "os/exec" + "path/filepath" + "runtime" + "time" + + "github.com/dushixiang/pika/pkg/agent/config" +) + +// VersionInfo 版本信息 +type VersionInfo struct { + Version string `json:"version"` +} + +// Updater 自动更新器 +type Updater struct { + cfg *config.Config + currentVer string + httpClient *http.Client + executablePath string +} + +// New 创建更新器 +func New(cfg *config.Config, currentVer string) (*Updater, error) { + execPath, err := os.Executable() + if err != nil { + return nil, fmt.Errorf("获取可执行文件路径失败: %w", err) + } + + return &Updater{ + cfg: cfg, + currentVer: currentVer, + httpClient: &http.Client{ + Timeout: 60 * time.Second, + }, + executablePath: execPath, + }, nil +} + +// Start 启动自动更新检查 +func (u *Updater) Start(ctx context.Context) { + if !u.cfg.AutoUpdate.Enabled { + log.Println("自动更新已禁用") + return + } + + log.Printf("自动更新已启用,检查间隔: %v", u.cfg.GetUpdateCheckInterval()) + + // 立即检查一次 + u.checkAndUpdate() + + // 定时检查 + ticker := time.NewTicker(u.cfg.GetUpdateCheckInterval()) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + u.checkAndUpdate() + case <-ctx.Done(): + log.Println("停止自动更新检查") + return + } + } +} + +// checkAndUpdate 检查并更新 +func (u *Updater) checkAndUpdate() { + log.Println("🔍 检查更新...") + + // 获取最新版本信息 + versionInfo, err := u.fetchLatestVersion() + if err != nil { + log.Printf("⚠️ 获取版本信息失败: %v", err) + return + } + + // 比较版本 + if versionInfo.Version == u.currentVer { + log.Printf("✅ 当前已是最新版本: %s", u.currentVer) + return + } + + log.Printf("🆕 发现新版本: %s (当前版本: %s)", versionInfo.Version, u.currentVer) + + // 下载新版本 + if err := u.downloadAndUpdate(versionInfo); err != nil { + log.Printf("❌ 更新失败: %v", err) + return + } + + log.Println("✅ 更新成功,将在下次重启时生效") +} + +// fetchLatestVersion 获取最新版本信息 +func (u *Updater) fetchLatestVersion() (*VersionInfo, error) { + updateURL := u.cfg.GetUpdateURL() + return CheckUpdate(updateURL, u.currentVer) +} + +// downloadAndUpdate 下载并更新 +func (u *Updater) downloadAndUpdate(versionInfo *VersionInfo) error { + log.Printf("📥 下载新版本: %s", versionInfo.Version) + + downloadURL := u.cfg.GetDownloadURL() + if err := Update(downloadURL); err != nil { + return err + } + + log.Printf("✅ 新版本已安装到: %s", u.executablePath) + return nil +} + +// CheckUpdate 手动检查更新(用于命令行) +func CheckUpdate(updateURL, currentVer string) (*VersionInfo, error) { + client := &http.Client{ + Timeout: 30 * time.Second, + } + + url := fmt.Sprintf("%s?os=%s&arch=%s", updateURL, runtime.GOOS, runtime.GOARCH) + + resp, err := client.Get(url) + if err != nil { + return nil, fmt.Errorf("请求失败: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("HTTP 状态码: %d", resp.StatusCode) + } + + var versionInfo VersionInfo + if err := json.NewDecoder(resp.Body).Decode(&versionInfo); err != nil { + return nil, fmt.Errorf("解析响应失败: %w", err) + } + + return &versionInfo, nil +} + +// Update 手动更新(用于命令行) +func Update(downloadURL string) error { + execPath, err := os.Executable() + if err != nil { + return fmt.Errorf("获取可执行文件路径失败: %w", err) + } + + // 解析实际路径(处理符号链接) + execPath, err = filepath.EvalSymlinks(execPath) + if err != nil { + return fmt.Errorf("解析可执行文件路径失败: %w", err) + } + + client := &http.Client{ + Timeout: 300 * time.Second, + } + + // 下载文件 + resp, err := client.Get(downloadURL) + if err != nil { + return fmt.Errorf("下载失败: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("HTTP 状态码: %d", resp.StatusCode) + } + + // 创建临时文件 + tmpFile := execPath + ".new" + out, err := os.OpenFile(tmpFile, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755) + if err != nil { + return fmt.Errorf("创建临时文件失败: %w", err) + } + defer out.Close() + defer os.Remove(tmpFile) + + // 写入文件 + written, err := io.Copy(out, resp.Body) + if err != nil { + return fmt.Errorf("写入文件失败: %w", err) + } + + log.Printf("下载完成,文件大小: %d 字节", written) + + // 根据操作系统选择不同的更新策略 + if runtime.GOOS == "windows" { + // Windows: 使用批处理脚本延迟替换 + return updateOnWindows(execPath, tmpFile) + } + + // Unix-like: 直接替换 + return updateOnUnix(execPath, tmpFile) +} + +// updateOnUnix Unix-like 系统的更新逻辑 +func updateOnUnix(execPath, tmpFile string) error { + // 备份旧版本 + backupPath := execPath + ".bak" + if err := os.Rename(execPath, backupPath); err != nil { + return fmt.Errorf("备份旧版本失败: %w", err) + } + + // 替换为新版本 + if err := os.Rename(tmpFile, execPath); err != nil { + // 恢复备份 + os.Rename(backupPath, execPath) + return fmt.Errorf("替换新版本失败: %w", err) + } + + // 删除备份 + os.Remove(backupPath) + + log.Println("✅ 更新完成,进程即将退出,等待系统服务重启...") + + // 退出当前进程,让系统服务管理器(systemd/supervisor等)自动重启 + // 注意:这要求服务配置了自动重启(如 systemd 的 Restart=always) + os.Exit(0) + + return nil +} + +// updateOnWindows Windows 系统的更新逻辑 +func updateOnWindows(execPath, tmpFile string) error { + // 在 Windows 上,无法直接替换正在运行的可执行文件 + // 策略: 创建一个批处理脚本来延迟替换和重启 + + batScript := execPath + ".update.bat" + batContent := fmt.Sprintf(`@echo off +timeout /t 2 /nobreak >nul +move /y "%s" "%s.bak" +move /y "%s" "%s" +del "%s.bak" +start "" "%s" +del "%%~f0" +`, execPath, execPath, tmpFile, execPath, execPath, execPath) + + if err := os.WriteFile(batScript, []byte(batContent), 0755); err != nil { + return fmt.Errorf("创建更新脚本失败: %w", err) + } + + log.Println("✅ 更新完成,准备重启进程...") + + // 启动批处理脚本并退出当前进程 + cmd := exec.Command("cmd.exe", "/C", batScript) + cmd.Stdout = nil + cmd.Stderr = nil + cmd.Stdin = nil + + if err := cmd.Start(); err != nil { + os.Remove(batScript) + return fmt.Errorf("启动更新脚本失败: %w", err) + } + + // 让系统服务管理器来重启(当前进程退出后) + log.Println("进程即将退出,等待系统服务重启...") + os.Exit(0) + + return nil +} diff --git a/pkg/version/version.go b/pkg/version/version.go new file mode 100644 index 0000000..7ef48f5 --- /dev/null +++ b/pkg/version/version.go @@ -0,0 +1,12 @@ +package version + +// Version 版本号(通过 -ldflags 注入) +var Version = "dev" + +// GetVersion 获取版本号 +func GetVersion() string { + if Version == "" { + return "dev" + } + return Version +} diff --git a/playground/docker-compose.yml b/playground/docker-compose.yml new file mode 100644 index 0000000..f293f4b --- /dev/null +++ b/playground/docker-compose.yml @@ -0,0 +1,16 @@ +services: + postgresql: + image: registry.cn-beijing.aliyuncs.com/dushixiang/postgres:16.4 + environment: + POSTGRES_DB: pika + POSTGRES_USER: pika + POSTGRES_PASSWORD: pika + volumes: + - ./data/postgresql:/var/lib/postgresql/data + ports: + - "25432:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U pika"] + interval: 10s + timeout: 5s + retries: 5 diff --git a/web/.gitignore b/web/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/web/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/web/README.md b/web/README.md new file mode 100644 index 0000000..d2e7761 --- /dev/null +++ b/web/README.md @@ -0,0 +1,73 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/web/assets.go b/web/assets.go new file mode 100644 index 0000000..8b5c29f --- /dev/null +++ b/web/assets.go @@ -0,0 +1,14 @@ +package web + +import ( + "embed" + "io/fs" +) + +//go:embed dist/* +var distFS embed.FS + +func Assets() fs.FS { + sub, _ := fs.Sub(distFS, "dist") + return sub +} diff --git a/web/eslint.config.js b/web/eslint.config.js new file mode 100644 index 0000000..b19330b --- /dev/null +++ b/web/eslint.config.js @@ -0,0 +1,23 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs['recommended-latest'], + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]) diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..5926f6a --- /dev/null +++ b/web/index.html @@ -0,0 +1,13 @@ + + + + + + + 皮卡监控 + + +
+ + + diff --git a/web/package-lock.json b/web/package-lock.json new file mode 100644 index 0000000..7f05595 --- /dev/null +++ b/web/package-lock.json @@ -0,0 +1,6364 @@ +{ + "name": "web", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "web", + "version": "0.0.0", + "dependencies": { + "@ant-design/pro-components": "^2.8.10", + "@tailwindcss/vite": "^4.1.14", + "@tanstack/react-query": "^5.90.6", + "antd": "^5.27.4", + "axios": "^1.12.2", + "dayjs": "^1.11.18", + "ip-regex": "^5.0.0", + "is-cidr": "^6.0.1", + "lucide-react": "^0.545.0", + "react": "^19.1.1", + "react-dom": "^19.1.1", + "react-router-dom": "^7.9.4", + "recharts": "^3.3.0", + "tailwind-merge": "^3.3.1", + "tailwindcss": "^4.1.14", + "validator": "^13.15.15" + }, + "devDependencies": { + "@eslint/js": "^9.36.0", + "@types/node": "^24.6.0", + "@types/react": "^19.1.16", + "@types/react-dom": "^19.1.9", + "@types/validator": "^13.15.3", + "@vitejs/plugin-react": "^5.0.4", + "eslint": "^9.36.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.22", + "globals": "^16.4.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.45.0", + "vite": "^7.1.7" + } + }, + "node_modules/@ant-design/colors": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-7.2.1.tgz", + "integrity": "sha512-lCHDcEzieu4GA3n8ELeZ5VQ8pKQAWcGGLRTQ50aQM2iqPpq2evTxER84jfdPvsPAtEcZ7m44NI45edFMo8oOYQ==", + "license": "MIT", + "dependencies": { + "@ant-design/fast-color": "^2.0.6" + } + }, + "node_modules/@ant-design/cssinjs": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/@ant-design/cssinjs/-/cssinjs-1.24.0.tgz", + "integrity": "sha512-K4cYrJBsgvL+IoozUXYjbT6LHHNt+19a9zkvpBPxLjFHas1UpPM2A5MlhROb0BT8N8WoavM5VsP9MeSeNK/3mg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "@emotion/hash": "^0.8.0", + "@emotion/unitless": "^0.7.5", + "classnames": "^2.3.1", + "csstype": "^3.1.3", + "rc-util": "^5.35.0", + "stylis": "^4.3.4" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@ant-design/cssinjs-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@ant-design/cssinjs-utils/-/cssinjs-utils-1.1.3.tgz", + "integrity": "sha512-nOoQMLW1l+xR1Co8NFVYiP8pZp3VjIIzqV6D6ShYF2ljtdwWJn5WSsH+7kvCktXL/yhEtWURKOfH5Xz/gzlwsg==", + "license": "MIT", + "dependencies": { + "@ant-design/cssinjs": "^1.21.0", + "@babel/runtime": "^7.23.2", + "rc-util": "^5.38.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@ant-design/fast-color": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@ant-design/fast-color/-/fast-color-2.0.6.tgz", + "integrity": "sha512-y2217gk4NqL35giHl72o6Zzqji9O7vHh9YmhUVkPtAOpoTCH4uWxo/pr4VE8t0+ChEPs0qo4eJRC5Q1eXWo3vA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.7" + }, + "engines": { + "node": ">=8.x" + } + }, + "node_modules/@ant-design/icons": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.6.1.tgz", + "integrity": "sha512-0/xS39c91WjPAZOWsvi1//zjx6kAp4kxWwctR6kuU6p133w8RU0D2dSCvZC19uQyharg/sAvYxGYWl01BbZZfg==", + "license": "MIT", + "dependencies": { + "@ant-design/colors": "^7.0.0", + "@ant-design/icons-svg": "^4.4.0", + "@babel/runtime": "^7.24.8", + "classnames": "^2.2.6", + "rc-util": "^5.31.1" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@ant-design/icons-svg": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz", + "integrity": "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==", + "license": "MIT" + }, + "node_modules/@ant-design/pro-card": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@ant-design/pro-card/-/pro-card-2.10.0.tgz", + "integrity": "sha512-sLONn1odmE0Wkbse8pol4WiaEzBV8JU5s3FAMflPpycfUcbSaa1ktXzQ7LCo2SAvOS7gkfmpFjBPtrfbigKh4g==", + "license": "MIT", + "dependencies": { + "@ant-design/cssinjs": "^1.21.1", + "@ant-design/icons": "^5.0.0", + "@ant-design/pro-provider": "2.16.2", + "@ant-design/pro-utils": "2.18.0", + "@babel/runtime": "^7.18.0", + "classnames": "^2.3.2", + "rc-resize-observer": "^1.0.0", + "rc-util": "^5.4.0" + }, + "peerDependencies": { + "antd": "^4.24.15 || ^5.11.2", + "react": ">=17.0.0" + } + }, + "node_modules/@ant-design/pro-components": { + "version": "2.8.10", + "resolved": "https://registry.npmjs.org/@ant-design/pro-components/-/pro-components-2.8.10.tgz", + "integrity": "sha512-QHnnIXdmC5GTAtm6i8eeJy5yT9npPlFyxpDm+duiDrTRKRFaAQBduArxlH3DA/hoRCCypzPONxfK9BQNIhIyZA==", + "license": "MIT", + "dependencies": { + "@ant-design/pro-card": "2.10.0", + "@ant-design/pro-descriptions": "2.6.10", + "@ant-design/pro-field": "3.1.0", + "@ant-design/pro-form": "2.32.0", + "@ant-design/pro-layout": "7.22.7", + "@ant-design/pro-list": "2.6.10", + "@ant-design/pro-provider": "2.16.2", + "@ant-design/pro-skeleton": "2.2.1", + "@ant-design/pro-table": "3.21.0", + "@ant-design/pro-utils": "2.18.0", + "@babel/runtime": "^7.16.3" + }, + "peerDependencies": { + "antd": "^4.24.15 || ^5.11.2", + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, + "node_modules/@ant-design/pro-descriptions": { + "version": "2.6.10", + "resolved": "https://registry.npmjs.org/@ant-design/pro-descriptions/-/pro-descriptions-2.6.10.tgz", + "integrity": "sha512-+4MbiOfumnWlW0Awm4m8JML5o3lR649FD24AaivCmr8BQvIAAXdTITnDMXEg8BqvdP4KOvNsStZrvYfqoev33A==", + "license": "MIT", + "dependencies": { + "@ant-design/pro-field": "3.1.0", + "@ant-design/pro-form": "2.32.0", + "@ant-design/pro-provider": "2.16.2", + "@ant-design/pro-skeleton": "2.2.1", + "@ant-design/pro-utils": "2.18.0", + "@babel/runtime": "^7.18.0", + "rc-resize-observer": "^0.2.3", + "rc-util": "^5.0.6" + }, + "peerDependencies": { + "antd": "^4.24.15 || ^5.11.2", + "react": ">=17.0.0" + } + }, + "node_modules/@ant-design/pro-descriptions/node_modules/rc-resize-observer": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/rc-resize-observer/-/rc-resize-observer-0.2.6.tgz", + "integrity": "sha512-YX6nYnd6fk7zbuvT6oSDMKiZjyngjHoy+fz+vL3Tez38d/G5iGdaDJa2yE7345G6sc4Mm1IGRUIwclvltddhmA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.1", + "rc-util": "^5.0.0", + "resize-observer-polyfill": "^1.5.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@ant-design/pro-field": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@ant-design/pro-field/-/pro-field-3.1.0.tgz", + "integrity": "sha512-+Dgp31WjD+iwg9KIRAMgNkfQivkJKMcYBrIBmho1e8ep/O0HgWSp48g70tBIWi/Lfem/Ky2schF7O8XCFouczw==", + "license": "MIT", + "dependencies": { + "@ant-design/icons": "^5.0.0", + "@ant-design/pro-provider": "2.16.2", + "@ant-design/pro-utils": "2.18.0", + "@babel/runtime": "^7.18.0", + "@chenshuai2144/sketch-color": "^1.0.8", + "classnames": "^2.3.2", + "dayjs": "^1.11.10", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "rc-util": "^5.4.0", + "swr": "^2.0.0" + }, + "peerDependencies": { + "antd": "^4.24.15 || ^5.11.2", + "react": ">=17.0.0" + } + }, + "node_modules/@ant-design/pro-form": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/@ant-design/pro-form/-/pro-form-2.32.0.tgz", + "integrity": "sha512-GZnVAMeYv+YHJb17lJ7rX5PYuQPvEA6EotQnPbHi9tGLN3PfexcAd21rqzuO+OrulU2x7TEMDIxtY9MzvvOGbg==", + "license": "MIT", + "dependencies": { + "@ant-design/icons": "^5.0.0", + "@ant-design/pro-field": "3.1.0", + "@ant-design/pro-provider": "2.16.2", + "@ant-design/pro-utils": "2.18.0", + "@babel/runtime": "^7.18.0", + "@chenshuai2144/sketch-color": "^1.0.7", + "@umijs/use-params": "^1.0.9", + "classnames": "^2.3.2", + "dayjs": "^1.11.10", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "rc-resize-observer": "^1.1.0", + "rc-util": "^5.0.6" + }, + "peerDependencies": { + "antd": "^4.24.15 || ^5.11.2", + "rc-field-form": ">=1.22.0", + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, + "node_modules/@ant-design/pro-layout": { + "version": "7.22.7", + "resolved": "https://registry.npmjs.org/@ant-design/pro-layout/-/pro-layout-7.22.7.tgz", + "integrity": "sha512-fvmtNA1r9SaasVIQIQt611VSlNxtVxDbQ3e+1GhYQza3tVJi/3gCZuDyfMfTnbLmf3PaW/YvLkn7MqDbzAzoLA==", + "license": "MIT", + "dependencies": { + "@ant-design/cssinjs": "^1.21.1", + "@ant-design/icons": "^5.0.0", + "@ant-design/pro-provider": "2.16.2", + "@ant-design/pro-utils": "2.18.0", + "@babel/runtime": "^7.18.0", + "@umijs/route-utils": "^4.0.0", + "@umijs/use-params": "^1.0.9", + "classnames": "^2.3.2", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "path-to-regexp": "8.2.0", + "rc-resize-observer": "^1.1.0", + "rc-util": "^5.0.6", + "swr": "^2.0.0", + "warning": "^4.0.3" + }, + "peerDependencies": { + "antd": "^4.24.15 || ^5.11.2", + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, + "node_modules/@ant-design/pro-list": { + "version": "2.6.10", + "resolved": "https://registry.npmjs.org/@ant-design/pro-list/-/pro-list-2.6.10.tgz", + "integrity": "sha512-xSWwnqCr+hPEYR4qY7nFUaxO5RQBxNlFaPNmobP2i+Im31slk9JuAusgWeIYO0mNhLJuLbxd8CCma2AZij3fBQ==", + "license": "MIT", + "dependencies": { + "@ant-design/cssinjs": "^1.21.1", + "@ant-design/icons": "^5.0.0", + "@ant-design/pro-card": "2.10.0", + "@ant-design/pro-field": "3.1.0", + "@ant-design/pro-table": "3.21.0", + "@ant-design/pro-utils": "2.18.0", + "@babel/runtime": "^7.18.0", + "classnames": "^2.3.2", + "dayjs": "^1.11.10", + "rc-resize-observer": "^1.0.0", + "rc-util": "^4.19.0" + }, + "peerDependencies": { + "antd": "^4.24.15 || ^5.11.2", + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, + "node_modules/@ant-design/pro-list/node_modules/rc-util": { + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-4.21.1.tgz", + "integrity": "sha512-Z+vlkSQVc1l8O2UjR3WQ+XdWlhj5q9BMQNLk2iOBch75CqPfrJyGtcWMcnhRlNuDu0Ndtt4kLVO8JI8BrABobg==", + "license": "MIT", + "dependencies": { + "add-dom-event-listener": "^1.1.0", + "prop-types": "^15.5.10", + "react-is": "^16.12.0", + "react-lifecycles-compat": "^3.0.4", + "shallowequal": "^1.1.0" + } + }, + "node_modules/@ant-design/pro-list/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/@ant-design/pro-provider": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/@ant-design/pro-provider/-/pro-provider-2.16.2.tgz", + "integrity": "sha512-0KmCH1EaOND787Jz6VRMYtLNZmqfT0JPjdUfxhyOxFfnBRfrjyfZgIa6CQoAJLEUMWv57PccWS8wRHVUUk2Yiw==", + "license": "MIT", + "dependencies": { + "@ant-design/cssinjs": "^1.21.1", + "@babel/runtime": "^7.18.0", + "@ctrl/tinycolor": "^3.4.0", + "dayjs": "^1.11.10", + "rc-util": "^5.0.1", + "swr": "^2.0.0" + }, + "peerDependencies": { + "antd": "^4.24.15 || ^5.11.2", + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, + "node_modules/@ant-design/pro-skeleton": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ant-design/pro-skeleton/-/pro-skeleton-2.2.1.tgz", + "integrity": "sha512-3M2jNOZQZWEDR8pheY00OkHREfb0rquvFZLCa6DypGmiksiuuYuR9Y4iA82ZF+mva2FmpHekdwbje/GpbxqBeg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.0" + }, + "peerDependencies": { + "antd": "^4.24.15 || ^5.11.2", + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, + "node_modules/@ant-design/pro-table": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@ant-design/pro-table/-/pro-table-3.21.0.tgz", + "integrity": "sha512-sI81d3FYRv5sXamUc+M5CsHZ9CchuUQgOAPzo5H4oPAVL5h+mkYGRsBzPsxQX7khTNpWjrAtPoRm5ipx3vvWog==", + "license": "MIT", + "dependencies": { + "@ant-design/cssinjs": "^1.21.1", + "@ant-design/icons": "^5.0.0", + "@ant-design/pro-card": "2.10.0", + "@ant-design/pro-field": "3.1.0", + "@ant-design/pro-form": "2.32.0", + "@ant-design/pro-provider": "2.16.2", + "@ant-design/pro-utils": "2.18.0", + "@babel/runtime": "^7.18.0", + "@dnd-kit/core": "^6.0.8", + "@dnd-kit/modifiers": "^6.0.1", + "@dnd-kit/sortable": "^7.0.2", + "@dnd-kit/utilities": "^3.2.1", + "classnames": "^2.3.2", + "dayjs": "^1.11.10", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "rc-resize-observer": "^1.0.0", + "rc-util": "^5.0.1" + }, + "peerDependencies": { + "antd": "^4.24.15 || ^5.11.2", + "rc-field-form": ">=1.22.0", + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, + "node_modules/@ant-design/pro-utils": { + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/@ant-design/pro-utils/-/pro-utils-2.18.0.tgz", + "integrity": "sha512-8+ikyrN8L8a8Ph4oeHTOJEiranTj18+9+WHCHjKNdEfukI7Rjn8xpYdLJWb2AUJkb9d4eoAqjd5+k+7w81Df0w==", + "license": "MIT", + "dependencies": { + "@ant-design/icons": "^5.0.0", + "@ant-design/pro-provider": "2.16.2", + "@babel/runtime": "^7.18.0", + "classnames": "^2.3.2", + "dayjs": "^1.11.10", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "rc-util": "^5.0.6", + "safe-stable-stringify": "^2.4.3", + "swr": "^2.0.0" + }, + "peerDependencies": { + "antd": "^4.24.15 || ^5.11.2", + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, + "node_modules/@ant-design/react-slick": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@ant-design/react-slick/-/react-slick-1.1.2.tgz", + "integrity": "sha512-EzlvzE6xQUBrZuuhSAFTdsr4P2bBBHGZwKFemEfq8gIGyIQCxalYfZW/T2ORbtQx5rU69o+WycP3exY/7T1hGA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.4", + "classnames": "^2.2.5", + "json2mq": "^0.2.0", + "resize-observer-polyfill": "^1.5.1", + "throttle-debounce": "^5.0.0" + }, + "peerDependencies": { + "react": ">=16.9.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz", + "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", + "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.4", + "@babel/types": "^7.28.4", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.4" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", + "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", + "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@chenshuai2144/sketch-color": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@chenshuai2144/sketch-color/-/sketch-color-1.0.9.tgz", + "integrity": "sha512-obzSy26cb7Pm7OprWyVpgMpIlrZpZ0B7vbrU0RMbvRg0YAI890S5Xy02Aj1Nhl4+KTbi1lVYHt6HQP8Hm9s+1w==", + "license": "MIT", + "dependencies": { + "reactcss": "^1.2.3", + "tinycolor2": "^1.4.2" + }, + "peerDependencies": { + "react": ">=16.12.0" + } + }, + "node_modules/@ctrl/tinycolor": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz", + "integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/@dnd-kit/accessibility": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", + "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/core": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", + "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", + "license": "MIT", + "dependencies": { + "@dnd-kit/accessibility": "^3.1.1", + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/modifiers": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/modifiers/-/modifiers-6.0.1.tgz", + "integrity": "sha512-rbxcsg3HhzlcMHVHWDuh9LCjpOVAgqbV78wLGI8tziXY3+qcMQ61qVXIvNKQFuhj75dSfD+o+PYZQ/NUk2A23A==", + "license": "MIT", + "dependencies": { + "@dnd-kit/utilities": "^3.2.1", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.0.6", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/sortable": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-7.0.2.tgz", + "integrity": "sha512-wDkBHHf9iCi1veM834Gbk1429bd4lHX4RpAwT0y2cHLf246GAvU2sVw/oxWNpPKQNQRQaeGXhAVgrOl1IT+iyA==", + "license": "MIT", + "dependencies": { + "@dnd-kit/utilities": "^3.2.0", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.0.7", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/utilities": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", + "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", + "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==", + "license": "MIT" + }, + "node_modules/@emotion/unitless": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==", + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz", + "integrity": "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.10.tgz", + "integrity": "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.10.tgz", + "integrity": "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.10.tgz", + "integrity": "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.10.tgz", + "integrity": "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.10.tgz", + "integrity": "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.10.tgz", + "integrity": "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.10.tgz", + "integrity": "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.10.tgz", + "integrity": "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.10.tgz", + "integrity": "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.10.tgz", + "integrity": "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.10.tgz", + "integrity": "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.10.tgz", + "integrity": "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.10.tgz", + "integrity": "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.10.tgz", + "integrity": "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.10.tgz", + "integrity": "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.10.tgz", + "integrity": "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.10.tgz", + "integrity": "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.10.tgz", + "integrity": "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.10.tgz", + "integrity": "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.10.tgz", + "integrity": "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.10.tgz", + "integrity": "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.10.tgz", + "integrity": "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.10.tgz", + "integrity": "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.10.tgz", + "integrity": "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.10.tgz", + "integrity": "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.0.tgz", + "integrity": "sha512-WUFvV4WoIwW8Bv0KeKCIIEgdSiFOsulyN0xrMu+7z43q/hkOLXjvb5u7UC9jDxvRzcrbEmuZBX5yJZz1741jog==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.16.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz", + "integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.37.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.37.0.tgz", + "integrity": "sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz", + "integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.16.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@rc-component/async-validator": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@rc-component/async-validator/-/async-validator-5.0.4.tgz", + "integrity": "sha512-qgGdcVIF604M9EqjNF0hbUTz42bz/RDtxWdWuU5EQe3hi7M8ob54B6B35rOsvX5eSvIHIzT9iH1R3n+hk3CGfg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.4" + }, + "engines": { + "node": ">=14.x" + } + }, + "node_modules/@rc-component/color-picker": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@rc-component/color-picker/-/color-picker-2.0.1.tgz", + "integrity": "sha512-WcZYwAThV/b2GISQ8F+7650r5ZZJ043E57aVBFkQ+kSY4C6wdofXgB0hBx+GPGpIU0Z81eETNoDUJMr7oy/P8Q==", + "license": "MIT", + "dependencies": { + "@ant-design/fast-color": "^2.0.6", + "@babel/runtime": "^7.23.6", + "classnames": "^2.2.6", + "rc-util": "^5.38.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/context": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@rc-component/context/-/context-1.4.0.tgz", + "integrity": "sha512-kFcNxg9oLRMoL3qki0OMxK+7g5mypjgaaJp/pkOis/6rVxma9nJBF/8kCIuTYHUQNr0ii7MxqE33wirPZLJQ2w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "rc-util": "^5.27.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/mini-decimal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rc-component/mini-decimal/-/mini-decimal-1.1.0.tgz", + "integrity": "sha512-jS4E7T9Li2GuYwI6PyiVXmxTiM6b07rlD9Ge8uGZSCz3WlzcG5ZK7g5bbuKNeZ9pgUuPK/5guV781ujdVpm4HQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.0" + }, + "engines": { + "node": ">=8.x" + } + }, + "node_modules/@rc-component/mutate-observer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rc-component/mutate-observer/-/mutate-observer-1.1.0.tgz", + "integrity": "sha512-QjrOsDXQusNwGZPf4/qRQasg7UFEj06XiCJ8iuiq/Io7CrHrgVi6Uuetw60WAMG1799v+aM8kyc+1L/GBbHSlw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.0", + "classnames": "^2.3.2", + "rc-util": "^5.24.4" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/portal": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@rc-component/portal/-/portal-1.1.2.tgz", + "integrity": "sha512-6f813C0IsasTZms08kfA8kPAGxbbkYToa8ALaiDIGGECU4i9hj8Plgbx0sNJDrey3EtHO30hmdaxtT0138xZcg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.0", + "classnames": "^2.3.2", + "rc-util": "^5.24.4" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/qrcode": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rc-component/qrcode/-/qrcode-1.0.1.tgz", + "integrity": "sha512-g8eeeaMyFXVlq8cZUeaxCDhfIYjpao0l9cvm5gFwKXy/Vm1yDWV7h2sjH5jHYzdFedlVKBpATFB1VKMrHzwaWQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.7", + "classnames": "^2.3.2" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/tour": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@rc-component/tour/-/tour-1.15.1.tgz", + "integrity": "sha512-Tr2t7J1DKZUpfJuDZWHxyxWpfmj8EZrqSgyMZ+BCdvKZ6r1UDsfU46M/iWAAFBy961Ssfom2kv5f3UcjIL2CmQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.0", + "@rc-component/portal": "^1.0.0-9", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.3.2", + "rc-util": "^5.24.4" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/trigger": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@rc-component/trigger/-/trigger-2.3.0.tgz", + "integrity": "sha512-iwaxZyzOuK0D7lS+0AQEtW52zUWxoGqTGkke3dRyb8pYiShmRpCjB/8TzPI4R6YySCH7Vm9BZj/31VPiiQTLBg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2", + "@rc-component/portal": "^1.1.0", + "classnames": "^2.3.2", + "rc-motion": "^2.0.0", + "rc-resize-observer": "^1.3.1", + "rc-util": "^5.44.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@reduxjs/toolkit": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.10.0.tgz", + "integrity": "sha512-DCDKKB+DDy00kULFGoj5jtxRXksCGtEKEPbeSqRi20/vUrwrBD+zyJx+jQ7xkIU1pUHQ6OIYHyc9NiVSEjAlNA==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^10.2.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.38", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.38.tgz", + "integrity": "sha512-N/ICGKleNhA5nc9XXQG/kkKHJ7S55u0x0XUJbbkmdCnFuoRkM1Il12q9q0eX19+M7KKUEPw/daUPIRnxhcxAIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.4.tgz", + "integrity": "sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.4.tgz", + "integrity": "sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.4.tgz", + "integrity": "sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.4.tgz", + "integrity": "sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.4.tgz", + "integrity": "sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.4.tgz", + "integrity": "sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.4.tgz", + "integrity": "sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.4.tgz", + "integrity": "sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.4.tgz", + "integrity": "sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.4.tgz", + "integrity": "sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.4.tgz", + "integrity": "sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.4.tgz", + "integrity": "sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.4.tgz", + "integrity": "sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.4.tgz", + "integrity": "sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.4.tgz", + "integrity": "sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.4.tgz", + "integrity": "sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.4.tgz", + "integrity": "sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.4.tgz", + "integrity": "sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.4.tgz", + "integrity": "sha512-8GKr640PdFNXwzIE0IrkMWUNUomILLkfeHjXBi/nUvFlpZP+FA8BKGKpacjW6OUUHaNI6sUURxR2U2g78FOHWQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.4.tgz", + "integrity": "sha512-AIy/jdJ7WtJ/F6EcfOb2GjR9UweO0n43jNObQMb6oGxkYTfLcnN7vYYpG+CN3lLxrQkzWnMOoNSHTW54pgbVxw==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.4.tgz", + "integrity": "sha512-UF9KfsH9yEam0UjTwAgdK0anlQ7c8/pWPU2yVjyWcF1I1thABt6WXE47cI71pGiZ8wGvxohBoLnxM04L/wj8mQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.4.tgz", + "integrity": "sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.14.tgz", + "integrity": "sha512-hpz+8vFk3Ic2xssIA3e01R6jkmsAhvkQdXlEbRTk6S10xDAtiQiM3FyvZVGsucefq764euO/b8WUW9ysLdThHw==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.6.0", + "lightningcss": "1.30.1", + "magic-string": "^0.30.19", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.14" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.14.tgz", + "integrity": "sha512-23yx+VUbBwCg2x5XWdB8+1lkPajzLmALEfMb51zZUBYaYVPDQvBSD/WYDqiVyBIo2BZFa3yw1Rpy3G2Jp+K0dw==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.4", + "tar": "^7.5.1" + }, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.14", + "@tailwindcss/oxide-darwin-arm64": "4.1.14", + "@tailwindcss/oxide-darwin-x64": "4.1.14", + "@tailwindcss/oxide-freebsd-x64": "4.1.14", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.14", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.14", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.14", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.14", + "@tailwindcss/oxide-linux-x64-musl": "4.1.14", + "@tailwindcss/oxide-wasm32-wasi": "4.1.14", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.14", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.14" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.14.tgz", + "integrity": "sha512-a94ifZrGwMvbdeAxWoSuGcIl6/DOP5cdxagid7xJv6bwFp3oebp7y2ImYsnZBMTwjn5Ev5xESvS3FFYUGgPODQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.14.tgz", + "integrity": "sha512-HkFP/CqfSh09xCnrPJA7jud7hij5ahKyWomrC3oiO2U9i0UjP17o9pJbxUN0IJ471GTQQmzwhp0DEcpbp4MZTA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.14.tgz", + "integrity": "sha512-eVNaWmCgdLf5iv6Qd3s7JI5SEFBFRtfm6W0mphJYXgvnDEAZ5sZzqmI06bK6xo0IErDHdTA5/t7d4eTfWbWOFw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.14.tgz", + "integrity": "sha512-QWLoRXNikEuqtNb0dhQN6wsSVVjX6dmUFzuuiL09ZeXju25dsei2uIPl71y2Ic6QbNBsB4scwBoFnlBfabHkEw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.14.tgz", + "integrity": "sha512-VB4gjQni9+F0VCASU+L8zSIyjrLLsy03sjcR3bM0V2g4SNamo0FakZFKyUQ96ZVwGK4CaJsc9zd/obQy74o0Fw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.14.tgz", + "integrity": "sha512-qaEy0dIZ6d9vyLnmeg24yzA8XuEAD9WjpM5nIM1sUgQ/Zv7cVkharPDQcmm/t/TvXoKo/0knI3me3AGfdx6w1w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.14.tgz", + "integrity": "sha512-ISZjT44s59O8xKsPEIesiIydMG/sCXoMBCqsphDm/WcbnuWLxxb+GcvSIIA5NjUw6F8Tex7s5/LM2yDy8RqYBQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.14.tgz", + "integrity": "sha512-02c6JhLPJj10L2caH4U0zF8Hji4dOeahmuMl23stk0MU1wfd1OraE7rOloidSF8W5JTHkFdVo/O7uRUJJnUAJg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.14.tgz", + "integrity": "sha512-TNGeLiN1XS66kQhxHG/7wMeQDOoL0S33x9BgmydbrWAb9Qw0KYdd8o1ifx4HOGDWhVmJ+Ul+JQ7lyknQFilO3Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.14.tgz", + "integrity": "sha512-uZYAsaW/jS/IYkd6EWPJKW/NlPNSkWkBlaeVBi/WsFQNP05/bzkebUL8FH1pdsqx4f2fH/bWFcUABOM9nfiJkQ==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.5.0", + "@emnapi/runtime": "^1.5.0", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.0.5", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.14.tgz", + "integrity": "sha512-Az0RnnkcvRqsuoLH2Z4n3JfAef0wElgzHD5Aky/e+0tBUxUhIeIqFBTMNQvmMRSP15fWwmvjBxZ3Q8RhsDnxAA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.14.tgz", + "integrity": "sha512-ttblVGHgf68kEE4om1n/n44I0yGPkCPbLsqzjvybhpwa6mKKtgFfAzy6btc3HRmuW7nHe0OOrSeNP9sQmmH9XA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.14.tgz", + "integrity": "sha512-BoFUoU0XqgCUS1UXWhmDJroKKhNXeDzD7/XwabjkDIAbMnc4ULn5e2FuEuBbhZ6ENZoSYzKlzvZ44Yr6EUDUSA==", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.1.14", + "@tailwindcss/oxide": "4.1.14", + "tailwindcss": "4.1.14" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.90.6", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.6.tgz", + "integrity": "sha512-AnZSLF26R8uX+tqb/ivdrwbVdGemdEDm1Q19qM6pry6eOZ6bEYiY7mWhzXT1YDIPTNEVcZ5kYP9nWjoxDLiIVw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.90.6", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.6.tgz", + "integrity": "sha512-gB1sljYjcobZKxjPbKSa31FUTyr+ROaBdoH+wSSs9Dk+yDCmMs+TkTV3PybRRVLC7ax7q0erJ9LvRWnMktnRAw==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.90.6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.7.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.7.1.tgz", + "integrity": "sha512-CmyhGZanP88uuC5GpWU9q+fI61j2SkhO3UGMUdfYRE6Bcy0ccyzn1Rqj9YAB/ZY4kOXmNf0ocah5GtphmLMP6Q==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.14.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.2", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", + "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.1", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.1.tgz", + "integrity": "sha512-/EEvYBdT3BflCWvTMO7YkYBHVE9Ci6XdqZciZANQgKpaiDRGOLIlRo91jbTNRQjgPFWVaRxcYc0luVNFitz57A==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, + "node_modules/@types/validator": { + "version": "13.15.3", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.3.tgz", + "integrity": "sha512-7bcUmDyS6PN3EuD9SlGGOxM77F8WLVsrwkxyWxKnxzmXoequ6c7741QBrANq6htVRGOITJ7z72mTP6Z4XyuG+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.0.tgz", + "integrity": "sha512-hA8gxBq4ukonVXPy0OKhiaUh/68D0E88GSmtC1iAEnGaieuDi38LhS7jdCHRLi6ErJBNDGCzvh5EnzdPwUc0DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.46.0", + "@typescript-eslint/type-utils": "8.46.0", + "@typescript-eslint/utils": "8.46.0", + "@typescript-eslint/visitor-keys": "8.46.0", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.46.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.0.tgz", + "integrity": "sha512-n1H6IcDhmmUEG7TNVSspGmiHHutt7iVKtZwRppD7e04wha5MrkV1h3pti9xQLcCMt6YWsncpoT0HMjkH1FNwWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.46.0", + "@typescript-eslint/types": "8.46.0", + "@typescript-eslint/typescript-estree": "8.46.0", + "@typescript-eslint/visitor-keys": "8.46.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.0.tgz", + "integrity": "sha512-OEhec0mH+U5Je2NZOeK1AbVCdm0ChyapAyTeXVIYTPXDJ3F07+cu87PPXcGoYqZ7M9YJVvFnfpGg1UmCIqM+QQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.46.0", + "@typescript-eslint/types": "^8.46.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.0.tgz", + "integrity": "sha512-lWETPa9XGcBes4jqAMYD9fW0j4n6hrPtTJwWDmtqgFO/4HF4jmdH/Q6wggTw5qIT5TXjKzbt7GsZUBnWoO3dqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.0", + "@typescript-eslint/visitor-keys": "8.46.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.0.tgz", + "integrity": "sha512-WrYXKGAHY836/N7zoK/kzi6p8tXFhasHh8ocFL9VZSAkvH956gfeRfcnhs3xzRy8qQ/dq3q44v1jvQieMFg2cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.0.tgz", + "integrity": "sha512-hy+lvYV1lZpVs2jRaEYvgCblZxUoJiPyCemwbQZ+NGulWkQRy0HRPYAoef/CNSzaLt+MLvMptZsHXHlkEilaeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.0", + "@typescript-eslint/typescript-estree": "8.46.0", + "@typescript-eslint/utils": "8.46.0", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.0.tgz", + "integrity": "sha512-bHGGJyVjSE4dJJIO5yyEWt/cHyNwga/zXGJbJJ8TiO01aVREK6gCTu3L+5wrkb1FbDkQ+TKjMNe9R/QQQP9+rA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.0.tgz", + "integrity": "sha512-ekDCUfVpAKWJbRfm8T1YRrCot1KFxZn21oV76v5Fj4tr7ELyk84OS+ouvYdcDAwZL89WpEkEj2DKQ+qg//+ucg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.46.0", + "@typescript-eslint/tsconfig-utils": "8.46.0", + "@typescript-eslint/types": "8.46.0", + "@typescript-eslint/visitor-keys": "8.46.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.0.tgz", + "integrity": "sha512-nD6yGWPj1xiOm4Gk0k6hLSZz2XkNXhuYmyIrOWcHoPuAhjT9i5bAG+xbWPgFeNR8HPHHtpNKdYUXJl/D3x7f5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.46.0", + "@typescript-eslint/types": "8.46.0", + "@typescript-eslint/typescript-estree": "8.46.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.0.tgz", + "integrity": "sha512-FrvMpAK+hTbFy7vH5j1+tMYHMSKLE6RzluFJlkFNKD0p9YsUT75JlBSmr5so3QRzvMwU5/bIEdeNrxm8du8l3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@umijs/route-utils": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@umijs/route-utils/-/route-utils-4.0.3.tgz", + "integrity": "sha512-zPEcYhl1cSfkSRDzzGgoD1mDvGjxoOTJFvkn55srfgdQ3NZe2ZMCScCU6DEnOxuKP1XDVf8pqyqCDVd2+RCQIw==", + "license": "MIT" + }, + "node_modules/@umijs/use-params": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@umijs/use-params/-/use-params-1.0.9.tgz", + "integrity": "sha512-QlN0RJSBVQBwLRNxbxjQ5qzqYIGn+K7USppMoIOVlf7fxXHsnQZ2bEsa6Pm74bt6DVQxpUE8HqvdStn6Y9FV1w==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.0.4.tgz", + "integrity": "sha512-La0KD0vGkVkSk6K+piWDKRUyg8Rl5iAIKRMH0vMJI0Eg47bq1eOxmoObAaQG37WMW9MSyk7Cs8EIWwJC1PtzKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.4", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.38", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/add-dom-event-listener": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/add-dom-event-listener/-/add-dom-event-listener-1.1.0.tgz", + "integrity": "sha512-WCxx1ixHT0GQU9hb0KI/mhgRQhnU+U3GvwY6ZvVjYq8rsihIGoaIOUbY0yMPBxLH5MDtr0kz3fisWGNcbWW7Jw==", + "license": "MIT", + "dependencies": { + "object-assign": "4.x" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/antd": { + "version": "5.27.4", + "resolved": "https://registry.npmjs.org/antd/-/antd-5.27.4.tgz", + "integrity": "sha512-rhArohoAUCxhkPjGI/BXthOrrjaElL4Fb7d4vEHnIR3DpxFXfegd4rN21IgGdiF+Iz4EFuUZu8MdS8NuJHLSVQ==", + "license": "MIT", + "dependencies": { + "@ant-design/colors": "^7.2.1", + "@ant-design/cssinjs": "^1.23.0", + "@ant-design/cssinjs-utils": "^1.1.3", + "@ant-design/fast-color": "^2.0.6", + "@ant-design/icons": "^5.6.1", + "@ant-design/react-slick": "~1.1.2", + "@babel/runtime": "^7.26.0", + "@rc-component/color-picker": "~2.0.1", + "@rc-component/mutate-observer": "^1.1.0", + "@rc-component/qrcode": "~1.0.0", + "@rc-component/tour": "~1.15.1", + "@rc-component/trigger": "^2.3.0", + "classnames": "^2.5.1", + "copy-to-clipboard": "^3.3.3", + "dayjs": "^1.11.11", + "rc-cascader": "~3.34.0", + "rc-checkbox": "~3.5.0", + "rc-collapse": "~3.9.0", + "rc-dialog": "~9.6.0", + "rc-drawer": "~7.3.0", + "rc-dropdown": "~4.2.1", + "rc-field-form": "~2.7.0", + "rc-image": "~7.12.0", + "rc-input": "~1.8.0", + "rc-input-number": "~9.5.0", + "rc-mentions": "~2.20.0", + "rc-menu": "~9.16.1", + "rc-motion": "^2.9.5", + "rc-notification": "~5.6.4", + "rc-pagination": "~5.1.0", + "rc-picker": "~4.11.3", + "rc-progress": "~4.0.0", + "rc-rate": "~2.13.1", + "rc-resize-observer": "^1.4.3", + "rc-segmented": "~2.7.0", + "rc-select": "~14.16.8", + "rc-slider": "~11.1.9", + "rc-steps": "~6.0.1", + "rc-switch": "~4.1.0", + "rc-table": "~7.53.0", + "rc-tabs": "~15.7.0", + "rc-textarea": "~1.10.2", + "rc-tooltip": "~6.4.0", + "rc-tree": "~5.13.1", + "rc-tree-select": "~5.27.0", + "rc-upload": "~4.9.2", + "rc-util": "^5.44.4", + "scroll-into-view-if-needed": "^3.1.0", + "throttle-debounce": "^5.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ant-design" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", + "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.15", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.15.tgz", + "integrity": "sha512-qsJ8/X+UypqxHXN75M7dF88jNK37dLBRW7LeUzCPz+TNs37G8cfWy9nWzS+LS//g600zrt2le9KuXt0rWfDz5Q==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.26.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz", + "integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.8.9", + "caniuse-lite": "^1.0.30001746", + "electron-to-chromium": "^1.5.227", + "node-releases": "^2.0.21", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001749", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001749.tgz", + "integrity": "sha512-0rw2fJOmLfnzCRbkm8EyHL8SvI2Apu5UbnQuTsJ0ClgrH8hcwFooJ1s5R0EP8o8aVrFu8++ae29Kt9/gZAZp/Q==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/cidr-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/cidr-regex/-/cidr-regex-5.0.1.tgz", + "integrity": "sha512-2Apfc6qH9uwF3QHmlYBA8ExB9VHq+1/Doj9sEMY55TVBcpQ3y/+gmMpcNIBBtfb5k54Vphmta+1IxjMqPlWWAA==", + "license": "BSD-2-Clause", + "dependencies": { + "ip-regex": "5.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "license": "MIT" + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/compute-scroll-into-view": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz", + "integrity": "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==", + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/copy-to-clipboard": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", + "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", + "license": "MIT", + "dependencies": { + "toggle-selection": "^1.0.6" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/dayjs": { + "version": "1.11.18", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.18.tgz", + "integrity": "sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.234", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.234.tgz", + "integrity": "sha512-RXfEp2x+VRYn8jbKfQlRImzoJU01kyDvVPBmG39eU2iuRVhuS6vQNocB8J0/8GrIMLnPzgz4eW6WiRnJkTuNWg==", + "dev": true, + "license": "ISC" + }, + "node_modules/enhanced-resolve": { + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-toolkit": { + "version": "1.41.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.41.0.tgz", + "integrity": "sha512-bDd3oRmbVgqZCJS6WmeQieOrzpl3URcWBUVDXxOELlUW2FuW+0glPOz1n0KnRie+PdyvUZcXz2sOn00c6pPRIA==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, + "node_modules/esbuild": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz", + "integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.10", + "@esbuild/android-arm": "0.25.10", + "@esbuild/android-arm64": "0.25.10", + "@esbuild/android-x64": "0.25.10", + "@esbuild/darwin-arm64": "0.25.10", + "@esbuild/darwin-x64": "0.25.10", + "@esbuild/freebsd-arm64": "0.25.10", + "@esbuild/freebsd-x64": "0.25.10", + "@esbuild/linux-arm": "0.25.10", + "@esbuild/linux-arm64": "0.25.10", + "@esbuild/linux-ia32": "0.25.10", + "@esbuild/linux-loong64": "0.25.10", + "@esbuild/linux-mips64el": "0.25.10", + "@esbuild/linux-ppc64": "0.25.10", + "@esbuild/linux-riscv64": "0.25.10", + "@esbuild/linux-s390x": "0.25.10", + "@esbuild/linux-x64": "0.25.10", + "@esbuild/netbsd-arm64": "0.25.10", + "@esbuild/netbsd-x64": "0.25.10", + "@esbuild/openbsd-arm64": "0.25.10", + "@esbuild/openbsd-x64": "0.25.10", + "@esbuild/openharmony-arm64": "0.25.10", + "@esbuild/sunos-x64": "0.25.10", + "@esbuild/win32-arm64": "0.25.10", + "@esbuild/win32-ia32": "0.25.10", + "@esbuild/win32-x64": "0.25.10" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.37.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.37.0.tgz", + "integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.4.0", + "@eslint/core": "^0.16.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.37.0", + "@eslint/plugin-kit": "^0.4.0", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.23.tgz", + "integrity": "sha512-G4j+rv0NmbIR45kni5xJOrYvCtyD3/7LjpVH8MPPcudXDcNu8gv+4ATTDXTtbRR8rTCM5HxECvCSsRmxKnWDsA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", + "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immer": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/ip-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-5.0.0.tgz", + "integrity": "sha512-fOCG6lhoKKakwv+C6KdsOnGvgXnmgfmp0myi3bcNwj3qfwPAxRKWEuFhvEFF7ceYIz6+1jRZ+yguLFAmUNPEfw==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-cidr": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/is-cidr/-/is-cidr-6.0.1.tgz", + "integrity": "sha512-JIJlvXodfsoWFAvvjB7Elqu8qQcys2SZjkIJCLdk4XherUqZ6+zH7WIpXkp4B3ZxMH0Fz7zIsZwyvs6JfM0csw==", + "license": "BSD-2-Clause", + "dependencies": { + "cidr-regex": "5.0.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json2mq": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/json2mq/-/json2mq-0.2.0.tgz", + "integrity": "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==", + "license": "MIT", + "dependencies": { + "string-convert": "^0.2.0" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", + "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-darwin-arm64": "1.30.1", + "lightningcss-darwin-x64": "1.30.1", + "lightningcss-freebsd-x64": "1.30.1", + "lightningcss-linux-arm-gnueabihf": "1.30.1", + "lightningcss-linux-arm64-gnu": "1.30.1", + "lightningcss-linux-arm64-musl": "1.30.1", + "lightningcss-linux-x64-gnu": "1.30.1", + "lightningcss-linux-x64-musl": "1.30.1", + "lightningcss-win32-arm64-msvc": "1.30.1", + "lightningcss-win32-x64-msvc": "1.30.1" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz", + "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", + "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz", + "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz", + "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz", + "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz", + "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", + "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz", + "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz", + "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz", + "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.545.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.545.0.tgz", + "integrity": "sha512-7r1/yUuflQDSt4f1bpn5ZAocyIxcTyVyBBChSVtBKn5M+392cPmI5YJMWOJKk/HUWGm5wg83chlAZtCcGbEZtw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.19", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", + "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.23", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.23.tgz", + "integrity": "sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/rc-cascader": { + "version": "3.34.0", + "resolved": "https://registry.npmjs.org/rc-cascader/-/rc-cascader-3.34.0.tgz", + "integrity": "sha512-KpXypcvju9ptjW9FaN2NFcA2QH9E9LHKq169Y0eWtH4e/wHQ5Wh5qZakAgvb8EKZ736WZ3B0zLLOBsrsja5Dag==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.7", + "classnames": "^2.3.1", + "rc-select": "~14.16.2", + "rc-tree": "~5.13.0", + "rc-util": "^5.43.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-checkbox": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/rc-checkbox/-/rc-checkbox-3.5.0.tgz", + "integrity": "sha512-aOAQc3E98HteIIsSqm6Xk2FPKIER6+5vyEFMZfo73TqM+VVAIqOkHoPjgKLqSNtVLWScoaM7vY2ZrGEheI79yg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.3.2", + "rc-util": "^5.25.2" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-collapse": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/rc-collapse/-/rc-collapse-3.9.0.tgz", + "integrity": "sha512-swDdz4QZ4dFTo4RAUMLL50qP0EY62N2kvmk2We5xYdRwcRn8WcYtuetCJpwpaCbUfUt5+huLpVxhvmnK+PHrkA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "2.x", + "rc-motion": "^2.3.4", + "rc-util": "^5.27.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-dialog": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/rc-dialog/-/rc-dialog-9.6.0.tgz", + "integrity": "sha512-ApoVi9Z8PaCQg6FsUzS8yvBEQy0ZL2PkuvAgrmohPkN3okps5WZ5WQWPc1RNuiOKaAYv8B97ACdsFU5LizzCqg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/portal": "^1.0.0-8", + "classnames": "^2.2.6", + "rc-motion": "^2.3.0", + "rc-util": "^5.21.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-drawer": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/rc-drawer/-/rc-drawer-7.3.0.tgz", + "integrity": "sha512-DX6CIgiBWNpJIMGFO8BAISFkxiuKitoizooj4BDyee8/SnBn0zwO2FHrNDpqqepj0E/TFTDpmEBCyFuTgC7MOg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@rc-component/portal": "^1.1.1", + "classnames": "^2.2.6", + "rc-motion": "^2.6.1", + "rc-util": "^5.38.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-dropdown": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/rc-dropdown/-/rc-dropdown-4.2.1.tgz", + "integrity": "sha512-YDAlXsPv3I1n42dv1JpdM7wJ+gSUBfeyPK59ZpBD9jQhK9jVuxpjj3NmWQHOBceA1zEPVX84T2wbdb2SD0UjmA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.2.6", + "rc-util": "^5.44.1" + }, + "peerDependencies": { + "react": ">=16.11.0", + "react-dom": ">=16.11.0" + } + }, + "node_modules/rc-field-form": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/rc-field-form/-/rc-field-form-2.7.0.tgz", + "integrity": "sha512-hgKsCay2taxzVnBPZl+1n4ZondsV78G++XVsMIJCAoioMjlMQR9YwAp7JZDIECzIu2Z66R+f4SFIRrO2DjDNAA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.0", + "@rc-component/async-validator": "^5.0.3", + "rc-util": "^5.32.2" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-image": { + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/rc-image/-/rc-image-7.12.0.tgz", + "integrity": "sha512-cZ3HTyyckPnNnUb9/DRqduqzLfrQRyi+CdHjdqgsyDpI3Ln5UX1kXnAhPBSJj9pVRzwRFgqkN7p9b6HBDjmu/Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.2", + "@rc-component/portal": "^1.0.2", + "classnames": "^2.2.6", + "rc-dialog": "~9.6.0", + "rc-motion": "^2.6.2", + "rc-util": "^5.34.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-input": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/rc-input/-/rc-input-1.8.0.tgz", + "integrity": "sha512-KXvaTbX+7ha8a/k+eg6SYRVERK0NddX8QX7a7AnRvUa/rEH0CNMlpcBzBkhI0wp2C8C4HlMoYl8TImSN+fuHKA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-util": "^5.18.1" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/rc-input-number": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/rc-input-number/-/rc-input-number-9.5.0.tgz", + "integrity": "sha512-bKaEvB5tHebUURAEXw35LDcnRZLq3x1k7GxfAqBMzmpHkDGzjAtnUL8y4y5N15rIFIg5IJgwr211jInl3cipag==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/mini-decimal": "^1.0.1", + "classnames": "^2.2.5", + "rc-input": "~1.8.0", + "rc-util": "^5.40.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-mentions": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/rc-mentions/-/rc-mentions-2.20.0.tgz", + "integrity": "sha512-w8HCMZEh3f0nR8ZEd466ATqmXFCMGMN5UFCzEUL0bM/nGw/wOS2GgRzKBcm19K++jDyuWCOJOdgcKGXU3fXfbQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.22.5", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.2.6", + "rc-input": "~1.8.0", + "rc-menu": "~9.16.0", + "rc-textarea": "~1.10.0", + "rc-util": "^5.34.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-menu": { + "version": "9.16.1", + "resolved": "https://registry.npmjs.org/rc-menu/-/rc-menu-9.16.1.tgz", + "integrity": "sha512-ghHx6/6Dvp+fw8CJhDUHFHDJ84hJE3BXNCzSgLdmNiFErWSOaZNsihDAsKq9ByTALo/xkNIwtDFGIl6r+RPXBg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/trigger": "^2.0.0", + "classnames": "2.x", + "rc-motion": "^2.4.3", + "rc-overflow": "^1.3.1", + "rc-util": "^5.27.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-motion": { + "version": "2.9.5", + "resolved": "https://registry.npmjs.org/rc-motion/-/rc-motion-2.9.5.tgz", + "integrity": "sha512-w+XTUrfh7ArbYEd2582uDrEhmBHwK1ZENJiSJVb7uRxdE7qJSYjbO2eksRXmndqyKqKoYPc9ClpPh5242mV1vA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-util": "^5.44.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-notification": { + "version": "5.6.4", + "resolved": "https://registry.npmjs.org/rc-notification/-/rc-notification-5.6.4.tgz", + "integrity": "sha512-KcS4O6B4qzM3KH7lkwOB7ooLPZ4b6J+VMmQgT51VZCeEcmghdeR4IrMcFq0LG+RPdnbe/ArT086tGM8Snimgiw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "2.x", + "rc-motion": "^2.9.0", + "rc-util": "^5.20.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-overflow": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rc-overflow/-/rc-overflow-1.4.1.tgz", + "integrity": "sha512-3MoPQQPV1uKyOMVNd6SZfONi+f3st0r8PksexIdBTeIYbMX0Jr+k7pHEDvsXtR4BpCv90/Pv2MovVNhktKrwvw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-resize-observer": "^1.0.0", + "rc-util": "^5.37.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-pagination": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/rc-pagination/-/rc-pagination-5.1.0.tgz", + "integrity": "sha512-8416Yip/+eclTFdHXLKTxZvn70duYVGTvUUWbckCCZoIl3jagqke3GLsFrMs0bsQBikiYpZLD9206Ej4SOdOXQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.3.2", + "rc-util": "^5.38.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-picker": { + "version": "4.11.3", + "resolved": "https://registry.npmjs.org/rc-picker/-/rc-picker-4.11.3.tgz", + "integrity": "sha512-MJ5teb7FlNE0NFHTncxXQ62Y5lytq6sh5nUw0iH8OkHL/TjARSEvSHpr940pWgjGANpjCwyMdvsEV55l5tYNSg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.7", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.2.1", + "rc-overflow": "^1.3.2", + "rc-resize-observer": "^1.4.0", + "rc-util": "^5.43.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "date-fns": ">= 2.x", + "dayjs": ">= 1.x", + "luxon": ">= 3.x", + "moment": ">= 2.x", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + }, + "peerDependenciesMeta": { + "date-fns": { + "optional": true + }, + "dayjs": { + "optional": true + }, + "luxon": { + "optional": true + }, + "moment": { + "optional": true + } + } + }, + "node_modules/rc-progress": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/rc-progress/-/rc-progress-4.0.0.tgz", + "integrity": "sha512-oofVMMafOCokIUIBnZLNcOZFsABaUw8PPrf1/y0ZBvKZNpOiu5h4AO9vv11Sw0p4Hb3D0yGWuEattcQGtNJ/aw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.6", + "rc-util": "^5.16.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-rate": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/rc-rate/-/rc-rate-2.13.1.tgz", + "integrity": "sha512-QUhQ9ivQ8Gy7mtMZPAjLbxBt5y9GRp65VcUyGUMF3N3fhiftivPHdpuDIaWIMOTEprAjZPC08bls1dQB+I1F2Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.5", + "rc-util": "^5.0.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-resize-observer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/rc-resize-observer/-/rc-resize-observer-1.4.3.tgz", + "integrity": "sha512-YZLjUbyIWox8E9i9C3Tm7ia+W7euPItNWSPX5sCcQTYbnwDb5uNpnLHQCG1f22oZWUhLw4Mv2tFmeWe68CDQRQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.20.7", + "classnames": "^2.2.1", + "rc-util": "^5.44.1", + "resize-observer-polyfill": "^1.5.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-segmented": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/rc-segmented/-/rc-segmented-2.7.0.tgz", + "integrity": "sha512-liijAjXz+KnTRVnxxXG2sYDGd6iLL7VpGGdR8gwoxAXy2KglviKCxLWZdjKYJzYzGSUwKDSTdYk8brj54Bn5BA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-motion": "^2.4.4", + "rc-util": "^5.17.0" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/rc-select": { + "version": "14.16.8", + "resolved": "https://registry.npmjs.org/rc-select/-/rc-select-14.16.8.tgz", + "integrity": "sha512-NOV5BZa1wZrsdkKaiK7LHRuo5ZjZYMDxPP6/1+09+FB4KoNi8jcG1ZqLE3AVCxEsYMBe65OBx71wFoHRTP3LRg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/trigger": "^2.1.1", + "classnames": "2.x", + "rc-motion": "^2.0.1", + "rc-overflow": "^1.3.1", + "rc-util": "^5.16.1", + "rc-virtual-list": "^3.5.2" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/rc-slider": { + "version": "11.1.9", + "resolved": "https://registry.npmjs.org/rc-slider/-/rc-slider-11.1.9.tgz", + "integrity": "sha512-h8IknhzSh3FEM9u8ivkskh+Ef4Yo4JRIY2nj7MrH6GQmrwV6mcpJf5/4KgH5JaVI1H3E52yCdpOlVyGZIeph5A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.5", + "rc-util": "^5.36.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-steps": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/rc-steps/-/rc-steps-6.0.1.tgz", + "integrity": "sha512-lKHL+Sny0SeHkQKKDJlAjV5oZ8DwCdS2hFhAkIjuQt1/pB81M0cA0ErVFdHq9+jmPmFw1vJB2F5NBzFXLJxV+g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.16.7", + "classnames": "^2.2.3", + "rc-util": "^5.16.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-switch": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/rc-switch/-/rc-switch-4.1.0.tgz", + "integrity": "sha512-TI8ufP2Az9oEbvyCeVE4+90PDSljGyuwix3fV58p7HV2o4wBnVToEyomJRVyTaZeqNPAp+vqeo4Wnj5u0ZZQBg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.21.0", + "classnames": "^2.2.1", + "rc-util": "^5.30.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-table": { + "version": "7.53.1", + "resolved": "https://registry.npmjs.org/rc-table/-/rc-table-7.53.1.tgz", + "integrity": "sha512-firAd7Z+liqIDS5TubJ1qqcoBd6YcANLKWQDZhFf3rfoOTt/UNPj4n3O+2vhl+z4QMqwPEUVAil661WHA8H8Aw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/context": "^1.4.0", + "classnames": "^2.2.5", + "rc-resize-observer": "^1.1.0", + "rc-util": "^5.44.3", + "rc-virtual-list": "^3.14.2" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-tabs": { + "version": "15.7.0", + "resolved": "https://registry.npmjs.org/rc-tabs/-/rc-tabs-15.7.0.tgz", + "integrity": "sha512-ZepiE+6fmozYdWf/9gVp7k56PKHB1YYoDsKeQA1CBlJ/POIhjkcYiv0AGP0w2Jhzftd3AVvZP/K+V+Lpi2ankA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.2", + "classnames": "2.x", + "rc-dropdown": "~4.2.0", + "rc-menu": "~9.16.0", + "rc-motion": "^2.6.2", + "rc-resize-observer": "^1.0.0", + "rc-util": "^5.34.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-textarea": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/rc-textarea/-/rc-textarea-1.10.2.tgz", + "integrity": "sha512-HfaeXiaSlpiSp0I/pvWpecFEHpVysZ9tpDLNkxQbMvMz6gsr7aVZ7FpWP9kt4t7DB+jJXesYS0us1uPZnlRnwQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.1", + "rc-input": "~1.8.0", + "rc-resize-observer": "^1.0.0", + "rc-util": "^5.27.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-tooltip": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/rc-tooltip/-/rc-tooltip-6.4.0.tgz", + "integrity": "sha512-kqyivim5cp8I5RkHmpsp1Nn/Wk+1oeloMv9c7LXNgDxUpGm+RbXJGL+OPvDlcRnx9DBeOe4wyOIl4OKUERyH1g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.2", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.3.1", + "rc-util": "^5.44.3" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-tree": { + "version": "5.13.1", + "resolved": "https://registry.npmjs.org/rc-tree/-/rc-tree-5.13.1.tgz", + "integrity": "sha512-FNhIefhftobCdUJshO7M8uZTA9F4OPGVXqGfZkkD/5soDeOhwO06T/aKTrg0WD8gRg/pyfq+ql3aMymLHCTC4A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "2.x", + "rc-motion": "^2.0.1", + "rc-util": "^5.16.1", + "rc-virtual-list": "^3.5.1" + }, + "engines": { + "node": ">=10.x" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/rc-tree-select": { + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/rc-tree-select/-/rc-tree-select-5.27.0.tgz", + "integrity": "sha512-2qTBTzwIT7LRI1o7zLyrCzmo5tQanmyGbSaGTIf7sYimCklAToVVfpMC6OAldSKolcnjorBYPNSKQqJmN3TCww==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.7", + "classnames": "2.x", + "rc-select": "~14.16.2", + "rc-tree": "~5.13.0", + "rc-util": "^5.43.0" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/rc-upload": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/rc-upload/-/rc-upload-4.9.2.tgz", + "integrity": "sha512-nHx+9rbd1FKMiMRYsqQ3NkXUv7COHPBo3X1Obwq9SWS6/diF/A0aJ5OHubvwUAIDs+4RMleljV0pcrNUc823GQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "classnames": "^2.2.5", + "rc-util": "^5.2.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-util": { + "version": "5.44.4", + "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-5.44.4.tgz", + "integrity": "sha512-resueRJzmHG9Q6rI/DfK6Kdv9/Lfls05vzMs1Sk3M2P+3cJa+MakaZyWY8IPfehVuhPJFKrIY1IK4GqbiaiY5w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "react-is": "^18.2.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-virtual-list": { + "version": "3.19.2", + "resolved": "https://registry.npmjs.org/rc-virtual-list/-/rc-virtual-list-3.19.2.tgz", + "integrity": "sha512-Ys6NcjwGkuwkeaWBDqfI3xWuZ7rDiQXlH1o2zLfFzATfEgXcqpk8CkgMfbJD81McqjcJVez25a3kPxCR807evA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.20.0", + "classnames": "^2.2.6", + "rc-resize-observer": "^1.0.0", + "rc-util": "^5.36.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/react": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", + "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", + "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.0" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==", + "license": "MIT" + }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.9.4", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.4.tgz", + "integrity": "sha512-SD3G8HKviFHg9xj7dNODUKDFgpG4xqD5nhyd0mYoB5iISepuZAvzSr8ywxgxKJ52yRzf/HWtVHc9AWwoTbljvA==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.9.4", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.9.4.tgz", + "integrity": "sha512-f30P6bIkmYvnHHa5Gcu65deIXoA2+r3Eb6PJIAddvsT9aGlchMatJ51GgpU470aSqRRbFX22T70yQNUGuW3DfA==", + "license": "MIT", + "dependencies": { + "react-router": "7.9.4" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/reactcss": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz", + "integrity": "sha512-KiwVUcFu1RErkI97ywr8nvx8dNOpT03rbnma0SSalTYjkrPYaEajR4a/MRt6DZ46K6arDRbWMNHF+xH7G7n/8A==", + "license": "MIT", + "dependencies": { + "lodash": "^4.0.1" + } + }, + "node_modules/recharts": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.3.0.tgz", + "integrity": "sha512-Vi0qmTB0iz1+/Cz9o5B7irVyUjX2ynvEgImbgMt/3sKRREcUM07QiYjS1QpAVrkmVlXqy5gykq4nGWMz9AS4Rg==", + "license": "MIT", + "dependencies": { + "@reduxjs/toolkit": "1.x.x || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^10.1.1", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.1.1", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, + "node_modules/resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", + "license": "MIT" + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.4.tgz", + "integrity": "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.52.4", + "@rollup/rollup-android-arm64": "4.52.4", + "@rollup/rollup-darwin-arm64": "4.52.4", + "@rollup/rollup-darwin-x64": "4.52.4", + "@rollup/rollup-freebsd-arm64": "4.52.4", + "@rollup/rollup-freebsd-x64": "4.52.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.52.4", + "@rollup/rollup-linux-arm-musleabihf": "4.52.4", + "@rollup/rollup-linux-arm64-gnu": "4.52.4", + "@rollup/rollup-linux-arm64-musl": "4.52.4", + "@rollup/rollup-linux-loong64-gnu": "4.52.4", + "@rollup/rollup-linux-ppc64-gnu": "4.52.4", + "@rollup/rollup-linux-riscv64-gnu": "4.52.4", + "@rollup/rollup-linux-riscv64-musl": "4.52.4", + "@rollup/rollup-linux-s390x-gnu": "4.52.4", + "@rollup/rollup-linux-x64-gnu": "4.52.4", + "@rollup/rollup-linux-x64-musl": "4.52.4", + "@rollup/rollup-openharmony-arm64": "4.52.4", + "@rollup/rollup-win32-arm64-msvc": "4.52.4", + "@rollup/rollup-win32-ia32-msvc": "4.52.4", + "@rollup/rollup-win32-x64-gnu": "4.52.4", + "@rollup/rollup-win32-x64-msvc": "4.52.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/scroll-into-view-if-needed": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz", + "integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==", + "license": "MIT", + "dependencies": { + "compute-scroll-into-view": "^3.0.2" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "license": "MIT" + }, + "node_modules/shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-convert": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz", + "integrity": "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==", + "license": "MIT" + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stylis": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", + "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==", + "license": "MIT" + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/swr": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.3.6.tgz", + "integrity": "sha512-wfHRmHWk/isGNMwlLGlZX5Gzz/uTgo0o2IRuTMcf4CPuPFJZlq0rDaKUx+ozB5nBOReNV1kiOyzMfj+MBMikLw==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/tailwind-merge": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz", + "integrity": "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.14.tgz", + "integrity": "sha512-b7pCxjGO98LnxVkKjaZSDeNuljC4ueKUddjENJOADtubtdo8llTaJy7HwBMeLNSSo2N5QIAgklslK1+Ir8r6CA==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tar": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.1.tgz", + "integrity": "sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==", + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/throttle-debounce": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.2.tgz", + "integrity": "sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==", + "license": "MIT", + "engines": { + "node": ">=12.22" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tinycolor2": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", + "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==", + "license": "MIT" + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.46.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.46.0.tgz", + "integrity": "sha512-6+ZrB6y2bT2DX3K+Qd9vn7OFOJR+xSLDj+Aw/N3zBwUt27uTw2sw2TE2+UcY1RiyBZkaGbTkVg9SSdPNUG6aUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.46.0", + "@typescript-eslint/parser": "8.46.0", + "@typescript-eslint/typescript-estree": "8.46.0", + "@typescript-eslint/utils": "8.46.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/undici-types": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz", + "integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/validator": { + "version": "13.15.15", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz", + "integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/victory-vendor": { + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/vite": { + "version": "7.1.9", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.9.tgz", + "integrity": "sha512-4nVGliEpxmhCL8DslSAUdxlB6+SMrhB0a1v5ijlh1xB1nEPuy1mxaHxysVucLHuWryAxLWg6a5ei+U4TLn/rFg==", + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..da8b713 --- /dev/null +++ b/web/package.json @@ -0,0 +1,41 @@ +{ + "name": "web", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@ant-design/pro-components": "^2.8.10", + "@tailwindcss/vite": "^4.1.14", + "@tanstack/react-query": "^5.90.6", + "antd": "^5.27.4", + "dayjs": "^1.11.18", + "lucide-react": "^0.545.0", + "react": "^19.1.1", + "react-dom": "^19.1.1", + "react-router-dom": "^7.9.4", + "recharts": "^3.3.0", + "tailwind-merge": "^3.3.1", + "tailwindcss": "^4.1.14" + }, + "devDependencies": { + "@eslint/js": "^9.36.0", + "@types/node": "^24.6.0", + "@types/react": "^19.1.16", + "@types/react-dom": "^19.1.9", + "@types/validator": "^13.15.3", + "@vitejs/plugin-react": "^5.0.4", + "eslint": "^9.36.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.22", + "globals": "^16.4.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.45.0", + "vite": "^7.1.7" + } +} diff --git a/web/public/logo.png b/web/public/logo.png new file mode 100644 index 0000000..59bfd1b Binary files /dev/null and b/web/public/logo.png differ diff --git a/web/src/App.css b/web/src/App.css new file mode 100644 index 0000000..3db142d --- /dev/null +++ b/web/src/App.css @@ -0,0 +1,6 @@ +.ant-table-container .ant-table-body, +.ant-table-container .ant-table-content { + scrollbar-width: thin; + scrollbar-color: #eaeaea transparent; + scrollbar-gutter: stable; +} \ No newline at end of file diff --git a/web/src/App.tsx b/web/src/App.tsx new file mode 100644 index 0000000..95b732c --- /dev/null +++ b/web/src/App.tsx @@ -0,0 +1,22 @@ +import {RouterProvider} from 'react-router-dom'; +import {App as AntdApp, ConfigProvider} from 'antd'; +import zhCN from 'antd/locale/zh_CN'; +import router from './router'; +import 'dayjs/locale/zh-cn'; +import dayjs from 'dayjs'; +import './App.css'; + +// 设置 dayjs 为中文 +dayjs.locale('zh-cn'); + +function App() { + return ( + + + + + + ); +} + +export default App; diff --git a/web/src/api/agent.ts b/web/src/api/agent.ts new file mode 100644 index 0000000..664581f --- /dev/null +++ b/web/src/api/agent.ts @@ -0,0 +1,187 @@ +import { get, post, put } from './request'; +import type { Agent, LatestMetrics } from '../types'; + +export interface ListAgentsResponse { + items: Agent[]; + total: number; +} + +export interface GetAgentMetricsRequest { + agentId: string; + type: 'cpu' | 'memory' | 'disk' | 'network' | 'load'; + range?: '1h' | '6h' | '12h' | '24h' | '1d' | '3d' | '7d' | '1w' | '30d' | '1M' | '90d' | '3M'; +} + +export interface GetAgentMetricsResponse { + agentId: string; + type: string; + range: string; + start: number; + end: number; + interval: number; + metrics: any[]; +} + +// 管理员接口 - 获取所有探针(需要认证) +export const getAgentPaging = (pageIndex: number = 1, pageSize: number = 10, hostname?: string, ip?: string, status?: string) => { + const params = new URLSearchParams(); + params.append('pageIndex', pageIndex.toString()); + params.append('pageSize', pageSize.toString()); + if (hostname) { + params.append('hostname', hostname); + } + if (ip) { + params.append('ip', ip); + } + if (status) { + params.append('status', status); + } + return get(`/admin/agents?${params.toString()}`); +}; + +export const listAgents = () => { + return get('/agents'); +}; + +// 获取所有探针(不分页,用于选择器) +export const getAgents = async () => { + const response = await get('/api/agents'); + return response.data; +}; + +export const getAgent = (id: string) => { + return get(`/agents/${id}`); +}; + +// 管理员接口 - 获取探针详情(显示完整信息) +export const getAgentForAdmin = (id: string) => { + return get(`/admin/agents/${id}`); +}; + +export const getAgentMetrics = (params: GetAgentMetricsRequest) => { + const { agentId, type, range = '1h' } = params; + const query = new URLSearchParams(); + query.append('type', type); + query.append('range', range); + return get(`/agents/${agentId}/metrics?${query.toString()}`); +}; + +export const getAgentLatestMetrics = (agentId: string) => { + return get(`/agents/${agentId}/metrics/latest`); +}; + +// VPS 安全审计相关接口 + +export interface SystemInfo { + hostname: string; + os: string; + kernelVersion: string; + uptime: number; + publicIP?: string; +} + +export interface Evidence { + fileHash?: string; + processTree?: string[]; + filePath?: string; + timestamp?: number; + networkConn?: string; + riskLevel?: 'low' | 'medium' | 'high'; +} + +export interface SecurityCheckSub { + name: string; + status: 'pass' | 'fail' | 'warn' | 'skip'; + message: string; + evidence?: Evidence; +} + +export interface SecurityCheck { + category: string; + status: 'pass' | 'fail' | 'warn' | 'skip'; + message: string; + details?: SecurityCheckSub[]; +} + +export interface VPSAuditResult { + systemInfo: SystemInfo; + securityChecks: SecurityCheck[]; + startTime: number; + endTime: number; + riskScore: number; + threatLevel: 'low' | 'medium' | 'high' | 'critical'; + recommendations?: string[]; +} + +export interface AuditResultSummary { + id: number; + agentId: string; + type: string; + startTime: number; + endTime: number; + createdAt: number; + passCount: number; + failCount: number; + warnCount: number; + totalCount: number; + systemInfo: SystemInfo; +} + +export interface SendCommandResponse { + commandId: string; + status: string; +} + +// 发送审计指令 +export const sendAuditCommand = (agentId: string) => { + return post(`/admin/agents/${agentId}/command?type=vps_audit`, {}); +}; + +// 获取最新的审计结果(管理员接口) +export const getAuditResult = (agentId: string) => { + return get(`/admin/agents/${agentId}/audit/result`); +}; + +// 获取审计结果列表(管理员接口) +export const listAuditResults = (agentId: string) => { + return get<{ items: AuditResultSummary[]; total: number }>(`/admin/agents/${agentId}/audit/results`); +}; + +// 更新探针名称 +export const updateAgentName = (agentId: string, name: string) => { + return put(`/admin/agents/${agentId}/name`, { name }); +}; + +// 更新探针信息(名称、平台、位置、到期时间) +export interface UpdateAgentInfoRequest { + name?: string; + platform?: string; + location?: string; + expireTime?: number; +} + +export const updateAgentInfo = (agentId: string, data: UpdateAgentInfoRequest) => { + return put(`/admin/agents/${agentId}`, data); +}; + +// 获取探针统计数据 +export interface AgentStatistics { + total: number; + online: number; + offline: number; + onlineRate: number; +} + +export const getAgentStatistics = () => { + return get('/admin/agents/statistics'); +}; + +// 获取服务端版本信息 +export interface VersionInfo { + version: string; +} + +export const getServerVersion = () => { + return get('/agent/version'); +}; + diff --git a/web/src/api/alert.ts b/web/src/api/alert.ts new file mode 100644 index 0000000..ca439ab --- /dev/null +++ b/web/src/api/alert.ts @@ -0,0 +1,62 @@ +import { get, post, put, del } from './request'; +import type { AlertConfig, AlertRecord } from '../types'; + +// 获取探针的告警配置列表 +export const getAlertConfigsByAgent = async (agentId: string): Promise => { + const response = await get(`/admin/agents/${agentId}/alert-configs`); + return response.data; +}; + +// 创建告警配置 +export const createAlertConfig = async (config: AlertConfig): Promise => { + const response = await post('/admin/alert-configs', config); + return response.data; +}; + +// 获取告警配置详情 +export const getAlertConfig = async (id: string): Promise => { + const response = await get(`/admin/alert-configs/${id}`); + return response.data; +}; + +// 更新告警配置 +export const updateAlertConfig = async (id: string, config: AlertConfig): Promise => { + const response = await put(`/admin/alert-configs/${id}`, config); + return response.data; +}; + +// 删除告警配置 +export const deleteAlertConfig = async (id: string): Promise => { + await del(`/admin/alert-configs/${id}`); +}; + +// 测试告警通知 +export const testNotification = async (id: string): Promise<{ message: string }> => { + const response = await post<{ message: string }>(`/admin/alert-configs/${id}/test`); + return response.data; +}; + +// 获取告警记录列表 +export const getAlertRecords = async ( + agentId?: string, + limit?: number, + offset?: number +): Promise<{ + records: AlertRecord[]; + total: number; + limit: number; + offset: number; +}> => { + let url = '/admin/alert-records?'; + if (agentId) url += `agentId=${agentId}&`; + if (limit) url += `limit=${limit}&`; + if (offset) url += `offset=${offset}&`; + + const response = await get<{ + records: AlertRecord[]; + total: number; + limit: number; + offset: number; + }>(url); + return response.data; +}; diff --git a/web/src/api/apiKey.ts b/web/src/api/apiKey.ts new file mode 100644 index 0000000..e36bc7c --- /dev/null +++ b/web/src/api/apiKey.ts @@ -0,0 +1,48 @@ +import { get, post, put, del } from './request'; +import type { ApiKey, GenerateApiKeyRequest, UpdateApiKeyNameRequest } from '../types'; + +export interface ListApiKeysResponse { + items: ApiKey[]; + total: number; +} + +// 生成 API Key +export const generateApiKey = (data: GenerateApiKeyRequest) => { + return post('/admin/api-keys', data); +}; + +// 获取 API Key 列表 +export const listApiKeys = (page: number = 1, pageSize: number = 10, name?: string) => { + const params = new URLSearchParams(); + params.append('page', page.toString()); + params.append('pageSize', pageSize.toString()); + if (name) { + params.append('name', name); + } + return get(`/admin/api-keys?${params.toString()}`); +}; + +// 获取 API Key 详情 +export const getApiKey = (id: string) => { + return get(`/admin/api-keys/${id}`); +}; + +// 更新 API Key 名称 +export const updateApiKeyName = (id: string, data: UpdateApiKeyNameRequest) => { + return put(`/admin/api-keys/${id}`, data); +}; + +// 启用 API Key +export const enableApiKey = (id: string) => { + return post(`/admin/api-keys/${id}/enable`, {}); +}; + +// 禁用 API Key +export const disableApiKey = (id: string) => { + return post(`/admin/api-keys/${id}/disable`, {}); +}; + +// 删除 API Key +export const deleteApiKey = (id: string) => { + return del(`/admin/api-keys/${id}`); +}; diff --git a/web/src/api/auth.ts b/web/src/api/auth.ts new file mode 100644 index 0000000..206d449 --- /dev/null +++ b/web/src/api/auth.ts @@ -0,0 +1,11 @@ +import { post } from './request'; +import type { LoginRequest, LoginResponse } from '../types'; + +export const login = (data: LoginRequest) => { + return post('/login', data); +}; + +export const logout = () => { + return post('/admin/logout'); +}; + diff --git a/web/src/api/request.ts b/web/src/api/request.ts new file mode 100644 index 0000000..dc42f9d --- /dev/null +++ b/web/src/api/request.ts @@ -0,0 +1,182 @@ +const BASE_URL = '/api'; +const DEFAULT_TIMEOUT = 30000; + +export interface RequestConfig extends RequestInit { + timeout?: number; +} + +export interface HttpResponse { + data: T; + status: number; + statusText: string; + headers: Record; + url: string; +} + +class HttpError extends Error { + response?: { + status: number; + statusText: string; + data?: unknown; + }; + + constructor(message: string, response?: HttpError['response']) { + super(message); + this.name = 'HttpError'; + this.response = response; + } +} + +const parseResponseData = async (response: Response) => { + const contentType = response.headers.get('content-type') || ''; + if (response.status === 204) { + return undefined; + } + if (contentType.includes('application/json')) { + try { + return await response.json(); + } catch (error) { + console.warn('解析 JSON 响应失败', error); + return undefined; + } + } + return response.text(); +}; + +const headersToRecord = (headers: Headers) => { + const record: Record = {}; + headers.forEach((value, key) => { + record[key] = value; + }); + return record; +}; + +const sendRequest = async (url: string, config: RequestConfig = {}): Promise> => { + const {timeout = DEFAULT_TIMEOUT, headers, body, ...restConfig} = config; + const controller = new AbortController(); + const timer = window.setTimeout(() => controller.abort(), timeout); + + try { + const token = localStorage.getItem('token'); + const finalHeaders = new Headers(headers || {}); + if (token) { + finalHeaders.set('Authorization', `Bearer ${token}`); + } + + const hasBody = body !== undefined && body !== null; + const isFormData = typeof FormData !== 'undefined' && body instanceof FormData; + if (hasBody && !isFormData && !finalHeaders.has('Content-Type')) { + finalHeaders.set('Content-Type', 'application/json'); + } + + const response = await fetch(`${BASE_URL}${url}`, { + ...restConfig, + body, + headers: finalHeaders, + signal: controller.signal, + }); + + const data = await parseResponseData(response); + + if (response.status === 401) { + localStorage.removeItem('token'); + localStorage.removeItem('userInfo'); + window.location.href = '/login'; + throw new HttpError('未认证或认证已过期', { + status: response.status, + statusText: response.statusText, + data, + }); + } + + if (!response.ok) { + const message = + typeof data === 'object' && data !== null && 'message' in data + ? (data as { message?: string }).message + : undefined; + throw new HttpError(message || '请求失败,请稍后重试', { + status: response.status, + statusText: response.statusText, + data, + }); + } + + return { + data: data as T, + status: response.status, + statusText: response.statusText, + headers: headersToRecord(response.headers), + url: response.url, + }; + } catch (error) { + if (error instanceof DOMException && error.name === 'AbortError') { + throw new Error('请求超时'); + } + throw error; + } finally { + clearTimeout(timer); + } +}; + +const normalizeBody = (data?: any) => { + if (data === undefined || data === null) { + return undefined; + } + const isForm = + (typeof FormData !== 'undefined' && data instanceof FormData) || + (typeof Blob !== 'undefined' && data instanceof Blob) || + (typeof URLSearchParams !== 'undefined' && data instanceof URLSearchParams); + + if (isForm) { + return data; + } + if (typeof data === 'string') { + return data; + } + return JSON.stringify(data); +}; + +export interface ApiResponse { + code?: number; + message?: string; + data?: T; +} + +export const get = (url: string, config?: RequestConfig) => { + return sendRequest(url, { + ...(config || {}), + method: 'GET', + }); +}; + +export const post = (url: string, data?: any, config?: RequestConfig) => { + return sendRequest(url, { + ...(config || {}), + method: 'POST', + body: normalizeBody(data), + }); +}; + +export const put = (url: string, data?: any, config?: RequestConfig) => { + return sendRequest(url, { + ...(config || {}), + method: 'PUT', + body: normalizeBody(data), + }); +}; + +export const del = (url: string, config?: RequestConfig) => { + return sendRequest(url, { + ...(config || {}), + method: 'DELETE', + }); +}; + +const request = { + get, + post, + put, + delete: del, +}; + +export default request; diff --git a/web/src/api/user.ts b/web/src/api/user.ts new file mode 100644 index 0000000..b5197e0 --- /dev/null +++ b/web/src/api/user.ts @@ -0,0 +1,46 @@ +import { get, post, put, del } from './request'; +import type { User, CreateUserRequest, UpdateUserRequest, ChangePasswordRequest } from '../types'; + +export interface ListUsersResponse { + items: User[]; + total: number; +} + +export const listUsers = (page: number = 1, pageSize: number = 10, username?: string) => { + const params = new URLSearchParams(); + params.append('page', page.toString()); + params.append('pageSize', pageSize.toString()); + if (username) { + params.append('username', username); + } + return get(`/admin/users?${params.toString()}`); +}; + +export const getUser = (id: string) => { + return get(`/admin/users/${id}`); +}; + +export const createUser = (data: CreateUserRequest) => { + return post('/admin/users', data); +}; + +export const updateUser = (id: string, data: UpdateUserRequest) => { + return put(`/admin/users/${id}`, data); +}; + +export const deleteUser = (id: string) => { + return del(`/admin/users/${id}`); +}; + +export const changePassword = (id: string, data: ChangePasswordRequest) => { + return post(`/admin/users/${id}/password`, data); +}; + +export const resetPassword = (id: string, newPassword: string) => { + return post(`/admin/users/${id}/reset-password`, { newPassword }); +}; + +export const updateUserStatus = (id: string, status: number) => { + return post(`/admin/users/${id}/status`, { status }); +}; + diff --git a/web/src/assets/github.svg b/web/src/assets/github.svg new file mode 100644 index 0000000..538ec5b --- /dev/null +++ b/web/src/assets/github.svg @@ -0,0 +1 @@ +GitHub \ No newline at end of file diff --git a/web/src/assets/os/apple.png b/web/src/assets/os/apple.png new file mode 100644 index 0000000..8402a0a Binary files /dev/null and b/web/src/assets/os/apple.png differ diff --git a/web/src/assets/os/linux.png b/web/src/assets/os/linux.png new file mode 100644 index 0000000..aa4fc92 Binary files /dev/null and b/web/src/assets/os/linux.png differ diff --git a/web/src/assets/os/win11.png b/web/src/assets/os/win11.png new file mode 100644 index 0000000..88bc92d Binary files /dev/null and b/web/src/assets/os/win11.png differ diff --git a/web/src/assets/react.svg b/web/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/web/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/src/components/PageHeader.tsx b/web/src/components/PageHeader.tsx new file mode 100644 index 0000000..e91246e --- /dev/null +++ b/web/src/components/PageHeader.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import {Button, Space} from 'antd'; +import type {LucideIcon} from 'lucide-react'; + +export interface Action { + key: string; + label: string; + icon?: React.ReactElement; + type?: 'default' | 'primary'; + onClick: () => void; +} + +interface PageHeaderProps { + title: string; + description?: string; + actions?: Action[]; +} + +/** + * 统一的页面头部组件 + */ +export const PageHeader: React.FC = ({title, description, actions}) => { + return ( +
+
+

{title}

+ {description &&

{description}

} +
+ {actions && actions.length > 0 && ( +
+ {actions.map((action) => ( + + ))} +
+ )} +
+ ); +}; diff --git a/web/src/components/PrivateRoute.tsx b/web/src/components/PrivateRoute.tsx new file mode 100644 index 0000000..d27aabc --- /dev/null +++ b/web/src/components/PrivateRoute.tsx @@ -0,0 +1,25 @@ +import { Navigate } from 'react-router-dom'; +import type { ReactNode } from 'react'; + +interface PrivateRouteProps { + children: ReactNode; +} + +/** + * 私有路由守卫组件 + * 用于保护需要登录才能访问的路由 + */ +const PrivateRoute = ({ children }: PrivateRouteProps) => { + const token = localStorage.getItem('token'); + const userInfo = localStorage.getItem('userInfo'); + + // 如果没有登录信息,重定向到登录页 + if (!token || !userInfo) { + return ; + } + + // 已登录,渲染子组件 + return <>{children}; +}; + +export default PrivateRoute; diff --git a/web/src/components/index.ts b/web/src/components/index.ts new file mode 100644 index 0000000..2c0d42f --- /dev/null +++ b/web/src/components/index.ts @@ -0,0 +1,2 @@ +export { PageHeader } from './PageHeader'; +export type { Action } from './PageHeader'; diff --git a/web/src/index.css b/web/src/index.css new file mode 100644 index 0000000..2439530 --- /dev/null +++ b/web/src/index.css @@ -0,0 +1,25 @@ +@import "tailwindcss"; + +/* 全局样式 */ +* { + scrollbar-width: thin; +} + +/* 滚动条样式 */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: rgb(209, 213, 219); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: rgb(156, 163, 175); +} \ No newline at end of file diff --git a/web/src/lib/utils.ts b/web/src/lib/utils.ts new file mode 100644 index 0000000..ad2931c --- /dev/null +++ b/web/src/lib/utils.ts @@ -0,0 +1,29 @@ +import {type ClassValue, clsx} from 'clsx'; +import {twMerge} from 'tailwind-merge'; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} + +interface ErrorWithResponse { + response?: { + data?: { + message?: string; + }; + }; +} + +export function getErrorMessage(error: unknown, fallback: string) { + if (typeof error === 'object' && error !== null) { + const {response} = error as ErrorWithResponse; + if (response?.data?.message) { + return response.data.message; + } + } + + if (error instanceof Error && error.message) { + return error.message; + } + + return fallback; +} diff --git a/web/src/main.tsx b/web/src/main.tsx new file mode 100644 index 0000000..38180b9 --- /dev/null +++ b/web/src/main.tsx @@ -0,0 +1,22 @@ +import {StrictMode} from 'react' +import {createRoot} from 'react-dom/client' +import {QueryClient, QueryClientProvider} from '@tanstack/react-query' +import './index.css' +import App from './App.tsx' + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + refetchOnWindowFocus: false, + retry: 1, + }, + }, +}) + +createRoot(document.getElementById('root')!).render( + + + + + , +) diff --git a/web/src/pages/AdminLayout/index.tsx b/web/src/pages/AdminLayout/index.tsx new file mode 100644 index 0000000..e85a547 --- /dev/null +++ b/web/src/pages/AdminLayout/index.tsx @@ -0,0 +1,278 @@ +import { type JSX, useEffect, useMemo, useState } from 'react'; +import { Outlet, useLocation, useNavigate } from 'react-router-dom'; +import type { MenuProps } from 'antd'; +import { App, Avatar, Button, Dropdown, Space } from 'antd'; +import { Eye, Key, LogOut, Server, User as UserIcon, Users, BookOpen, Settings } from 'lucide-react'; +import { logout } from '../../api/auth'; +import { getServerVersion } from '../../api/agent'; +import type { User } from '../../types'; +import { cn } from '../../lib/utils'; + +interface NavItem { + key: string; + label: string; + path: string; + icon: JSX.Element; +} + +const SIDEBAR_WIDTH = 240; +const HEADER_HEIGHT = 56; + +const AdminLayout = () => { + const navigate = useNavigate(); + const location = useLocation(); + const { message: messageApi, modal } = App.useApp(); + const [userInfo, setUserInfo] = useState(null); + const [selectedKey, setSelectedKey] = useState('agents'); + const [version, setVersion] = useState(''); + + const menuItems: NavItem[] = useMemo( + () => [ + { + key: 'agents', + label: '探针管理', + path: '/admin/agents', + icon: , + }, + { + key: 'api-keys', + label: 'API密钥', + path: '/admin/api-keys', + icon: , + }, + { + key: 'users', + label: '用户管理', + path: '/admin/users', + icon: , + }, + { + key: 'settings', + label: '系统设置', + path: '/admin/settings', + icon: , + }, + ], + [], + ); + + useEffect(() => { + const token = localStorage.getItem('token'); + const userInfoStr = localStorage.getItem('userInfo'); + + if (!token || !userInfoStr) { + navigate('/login'); + return; + } + + setUserInfo(JSON.parse(userInfoStr)); + + const path = location.pathname; + if (path.startsWith('/admin/users')) { + setSelectedKey('users'); + } else if (path.startsWith('/admin/api-keys')) { + setSelectedKey('api-keys'); + } else if (path.startsWith('/admin/settings')) { + setSelectedKey('settings'); + } else { + setSelectedKey('agents'); + } + + // 获取服务端版本信息 + getServerVersion() + .then((res) => { + setVersion(res.data.version); + }) + .catch((err) => { + console.error('获取版本信息失败:', err); + }); + }, [navigate, location]); + + const handleLogout = () => { + modal.confirm({ + title: '确认退出', + content: '确定要退出登录吗?', + onOk: async () => { + try { + await logout(); + } finally { + localStorage.removeItem('token'); + localStorage.removeItem('userInfo'); + messageApi.success('已退出登录'); + navigate('/login'); + } + }, + }); + }; + + const userMenuItems: MenuProps['items'] = [ + { + key: 'logout', + icon: , + label: '退出登录', + onClick: handleLogout, + }, + ]; + + const handleNavigate = (item: NavItem) => { + if (item.key === selectedKey) { + return; + } + + setSelectedKey(item.key); + navigate(item.path); + }; + + return ( +
+ {/* 顶部导航栏 */} +
+
+
+
+ Pika +
+
+

Pika Monitor

+

控制台

+
+
+ + + + + + + + +
+
+ + {/* 侧边栏 */} + + + {/* 主内容区 */} +
+ {/* 内容区域 */} +
+
+ +
+
+
+ + {/* 移动端底部导航栏 */} + +
+ ); +}; + +export default AdminLayout; diff --git a/web/src/pages/Agents/AgentDetail.tsx b/web/src/pages/Agents/AgentDetail.tsx new file mode 100644 index 0000000..b25cebf --- /dev/null +++ b/web/src/pages/Agents/AgentDetail.tsx @@ -0,0 +1,249 @@ +import {useEffect, useState} from 'react'; +import {useNavigate, useParams} from 'react-router-dom'; +import type {MenuProps} from 'antd'; +import {App, Button, Card, Col, Descriptions, Dropdown, Row, Space, Spin, Tag} from 'antd'; +import {Activity, ArrowLeft, Clock, RefreshCw, Shield, Terminal} from 'lucide-react'; +import {getAgentForAdmin, getAuditResult, sendAuditCommand, type VPSAuditResult} from '../../api/agent'; +import type {Agent} from '../../types'; +import dayjs from 'dayjs'; +import {getErrorMessage} from '../../lib/utils'; + +const AgentDetail = () => { + const {id} = useParams<{ id: string }>(); + const navigate = useNavigate(); + const {message: messageApi} = App.useApp(); + const [loading, setLoading] = useState(false); + const [agent, setAgent] = useState(null); + const [auditResult, setAuditResult] = useState(null); + const [auditing, setAuditing] = useState(false); + + const fetchData = async () => { + if (!id) return; + + setLoading(true); + try { + const [agentRes, auditRes] = await Promise.all([ + getAgentForAdmin(id), + getAuditResult(id).catch(() => ({data: null})), + ]); + + setAgent(agentRes.data); + setAuditResult(auditRes.data); + } catch (error: any) { + messageApi.error(error.response?.data?.message || '获取探针信息失败'); + } finally { + setLoading(false); + } + }; + + const handleStartAudit = async () => { + if (!id) return; + + setAuditing(true); + try { + await sendAuditCommand(id); + messageApi.success('安全审计已启动,请稍后查看结果'); + + // 5秒后刷新结果 + setTimeout(() => { + fetchData(); + }, 5000); + } catch (error: unknown) { + messageApi.error(getErrorMessage(error, '启动审计失败')); + } finally { + setAuditing(false); + } + }; + + useEffect(() => { + fetchData(); + }, [id]); + + if (loading && !agent) { + return ( +
+ +
+ ); + } + + // 命令菜单配置 - 方便扩展新命令 + const commandMenuItems: MenuProps['items'] = [ + { + key: 'audit', + icon: , + label: '安全审计', + onClick: handleStartAudit, + }, + { + type: 'divider', + }, + { + key: 'refresh', + icon: , + label: '刷新数据', + onClick: fetchData, + }, + // 可以在这里添加更多命令 + // { + // key: 'restart', + // icon: , + // label: '重启探针', + // onClick: handleRestartAgent, + // }, + // { + // key: 'update', + // icon: , + // label: '更新探针', + // onClick: handleUpdateAgent, + // }, + ]; + + return ( +
+ {/* 页面头部 */} +
+ + + + {auditResult && ( + + )} + + + + +
+ + {/* 基本信息 */} +
+ + + 探针信息 + {agent?.status === 1 && 在线} + {agent?.status !== 1 && 离线} + + } + className="rounded-lg border border-gray-200 mt-0" + > + + {agent?.name} + {agent?.id} + {agent?.hostname} + {agent?.ip} + + {agent?.os} + + + {agent?.arch} + + {agent?.version} + + + + {agent?.lastSeenAt && dayjs(agent.lastSeenAt).format('YYYY-MM-DD HH:mm:ss')} + + + + {agent?.createdAt && dayjs(agent.createdAt).format('YYYY-MM-DD HH:mm:ss')} + + + {agent?.updatedAt && dayjs(agent.updatedAt).format('YYYY-MM-DD HH:mm:ss')} + + + +
+ + {/* 审计结果预览 */} +
+ {auditResult && ( + + + 最近审计结果 + + {dayjs(auditResult.startTime).format('YYYY-MM-DD HH:mm')} + + + } + className="rounded-lg border border-gray-200 mt-4 lg:mt-6" + extra={ + + } + > + + +
+
+ {auditResult.securityChecks.length} +
+
总检查项
+
+ + +
+
+ {auditResult.securityChecks.filter(c => c.status === 'pass').length} +
+
通过
+
+ + +
+
+ {auditResult.securityChecks.filter(c => c.status === 'fail').length} +
+
失败
+
+ + +
+
+ {auditResult.securityChecks.filter(c => c.status === 'warn').length} +
+
警告
+
+ +
+
+ )} +
+
+ ); +}; + +export default AgentDetail; + diff --git a/web/src/pages/Agents/AgentInstall.tsx b/web/src/pages/Agents/AgentInstall.tsx new file mode 100644 index 0000000..8ccebd3 --- /dev/null +++ b/web/src/pages/Agents/AgentInstall.tsx @@ -0,0 +1,241 @@ +import {type ReactElement, useEffect, useState} from 'react'; +import {Alert, App, Button, Card, Select, Space, Tabs, Typography} from 'antd'; +import {CopyIcon, Download} from 'lucide-react'; +import {listApiKeys} from '../../api/apiKey'; +import type {ApiKey} from '../../types'; +import linuxPng from '../../assets/os/linux.png'; +import applePng from '../../assets/os/apple.png'; +import windowsPng from '../../assets/os/win11.png'; + +const {Paragraph, Text} = Typography; +const {TabPane} = Tabs; + +interface OSConfig { + name: string; + icon: ReactElement; + downloadUrl: string; +} + +const AgentInstall = () => { + const [selectedOS, setSelectedOS] = useState('linux-amd64'); + const [apiKeys, setApiKeys] = useState([]); + const [selectedApiKey, setSelectedApiKey] = useState(''); + + const {message} = App.useApp(); + const serverUrl = window.location.origin; + + // 加载API密钥列表 + useEffect(() => { + const fetchApiKeys = async () => { + try { + const keys = await listApiKeys(); + const enabledKeys = keys.data?.items.filter(k => k.enabled); + setApiKeys(enabledKeys); + if (enabledKeys.length > 0) { + setSelectedApiKey(enabledKeys[0].key); + } + } catch (error) { + console.error('Failed to load API keys:', error); + } + }; + fetchApiKeys(); + }, []); + + const osConfigs: Record = { + 'linux-amd64': { + name: 'Linux (AMD64)', + icon: Linux, + downloadUrl: '/api/agent/downloads/agent-linux-amd64', + }, + 'linux-arm64': { + name: 'Linux (ARM64)', + icon: Linux, + downloadUrl: '/api/agent/downloads/agent-linux-arm64', + }, + 'darwin-amd64': { + name: 'macOS (Intel)', + icon: macOS, + downloadUrl: '/api/agent/downloads/agent-darwin-amd64', + }, + 'darwin-arm64': { + name: 'macOS (Apple Silicon)', + icon: macOS, + downloadUrl: '/api/agent/downloads/agent-darwin-arm64', + }, + 'windows-amd64': { + name: 'Windows (AMD64)', + icon: Windows, + downloadUrl: '/api/agent/downloads/agent-windows-amd64.exe', + }, + }; + + const copyToClipboard = (text: string) => { + navigator.clipboard.writeText(text); + message.success('已复制到剪贴板'); + }; + + // 一键注册命令 + const getRegisterCommand = (os: string) => { + const config = osConfigs[os]; + const agentName = os.startsWith('windows') ? 'pika-agent.exe' : 'pika-agent'; + const token = selectedApiKey; + + if (os.startsWith('windows')) { + return `# 1. 下载探针 +Invoke-WebRequest -Uri "${serverUrl}${config.downloadUrl}" -OutFile "${agentName}" + +# 2. 运行注册命令(需要管理员权限) +.\\${agentName} register --endpoint "${serverUrl}" --token "${token}"`; + } else if (os.startsWith('linux')) { + return `# 1. 下载探针 +wget ${serverUrl}${config.downloadUrl} -O ${agentName} + +# 2. 添加执行权限 +chmod +x ${agentName} + +# 3. 运行注册命令(需要 root 权限) +sudo ./${agentName} register --endpoint "${serverUrl}" --token "${token}"`; + } else { // macOS + return `# 1. 下载探针 +curl -L ${serverUrl}${config.downloadUrl} -o ${agentName} + +# 2. 添加执行权限 +chmod +x ${agentName} + +# 3. 运行注册命令(需要 root 权限) +sudo ./${agentName} register --endpoint "${serverUrl}" --token "${token}"`; + } + }; + + // 常用命令 + const getCommonCommands = (os: string) => { + const agentCmd = os.startsWith('windows') ? '.\\pika-agent.exe' : 'pika-agent'; + const sudo = os.startsWith('windows') ? '' : 'sudo '; + + return `# 查看服务状态 +${sudo}${agentCmd} status + +# 停止服务 +${sudo}${agentCmd} stop + +# 启动服务 +${sudo}${agentCmd} start + +# 重启服务 +${sudo}${agentCmd} restart + +# 卸载服务 +${sudo}${agentCmd} uninstall + +# 查看版本 +${agentCmd} version`; + }; + + return ( +
+ + + 探针部署指南 + + } + className="rounded-lg border border-gray-200" + > + + {/* API Token 选择 */} + + + 选择 API Token: + {apiKeys.length === 0 ? ( + + 请先前往 API密钥管理 页面生成一个 API Token + + } + type="warning" + showIcon + className="mt-2" + /> + ) : ( + + + + + + + + + + + + + +
+ ); +}; + +export default AgentList; diff --git a/web/src/pages/Agents/AuditResult.tsx b/web/src/pages/Agents/AuditResult.tsx new file mode 100644 index 0000000..062bcad --- /dev/null +++ b/web/src/pages/Agents/AuditResult.tsx @@ -0,0 +1,483 @@ +import { useEffect, useState } from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; +import { + Card, + Button, + Space, + Spin, + App, + Descriptions, + Tag, + List, + Collapse, + Alert, + Row, + Col, + Statistic +} from 'antd'; +import { + ArrowLeft, + Shield, + CheckCircle, + XCircle, + AlertTriangle, + MinusCircle, + RefreshCw, + FileText, + Hash, + GitBranch, + Clock, + Activity +} from 'lucide-react'; +import { getAuditResult, sendAuditCommand, listAuditResults } from '../../api/agent'; +import type { VPSAuditResult, AuditResultSummary } from '../../api/agent'; +import dayjs from 'dayjs'; +import duration from 'dayjs/plugin/duration'; + +dayjs.extend(duration); + +const { Panel } = Collapse; + +const AuditResult = () => { + const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); + const { message: messageApi } = App.useApp(); + const [loading, setLoading] = useState(false); + const [auditing, setAuditing] = useState(false); + const [result, setResult] = useState(null); + const [historyList, setHistoryList] = useState([]); + + const fetchData = async () => { + if (!id) return; + + setLoading(true); + try { + const [resultRes, historyRes] = await Promise.all([ + getAuditResult(id).catch(() => ({ data: null })), + listAuditResults(id), + ]); + + setResult(resultRes.data); + setHistoryList(historyRes.data.items || []); + } catch (error: any) { + // 如果没有审计结果,不显示错误 + if (error.response?.status !== 404) { + messageApi.error(error.response?.data?.message || '获取审计结果失败'); + } + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchData(); + }, [id]); + + const handleStartAudit = async () => { + if (!id) return; + + setAuditing(true); + try { + await sendAuditCommand(id); + messageApi.success('安全审计已启动,请稍后查看结果'); + + // 5秒后刷新结果 + setTimeout(() => { + fetchData(); + }, 5000); + } catch (error: any) { + messageApi.error(error.response?.data?.message || '启动审计失败'); + } finally { + setAuditing(false); + } + }; + + const getStatusIcon = (status: string) => { + switch (status) { + case 'pass': + return ; + case 'fail': + return ; + case 'warn': + return ; + case 'skip': + return ; + default: + return null; + } + }; + + const getStatusTag = (status: string) => { + const configs = { + pass: { color: 'success', text: '通过' }, + fail: { color: 'error', text: '失败' }, + warn: { color: 'warning', text: '警告' }, + skip: { color: 'default', text: '跳过' }, + }; + const config = configs[status as keyof typeof configs] || { color: 'default', text: status }; + return {config.text}; + }; + + const getCategoryName = (category: string) => { + const names: Record = { + 'non_root_user': '非Root用户', + 'ufw_security': 'UFW防火墙', + 'ssh_security': 'SSH安全', + 'access_control': '访问控制', + 'port_security': '端口安全', + 'unattended_upgrades': '自动更新', + 'fail2ban': 'Fail2ban', + 'rootkit_detection': 'Rootkit检测', + 'suspicious_processes': '可疑进程检测', + 'listening_ports': '端口监听检查', + 'cron_jobs': '定时任务检查', + 'suspicious_files': '可疑文件检查', + 'system_accounts': '系统账户检查', + 'network_connections': '网络连接检查', + 'file_integrity': '文件完整性检查', + 'login_history': '登录历史检查', + 'immutable_files': '不可变文件检查', + 'suspicious_env_vars': '环境变量检查', + }; + return names[category] || category; + }; + + const getThreatLevelConfig = (level: string) => { + const configs = { + low: { color: 'success', text: '低风险', icon: CheckCircle }, + medium: { color: 'warning', text: '中风险', icon: AlertTriangle }, + high: { color: 'error', text: '高风险', icon: XCircle }, + critical: { color: 'error', text: '严重风险', icon: XCircle }, + }; + return configs[level as keyof typeof configs] || configs.low; + }; + + const formatUptime = (seconds: number) => { + const d = dayjs.duration(seconds, 'seconds'); + const days = Math.floor(d.asDays()); + const hours = d.hours(); + const minutes = d.minutes(); + return `${days}天 ${hours}小时 ${minutes}分钟`; + }; + + if (loading) { + return ( +
+ +
+ ); + } + + const passCount = result?.securityChecks.filter(c => c.status === 'pass').length || 0; + const failCount = result?.securityChecks.filter(c => c.status === 'fail').length || 0; + const warnCount = result?.securityChecks.filter(c => c.status === 'warn').length || 0; + const totalCount = result?.securityChecks.length || 0; + + return ( + + {/* 页面头部 */} +
+ + + +
+ + {!result && ( + + )} + + {result && ( + <> + {/* 统计信息 */} + + + + } + /> + + + } + /> + + + } + /> + + + } + /> + + + = 80 ? '#ff4d4f' : + result.riskScore >= 50 ? '#faad14' : + result.riskScore >= 20 ? '#1890ff' : '#52c41a' + }} + prefix={} + /> + + +
威胁等级
+ + {getThreatLevelConfig(result.threatLevel).text} + + +
+
+ + {/* 系统信息 */} + 系统信息} + className="rounded-lg border border-gray-200 mt-4 lg:mt-6" + > + + + {result.systemInfo.hostname} + + + {result.systemInfo.os} + + + {result.systemInfo.kernelVersion} + + + {formatUptime(result.systemInfo.uptime)} + + {result.systemInfo.publicIP && ( + + {result.systemInfo.publicIP} + + )} + + {dayjs(result.startTime).format('YYYY-MM-DD HH:mm:ss')} - {dayjs(result.endTime).format('HH:mm:ss')} + + (耗时: {((result.endTime - result.startTime) / 1000).toFixed(2)}秒) + + + + + + {/* 安全检查结果 */} + 安全检查结果} + className="rounded-lg border border-gray-200 mt-4 lg:mt-6" + > + + {result.securityChecks.map((check, index) => ( + + {getStatusIcon(check.status)} + + {getCategoryName(check.category)} + + {getStatusTag(check.status)} + + {check.message} + +
+ } + > + {check.details && check.details.length > 0 && ( + ( + + + {getStatusIcon(detail.status)} + + {detail.name} + + {getStatusTag(detail.status)} + + {detail.message} + + + {detail.evidence && ( +
+
+ + 证据信息 + {detail.evidence.riskLevel && ( + + {detail.evidence.riskLevel === 'high' ? '高危' : + detail.evidence.riskLevel === 'medium' ? '中危' : '低危'} + + )} +
+ {detail.evidence.filePath && ( +
+ 文件路径: + {detail.evidence.filePath} +
+ )} + {detail.evidence.fileHash && ( +
+ + SHA256: + {detail.evidence.fileHash} +
+ )} + {detail.evidence.timestamp && ( +
+ + 时间: + {dayjs(detail.evidence.timestamp).format('YYYY-MM-DD HH:mm:ss')} +
+ )} + {detail.evidence.networkConn && ( +
+ 网络连接: + {detail.evidence.networkConn} +
+ )} + {detail.evidence.processTree && detail.evidence.processTree.length > 0 && ( +
+
+ + 进程树: +
+
+ {detail.evidence.processTree.map((line, idx) => ( +
+ {line} +
+ ))} +
+
+ )} +
+ )} +
+ )} + /> + )} + + ))} + + + + {/* 修复建议 */} + {result.recommendations && result.recommendations.length > 0 && ( + 修复建议} + className="rounded-lg border border-gray-200 mt-4 lg:mt-6" + > + ( + + + {index + 1}. + + {item} + + + + )} + /> + + )} + + )} + + {/* 历史记录 */} + {historyList.length > 0 && ( + 审计历史} + className="rounded-lg border border-gray-200 mt-4 lg:mt-6" + > + ( + + + + {dayjs(item.createdAt).format('YYYY-MM-DD HH:mm:ss')} + + {item.systemInfo.os} + + } + description={ + + + + 通过: {item.passCount} + + + + 失败: {item.failCount} + + + + 警告: {item.warnCount} + + + } + /> + + )} + /> + + )} + + ); +}; + +export default AuditResult; diff --git a/web/src/pages/ApiKeys/ApiKeyList.tsx b/web/src/pages/ApiKeys/ApiKeyList.tsx new file mode 100644 index 0000000..fb3637d --- /dev/null +++ b/web/src/pages/ApiKeys/ApiKeyList.tsx @@ -0,0 +1,382 @@ +import {useRef, useState} from 'react'; +import type {ActionType, ProColumns} from '@ant-design/pro-components'; +import {ProTable} from '@ant-design/pro-components'; +import {App, Button, Form, Input, message as antMessage, Modal, Popconfirm, Tag,} from 'antd'; +import {Copy, Edit, Eye, EyeOff, Plus, Power, PowerOff, RefreshCw, Trash2} from 'lucide-react'; +import { + deleteApiKey, + disableApiKey, + enableApiKey, + generateApiKey, + listApiKeys, + updateApiKeyName, +} from '../../api/apiKey'; +import type {ApiKey, GenerateApiKeyRequest} from '../../types'; +import dayjs from 'dayjs'; +import {getErrorMessage} from '../../lib/utils'; +import {PageHeader} from '../../components'; + +const ApiKeyList = () => { + const {message: messageApi} = App.useApp(); + const actionRef = useRef(null); + const [submitting, setSubmitting] = useState(false); + const [isModalVisible, setIsModalVisible] = useState(false); + const [editingApiKey, setEditingApiKey] = useState(null); + const [newApiKeyData, setNewApiKeyData] = useState(null); + const [showApiKeyModal, setShowApiKeyModal] = useState(false); + const [visibleKeys, setVisibleKeys] = useState>({}); + const [form] = Form.useForm(); + + const handleCreate = () => { + setEditingApiKey(null); + setIsModalVisible(true); + form.resetFields(); + }; + + const handleEdit = (apiKey: ApiKey) => { + setEditingApiKey(apiKey); + form.setFieldsValue({ + name: apiKey.name, + }); + setIsModalVisible(true); + }; + + const handleToggleEnabled = async (apiKey: ApiKey) => { + try { + if (apiKey.enabled) { + await disableApiKey(apiKey.id); + messageApi.success('API密钥已禁用'); + } else { + await enableApiKey(apiKey.id); + messageApi.success('API密钥已启用'); + } + actionRef.current?.reload(); + } catch (error: unknown) { + messageApi.error(getErrorMessage(error, '操作失败')); + } + }; + + const handleDelete = async (id: string) => { + try { + await deleteApiKey(id); + messageApi.success('删除成功'); + actionRef.current?.reload(); + } catch (error: unknown) { + messageApi.error(getErrorMessage(error, '删除失败')); + } + }; + + const handleModalOk = async () => { + try { + const values = await form.validateFields(); + const name = values.name?.trim(); + + if (!name) { + messageApi.warning('名称不能为空'); + return; + } + + setSubmitting(true); + + if (editingApiKey) { + // 编辑模式 + if (name === editingApiKey.name) { + messageApi.info('名称未发生变化'); + return; + } + await updateApiKeyName(editingApiKey.id, {name}); + messageApi.success('更新成功'); + setIsModalVisible(false); + } else { + // 创建模式 + const createData: GenerateApiKeyRequest = {name}; + const response = await generateApiKey(createData); + setNewApiKeyData(response.data); + messageApi.success('API密钥生成成功'); + setIsModalVisible(false); + setShowApiKeyModal(true); + } + + form.resetFields(); + actionRef.current?.reload(); + } catch (error: unknown) { + if (typeof error === 'object' && error !== null && 'errorFields' in error) { + return; + } + messageApi.error(getErrorMessage(error, '操作失败')); + } finally { + setSubmitting(false); + } + }; + + const handleCopyApiKey = (key: string) => { + navigator.clipboard.writeText(key); + antMessage.success('已复制到剪贴板'); + }; + + const toggleKeyVisibility = (id: string) => { + setVisibleKeys((prev) => ({ + ...prev, + [id]: !prev[id], + })); + }; + + const columns: ProColumns[] = [ + { + title: '名称', + dataIndex: 'name', + key: 'name', + render: (text) => {text}, + }, + { + title: 'API密钥', + dataIndex: 'key', + key: 'key', + hideInSearch: true, + render: (_, record) => { + const fullKey = record.key || ''; + const isVisible = visibleKeys[record.id]; + const displayText = isVisible ? fullKey : (fullKey.length > 8 ? `${fullKey.substring(0, 8)}...` : fullKey); + return ( +
+ + {displayText} + +
+ ); + }, + }, + { + title: '状态', + dataIndex: 'enabled', + key: 'enabled', + hideInSearch: true, + render: (enabled) => ( + {enabled ? '启用' : '禁用'} + ), + width: 80, + }, + { + title: '创建时间', + dataIndex: 'createdAt', + key: 'createdAt', + hideInSearch: true, + render: (value: number) => ( + {dayjs(value).format('YYYY-MM-DD HH:mm')} + ), + width: 180, + }, + { + title: '更新时间', + dataIndex: 'updatedAt', + key: 'updatedAt', + hideInSearch: true, + render: (value: number) => ( + {dayjs(value).format('YYYY-MM-DD HH:mm')} + ), + width: 180, + }, + { + title: '操作', + key: 'action', + valueType: 'option', + width: 200, + render: (_, record) => [ + , + , + handleDelete(record.id)} + okText="确定" + cancelText="取消" + > + + , + ], + }, + ]; + + return ( +
+ {/* 页面头部 */} + , + onClick: () => actionRef.current?.reload(), + }, + { + key: 'create', + label: '生成密钥', + icon: , + type: 'primary', + onClick: handleCreate, + }, + ]} + /> + + {/* API密钥列表 */} +
+ + + actionRef={actionRef} + rowKey="id" + search={{labelWidth: 80}} + columns={columns} + pagination={{ + defaultPageSize: 10, + showSizeChanger: true, + }} + options={false} + request={async (params) => { + const {current = 1, pageSize = 10, name} = params; + try { + const response = await listApiKeys(current, pageSize, name); + return { + data: response.data.items || [], + success: true, + total: response.data.total, + }; + } catch (error: unknown) { + messageApi.error(getErrorMessage(error, '获取API密钥列表失败')); + return { + data: [], + success: false, + }; + } + }} + /> +
+ + {/* 新建/编辑API密钥弹窗 */} + { + setIsModalVisible(false); + form.resetFields(); + }} + okText={editingApiKey ? '保存' : '生成'} + cancelText="取消" + confirmLoading={submitting} + destroyOnHidden={true} + > +
+ + + +
+
+ + {/* 显示新生成的API密钥 */} + { + setShowApiKeyModal(false); + setNewApiKeyData(null); + }} + onCancel={() => { + setShowApiKeyModal(false); + setNewApiKeyData(null); + }} + footer={[ + , + , + ]} + > +
+
+

+ ⚠️ 重要提示:请妥善保管此密钥,关闭后将无法再次查看完整密钥! +

+
+
+ +
{newApiKeyData?.name}
+
+
+ + + {newApiKeyData?.key} + +
+
+
+
+ ); +}; + +export default ApiKeyList; diff --git a/web/src/pages/Login/index.tsx b/web/src/pages/Login/index.tsx new file mode 100644 index 0000000..9b172a5 --- /dev/null +++ b/web/src/pages/Login/index.tsx @@ -0,0 +1,83 @@ +import {useState} from 'react'; +import {useNavigate} from 'react-router-dom'; +import {App, Button, Form, Input} from 'antd'; +import {LockOutlined, UserOutlined} from '@ant-design/icons'; +import {login} from '../../api/auth'; +import type {LoginRequest} from '../../types'; + +const Login = () => { + const [loading, setLoading] = useState(false); + const navigate = useNavigate(); + const {message: messageApi} = App.useApp(); + + const onFinish = async (values: LoginRequest) => { + setLoading(true); + try { + const response = await login(values); + const {token, user} = response.data; + + // 保存 token 和用户信息 + localStorage.setItem('token', token); + localStorage.setItem('userInfo', JSON.stringify(user)); + + messageApi.success('登录成功'); + navigate('/admin/agents'); + } catch (error: any) { + messageApi.error(error.response?.data?.message || '登录失败,请检查用户名和密码'); + } finally { + setLoading(false); + } + }; + + return ( +
+
+
+

Pika 探针

+

老鸡专用监控管理平台

+
+ +
+ + } + placeholder="用户名" + /> + + + + } + placeholder="密码" + /> + + + + + +
+
+
+ ); +}; + +export default Login; + diff --git a/web/src/pages/Public/ServerDetail.tsx b/web/src/pages/Public/ServerDetail.tsx new file mode 100644 index 0000000..db99999 --- /dev/null +++ b/web/src/pages/Public/ServerDetail.tsx @@ -0,0 +1,832 @@ +import type {ReactNode} from 'react'; +import {useEffect, useMemo, useState} from 'react'; +import {useNavigate, useParams} from 'react-router-dom'; +import {Activity, ArrowLeft, Cpu, HardDrive, Loader2, MemoryStick, Network, Server,} from 'lucide-react'; +import type {TooltipProps} from 'recharts'; +import { + Area, + AreaChart, + CartesianGrid, + Legend, + Line, + LineChart, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from 'recharts'; +import {getAgent, getAgentLatestMetrics, getAgentMetrics} from '../../api/agent'; +import type { + Agent, + AggregatedCPUMetric, + AggregatedMemoryMetric, + AggregatedNetworkMetric, + LatestMetrics +} from '../../types'; + +const formatBytes = (bytes: number | undefined | null): string => { + if (!bytes || bytes <= 0) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']; + const i = Math.min(Math.floor(Math.log(bytes) / Math.log(k)), sizes.length - 1); + return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`; +}; + +const formatPercentValue = (value: number | undefined | null): string => { + if (value === undefined || value === null || Number.isNaN(value)) return '0.0'; + return value.toFixed(1); +}; + +const formatUptime = (seconds: number | undefined | null): string => { + if (seconds === undefined || seconds === null) return '-'; + if (seconds <= 0) return '0 秒'; + + const days = Math.floor(seconds / 86400); + const hours = Math.floor((seconds % 86400) / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + + const parts: string[] = []; + if (days > 0) parts.push(`${days} 天`); + if (hours > 0) parts.push(`${hours} 小时`); + if (minutes > 0) parts.push(`${minutes} 分钟`); + + return parts.length > 0 ? parts.join(' ') : '不到 1 分钟'; +}; + +const formatDateTime = (value: string | number | undefined | null): string => { + if (value === undefined || value === null || value === '') { + return '-'; + } + + const date = typeof value === 'number' ? new Date(value) : new Date(value); + + if (Number.isNaN(date.getTime())) { + return '-'; + } + + return date.toLocaleString('zh-CN'); +}; + +const timeRangeOptions = [ + {label: '1小时', value: '1h'}, + {label: '6小时', value: '6h'}, + {label: '12小时', value: '12h'}, + {label: '24小时', value: '24h'}, + {label: '3天', value: '3d'}, + {label: '7天', value: '7d'}, + {label: '30天', value: '30d'}, +] as const; + +const LoadingSpinner = () => ( +
+
+ +

数据加载中,请稍候...

+
+
+); + +const EmptyState = ({message = '服务器不存在或已离线'}: { message?: string }) => ( +
+
+
+ +
+

{message}

+
+
+); + +const Card = ({ + title, + description, + action, + children, + }: { + title?: string; + description?: string; + action?: ReactNode; + children: ReactNode; +}) => ( +
+ {(title || description || action) && ( +
+
+ {title ?

{title}

: null} + {description ?

{description}

: null} +
+ {action ?
{action}
: null} +
+ )} +
{children}
+
+); + +type AccentVariant = 'indigo' | 'emerald' | 'purple' | 'sky' | 'amber'; + +const accentThemes: Record = { + indigo: { + icon: 'bg-indigo-50 text-indigo-600', + badge: 'bg-indigo-100 text-indigo-600', + highlight: 'text-indigo-600', + }, + emerald: { + icon: 'bg-emerald-50 text-emerald-600', + badge: 'bg-emerald-100 text-emerald-600', + highlight: 'text-emerald-600', + }, + purple: { + icon: 'bg-purple-50 text-purple-600', + badge: 'bg-purple-100 text-purple-600', + highlight: 'text-purple-600', + }, + sky: { + icon: 'bg-sky-50 text-sky-600', + badge: 'bg-sky-100 text-sky-600', + highlight: 'text-sky-600', + }, + amber: { + icon: 'bg-amber-50 text-amber-600', + badge: 'bg-amber-100 text-amber-600', + highlight: 'text-amber-600', + }, +}; + +const InfoGrid = ({items}: { items: Array<{ label: string; value: ReactNode }> }) => ( +
+ {items.map((item) => ( +
+
{item.label}
+
{item.value}
+
+ ))} +
+); + +const TimeRangeSelector = ({ + value, + onChange, + }: { + value: '1h' | '6h' | '12h' | '24h' | '1d' | '3d' | '7d' | '1w' | '30d' | '1M' | '90d' | '3M'; + onChange: (value: '1h' | '6h' | '12h' | '24h' | '1d' | '3d' | '7d' | '1w' | '30d' | '1M' | '90d' | '3M') => void; +}) => ( +
+ {timeRangeOptions.map((option) => { + const isActive = option.value === value; + return ( + + ); + })} +
+); + +type MetricsTooltipProps = TooltipProps & { unit?: string, label?: string, payload?: any[] }; + +const CustomTooltip = ({active, payload, label, unit = '%'}: MetricsTooltipProps) => { + if (!active || !payload || payload.length === 0) { + return null; + } + + return ( +
+

{label}

+
+ {payload.map((entry, index) => { + if (!entry) { + return null; + } + + const dotColor = entry.color ?? '#6366f1'; + const title = entry.name ?? entry.dataKey ?? `系列 ${index + 1}`; + const value = + typeof entry.value === 'number' + ? Number.isFinite(entry.value) + ? entry.value.toFixed(2) + : '-' + : entry.value; + + return ( +

+ + + {title}: {value} + {unit} + +

+ ); + })} +
+
+ ); +}; + +const ServerDetail = () => { + const {id} = useParams<{ id: string }>(); + const navigate = useNavigate(); + + const [loading, setLoading] = useState(true); + const [agent, setAgent] = useState(null); + const [latestMetrics, setLatestMetrics] = useState(null); + const [timeRange, setTimeRange] = useState<'1h' | '6h' | '12h' | '24h' | '1d' | '3d' | '7d' | '1w' | '30d' | '1M' | '90d' | '3M'>('1h'); + const [metricsData, setMetricsData] = useState<{ + cpu: AggregatedCPUMetric[]; + memory: AggregatedMemoryMetric[]; + network: AggregatedNetworkMetric[]; + }>({ + cpu: [], + memory: [], + network: [], + }); + + const loadMetrics = async () => { + if (!id) return; + + try { + const [cpuRes, memoryRes, networkRes] = await Promise.all([ + getAgentMetrics({agentId: id, type: 'cpu', range: timeRange}), + getAgentMetrics({agentId: id, type: 'memory', range: timeRange}), + getAgentMetrics({agentId: id, type: 'network', range: timeRange}), + ]); + + setMetricsData({ + cpu: cpuRes.data.metrics || [], + memory: memoryRes.data.metrics || [], + network: networkRes.data.metrics || [], + }); + } catch (error) { + console.error('Failed to load metrics:', error); + } + }; + + const loadLatestMetrics = async () => { + if (!id) return; + try { + const latestRes = await getAgentLatestMetrics(id); + setLatestMetrics(latestRes.data); + } catch (error) { + console.error('Failed to refresh latest metrics:', error); + } + }; + + useEffect(() => { + if (!id) return; + + let isMounted = true; + + const initialize = async () => { + try { + setLoading(true); + const [agentRes, latestRes] = await Promise.all([getAgent(id), getAgentLatestMetrics(id)]); + if (!isMounted) return; + setAgent(agentRes.data); + setLatestMetrics(latestRes.data); + } catch (error) { + console.error('Failed to load agent details:', error); + } finally { + if (isMounted) { + setLoading(false); + } + } + }; + + initialize(); + + return () => { + isMounted = false; + }; + }, [id]); + + useEffect(() => { + if (!id) return; + + loadMetrics(); + const chartInterval = setInterval(loadMetrics, 30000); + return () => clearInterval(chartInterval); + }, [id, timeRange]); + + useEffect(() => { + if (!id) return; + + const interval = setInterval(loadLatestMetrics, 5000); + return () => clearInterval(interval); + }, [id]); + + const cpuChartData = useMemo( + () => + metricsData.cpu.map((item) => ({ + time: new Date(item.timestamp).toLocaleTimeString('zh-CN', { + hour: '2-digit', + minute: '2-digit', + }), + usage: Number(item.avgUsage.toFixed(2)), + })), + [metricsData.cpu] + ); + + const memoryChartData = useMemo( + () => + metricsData.memory.map((item) => ({ + time: new Date(item.timestamp).toLocaleTimeString('zh-CN', { + hour: '2-digit', + minute: '2-digit', + }), + usage: Number(item.avgUsage.toFixed(2)), + })), + [metricsData.memory] + ); + + const networkChartData = useMemo(() => { + const aggregated: Record< + string, + { time: string; upload: number; download: number } + > = {}; + + metricsData.network.forEach((item) => { + const time = new Date(item.timestamp).toLocaleTimeString('zh-CN', { + hour: '2-digit', + minute: '2-digit', + }); + + if (!aggregated[time]) { + aggregated[time] = {time, upload: 0, download: 0}; + } + + // 聚合数据:使用平均速率(字节/秒)转换为 MB/s + aggregated[time].upload += item.avgSentRate / 1024 / 1024; + aggregated[time].download += item.avgRecvRate / 1024 / 1024; + }); + + return Object.values(aggregated).map((item) => ({ + ...item, + upload: Number(item.upload.toFixed(2)), + download: Number(item.download.toFixed(2)), + })); + }, [metricsData.network]); + + const snapshotCards = useMemo(() => { + if (!latestMetrics) { + return [] as Array<{ + key: string; + icon: typeof Cpu; + title: string; + usagePercent: string; + accent: AccentVariant; + metrics: Array<{ label: string; value: ReactNode }>; + }>; + } + + const cards: Array<{ + key: string; + icon: typeof Cpu; + title: string; + usagePercent: string; + accent: AccentVariant; + metrics: Array<{ label: string; value: ReactNode }>; + }> = []; + + cards.push({ + key: 'cpu', + icon: Cpu, + title: 'CPU 使用', + usagePercent: `${formatPercentValue(latestMetrics.cpu?.usagePercent)}%`, + accent: 'indigo', + metrics: [ + {label: '当前使用', value: `${formatPercentValue(latestMetrics.cpu?.usagePercent)}%`}, + { + label: '采样时间', + value: latestMetrics.cpu ? formatDateTime(latestMetrics.cpu.timestamp) : '-', + }, + ], + }); + + cards.push({ + key: 'memory', + icon: MemoryStick, + title: '内存使用', + usagePercent: `${formatPercentValue(latestMetrics.memory?.usagePercent)}%`, + accent: 'emerald', + metrics: [ + { + label: '已用 / 总量', + value: `${formatBytes(latestMetrics.memory?.used)} / ${formatBytes(latestMetrics.memory?.total)}` + }, + {label: 'Swap 已用', value: formatBytes(latestMetrics.memory?.swapUsed)}, + ], + }); + + cards.push({ + key: 'disk', + icon: HardDrive, + title: '磁盘使用', + usagePercent: latestMetrics.disk + ? `${formatPercentValue(latestMetrics.disk.avgUsagePercent)}%` + : '—', + accent: 'purple', + metrics: [ + { + label: '已用 / 总量', + value: `${formatBytes(latestMetrics.disk?.used)} / ${formatBytes(latestMetrics.disk?.total)}` + }, + {label: '磁盘数量', value: latestMetrics.disk?.totalDisks ?? '-'}, + ], + }); + + cards.push({ + key: 'load', + icon: Activity, + title: '系统负载', + usagePercent: latestMetrics.load + ? `${latestMetrics.load.load1.toFixed(2)}` + : '—', + accent: 'amber', + metrics: [ + { + label: '1 / 5 / 15 分钟', + value: latestMetrics.load + ? `${latestMetrics.load.load1.toFixed(2)} / ${latestMetrics.load.load5.toFixed(2)} / ${latestMetrics.load.load15.toFixed(2)}` + : '-', + }, + { + label: '网络累计', + value: `${formatBytes(latestMetrics.network?.totalBytesSentTotal)} ↑ / ${formatBytes( + latestMetrics.network?.totalBytesRecvTotal, + )} ↓`, + }, + ], + }); + + return cards; + }, [agent, latestMetrics]); + + const platformDisplay = latestMetrics?.host?.platform + ? `${latestMetrics.host.platform} ${latestMetrics.host.platformVersion || ''}`.trim() + : agent?.os || '-'; + const architectureDisplay = latestMetrics?.host?.kernelArch || agent?.arch || '-'; + const uptimeDisplay = formatUptime(latestMetrics?.host?.uptime); + const bootTimeDisplay = latestMetrics?.host?.bootTime + ? formatDateTime(latestMetrics.host.bootTime * 1000) + : '-'; + const lastSeenDisplay = agent ? formatDateTime(agent.lastSeenAt) : '-'; + const displayName = agent?.name?.trim() ? agent.name : '未命名探针'; + const isOnline = agent?.status === 1; + const statusStyles = isOnline + ? 'inline-flex items-center gap-1 rounded-full bg-emerald-50 px-2 py-0.5 text-xs font-medium text-emerald-600' + : 'inline-flex items-center gap-1 rounded-full bg-slate-100 px-2 py-0.5 text-xs font-medium text-slate-500'; + const statusDotStyles = isOnline ? 'bg-emerald-500' : 'bg-slate-400'; + const statusText = isOnline ? '在线' : '离线'; + + const loadSummary = latestMetrics?.load + ? `${latestMetrics.load.load1.toFixed(2)} / ${latestMetrics.load.load5.toFixed(2)} / ${latestMetrics.load.load15.toFixed(2)}` + : '-'; + const networkSummary = latestMetrics?.network + ? `${formatBytes(latestMetrics.network.totalBytesSentTotal)} ↑ / ${formatBytes( + latestMetrics.network.totalBytesRecvTotal, + )} ↓` + : '—'; + + const environmentInfo = [ + {label: '操作系统', value: platformDisplay || '-'}, + {label: '内核版本', value: latestMetrics?.host?.kernelVersion || '-'}, + {label: '硬件架构', value: architectureDisplay || '-'}, + {label: 'CPU 型号', value: latestMetrics?.cpu?.modelName || '-'}, + {label: '逻辑核心', value: latestMetrics?.cpu?.logicalCores ?? '-'}, + {label: '物理核心', value: latestMetrics?.cpu?.physicalCores ?? '-'}, + ]; + + const statusInfo = [ + {label: '运行时间', value: uptimeDisplay}, + {label: '启动时间', value: bootTimeDisplay}, + {label: '最近心跳', value: lastSeenDisplay}, + {label: '进程数', value: latestMetrics?.host?.procs ?? '-'}, + {label: '负载 (1 / 5 / 15)', value: loadSummary}, + {label: '网络累计', value: networkSummary}, + ]; + + const heroStats = [ + {label: '运行系统', value: platformDisplay || '-'}, + {label: '硬件架构', value: architectureDisplay || '-'}, + {label: '最近心跳', value: lastSeenDisplay}, + {label: '运行时长', value: uptimeDisplay}, + ]; + + if (loading) { + return ; + } + + if (!agent) { + return ; + } + + return ( +
+
+
+
+
+
+
+ +
+
+ +
+
+
+

{displayName}

+ + + {statusText} + +
+

+ {agent.hostname} · {agent.ip} +

+

公共视图 · 实时监控概览

+
+
+
+ +
+ {heroStats.map((stat) => ( +
+

{stat.label}

+

{stat.value}

+
+ ))} +
+
+
+ 探针 ID:{agent.id} + + 版本:{agent.version || '-'} + + 负载:{loadSummary} + + 网络累计:{networkSummary} +
+
+
+ +
+ +
+
+
+

运行环境

+

来自最近一次探针上报的硬件与系统信息

+
+ +
+
+
+

运行状态

+

关键时间与负载指标,帮助快速判断主机健康状况

+
+ +
+
+
+ {snapshotCards.length > 0 ? ( +
+
+

资源快照

+

最近 5 秒采集的资源使用状况

+
+
+ {snapshotCards.map((card) => { + const theme = accentThemes[card.accent]; + return ( +
+
+
+ + + +

{card.title}

+
+ + {card.usagePercent} + +
+
+ {card.metrics.map((metric) => ( +
+ {metric.label} + {metric.value} +
+ ))} +
+
+ ); + })} +
+
+ ) : null} +
+
+ + } + > +
+
+

+ + + + CPU 使用率 +

+ {cpuChartData.length > 0 ? ( + + + + + + + + + + + `${value}%`} + /> + }/> + + + + ) : ( +
+ 暂无数据 +
+ )} +
+ +
+

+ + + + 内存使用率 +

+ {memoryChartData.length > 0 ? ( + + + + + + + + + + + `${value}%`} + /> + }/> + + + + ) : ( +
+ 暂无数据 +
+ )} +
+ +
+

+ + + + 网络流量(MB) +

+ {networkChartData.length > 0 ? ( + + + + + `${value} MB`} + /> + }/> + + + + + + ) : ( +
+ 暂无数据 +
+ )} +
+
+
+
+ +
+ © {new Date().getFullYear()} Pika Monitor · 保持洞察,稳定运行。 +
+
+
+ ); +}; + +export default ServerDetail; diff --git a/web/src/pages/Public/ServerList.tsx b/web/src/pages/Public/ServerList.tsx new file mode 100644 index 0000000..a6d53a8 --- /dev/null +++ b/web/src/pages/Public/ServerList.tsx @@ -0,0 +1,541 @@ +import {type ReactNode, useState} from 'react'; +import {useNavigate} from 'react-router-dom'; +import {useQuery} from '@tanstack/react-query'; +import {Cpu, EthernetPortIcon, HardDrive, LayoutGrid, List, Loader2, LogIn, MemoryStick, Network} from 'lucide-react'; +import {listAgents} from '../../api/agent'; +import type {Agent, LatestMetrics} from '../../types'; +import GithubSvg from "../../assets/github.svg" + +interface AgentWithMetrics extends Agent { + metrics?: LatestMetrics; +} + +type ViewMode = 'grid' | 'list'; + +const formatSpeed = (bytesPerSecond: number): string => { + if (!bytesPerSecond || bytesPerSecond <= 0) return '0 B/s'; + const k = 1024; + const sizes = ['B/s', 'KB/s', 'MB/s', 'GB/s', 'TB/s']; + const i = Math.min(Math.floor(Math.log(bytesPerSecond) / Math.log(k)), sizes.length - 1); + return `${(bytesPerSecond / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`; +}; + +const formatTraffic = (bytesPerSecond: number): string => { + if (!bytesPerSecond || bytesPerSecond <= 0) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; + const i = Math.min(Math.floor(Math.log(bytesPerSecond) / Math.log(k)), sizes.length - 1); + return `${(bytesPerSecond / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`; +}; + +const formatBytes = (bytes: number): string => { + if (!bytes || bytes <= 0) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; + const i = Math.min(Math.floor(Math.log(bytes) / Math.log(k)), sizes.length - 1); + return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`; +}; + +const formatPercentValue = (value: number): string => (Number.isFinite(value) ? value.toFixed(1) : '0.0'); + +const ProgressBar = ({percent, colorClass}: { percent: number; colorClass: string }) => ( +
+
+
+); + +const LoadingSpinner = () => ( +
+
+ +

数据加载中,请稍候...

+
+
+); + +interface EmptyStateProps { + title: string; + description: string; + extra?: ReactNode; +} + +const EmptyState = ({title, description, extra}: EmptyStateProps) => ( +
+
+ +
+

{title}

+

{description}

+ {extra ?
{extra}
: null} +
+); + +const calculateNetworkSpeed = (metrics?: LatestMetrics) => { + if (!metrics?.network) { + return {upload: 0, download: 0}; + } + + // 后端返回的已经是每秒速率(字节/秒),直接使用 + return { + upload: metrics.network.totalBytesSentRate, + download: metrics.network.totalBytesRecvRate + }; +}; + +const calculateNetworkTraffic = (metrics?: LatestMetrics) => { + if (!metrics?.network) { + return {totalUpload: 0, totalDownload: 0}; + } + + // 后端返回的累计流量,直接使用 + return { + totalUpload: metrics.network.totalBytesSentTotal, + totalDownload: metrics.network.totalBytesRecvTotal + }; +}; + +const calculateDiskUsage = (metrics?: LatestMetrics) => { + if (!metrics?.disk) { + return 0; + } + + // 后端已经计算好平均使用率,直接返回 + return metrics.disk.avgUsagePercent; +}; + +const getProgressColor = (percent: number) => { + if (percent >= 85) return 'bg-rose-500'; + if (percent >= 65) return 'bg-amber-500'; + return 'bg-emerald-500'; +}; + +const ServerList = () => { + const navigate = useNavigate(); + const [viewMode, setViewMode] = useState('grid'); + + const {data: agents = [], isLoading, dataUpdatedAt} = useQuery({ + queryKey: ['agents', 'online'], + queryFn: async () => { + const response = await listAgents(); + // 后端已经在列表中包含了 metrics 数据,直接使用即可 + return (response.data.items || []) as AgentWithMetrics[]; + }, + refetchInterval: 5000, + }); + + const filteredAgents = agents; + + const lastUpdatedDisplay = + dataUpdatedAt && dataUpdatedAt > 0 + ? new Date(dataUpdatedAt).toLocaleTimeString('zh-CN', { + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + }) + : '尚未刷新'; + + + const handleNavigate = (agentId: string) => { + navigate(`/servers/${agentId}`); + }; + + const renderGridView = () => ( +
+ {filteredAgents.map((agent) => { + const cpuUsage = agent.metrics?.cpu?.usagePercent ?? 0; + const memoryUsage = agent.metrics?.memory?.usagePercent ?? 0; + const diskUsage = calculateDiskUsage(agent.metrics); + const {upload, download} = calculateNetworkSpeed(agent.metrics); + const {totalUpload, totalDownload} = calculateNetworkTraffic(agent.metrics); + + return ( +
handleNavigate(agent.id)} + onKeyDown={(event) => { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + handleNavigate(agent.id); + } + }} + className="group relative flex h-full cursor-pointer flex-col gap-5 rounded-2xl border border-slate-200 bg-white p-5 transition duration-200 hover:border-indigo-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-300" + > +
+
+
+
+

+ {agent.name || agent.hostname} +

+ + + 在线 + +
+ + {agent.os} · {agent.arch} + +
+
+ {agent.platform && ( + + 平台: {agent.platform} + + )} + {agent.location && ( + + 位置: {agent.location} + + )} + {agent.expireTime > 0 && ( + + 到期: {new Date(agent.expireTime).toLocaleDateString('zh-CN')} + + )} +
+
+
+ +
+
+
+
+
+ +
+ CPU +
+
+ {formatPercentValue(cpuUsage)}% +
+ +
+ +
+
+
+ +
+ 内存 +
+
+ {formatPercentValue(memoryUsage)}% +
+ +
+ +
+
+
+ +
+ 磁盘 +
+
+ {formatPercentValue(diskUsage)}% +
+ +
+
+ +
+
+
+
+ +
+ 实时速率 +
+
+ + ↑ + {formatSpeed(upload)} + + + ↓ + {formatSpeed(download)} + +
+
+
+
+
+ +
+ 累计流量 +
+
+ + ↑ + {formatTraffic(totalUpload)} + + + ↓ + {formatTraffic(totalDownload)} + +
+
+
+
+
+ ); + })} +
+ ); + + const renderListView = () => ( +
+ + + + + + + + + + + + + {filteredAgents.map((agent) => { + const cpuUsage = agent.metrics?.cpu?.usagePercent ?? 0; + const cpuModel = agent.metrics?.cpu?.modelName || '未知'; + const cpuPhysicalCores = agent.metrics?.cpu?.physicalCores ?? 0; + const cpuLogicalCores = agent.metrics?.cpu?.logicalCores ?? 0; + + const memoryUsage = agent.metrics?.memory?.usagePercent ?? 0; + const memoryTotal = agent.metrics?.memory?.total ?? 0; + const memoryUsed = agent.metrics?.memory?.used ?? 0; + const memoryFree = agent.metrics?.memory?.free ?? 0; + + const diskUsage = calculateDiskUsage(agent.metrics); + const diskTotal = agent.metrics?.disk?.total ?? 0; + const diskUsed = agent.metrics?.disk?.used ?? 0; + const diskFree = agent.metrics?.disk?.free ?? 0; + + const {upload, download} = calculateNetworkSpeed(agent.metrics); + + return ( + handleNavigate(agent.id)} + onKeyDown={(event) => { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + handleNavigate(agent.id); + } + }} + className="cursor-pointer transition hover:bg-indigo-50 focus-within:bg-indigo-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-200" + > + + + + + + + + ); + })} + +
服务器操作系统CPU内存磁盘网络
+
+
+ + {agent.name || agent.hostname} + + + + 在线 + +
+
+ {agent.platform && ( + + 平台: {agent.platform} + + )} + {agent.location && ( + + 位置: {agent.location} + + )} + {agent.expireTime > 0 && ( + + 到期: {new Date(agent.expireTime).toLocaleDateString('zh-CN')} + + )} +
+
+
+
{agent.os}
+
{agent.arch}
+
+
+
+
+ +
+ + {formatPercentValue(cpuUsage)}% + +
+
+
{cpuModel}
+
{cpuPhysicalCores}核{cpuLogicalCores}线程
+
+
+
+
+
+
+ +
+ + {formatPercentValue(memoryUsage)}% + +
+
+
总计:{formatBytes(memoryTotal)}
+
已用:{formatBytes(memoryUsed)} / 剩余:{formatBytes(memoryFree)}
+
+
+
+
+
+
+ +
+ + {formatPercentValue(diskUsage)}% + +
+
+
总计:{formatBytes(diskTotal)}
+
已用:{formatBytes(diskUsed)} / 剩余:{formatBytes(diskFree)}
+
+
+
+
↑ {formatSpeed(upload)}
+
↓ {formatSpeed(download)}
+
+
+ ); + + if (isLoading) { + return ; + } + + return ( +
+
+
+
+
+

Pika Monitor

+

皮卡监控

+
+
+ + 最后更新: + {lastUpdatedDisplay} + + +
+ + +
+ + github + + + + 登录 + +
+
+
+
+ +
+
+ {filteredAgents.length === 0 ? ( + + ) : viewMode === 'grid' ? ( + renderGridView() + ) : ( + renderListView() + )} +
+
+ +
+
+ © {new Date().getFullYear()} Pika Monitor · 保持洞察,稳定运行。 +
+
+
+ ); +}; + +export default ServerList; diff --git a/web/src/pages/Settings/AlertSettings.tsx b/web/src/pages/Settings/AlertSettings.tsx new file mode 100644 index 0000000..e5d6347 --- /dev/null +++ b/web/src/pages/Settings/AlertSettings.tsx @@ -0,0 +1,384 @@ +import { useState, useEffect } from 'react'; +import { Form, Input, Switch, InputNumber, Button, Space, App, Card, Select, Divider } from 'antd'; +import { Save, TestTube } from 'lucide-react'; +import type { AlertConfig } from '../../types'; +import { getAlertConfigsByAgent, createAlertConfig, updateAlertConfig, testNotification } from '../../api/alert'; +import { getAgents } from '../../api/agent'; +import { getErrorMessage } from '../../lib/utils'; + +const AlertSettings = () => { + const [form] = Form.useForm(); + const { message: messageApi } = App.useApp(); + const [loading, setLoading] = useState(false); + const [testLoading, setTestLoading] = useState(false); + const [configId, setConfigId] = useState(null); + const [agents, setAgents] = useState<{ id: string; name: string }[]>([]); + + // 加载探针列表 + const loadAgents = async () => { + try { + const data = await getAgents(); + setAgents(data.items || []); + } catch (error: unknown) { + console.error('获取探针列表失败', error); + } + }; + + // 加载配置 + const loadConfig = async () => { + try { + // 获取全局配置(使用特殊的 agentId "global") + const configs = await getAlertConfigsByAgent('global'); + if (configs && configs.length > 0) { + const config = configs[0]; + setConfigId(config.id || null); + form.setFieldsValue(config); + } else { + // 使用默认值 + form.setFieldsValue({ + name: '全局告警配置', + enabled: true, + agentIds: [], // 空数组表示监控所有探针 + rules: { + cpuEnabled: true, + cpuThreshold: 80, + cpuDuration: 60, + memoryEnabled: true, + memoryThreshold: 80, + memoryDuration: 60, + diskEnabled: true, + diskThreshold: 85, + diskDuration: 60, + networkEnabled: false, + networkDuration: 60, + }, + notification: { + dingTalkEnabled: false, + dingTalkWebhook: '', + dingTalkSecret: '', + weComEnabled: false, + weComWebhook: '', + feishuEnabled: false, + feishuWebhook: '', + emailEnabled: false, + emailAddresses: [], + customWebhookEnabled: false, + customWebhookUrl: '', + }, + }); + } + } catch (error: unknown) { + messageApi.error(getErrorMessage(error, '加载配置失败')); + } + }; + + useEffect(() => { + loadAgents(); + loadConfig(); + }, []); + + const handleSubmit = async () => { + try { + const values = await form.validateFields(); + setLoading(true); + + const alertConfig: AlertConfig = { + ...values, + agentId: 'global', // 全局配置使用特殊的 agentId + }; + + if (configId) { + // 更新 + await updateAlertConfig(configId, alertConfig); + messageApi.success('告警配置更新成功'); + } else { + // 创建 + const created = await createAlertConfig(alertConfig); + setConfigId(created.id || null); + messageApi.success('告警配置创建成功'); + } + } catch (error: unknown) { + messageApi.error(getErrorMessage(error, '保存配置失败')); + } finally { + setLoading(false); + } + }; + + const handleTest = async () => { + if (!configId) { + messageApi.warning('请先保存配置后再测试'); + return; + } + + try { + setTestLoading(true); + await testNotification(configId); + messageApi.success('测试通知已发送,请检查您的通知渠道'); + } catch (error: unknown) { + messageApi.error(getErrorMessage(error, '发送测试通知失败')); + } finally { + setTestLoading(false); + } + }; + + return ( +
+
+ + {/* 基本信息 */} + + + + + + + + + + + + + + + + + ) : null + } + + + + {/* 企业微信通知 */} + + + + + + + prevValues.notification?.weComEnabled !== currentValues.notification?.weComEnabled + } + > + {({ getFieldValue }) => + getFieldValue(['notification', 'weComEnabled']) ? ( + + + + ) : null + } + + + + {/* 飞书通知 */} + + + + + + + prevValues.notification?.feishuEnabled !== currentValues.notification?.feishuEnabled + } + > + {({ getFieldValue }) => + getFieldValue(['notification', 'feishuEnabled']) ? ( + + + + ) : null + } + + + + {/* 自定义 Webhook */} + + + + + + + prevValues.notification?.customWebhookEnabled !== + currentValues.notification?.customWebhookEnabled + } + > + {({ getFieldValue }) => + getFieldValue(['notification', 'customWebhookEnabled']) ? ( + + + + ) : null + } + + + + {/* 操作按钮 */} +
+ + + + +
+
+ +
+
+ ); +}; + +export default AlertSettings; diff --git a/web/src/pages/Settings/index.tsx b/web/src/pages/Settings/index.tsx new file mode 100644 index 0000000..1d0dd58 --- /dev/null +++ b/web/src/pages/Settings/index.tsx @@ -0,0 +1,34 @@ +import {Tabs} from 'antd'; +import {Bell} from 'lucide-react'; +import AlertSettings from './AlertSettings'; +import {PageHeader} from "../../components"; + +const Settings = () => { + const items = [ + { + key: 'alert', + label: ( + + + 告警配置 + + ), + children: , + }, + ]; + + return ( +
+ + +
+ ); +}; + +export default Settings; diff --git a/web/src/pages/Users/UserList.tsx b/web/src/pages/Users/UserList.tsx new file mode 100644 index 0000000..55a6f43 --- /dev/null +++ b/web/src/pages/Users/UserList.tsx @@ -0,0 +1,330 @@ +import {useRef, useState} from 'react'; +import type {ActionType, ProColumns} from '@ant-design/pro-components'; +import {ProTable} from '@ant-design/pro-components'; +import {App, Button, Form, Input, Modal, Popconfirm} from 'antd'; +import {Edit, Key, Plus, RefreshCw, Trash2} from 'lucide-react'; +import {createUser, deleteUser, listUsers, resetPassword, updateUser} from '../../api/user'; +import type {CreateUserRequest, UpdateUserRequest, User} from '../../types'; +import dayjs from 'dayjs'; +import {getErrorMessage} from '../../lib/utils'; +import {PageHeader} from '../../components'; + +const UserList = () => { + const {message: messageApi, modal} = App.useApp(); + const actionRef = useRef(null); + const [submitting, setSubmitting] = useState(false); + const [isModalVisible, setIsModalVisible] = useState(false); + const [editingUser, setEditingUser] = useState(null); + const [form] = Form.useForm(); + + const handleCreate = () => { + setEditingUser(null); + setIsModalVisible(true); + form.resetFields(); + }; + + const handleEdit = (user: User) => { + setEditingUser(user); + form.setFieldsValue({ + username: user.username, + nickname: user.nickname, + }); + setIsModalVisible(true); + }; + + const handleDelete = async (userId: string) => { + try { + await deleteUser(userId); + messageApi.success('删除成功'); + actionRef.current?.reload(); + } catch (error: unknown) { + messageApi.error(getErrorMessage(error, '删除失败')); + } + }; + + const handleResetPassword = (user: User) => { + modal.confirm({ + title: `重置 ${user.username} 的密码`, + content: ( +
{ + try { + await resetPassword(user.id, values.newPassword); + messageApi.success('密码重置成功'); + Modal.destroyAll(); + } catch (error: unknown) { + messageApi.error(getErrorMessage(error, '密码重置失败')); + } + }} + > + + + +
+ ), + okText: '重置', + cancelText: '取消', + onOk: () => + new Promise((resolve) => { + const formElement = document.getElementById('resetPasswordForm') as HTMLFormElement | null; + if (formElement) { + formElement.dispatchEvent(new Event('submit', {cancelable: true, bubbles: true})); + } + resolve(undefined); + }), + }); + }; + + const handleModalOk = async () => { + try { + const values = await form.validateFields(); + const nickname = values.nickname?.trim(); + + if (!nickname) { + messageApi.warning('昵称不能为空'); + return; + } + + const isEdit = Boolean(editingUser); + let request: Promise | null = null; + + if (editingUser) { + if (nickname === editingUser.nickname) { + messageApi.info('昵称未发生变化'); + return; + } + + const updateData: UpdateUserRequest = {nickname}; + request = updateUser(editingUser.id, updateData); + } else { + const username = values.username.trim(); + if (!username) { + messageApi.warning('用户名不能为空'); + return; + } + + const createData: CreateUserRequest = { + username, + nickname, + password: values.password, + }; + request = createUser(createData); + } + + if (!request) { + return; + } + + setSubmitting(true); + await request; + messageApi.success(isEdit ? '更新成功' : '创建成功'); + + setIsModalVisible(false); + form.resetFields(); + actionRef.current?.reload(); + } catch (error: unknown) { + if (typeof error === 'object' && error !== null && 'errorFields' in error) { + return; + } + messageApi.error(getErrorMessage(error, '操作失败')); + } finally { + setSubmitting(false); + } + }; + + const columns: ProColumns[] = [ + { + title: '用户名', + dataIndex: 'username', + key: 'username', + render: (text) => {text}, + }, + { + title: '昵称', + dataIndex: 'nickname', + key: 'nickname', + hideInSearch: true, + render: (text) => {text || '未设置'}, + }, + { + title: '创建时间', + dataIndex: 'createdAt', + key: 'createdAt', + hideInSearch: true, + render: (value: number) => ( + {dayjs(value).format('YYYY-MM-DD HH:mm')} + ), + width: 180, + }, + { + title: '最近更新', + dataIndex: 'updatedAt', + key: 'updatedAt', + hideInSearch: true, + render: (value: number) => ( + {dayjs(value).format('YYYY-MM-DD HH:mm')} + ), + width: 180, + }, + { + title: '操作', + key: 'action', + valueType: 'option', + width: 200, + render: (_, record) => [ + , + , + handleDelete(record.id)} + okText="确定" + cancelText="取消" + > + + , + ], + }, + ]; + + return ( +
+ {/* 页面头部 */} + , + onClick: () => actionRef.current?.reload(), + }, + { + key: 'create', + label: '新建用户', + icon: , + type: 'primary', + onClick: handleCreate, + }, + ]} + /> + + {/* 用户列表 */} +
+ + actionRef={actionRef} + rowKey="id" + search={{labelWidth: 80}} + columns={columns} + pagination={{ + defaultPageSize: 10, + showSizeChanger: true, + }} + options={false} + request={async (params) => { + const {current = 1, pageSize = 10, username} = params; + try { + const response = await listUsers(current, pageSize, username); + const items = response.data.items || []; + return { + data: items, + success: true, + total: response.data.total, + }; + } catch (error: unknown) { + messageApi.error(getErrorMessage(error, '获取用户列表失败')); + return { + data: [], + success: false, + }; + } + }} + /> +
+ + {/* 新建/编辑用户弹窗 */} + { + setIsModalVisible(false); + form.resetFields(); + }} + okText={editingUser ? '保存' : '创建'} + cancelText="取消" + confirmLoading={submitting} + destroyOnClose + > +
+ + + + + + + + + {!editingUser && ( + + + + )} +
+
+
+ ); +}; + +export default UserList; diff --git a/web/src/router/index.tsx b/web/src/router/index.tsx new file mode 100644 index 0000000..1cc77f4 --- /dev/null +++ b/web/src/router/index.tsx @@ -0,0 +1,89 @@ +import { createBrowserRouter, Navigate } from 'react-router-dom'; +import { lazy, Suspense, type ComponentType, type LazyExoticComponent } from 'react'; +import PrivateRoute from '../components/PrivateRoute'; + +const LoginPage = lazy(() => import('../pages/Login')); +const AdminLayout = lazy(() => import('../pages/AdminLayout')); +const AgentListPage = lazy(() => import('../pages/Agents/AgentList')); +const AgentDetailPage = lazy(() => import('../pages/Agents/AgentDetail')); +const AgentInstallPage = lazy(() => import('../pages/Agents/AgentInstall')); +const AuditResultPage = lazy(() => import('../pages/Agents/AuditResult')); +const UserListPage = lazy(() => import('../pages/Users/UserList')); +const ApiKeyListPage = lazy(() => import('../pages/ApiKeys/ApiKeyList')); +const SettingsPage = lazy(() => import('../pages/Settings')); +const ServerListPage = lazy(() => import('../pages/Public/ServerList')); +const ServerDetailPage = lazy(() => import('../pages/Public/ServerDetail')); + +const LoadingFallback = () => ( +
+ 页面加载中... +
+); + +const lazyLoad = (Component: LazyExoticComponent>) => ( + }> + + +); + +const router = createBrowserRouter([ + // 登录页面 + { + path: '/login', + element: lazyLoad(LoginPage), + }, + // 公开页面 - 不需要登录 + { + path: '/', + element: lazyLoad(ServerListPage), + }, + { + path: '/servers/:id', + element: lazyLoad(ServerDetailPage), + }, + // 管理员页面 - 需要登录 + { + path: '/admin', + element: ( + + {lazyLoad(AdminLayout)} + + ), + children: [ + { + index: true, + element: , + }, + { + path: 'agents', + element: lazyLoad(AgentListPage), + }, + { + path: 'agents/:id', + element: lazyLoad(AgentDetailPage), + }, + { + path: 'agents/:id/audit', + element: lazyLoad(AuditResultPage), + }, + { + path: 'agents-install', + element: lazyLoad(AgentInstallPage), + }, + { + path: 'api-keys', + element: lazyLoad(ApiKeyListPage), + }, + { + path: 'users', + element: lazyLoad(UserListPage), + }, + { + path: 'settings', + element: lazyLoad(SettingsPage), + }, + ], + }, +]); + +export default router; diff --git a/web/src/types/index.ts b/web/src/types/index.ts new file mode 100644 index 0000000..5dc95f7 --- /dev/null +++ b/web/src/types/index.ts @@ -0,0 +1,278 @@ +// 用户相关 +export interface User { + id: string; + username: string; + nickname: string; + createdAt: number; + updatedAt: number; +} + +export interface LoginRequest { + username: string; + password: string; +} + +export interface LoginResponse { + token: string; + user: User; +} + +// 探针相关 +export interface Agent { + id: string; + name: string; + hostname: string; + ip: string; + os: string; + arch: string; + version: string; + platform?: string; // 平台 + location?: string; // 位置 + expireTime?: number; // 到期时间(时间戳毫秒) + status: number; + lastSeenAt: string | number; // 支持字符串或时间戳 + createdAt?: string; + updatedAt?: string; +} + +export interface AgentInfo { + name: string; + hostname: string; + ip: string; + os: string; + arch: string; + version: string; +} + +// 聚合指标数据(所有图表查询只返回聚合数据) +export interface AggregatedCPUMetric { + timestamp: number; + avgUsage: number; + minUsage: number; + maxUsage: number; + logicalCores: number; +} + +export interface AggregatedMemoryMetric { + timestamp: number; + avgUsage: number; + minUsage: number; + maxUsage: number; + total: number; +} + +export interface AggregatedNetworkMetric { + timestamp: number; + interface: string; + avgSentRate: number; + avgRecvRate: number; + maxSentRate: number; + maxRecvRate: number; + totalSent: number; + totalRecv: number; +} + +export interface AggregatedLoadMetric { + timestamp: number; + avgLoad1: number; + avgLoad5: number; + avgLoad15: number; + maxLoad1: number; + maxLoad5: number; + maxLoad15: number; +} + +// 最新实时数据(单点数据,不需要聚合) +export interface CPUMetric { + id: string; + agentId: string; + timestamp: number; + logicalCores: number; + physicalCores: number; + modelName: string; + usagePercent: number; +} + +export interface MemoryMetric { + id: string; + agentId: string; + timestamp: number; + total: number; + used: number; + free: number; + usagePercent: number; + swapTotal: number; + swapUsed: number; + swapFree: number; +} + +export interface LoadMetric { + id: string; + agentId: string; + timestamp: number; + load1: number; + load5: number; + load15: number; +} + +// 磁盘汇总数据 +export interface DiskSummary { + avgUsagePercent: number; // 平均使用率 + totalDisks: number; // 磁盘数量 + total: number; // 总容量(字节) + used: number; // 已使用(字节) + free: number; // 空闲(字节) +} + +// 磁盘详细数据 +export interface DiskMetric { + id: string; + agentId: string; + timestamp: number; + device: string; + mountPoint: string; + fsType: string; + total: number; + used: number; + free: number; + usagePercent: number; +} + +// 网络详细数据 +export interface NetworkMetric { + id: string; + agentId: string; + timestamp: number; + interface: string; + bytesSent: number; + bytesRecv: number; + packetsSent: number; + packetsRecv: number; +} + +// 网络汇总数据 +export interface NetworkSummary { + totalBytesSentRate: number; // 总发送速率(字节/秒) + totalBytesRecvRate: number; // 总接收速率(字节/秒) + totalBytesSentTotal: number; // 累计总发送流量 + totalBytesRecvTotal: number; // 累计总接收流量 + totalInterfaces: number; // 网卡数量 +} + +// 主机信息指标 +export interface HostMetric { + id: number; + agentId: string; + hostname: string; + os: string; + platform: string; + platformVersion: string; + kernelVersion: string; + kernelArch: string; + uptime: number; // 运行时间(秒) + bootTime: number; // 启动时间(Unix时间戳-秒) + procs: number; // 进程数 + timestamp: number; // 时间戳(毫秒) +} + +export interface LatestMetrics { + cpu?: CPUMetric; + memory?: MemoryMetric; + disk?: DiskSummary; // 改为汇总数据 + network?: NetworkSummary; // 改为汇总数据 + load?: LoadMetric; + host?: HostMetric; // 主机信息 +} + +// 用户管理相关 +export interface CreateUserRequest { + username: string; + nickname: string; + password: string; +} + +export interface UpdateUserRequest { + nickname?: string; +} + +export interface ChangePasswordRequest { + oldPassword: string; + newPassword: string; +} + +// API Key 相关 +export interface ApiKey { + id: string; + name: string; + key: string; + enabled: boolean; + createdBy: string; + createdAt: number; + updatedAt: number; +} + +export interface GenerateApiKeyRequest { + name: string; +} + +export interface UpdateApiKeyNameRequest { + name: string; +} + +// 告警配置相关 +export interface AlertRules { + cpuEnabled: boolean; + cpuThreshold: number; + cpuDuration: number; + memoryEnabled: boolean; + memoryThreshold: number; + memoryDuration: number; + diskEnabled: boolean; + diskThreshold: number; + diskDuration: number; + networkEnabled: boolean; + networkDuration: number; +} + +export interface NotificationConfig { + dingTalkEnabled: boolean; + dingTalkWebhook: string; + dingTalkSecret: string; + weComEnabled: boolean; + weComWebhook: string; + feishuEnabled: boolean; + feishuWebhook: string; + emailEnabled: boolean; + emailAddresses: string[]; + customWebhookEnabled: boolean; + customWebhookUrl: string; +} + +export interface AlertConfig { + id?: string; + agentId: string; + agentIds?: string[]; // 监控的探针列表,空数组表示监控所有 + name: string; + enabled: boolean; + rules: AlertRules; + notification: NotificationConfig; + createdAt?: number; + updatedAt?: number; +} + +export interface AlertRecord { + id: number; + agentId: string; + configId: string; + configName: string; + alertType: string; + message: string; + threshold: number; + actualValue: number; + level: string; + status: string; + firedAt: number; + resolvedAt?: number; + createdAt: number; + updatedAt: number; +} diff --git a/web/tsconfig.app.json b/web/tsconfig.app.json new file mode 100644 index 0000000..16502c4 --- /dev/null +++ b/web/tsconfig.app.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "types": ["vite/client"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + }, + "include": ["src"] +} diff --git a/web/tsconfig.json b/web/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/web/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/web/tsconfig.node.json b/web/tsconfig.node.json new file mode 100644 index 0000000..5104571 --- /dev/null +++ b/web/tsconfig.node.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + }, + "include": ["vite.config.ts"] +} diff --git a/web/vite.config.ts b/web/vite.config.ts new file mode 100644 index 0000000..1950513 --- /dev/null +++ b/web/vite.config.ts @@ -0,0 +1,19 @@ +import {defineConfig} from 'vite' +import react from '@vitejs/plugin-react' +import tailwindcss from '@tailwindcss/vite' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [ + react(), + tailwindcss(), + ], + server: { + proxy: { + '/api/': { + target: 'http://localhost:18888/', + changeOrigin: true, + }, + }, + }, +}) diff --git a/web/yarn.lock b/web/yarn.lock new file mode 100644 index 0000000..fe13dba --- /dev/null +++ b/web/yarn.lock @@ -0,0 +1,3172 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@ant-design/colors@^7.0.0", "@ant-design/colors@^7.2.1": + version "7.2.1" + resolved "https://registry.npmjs.org/@ant-design/colors/-/colors-7.2.1.tgz" + integrity sha512-lCHDcEzieu4GA3n8ELeZ5VQ8pKQAWcGGLRTQ50aQM2iqPpq2evTxER84jfdPvsPAtEcZ7m44NI45edFMo8oOYQ== + dependencies: + "@ant-design/fast-color" "^2.0.6" + +"@ant-design/cssinjs-utils@^1.1.3": + version "1.1.3" + resolved "https://registry.npmjs.org/@ant-design/cssinjs-utils/-/cssinjs-utils-1.1.3.tgz" + integrity sha512-nOoQMLW1l+xR1Co8NFVYiP8pZp3VjIIzqV6D6ShYF2ljtdwWJn5WSsH+7kvCktXL/yhEtWURKOfH5Xz/gzlwsg== + dependencies: + "@ant-design/cssinjs" "^1.21.0" + "@babel/runtime" "^7.23.2" + rc-util "^5.38.0" + +"@ant-design/cssinjs@^1.21.0", "@ant-design/cssinjs@^1.21.1", "@ant-design/cssinjs@^1.23.0": + version "1.24.0" + resolved "https://registry.npmjs.org/@ant-design/cssinjs/-/cssinjs-1.24.0.tgz" + integrity sha512-K4cYrJBsgvL+IoozUXYjbT6LHHNt+19a9zkvpBPxLjFHas1UpPM2A5MlhROb0BT8N8WoavM5VsP9MeSeNK/3mg== + dependencies: + "@babel/runtime" "^7.11.1" + "@emotion/hash" "^0.8.0" + "@emotion/unitless" "^0.7.5" + classnames "^2.3.1" + csstype "^3.1.3" + rc-util "^5.35.0" + stylis "^4.3.4" + +"@ant-design/fast-color@^2.0.6": + version "2.0.6" + resolved "https://registry.npmjs.org/@ant-design/fast-color/-/fast-color-2.0.6.tgz" + integrity sha512-y2217gk4NqL35giHl72o6Zzqji9O7vHh9YmhUVkPtAOpoTCH4uWxo/pr4VE8t0+ChEPs0qo4eJRC5Q1eXWo3vA== + dependencies: + "@babel/runtime" "^7.24.7" + +"@ant-design/icons-svg@^4.4.0": + version "4.4.2" + resolved "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz" + integrity sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA== + +"@ant-design/icons@^5.0.0", "@ant-design/icons@^5.6.1": + version "5.6.1" + resolved "https://registry.npmjs.org/@ant-design/icons/-/icons-5.6.1.tgz" + integrity sha512-0/xS39c91WjPAZOWsvi1//zjx6kAp4kxWwctR6kuU6p133w8RU0D2dSCvZC19uQyharg/sAvYxGYWl01BbZZfg== + dependencies: + "@ant-design/colors" "^7.0.0" + "@ant-design/icons-svg" "^4.4.0" + "@babel/runtime" "^7.24.8" + classnames "^2.2.6" + rc-util "^5.31.1" + +"@ant-design/pro-card@2.10.0": + version "2.10.0" + resolved "https://registry.npmjs.org/@ant-design/pro-card/-/pro-card-2.10.0.tgz" + integrity sha512-sLONn1odmE0Wkbse8pol4WiaEzBV8JU5s3FAMflPpycfUcbSaa1ktXzQ7LCo2SAvOS7gkfmpFjBPtrfbigKh4g== + dependencies: + "@ant-design/cssinjs" "^1.21.1" + "@ant-design/icons" "^5.0.0" + "@ant-design/pro-provider" "2.16.2" + "@ant-design/pro-utils" "2.18.0" + "@babel/runtime" "^7.18.0" + classnames "^2.3.2" + rc-resize-observer "^1.0.0" + rc-util "^5.4.0" + +"@ant-design/pro-components@^2.8.10": + version "2.8.10" + resolved "https://registry.npmjs.org/@ant-design/pro-components/-/pro-components-2.8.10.tgz" + integrity sha512-QHnnIXdmC5GTAtm6i8eeJy5yT9npPlFyxpDm+duiDrTRKRFaAQBduArxlH3DA/hoRCCypzPONxfK9BQNIhIyZA== + dependencies: + "@ant-design/pro-card" "2.10.0" + "@ant-design/pro-descriptions" "2.6.10" + "@ant-design/pro-field" "3.1.0" + "@ant-design/pro-form" "2.32.0" + "@ant-design/pro-layout" "7.22.7" + "@ant-design/pro-list" "2.6.10" + "@ant-design/pro-provider" "2.16.2" + "@ant-design/pro-skeleton" "2.2.1" + "@ant-design/pro-table" "3.21.0" + "@ant-design/pro-utils" "2.18.0" + "@babel/runtime" "^7.16.3" + +"@ant-design/pro-descriptions@2.6.10": + version "2.6.10" + resolved "https://registry.npmjs.org/@ant-design/pro-descriptions/-/pro-descriptions-2.6.10.tgz" + integrity sha512-+4MbiOfumnWlW0Awm4m8JML5o3lR649FD24AaivCmr8BQvIAAXdTITnDMXEg8BqvdP4KOvNsStZrvYfqoev33A== + dependencies: + "@ant-design/pro-field" "3.1.0" + "@ant-design/pro-form" "2.32.0" + "@ant-design/pro-provider" "2.16.2" + "@ant-design/pro-skeleton" "2.2.1" + "@ant-design/pro-utils" "2.18.0" + "@babel/runtime" "^7.18.0" + rc-resize-observer "^0.2.3" + rc-util "^5.0.6" + +"@ant-design/pro-field@3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@ant-design/pro-field/-/pro-field-3.1.0.tgz" + integrity sha512-+Dgp31WjD+iwg9KIRAMgNkfQivkJKMcYBrIBmho1e8ep/O0HgWSp48g70tBIWi/Lfem/Ky2schF7O8XCFouczw== + dependencies: + "@ant-design/icons" "^5.0.0" + "@ant-design/pro-provider" "2.16.2" + "@ant-design/pro-utils" "2.18.0" + "@babel/runtime" "^7.18.0" + "@chenshuai2144/sketch-color" "^1.0.8" + classnames "^2.3.2" + dayjs "^1.11.10" + lodash "^4.17.21" + lodash-es "^4.17.21" + rc-util "^5.4.0" + swr "^2.0.0" + +"@ant-design/pro-form@2.32.0": + version "2.32.0" + resolved "https://registry.npmjs.org/@ant-design/pro-form/-/pro-form-2.32.0.tgz" + integrity sha512-GZnVAMeYv+YHJb17lJ7rX5PYuQPvEA6EotQnPbHi9tGLN3PfexcAd21rqzuO+OrulU2x7TEMDIxtY9MzvvOGbg== + dependencies: + "@ant-design/icons" "^5.0.0" + "@ant-design/pro-field" "3.1.0" + "@ant-design/pro-provider" "2.16.2" + "@ant-design/pro-utils" "2.18.0" + "@babel/runtime" "^7.18.0" + "@chenshuai2144/sketch-color" "^1.0.7" + "@umijs/use-params" "^1.0.9" + classnames "^2.3.2" + dayjs "^1.11.10" + lodash "^4.17.21" + lodash-es "^4.17.21" + rc-resize-observer "^1.1.0" + rc-util "^5.0.6" + +"@ant-design/pro-layout@7.22.7": + version "7.22.7" + resolved "https://registry.npmjs.org/@ant-design/pro-layout/-/pro-layout-7.22.7.tgz" + integrity sha512-fvmtNA1r9SaasVIQIQt611VSlNxtVxDbQ3e+1GhYQza3tVJi/3gCZuDyfMfTnbLmf3PaW/YvLkn7MqDbzAzoLA== + dependencies: + "@ant-design/cssinjs" "^1.21.1" + "@ant-design/icons" "^5.0.0" + "@ant-design/pro-provider" "2.16.2" + "@ant-design/pro-utils" "2.18.0" + "@babel/runtime" "^7.18.0" + "@umijs/route-utils" "^4.0.0" + "@umijs/use-params" "^1.0.9" + classnames "^2.3.2" + lodash "^4.17.21" + lodash-es "^4.17.21" + path-to-regexp "8.2.0" + rc-resize-observer "^1.1.0" + rc-util "^5.0.6" + swr "^2.0.0" + warning "^4.0.3" + +"@ant-design/pro-list@2.6.10": + version "2.6.10" + resolved "https://registry.npmjs.org/@ant-design/pro-list/-/pro-list-2.6.10.tgz" + integrity sha512-xSWwnqCr+hPEYR4qY7nFUaxO5RQBxNlFaPNmobP2i+Im31slk9JuAusgWeIYO0mNhLJuLbxd8CCma2AZij3fBQ== + dependencies: + "@ant-design/cssinjs" "^1.21.1" + "@ant-design/icons" "^5.0.0" + "@ant-design/pro-card" "2.10.0" + "@ant-design/pro-field" "3.1.0" + "@ant-design/pro-table" "3.21.0" + "@ant-design/pro-utils" "2.18.0" + "@babel/runtime" "^7.18.0" + classnames "^2.3.2" + dayjs "^1.11.10" + rc-resize-observer "^1.0.0" + rc-util "^4.19.0" + +"@ant-design/pro-provider@2.16.2": + version "2.16.2" + resolved "https://registry.npmjs.org/@ant-design/pro-provider/-/pro-provider-2.16.2.tgz" + integrity sha512-0KmCH1EaOND787Jz6VRMYtLNZmqfT0JPjdUfxhyOxFfnBRfrjyfZgIa6CQoAJLEUMWv57PccWS8wRHVUUk2Yiw== + dependencies: + "@ant-design/cssinjs" "^1.21.1" + "@babel/runtime" "^7.18.0" + "@ctrl/tinycolor" "^3.4.0" + dayjs "^1.11.10" + rc-util "^5.0.1" + swr "^2.0.0" + +"@ant-design/pro-skeleton@2.2.1": + version "2.2.1" + resolved "https://registry.npmjs.org/@ant-design/pro-skeleton/-/pro-skeleton-2.2.1.tgz" + integrity sha512-3M2jNOZQZWEDR8pheY00OkHREfb0rquvFZLCa6DypGmiksiuuYuR9Y4iA82ZF+mva2FmpHekdwbje/GpbxqBeg== + dependencies: + "@babel/runtime" "^7.18.0" + +"@ant-design/pro-table@3.21.0": + version "3.21.0" + resolved "https://registry.npmjs.org/@ant-design/pro-table/-/pro-table-3.21.0.tgz" + integrity sha512-sI81d3FYRv5sXamUc+M5CsHZ9CchuUQgOAPzo5H4oPAVL5h+mkYGRsBzPsxQX7khTNpWjrAtPoRm5ipx3vvWog== + dependencies: + "@ant-design/cssinjs" "^1.21.1" + "@ant-design/icons" "^5.0.0" + "@ant-design/pro-card" "2.10.0" + "@ant-design/pro-field" "3.1.0" + "@ant-design/pro-form" "2.32.0" + "@ant-design/pro-provider" "2.16.2" + "@ant-design/pro-utils" "2.18.0" + "@babel/runtime" "^7.18.0" + "@dnd-kit/core" "^6.0.8" + "@dnd-kit/modifiers" "^6.0.1" + "@dnd-kit/sortable" "^7.0.2" + "@dnd-kit/utilities" "^3.2.1" + classnames "^2.3.2" + dayjs "^1.11.10" + lodash "^4.17.21" + lodash-es "^4.17.21" + rc-resize-observer "^1.0.0" + rc-util "^5.0.1" + +"@ant-design/pro-utils@2.18.0": + version "2.18.0" + resolved "https://registry.npmjs.org/@ant-design/pro-utils/-/pro-utils-2.18.0.tgz" + integrity sha512-8+ikyrN8L8a8Ph4oeHTOJEiranTj18+9+WHCHjKNdEfukI7Rjn8xpYdLJWb2AUJkb9d4eoAqjd5+k+7w81Df0w== + dependencies: + "@ant-design/icons" "^5.0.0" + "@ant-design/pro-provider" "2.16.2" + "@babel/runtime" "^7.18.0" + classnames "^2.3.2" + dayjs "^1.11.10" + lodash "^4.17.21" + lodash-es "^4.17.21" + rc-util "^5.0.6" + safe-stable-stringify "^2.4.3" + swr "^2.0.0" + +"@ant-design/react-slick@~1.1.2": + version "1.1.2" + resolved "https://registry.npmjs.org/@ant-design/react-slick/-/react-slick-1.1.2.tgz" + integrity sha512-EzlvzE6xQUBrZuuhSAFTdsr4P2bBBHGZwKFemEfq8gIGyIQCxalYfZW/T2ORbtQx5rU69o+WycP3exY/7T1hGA== + dependencies: + "@babel/runtime" "^7.10.4" + classnames "^2.2.5" + json2mq "^0.2.0" + resize-observer-polyfill "^1.5.1" + throttle-debounce "^5.0.0" + +"@babel/code-frame@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz" + integrity sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg== + dependencies: + "@babel/helper-validator-identifier" "^7.27.1" + js-tokens "^4.0.0" + picocolors "^1.1.1" + +"@babel/compat-data@^7.27.2": + version "7.28.4" + resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz" + integrity sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw== + +"@babel/core@^7.28.4": + version "7.28.4" + resolved "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz" + integrity sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA== + dependencies: + "@babel/code-frame" "^7.27.1" + "@babel/generator" "^7.28.3" + "@babel/helper-compilation-targets" "^7.27.2" + "@babel/helper-module-transforms" "^7.28.3" + "@babel/helpers" "^7.28.4" + "@babel/parser" "^7.28.4" + "@babel/template" "^7.27.2" + "@babel/traverse" "^7.28.4" + "@babel/types" "^7.28.4" + "@jridgewell/remapping" "^2.3.5" + convert-source-map "^2.0.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.3" + semver "^6.3.1" + +"@babel/generator@^7.28.3": + version "7.28.3" + resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz" + integrity sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw== + dependencies: + "@babel/parser" "^7.28.3" + "@babel/types" "^7.28.2" + "@jridgewell/gen-mapping" "^0.3.12" + "@jridgewell/trace-mapping" "^0.3.28" + jsesc "^3.0.2" + +"@babel/helper-compilation-targets@^7.27.2": + version "7.27.2" + resolved "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz" + integrity sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ== + dependencies: + "@babel/compat-data" "^7.27.2" + "@babel/helper-validator-option" "^7.27.1" + browserslist "^4.24.0" + lru-cache "^5.1.1" + semver "^6.3.1" + +"@babel/helper-globals@^7.28.0": + version "7.28.0" + resolved "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz" + integrity sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw== + +"@babel/helper-module-imports@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz" + integrity sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w== + dependencies: + "@babel/traverse" "^7.27.1" + "@babel/types" "^7.27.1" + +"@babel/helper-module-transforms@^7.28.3": + version "7.28.3" + resolved "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz" + integrity sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw== + dependencies: + "@babel/helper-module-imports" "^7.27.1" + "@babel/helper-validator-identifier" "^7.27.1" + "@babel/traverse" "^7.28.3" + +"@babel/helper-plugin-utils@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz" + integrity sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw== + +"@babel/helper-string-parser@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz" + integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA== + +"@babel/helper-validator-identifier@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz" + integrity sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow== + +"@babel/helper-validator-option@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz" + integrity sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg== + +"@babel/helpers@^7.28.4": + version "7.28.4" + resolved "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz" + integrity sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w== + dependencies: + "@babel/template" "^7.27.2" + "@babel/types" "^7.28.4" + +"@babel/parser@^7.1.0", "@babel/parser@^7.20.7", "@babel/parser@^7.27.2", "@babel/parser@^7.28.3", "@babel/parser@^7.28.4": + version "7.28.4" + resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz" + integrity sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg== + dependencies: + "@babel/types" "^7.28.4" + +"@babel/plugin-transform-react-jsx-self@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz" + integrity sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-react-jsx-source@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz" + integrity sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/runtime@^7.10.1", "@babel/runtime@^7.10.4", "@babel/runtime@^7.11.1", "@babel/runtime@^7.11.2", "@babel/runtime@^7.16.3", "@babel/runtime@^7.16.7", "@babel/runtime@^7.18.0", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.0", "@babel/runtime@^7.20.7", "@babel/runtime@^7.21.0", "@babel/runtime@^7.22.5", "@babel/runtime@^7.23.2", "@babel/runtime@^7.23.6", "@babel/runtime@^7.23.9", "@babel/runtime@^7.24.4", "@babel/runtime@^7.24.7", "@babel/runtime@^7.24.8", "@babel/runtime@^7.25.7", "@babel/runtime@^7.26.0": + version "7.28.4" + resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz" + integrity sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ== + +"@babel/template@^7.27.2": + version "7.27.2" + resolved "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz" + integrity sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw== + dependencies: + "@babel/code-frame" "^7.27.1" + "@babel/parser" "^7.27.2" + "@babel/types" "^7.27.1" + +"@babel/traverse@^7.27.1", "@babel/traverse@^7.28.3", "@babel/traverse@^7.28.4": + version "7.28.4" + resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz" + integrity sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ== + dependencies: + "@babel/code-frame" "^7.27.1" + "@babel/generator" "^7.28.3" + "@babel/helper-globals" "^7.28.0" + "@babel/parser" "^7.28.4" + "@babel/template" "^7.27.2" + "@babel/types" "^7.28.4" + debug "^4.3.1" + +"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.27.1", "@babel/types@^7.28.2", "@babel/types@^7.28.4": + version "7.28.4" + resolved "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz" + integrity sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q== + dependencies: + "@babel/helper-string-parser" "^7.27.1" + "@babel/helper-validator-identifier" "^7.27.1" + +"@chenshuai2144/sketch-color@^1.0.7", "@chenshuai2144/sketch-color@^1.0.8": + version "1.0.9" + resolved "https://registry.npmjs.org/@chenshuai2144/sketch-color/-/sketch-color-1.0.9.tgz" + integrity sha512-obzSy26cb7Pm7OprWyVpgMpIlrZpZ0B7vbrU0RMbvRg0YAI890S5Xy02Aj1Nhl4+KTbi1lVYHt6HQP8Hm9s+1w== + dependencies: + reactcss "^1.2.3" + tinycolor2 "^1.4.2" + +"@ctrl/tinycolor@^3.4.0": + version "3.6.1" + resolved "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz" + integrity sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA== + +"@dnd-kit/accessibility@^3.1.1": + version "3.1.1" + resolved "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz" + integrity sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw== + dependencies: + tslib "^2.0.0" + +"@dnd-kit/core@^6.0.8": + version "6.3.1" + resolved "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz" + integrity sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ== + dependencies: + "@dnd-kit/accessibility" "^3.1.1" + "@dnd-kit/utilities" "^3.2.2" + tslib "^2.0.0" + +"@dnd-kit/modifiers@^6.0.1": + version "6.0.1" + resolved "https://registry.npmjs.org/@dnd-kit/modifiers/-/modifiers-6.0.1.tgz" + integrity sha512-rbxcsg3HhzlcMHVHWDuh9LCjpOVAgqbV78wLGI8tziXY3+qcMQ61qVXIvNKQFuhj75dSfD+o+PYZQ/NUk2A23A== + dependencies: + "@dnd-kit/utilities" "^3.2.1" + tslib "^2.0.0" + +"@dnd-kit/sortable@^7.0.2": + version "7.0.2" + resolved "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-7.0.2.tgz" + integrity sha512-wDkBHHf9iCi1veM834Gbk1429bd4lHX4RpAwT0y2cHLf246GAvU2sVw/oxWNpPKQNQRQaeGXhAVgrOl1IT+iyA== + dependencies: + "@dnd-kit/utilities" "^3.2.0" + tslib "^2.0.0" + +"@dnd-kit/utilities@^3.2.0", "@dnd-kit/utilities@^3.2.1", "@dnd-kit/utilities@^3.2.2": + version "3.2.2" + resolved "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz" + integrity sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg== + dependencies: + tslib "^2.0.0" + +"@emnapi/core@^1.5.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.7.0.tgz#135de4e8858763989112281bdf38ca02439db7c3" + integrity sha512-pJdKGq/1iquWYtv1RRSljZklxHCOCAJFJrImO5ZLKPJVJlVUcs8yFwNQlqS0Lo8xT1VAXXTCZocF9n26FWEKsw== + dependencies: + "@emnapi/wasi-threads" "1.1.0" + tslib "^2.4.0" + +"@emnapi/runtime@^1.5.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.7.0.tgz#d7ef3832df8564fe5903bf0567aedbd19538ecbe" + integrity sha512-oAYoQnCYaQZKVS53Fq23ceWMRxq5EhQsE0x0RdQ55jT7wagMu5k+fS39v1fiSLrtrLQlXwVINenqhLMtTrV/1Q== + dependencies: + tslib "^2.4.0" + +"@emnapi/wasi-threads@1.1.0", "@emnapi/wasi-threads@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz#60b2102fddc9ccb78607e4a3cf8403ea69be41bf" + integrity sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ== + dependencies: + tslib "^2.4.0" + +"@emotion/hash@^0.8.0": + version "0.8.0" + resolved "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz" + integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow== + +"@emotion/unitless@^0.7.5": + version "0.7.5" + resolved "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz" + integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg== + +"@esbuild/aix-ppc64@0.25.10": + version "0.25.10" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz#ee6b7163a13528e099ecf562b972f2bcebe0aa97" + integrity sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw== + +"@esbuild/android-arm64@0.25.10": + version "0.25.10" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.10.tgz#115fc76631e82dd06811bfaf2db0d4979c16e2cb" + integrity sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg== + +"@esbuild/android-arm@0.25.10": + version "0.25.10" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.10.tgz#8d5811912da77f615398611e5bbc1333fe321aa9" + integrity sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w== + +"@esbuild/android-x64@0.25.10": + version "0.25.10" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.10.tgz#e3e96516b2d50d74105bb92594c473e30ddc16b1" + integrity sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg== + +"@esbuild/darwin-arm64@0.25.10": + version "0.25.10" + resolved "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.10.tgz" + integrity sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA== + +"@esbuild/darwin-x64@0.25.10": + version "0.25.10" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.10.tgz#99ae82347fbd336fc2d28ffd4f05694e6e5b723d" + integrity sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg== + +"@esbuild/freebsd-arm64@0.25.10": + version "0.25.10" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.10.tgz#0c6d5558a6322b0bdb17f7025c19bd7d2359437d" + integrity sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg== + +"@esbuild/freebsd-x64@0.25.10": + version "0.25.10" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.10.tgz#8c35873fab8c0857a75300a3dcce4324ca0b9844" + integrity sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA== + +"@esbuild/linux-arm64@0.25.10": + version "0.25.10" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.10.tgz#3edc2f87b889a15b4cedaf65f498c2bed7b16b90" + integrity sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ== + +"@esbuild/linux-arm@0.25.10": + version "0.25.10" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.10.tgz#86501cfdfb3d110176d80c41b27ed4611471cde7" + integrity sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg== + +"@esbuild/linux-ia32@0.25.10": + version "0.25.10" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.10.tgz#e6589877876142537c6864680cd5d26a622b9d97" + integrity sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ== + +"@esbuild/linux-loong64@0.25.10": + version "0.25.10" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.10.tgz#11119e18781f136d8083ea10eb6be73db7532de8" + integrity sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg== + +"@esbuild/linux-mips64el@0.25.10": + version "0.25.10" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.10.tgz#3052f5436b0c0c67a25658d5fc87f045e7def9e6" + integrity sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA== + +"@esbuild/linux-ppc64@0.25.10": + version "0.25.10" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.10.tgz#2f098920ee5be2ce799f35e367b28709925a8744" + integrity sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA== + +"@esbuild/linux-riscv64@0.25.10": + version "0.25.10" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.10.tgz#fa51d7fd0a22a62b51b4b94b405a3198cf7405dd" + integrity sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA== + +"@esbuild/linux-s390x@0.25.10": + version "0.25.10" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.10.tgz#a27642e36fc282748fdb38954bd3ef4f85791e8a" + integrity sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew== + +"@esbuild/linux-x64@0.25.10": + version "0.25.10" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.10.tgz#9d9b09c0033d17529570ced6d813f98315dfe4e9" + integrity sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA== + +"@esbuild/netbsd-arm64@0.25.10": + version "0.25.10" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.10.tgz#25c09a659c97e8af19e3f2afd1c9190435802151" + integrity sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A== + +"@esbuild/netbsd-x64@0.25.10": + version "0.25.10" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.10.tgz#7fa5f6ffc19be3a0f6f5fd32c90df3dc2506937a" + integrity sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig== + +"@esbuild/openbsd-arm64@0.25.10": + version "0.25.10" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.10.tgz#8faa6aa1afca0c6d024398321d6cb1c18e72a1c3" + integrity sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw== + +"@esbuild/openbsd-x64@0.25.10": + version "0.25.10" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.10.tgz#a42979b016f29559a8453d32440d3c8cd420af5e" + integrity sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw== + +"@esbuild/openharmony-arm64@0.25.10": + version "0.25.10" + resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.10.tgz#fd87bfeadd7eeb3aa384bbba907459ffa3197cb1" + integrity sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag== + +"@esbuild/sunos-x64@0.25.10": + version "0.25.10" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.10.tgz#3a18f590e36cb78ae7397976b760b2b8c74407f4" + integrity sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ== + +"@esbuild/win32-arm64@0.25.10": + version "0.25.10" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.10.tgz#e71741a251e3fd971408827a529d2325551f530c" + integrity sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw== + +"@esbuild/win32-ia32@0.25.10": + version "0.25.10" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.10.tgz#c6f010b5d3b943d8901a0c87ea55f93b8b54bf94" + integrity sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw== + +"@esbuild/win32-x64@0.25.10": + version "0.25.10" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.10.tgz#e4b3e255a1b4aea84f6e1d2ae0b73f826c3785bd" + integrity sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw== + +"@eslint-community/eslint-utils@^4.7.0", "@eslint-community/eslint-utils@^4.8.0": + version "4.9.0" + resolved "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz" + integrity sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g== + dependencies: + eslint-visitor-keys "^3.4.3" + +"@eslint-community/regexpp@^4.10.0", "@eslint-community/regexpp@^4.12.1": + version "4.12.1" + resolved "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz" + integrity sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ== + +"@eslint/config-array@^0.21.0": + version "0.21.0" + resolved "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz" + integrity sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ== + dependencies: + "@eslint/object-schema" "^2.1.6" + debug "^4.3.1" + minimatch "^3.1.2" + +"@eslint/config-helpers@^0.4.0": + version "0.4.0" + resolved "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.0.tgz" + integrity sha512-WUFvV4WoIwW8Bv0KeKCIIEgdSiFOsulyN0xrMu+7z43q/hkOLXjvb5u7UC9jDxvRzcrbEmuZBX5yJZz1741jog== + dependencies: + "@eslint/core" "^0.16.0" + +"@eslint/core@^0.16.0": + version "0.16.0" + resolved "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz" + integrity sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q== + dependencies: + "@types/json-schema" "^7.0.15" + +"@eslint/eslintrc@^3.3.1": + version "3.3.1" + resolved "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz" + integrity sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ== + dependencies: + ajv "^6.12.4" + debug "^4.3.2" + espree "^10.0.1" + globals "^14.0.0" + ignore "^5.2.0" + import-fresh "^3.2.1" + js-yaml "^4.1.0" + minimatch "^3.1.2" + strip-json-comments "^3.1.1" + +"@eslint/js@9.37.0", "@eslint/js@^9.36.0": + version "9.37.0" + resolved "https://registry.npmjs.org/@eslint/js/-/js-9.37.0.tgz" + integrity sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg== + +"@eslint/object-schema@^2.1.6": + version "2.1.6" + resolved "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz" + integrity sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA== + +"@eslint/plugin-kit@^0.4.0": + version "0.4.0" + resolved "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz" + integrity sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A== + dependencies: + "@eslint/core" "^0.16.0" + levn "^0.4.1" + +"@humanfs/core@^0.19.1": + version "0.19.1" + resolved "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz" + integrity sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA== + +"@humanfs/node@^0.16.6": + version "0.16.7" + resolved "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz" + integrity sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ== + dependencies: + "@humanfs/core" "^0.19.1" + "@humanwhocodes/retry" "^0.4.0" + +"@humanwhocodes/module-importer@^1.0.1": + version "1.0.1" + resolved "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz" + integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== + +"@humanwhocodes/retry@^0.4.0", "@humanwhocodes/retry@^0.4.2": + version "0.4.3" + resolved "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz" + integrity sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ== + +"@isaacs/fs-minipass@^4.0.0": + version "4.0.1" + resolved "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz" + integrity sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w== + dependencies: + minipass "^7.0.4" + +"@jridgewell/gen-mapping@^0.3.12", "@jridgewell/gen-mapping@^0.3.5": + version "0.3.13" + resolved "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz" + integrity sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA== + dependencies: + "@jridgewell/sourcemap-codec" "^1.5.0" + "@jridgewell/trace-mapping" "^0.3.24" + +"@jridgewell/remapping@^2.3.4", "@jridgewell/remapping@^2.3.5": + version "2.3.5" + resolved "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz" + integrity sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.24" + +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.2" + resolved "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + +"@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0", "@jridgewell/sourcemap-codec@^1.5.5": + version "1.5.5" + resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz" + integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og== + +"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.28": + version "0.3.31" + resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz" + integrity sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + +"@napi-rs/wasm-runtime@^1.0.5": + version "1.0.7" + resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.7.tgz#dcfea99a75f06209a235f3d941e3460a51e9b14c" + integrity sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw== + dependencies: + "@emnapi/core" "^1.5.0" + "@emnapi/runtime" "^1.5.0" + "@tybys/wasm-util" "^0.10.1" + +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": + version "2.0.5" + resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.3": + version "1.2.8" + resolved "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + +"@rc-component/async-validator@^5.0.3": + version "5.0.4" + resolved "https://registry.npmjs.org/@rc-component/async-validator/-/async-validator-5.0.4.tgz" + integrity sha512-qgGdcVIF604M9EqjNF0hbUTz42bz/RDtxWdWuU5EQe3hi7M8ob54B6B35rOsvX5eSvIHIzT9iH1R3n+hk3CGfg== + dependencies: + "@babel/runtime" "^7.24.4" + +"@rc-component/color-picker@~2.0.1": + version "2.0.1" + resolved "https://registry.npmjs.org/@rc-component/color-picker/-/color-picker-2.0.1.tgz" + integrity sha512-WcZYwAThV/b2GISQ8F+7650r5ZZJ043E57aVBFkQ+kSY4C6wdofXgB0hBx+GPGpIU0Z81eETNoDUJMr7oy/P8Q== + dependencies: + "@ant-design/fast-color" "^2.0.6" + "@babel/runtime" "^7.23.6" + classnames "^2.2.6" + rc-util "^5.38.1" + +"@rc-component/context@^1.4.0": + version "1.4.0" + resolved "https://registry.npmjs.org/@rc-component/context/-/context-1.4.0.tgz" + integrity sha512-kFcNxg9oLRMoL3qki0OMxK+7g5mypjgaaJp/pkOis/6rVxma9nJBF/8kCIuTYHUQNr0ii7MxqE33wirPZLJQ2w== + dependencies: + "@babel/runtime" "^7.10.1" + rc-util "^5.27.0" + +"@rc-component/mini-decimal@^1.0.1": + version "1.1.0" + resolved "https://registry.npmjs.org/@rc-component/mini-decimal/-/mini-decimal-1.1.0.tgz" + integrity sha512-jS4E7T9Li2GuYwI6PyiVXmxTiM6b07rlD9Ge8uGZSCz3WlzcG5ZK7g5bbuKNeZ9pgUuPK/5guV781ujdVpm4HQ== + dependencies: + "@babel/runtime" "^7.18.0" + +"@rc-component/mutate-observer@^1.1.0": + version "1.1.0" + resolved "https://registry.npmjs.org/@rc-component/mutate-observer/-/mutate-observer-1.1.0.tgz" + integrity sha512-QjrOsDXQusNwGZPf4/qRQasg7UFEj06XiCJ8iuiq/Io7CrHrgVi6Uuetw60WAMG1799v+aM8kyc+1L/GBbHSlw== + dependencies: + "@babel/runtime" "^7.18.0" + classnames "^2.3.2" + rc-util "^5.24.4" + +"@rc-component/portal@^1.0.0-8", "@rc-component/portal@^1.0.0-9", "@rc-component/portal@^1.0.2", "@rc-component/portal@^1.1.0", "@rc-component/portal@^1.1.1": + version "1.1.2" + resolved "https://registry.npmjs.org/@rc-component/portal/-/portal-1.1.2.tgz" + integrity sha512-6f813C0IsasTZms08kfA8kPAGxbbkYToa8ALaiDIGGECU4i9hj8Plgbx0sNJDrey3EtHO30hmdaxtT0138xZcg== + dependencies: + "@babel/runtime" "^7.18.0" + classnames "^2.3.2" + rc-util "^5.24.4" + +"@rc-component/qrcode@~1.0.0": + version "1.0.1" + resolved "https://registry.npmjs.org/@rc-component/qrcode/-/qrcode-1.0.1.tgz" + integrity sha512-g8eeeaMyFXVlq8cZUeaxCDhfIYjpao0l9cvm5gFwKXy/Vm1yDWV7h2sjH5jHYzdFedlVKBpATFB1VKMrHzwaWQ== + dependencies: + "@babel/runtime" "^7.24.7" + classnames "^2.3.2" + +"@rc-component/tour@~1.15.1": + version "1.15.1" + resolved "https://registry.npmjs.org/@rc-component/tour/-/tour-1.15.1.tgz" + integrity sha512-Tr2t7J1DKZUpfJuDZWHxyxWpfmj8EZrqSgyMZ+BCdvKZ6r1UDsfU46M/iWAAFBy961Ssfom2kv5f3UcjIL2CmQ== + dependencies: + "@babel/runtime" "^7.18.0" + "@rc-component/portal" "^1.0.0-9" + "@rc-component/trigger" "^2.0.0" + classnames "^2.3.2" + rc-util "^5.24.4" + +"@rc-component/trigger@^2.0.0", "@rc-component/trigger@^2.1.1", "@rc-component/trigger@^2.3.0": + version "2.3.0" + resolved "https://registry.npmjs.org/@rc-component/trigger/-/trigger-2.3.0.tgz" + integrity sha512-iwaxZyzOuK0D7lS+0AQEtW52zUWxoGqTGkke3dRyb8pYiShmRpCjB/8TzPI4R6YySCH7Vm9BZj/31VPiiQTLBg== + dependencies: + "@babel/runtime" "^7.23.2" + "@rc-component/portal" "^1.1.0" + classnames "^2.3.2" + rc-motion "^2.0.0" + rc-resize-observer "^1.3.1" + rc-util "^5.44.0" + +"@reduxjs/toolkit@1.x.x || 2.x.x": + version "2.10.0" + resolved "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.10.0.tgz" + integrity sha512-DCDKKB+DDy00kULFGoj5jtxRXksCGtEKEPbeSqRi20/vUrwrBD+zyJx+jQ7xkIU1pUHQ6OIYHyc9NiVSEjAlNA== + dependencies: + "@standard-schema/spec" "^1.0.0" + "@standard-schema/utils" "^0.3.0" + immer "^10.2.0" + redux "^5.0.1" + redux-thunk "^3.1.0" + reselect "^5.1.0" + +"@rolldown/pluginutils@1.0.0-beta.38": + version "1.0.0-beta.38" + resolved "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.38.tgz" + integrity sha512-N/ICGKleNhA5nc9XXQG/kkKHJ7S55u0x0XUJbbkmdCnFuoRkM1Il12q9q0eX19+M7KKUEPw/daUPIRnxhcxAIw== + +"@rollup/rollup-android-arm-eabi@4.52.4": + version "4.52.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.4.tgz#59e7478d310f7e6a7c72453978f562483828112f" + integrity sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA== + +"@rollup/rollup-android-arm64@4.52.4": + version "4.52.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.4.tgz#a825192a0b1b2f27a5c950c439e7e37a33c5d056" + integrity sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w== + +"@rollup/rollup-darwin-arm64@4.52.4": + version "4.52.4" + resolved "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.4.tgz" + integrity sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg== + +"@rollup/rollup-darwin-x64@4.52.4": + version "4.52.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.4.tgz#43cc08bd05bf9f388f125e7210a544e62d368d90" + integrity sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw== + +"@rollup/rollup-freebsd-arm64@4.52.4": + version "4.52.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.4.tgz#bc8e640e28abe52450baf3fc80d9b26d9bb6587d" + integrity sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ== + +"@rollup/rollup-freebsd-x64@4.52.4": + version "4.52.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.4.tgz#e981a22e057cc8c65bb523019d344d3a66b15bbc" + integrity sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw== + +"@rollup/rollup-linux-arm-gnueabihf@4.52.4": + version "4.52.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.4.tgz#4036b68904f392a20f3499d63b33e055b67eb274" + integrity sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ== + +"@rollup/rollup-linux-arm-musleabihf@4.52.4": + version "4.52.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.4.tgz#d3b1b9589606e0ff916801c855b1ace9e733427a" + integrity sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q== + +"@rollup/rollup-linux-arm64-gnu@4.52.4": + version "4.52.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.4.tgz#cbf0943c477e3b96340136dd3448eaf144378cf2" + integrity sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg== + +"@rollup/rollup-linux-arm64-musl@4.52.4": + version "4.52.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.4.tgz#837f5a428020d5dce1c3b4cc049876075402cf78" + integrity sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g== + +"@rollup/rollup-linux-loong64-gnu@4.52.4": + version "4.52.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.4.tgz#532c214ababb32ab4bc21b4054278b9a8979e516" + integrity sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ== + +"@rollup/rollup-linux-ppc64-gnu@4.52.4": + version "4.52.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.4.tgz#93900163b61b49cee666d10ee38257a8b1dd161a" + integrity sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g== + +"@rollup/rollup-linux-riscv64-gnu@4.52.4": + version "4.52.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.4.tgz#f0ffdcc7066ca04bc972370c74289f35c7a7dc42" + integrity sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg== + +"@rollup/rollup-linux-riscv64-musl@4.52.4": + version "4.52.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.4.tgz#361695c39dbe96773509745d77a870a32a9f8e48" + integrity sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA== + +"@rollup/rollup-linux-s390x-gnu@4.52.4": + version "4.52.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.4.tgz#09fc6cc2e266a2324e366486ae5d1bca48c43a6a" + integrity sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA== + +"@rollup/rollup-linux-x64-gnu@4.52.4": + version "4.52.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.4.tgz#aa9d5b307c08f05d3454225bb0a2b4cc87eeb2e1" + integrity sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg== + +"@rollup/rollup-linux-x64-musl@4.52.4": + version "4.52.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.4.tgz#26949e5b4645502a61daba2f7a8416bd17cb5382" + integrity sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw== + +"@rollup/rollup-openharmony-arm64@4.52.4": + version "4.52.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.4.tgz#ef493c072f9dac7e0edb6c72d63366846b6ffcd9" + integrity sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA== + +"@rollup/rollup-win32-arm64-msvc@4.52.4": + version "4.52.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.4.tgz#56e1aaa6a630d2202ee7ec0adddd05cf384ffd44" + integrity sha512-8GKr640PdFNXwzIE0IrkMWUNUomILLkfeHjXBi/nUvFlpZP+FA8BKGKpacjW6OUUHaNI6sUURxR2U2g78FOHWQ== + +"@rollup/rollup-win32-ia32-msvc@4.52.4": + version "4.52.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.4.tgz#0a44bbf933a9651c7da2b8569fa448dec0de7480" + integrity sha512-AIy/jdJ7WtJ/F6EcfOb2GjR9UweO0n43jNObQMb6oGxkYTfLcnN7vYYpG+CN3lLxrQkzWnMOoNSHTW54pgbVxw== + +"@rollup/rollup-win32-x64-gnu@4.52.4": + version "4.52.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.4.tgz#730e12f0b60b234a7c02d5d3179ca3ec7972033d" + integrity sha512-UF9KfsH9yEam0UjTwAgdK0anlQ7c8/pWPU2yVjyWcF1I1thABt6WXE47cI71pGiZ8wGvxohBoLnxM04L/wj8mQ== + +"@rollup/rollup-win32-x64-msvc@4.52.4": + version "4.52.4" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.4.tgz#5b2dd648a960b8fa00d76f2cc4eea2f03daa80f4" + integrity sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w== + +"@standard-schema/spec@^1.0.0": + version "1.0.0" + resolved "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz" + integrity sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA== + +"@standard-schema/utils@^0.3.0": + version "0.3.0" + resolved "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz" + integrity sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g== + +"@tailwindcss/node@4.1.14": + version "4.1.14" + resolved "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.14.tgz" + integrity sha512-hpz+8vFk3Ic2xssIA3e01R6jkmsAhvkQdXlEbRTk6S10xDAtiQiM3FyvZVGsucefq764euO/b8WUW9ysLdThHw== + dependencies: + "@jridgewell/remapping" "^2.3.4" + enhanced-resolve "^5.18.3" + jiti "^2.6.0" + lightningcss "1.30.1" + magic-string "^0.30.19" + source-map-js "^1.2.1" + tailwindcss "4.1.14" + +"@tailwindcss/oxide-android-arm64@4.1.14": + version "4.1.14" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.14.tgz#8903678d75715d913b8f7c5f6fa0517af83b5111" + integrity sha512-a94ifZrGwMvbdeAxWoSuGcIl6/DOP5cdxagid7xJv6bwFp3oebp7y2ImYsnZBMTwjn5Ev5xESvS3FFYUGgPODQ== + +"@tailwindcss/oxide-darwin-arm64@4.1.14": + version "4.1.14" + resolved "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.14.tgz" + integrity sha512-HkFP/CqfSh09xCnrPJA7jud7hij5ahKyWomrC3oiO2U9i0UjP17o9pJbxUN0IJ471GTQQmzwhp0DEcpbp4MZTA== + +"@tailwindcss/oxide-darwin-x64@4.1.14": + version "4.1.14" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.14.tgz#ac1af82da01299143129fdf615f6fcc046b4094e" + integrity sha512-eVNaWmCgdLf5iv6Qd3s7JI5SEFBFRtfm6W0mphJYXgvnDEAZ5sZzqmI06bK6xo0IErDHdTA5/t7d4eTfWbWOFw== + +"@tailwindcss/oxide-freebsd-x64@4.1.14": + version "4.1.14" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.14.tgz#a955cedf9b020147d222f92490e9d331db9b5c36" + integrity sha512-QWLoRXNikEuqtNb0dhQN6wsSVVjX6dmUFzuuiL09ZeXju25dsei2uIPl71y2Ic6QbNBsB4scwBoFnlBfabHkEw== + +"@tailwindcss/oxide-linux-arm-gnueabihf@4.1.14": + version "4.1.14" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.14.tgz#5474bee4d377144107f3f0198a3c0225a46c02e6" + integrity sha512-VB4gjQni9+F0VCASU+L8zSIyjrLLsy03sjcR3bM0V2g4SNamo0FakZFKyUQ96ZVwGK4CaJsc9zd/obQy74o0Fw== + +"@tailwindcss/oxide-linux-arm64-gnu@4.1.14": + version "4.1.14" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.14.tgz#b06ca140083b353735414e32f7a8786f55ce2dd6" + integrity sha512-qaEy0dIZ6d9vyLnmeg24yzA8XuEAD9WjpM5nIM1sUgQ/Zv7cVkharPDQcmm/t/TvXoKo/0knI3me3AGfdx6w1w== + +"@tailwindcss/oxide-linux-arm64-musl@4.1.14": + version "4.1.14" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.14.tgz#85f4cabea2a07609274d1f747bd098c5da2a7cd2" + integrity sha512-ISZjT44s59O8xKsPEIesiIydMG/sCXoMBCqsphDm/WcbnuWLxxb+GcvSIIA5NjUw6F8Tex7s5/LM2yDy8RqYBQ== + +"@tailwindcss/oxide-linux-x64-gnu@4.1.14": + version "4.1.14" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.14.tgz#0d7fbf91763a2f6886044a050298489107d120bd" + integrity sha512-02c6JhLPJj10L2caH4U0zF8Hji4dOeahmuMl23stk0MU1wfd1OraE7rOloidSF8W5JTHkFdVo/O7uRUJJnUAJg== + +"@tailwindcss/oxide-linux-x64-musl@4.1.14": + version "4.1.14" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.14.tgz#93578713064ba4c16df517df01b3c546ecc9878d" + integrity sha512-TNGeLiN1XS66kQhxHG/7wMeQDOoL0S33x9BgmydbrWAb9Qw0KYdd8o1ifx4HOGDWhVmJ+Ul+JQ7lyknQFilO3Q== + +"@tailwindcss/oxide-wasm32-wasi@4.1.14": + version "4.1.14" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.14.tgz#9e55999129a952a3dcc2196cc9cc55248cc1b1fe" + integrity sha512-uZYAsaW/jS/IYkd6EWPJKW/NlPNSkWkBlaeVBi/WsFQNP05/bzkebUL8FH1pdsqx4f2fH/bWFcUABOM9nfiJkQ== + dependencies: + "@emnapi/core" "^1.5.0" + "@emnapi/runtime" "^1.5.0" + "@emnapi/wasi-threads" "^1.1.0" + "@napi-rs/wasm-runtime" "^1.0.5" + "@tybys/wasm-util" "^0.10.1" + tslib "^2.4.0" + +"@tailwindcss/oxide-win32-arm64-msvc@4.1.14": + version "4.1.14" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.14.tgz#097c00bfc60cd84943a9cb5e853b25fa25525c77" + integrity sha512-Az0RnnkcvRqsuoLH2Z4n3JfAef0wElgzHD5Aky/e+0tBUxUhIeIqFBTMNQvmMRSP15fWwmvjBxZ3Q8RhsDnxAA== + +"@tailwindcss/oxide-win32-x64-msvc@4.1.14": + version "4.1.14" + resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.14.tgz#eaa49fa930ce16b23478d3b58c079a40ac0b6622" + integrity sha512-ttblVGHgf68kEE4om1n/n44I0yGPkCPbLsqzjvybhpwa6mKKtgFfAzy6btc3HRmuW7nHe0OOrSeNP9sQmmH9XA== + +"@tailwindcss/oxide@4.1.14": + version "4.1.14" + resolved "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.14.tgz" + integrity sha512-23yx+VUbBwCg2x5XWdB8+1lkPajzLmALEfMb51zZUBYaYVPDQvBSD/WYDqiVyBIo2BZFa3yw1Rpy3G2Jp+K0dw== + dependencies: + detect-libc "^2.0.4" + tar "^7.5.1" + optionalDependencies: + "@tailwindcss/oxide-android-arm64" "4.1.14" + "@tailwindcss/oxide-darwin-arm64" "4.1.14" + "@tailwindcss/oxide-darwin-x64" "4.1.14" + "@tailwindcss/oxide-freebsd-x64" "4.1.14" + "@tailwindcss/oxide-linux-arm-gnueabihf" "4.1.14" + "@tailwindcss/oxide-linux-arm64-gnu" "4.1.14" + "@tailwindcss/oxide-linux-arm64-musl" "4.1.14" + "@tailwindcss/oxide-linux-x64-gnu" "4.1.14" + "@tailwindcss/oxide-linux-x64-musl" "4.1.14" + "@tailwindcss/oxide-wasm32-wasi" "4.1.14" + "@tailwindcss/oxide-win32-arm64-msvc" "4.1.14" + "@tailwindcss/oxide-win32-x64-msvc" "4.1.14" + +"@tailwindcss/vite@^4.1.14": + version "4.1.14" + resolved "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.14.tgz" + integrity sha512-BoFUoU0XqgCUS1UXWhmDJroKKhNXeDzD7/XwabjkDIAbMnc4ULn5e2FuEuBbhZ6ENZoSYzKlzvZ44Yr6EUDUSA== + dependencies: + "@tailwindcss/node" "4.1.14" + "@tailwindcss/oxide" "4.1.14" + tailwindcss "4.1.14" + +"@tanstack/query-core@5.90.6": + version "5.90.6" + resolved "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.6.tgz" + integrity sha512-AnZSLF26R8uX+tqb/ivdrwbVdGemdEDm1Q19qM6pry6eOZ6bEYiY7mWhzXT1YDIPTNEVcZ5kYP9nWjoxDLiIVw== + +"@tanstack/react-query@^5.90.6": + version "5.90.6" + resolved "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.6.tgz" + integrity sha512-gB1sljYjcobZKxjPbKSa31FUTyr+ROaBdoH+wSSs9Dk+yDCmMs+TkTV3PybRRVLC7ax7q0erJ9LvRWnMktnRAw== + dependencies: + "@tanstack/query-core" "5.90.6" + +"@tybys/wasm-util@^0.10.1": + version "0.10.1" + resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.10.1.tgz#ecddd3205cf1e2d5274649ff0eedd2991ed7f414" + integrity sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg== + dependencies: + tslib "^2.4.0" + +"@types/babel__core@^7.20.5": + version "7.20.5" + resolved "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz" + integrity sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA== + dependencies: + "@babel/parser" "^7.20.7" + "@babel/types" "^7.20.7" + "@types/babel__generator" "*" + "@types/babel__template" "*" + "@types/babel__traverse" "*" + +"@types/babel__generator@*": + version "7.27.0" + resolved "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz" + integrity sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg== + dependencies: + "@babel/types" "^7.0.0" + +"@types/babel__template@*": + version "7.4.4" + resolved "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz" + integrity sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + +"@types/babel__traverse@*": + version "7.28.0" + resolved "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz" + integrity sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q== + dependencies: + "@babel/types" "^7.28.2" + +"@types/d3-array@^3.0.3": + version "3.2.2" + resolved "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz" + integrity sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw== + +"@types/d3-color@*": + version "3.1.3" + resolved "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz" + integrity sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A== + +"@types/d3-ease@^3.0.0": + version "3.0.2" + resolved "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz" + integrity sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA== + +"@types/d3-interpolate@^3.0.1": + version "3.0.4" + resolved "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz" + integrity sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA== + dependencies: + "@types/d3-color" "*" + +"@types/d3-path@*": + version "3.1.1" + resolved "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz" + integrity sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg== + +"@types/d3-scale@^4.0.2": + version "4.0.9" + resolved "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz" + integrity sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw== + dependencies: + "@types/d3-time" "*" + +"@types/d3-shape@^3.1.0": + version "3.1.7" + resolved "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz" + integrity sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg== + dependencies: + "@types/d3-path" "*" + +"@types/d3-time@*", "@types/d3-time@^3.0.0": + version "3.0.4" + resolved "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz" + integrity sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g== + +"@types/d3-timer@^3.0.0": + version "3.0.2" + resolved "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz" + integrity sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw== + +"@types/estree@1.0.8", "@types/estree@^1.0.6": + version "1.0.8" + resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz" + integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w== + +"@types/json-schema@^7.0.15": + version "7.0.15" + resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + +"@types/node@^24.6.0": + version "24.7.1" + resolved "https://registry.npmjs.org/@types/node/-/node-24.7.1.tgz" + integrity sha512-CmyhGZanP88uuC5GpWU9q+fI61j2SkhO3UGMUdfYRE6Bcy0ccyzn1Rqj9YAB/ZY4kOXmNf0ocah5GtphmLMP6Q== + dependencies: + undici-types "~7.14.0" + +"@types/react-dom@^19.1.9": + version "19.2.1" + resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.1.tgz" + integrity sha512-/EEvYBdT3BflCWvTMO7YkYBHVE9Ci6XdqZciZANQgKpaiDRGOLIlRo91jbTNRQjgPFWVaRxcYc0luVNFitz57A== + +"@types/react@^19.1.16": + version "19.2.2" + resolved "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz" + integrity sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA== + dependencies: + csstype "^3.0.2" + +"@types/use-sync-external-store@^0.0.6": + version "0.0.6" + resolved "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz" + integrity sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg== + +"@types/validator@^13.15.3": + version "13.15.3" + resolved "https://registry.npmjs.org/@types/validator/-/validator-13.15.3.tgz" + integrity sha512-7bcUmDyS6PN3EuD9SlGGOxM77F8WLVsrwkxyWxKnxzmXoequ6c7741QBrANq6htVRGOITJ7z72mTP6Z4XyuG+Q== + +"@typescript-eslint/eslint-plugin@8.46.0": + version "8.46.0" + resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.0.tgz" + integrity sha512-hA8gxBq4ukonVXPy0OKhiaUh/68D0E88GSmtC1iAEnGaieuDi38LhS7jdCHRLi6ErJBNDGCzvh5EnzdPwUc0DA== + dependencies: + "@eslint-community/regexpp" "^4.10.0" + "@typescript-eslint/scope-manager" "8.46.0" + "@typescript-eslint/type-utils" "8.46.0" + "@typescript-eslint/utils" "8.46.0" + "@typescript-eslint/visitor-keys" "8.46.0" + graphemer "^1.4.0" + ignore "^7.0.0" + natural-compare "^1.4.0" + ts-api-utils "^2.1.0" + +"@typescript-eslint/parser@8.46.0": + version "8.46.0" + resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.0.tgz" + integrity sha512-n1H6IcDhmmUEG7TNVSspGmiHHutt7iVKtZwRppD7e04wha5MrkV1h3pti9xQLcCMt6YWsncpoT0HMjkH1FNwWQ== + dependencies: + "@typescript-eslint/scope-manager" "8.46.0" + "@typescript-eslint/types" "8.46.0" + "@typescript-eslint/typescript-estree" "8.46.0" + "@typescript-eslint/visitor-keys" "8.46.0" + debug "^4.3.4" + +"@typescript-eslint/project-service@8.46.0": + version "8.46.0" + resolved "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.0.tgz" + integrity sha512-OEhec0mH+U5Je2NZOeK1AbVCdm0ChyapAyTeXVIYTPXDJ3F07+cu87PPXcGoYqZ7M9YJVvFnfpGg1UmCIqM+QQ== + dependencies: + "@typescript-eslint/tsconfig-utils" "^8.46.0" + "@typescript-eslint/types" "^8.46.0" + debug "^4.3.4" + +"@typescript-eslint/scope-manager@8.46.0": + version "8.46.0" + resolved "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.0.tgz" + integrity sha512-lWETPa9XGcBes4jqAMYD9fW0j4n6hrPtTJwWDmtqgFO/4HF4jmdH/Q6wggTw5qIT5TXjKzbt7GsZUBnWoO3dqw== + dependencies: + "@typescript-eslint/types" "8.46.0" + "@typescript-eslint/visitor-keys" "8.46.0" + +"@typescript-eslint/tsconfig-utils@8.46.0", "@typescript-eslint/tsconfig-utils@^8.46.0": + version "8.46.0" + resolved "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.0.tgz" + integrity sha512-WrYXKGAHY836/N7zoK/kzi6p8tXFhasHh8ocFL9VZSAkvH956gfeRfcnhs3xzRy8qQ/dq3q44v1jvQieMFg2cw== + +"@typescript-eslint/type-utils@8.46.0": + version "8.46.0" + resolved "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.0.tgz" + integrity sha512-hy+lvYV1lZpVs2jRaEYvgCblZxUoJiPyCemwbQZ+NGulWkQRy0HRPYAoef/CNSzaLt+MLvMptZsHXHlkEilaeg== + dependencies: + "@typescript-eslint/types" "8.46.0" + "@typescript-eslint/typescript-estree" "8.46.0" + "@typescript-eslint/utils" "8.46.0" + debug "^4.3.4" + ts-api-utils "^2.1.0" + +"@typescript-eslint/types@8.46.0", "@typescript-eslint/types@^8.46.0": + version "8.46.0" + resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.0.tgz" + integrity sha512-bHGGJyVjSE4dJJIO5yyEWt/cHyNwga/zXGJbJJ8TiO01aVREK6gCTu3L+5wrkb1FbDkQ+TKjMNe9R/QQQP9+rA== + +"@typescript-eslint/typescript-estree@8.46.0": + version "8.46.0" + resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.0.tgz" + integrity sha512-ekDCUfVpAKWJbRfm8T1YRrCot1KFxZn21oV76v5Fj4tr7ELyk84OS+ouvYdcDAwZL89WpEkEj2DKQ+qg//+ucg== + dependencies: + "@typescript-eslint/project-service" "8.46.0" + "@typescript-eslint/tsconfig-utils" "8.46.0" + "@typescript-eslint/types" "8.46.0" + "@typescript-eslint/visitor-keys" "8.46.0" + debug "^4.3.4" + fast-glob "^3.3.2" + is-glob "^4.0.3" + minimatch "^9.0.4" + semver "^7.6.0" + ts-api-utils "^2.1.0" + +"@typescript-eslint/utils@8.46.0": + version "8.46.0" + resolved "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.0.tgz" + integrity sha512-nD6yGWPj1xiOm4Gk0k6hLSZz2XkNXhuYmyIrOWcHoPuAhjT9i5bAG+xbWPgFeNR8HPHHtpNKdYUXJl/D3x7f5g== + dependencies: + "@eslint-community/eslint-utils" "^4.7.0" + "@typescript-eslint/scope-manager" "8.46.0" + "@typescript-eslint/types" "8.46.0" + "@typescript-eslint/typescript-estree" "8.46.0" + +"@typescript-eslint/visitor-keys@8.46.0": + version "8.46.0" + resolved "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.0.tgz" + integrity sha512-FrvMpAK+hTbFy7vH5j1+tMYHMSKLE6RzluFJlkFNKD0p9YsUT75JlBSmr5so3QRzvMwU5/bIEdeNrxm8du8l3Q== + dependencies: + "@typescript-eslint/types" "8.46.0" + eslint-visitor-keys "^4.2.1" + +"@umijs/route-utils@^4.0.0": + version "4.0.3" + resolved "https://registry.npmjs.org/@umijs/route-utils/-/route-utils-4.0.3.tgz" + integrity sha512-zPEcYhl1cSfkSRDzzGgoD1mDvGjxoOTJFvkn55srfgdQ3NZe2ZMCScCU6DEnOxuKP1XDVf8pqyqCDVd2+RCQIw== + +"@umijs/use-params@^1.0.9": + version "1.0.9" + resolved "https://registry.npmjs.org/@umijs/use-params/-/use-params-1.0.9.tgz" + integrity sha512-QlN0RJSBVQBwLRNxbxjQ5qzqYIGn+K7USppMoIOVlf7fxXHsnQZ2bEsa6Pm74bt6DVQxpUE8HqvdStn6Y9FV1w== + +"@vitejs/plugin-react@^5.0.4": + version "5.0.4" + resolved "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.0.4.tgz" + integrity sha512-La0KD0vGkVkSk6K+piWDKRUyg8Rl5iAIKRMH0vMJI0Eg47bq1eOxmoObAaQG37WMW9MSyk7Cs8EIWwJC1PtzKA== + dependencies: + "@babel/core" "^7.28.4" + "@babel/plugin-transform-react-jsx-self" "^7.27.1" + "@babel/plugin-transform-react-jsx-source" "^7.27.1" + "@rolldown/pluginutils" "1.0.0-beta.38" + "@types/babel__core" "^7.20.5" + react-refresh "^0.17.0" + +acorn-jsx@^5.3.2: + version "5.3.2" + resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== + +acorn@^8.15.0: + version "8.15.0" + resolved "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz" + integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg== + +add-dom-event-listener@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/add-dom-event-listener/-/add-dom-event-listener-1.1.0.tgz" + integrity sha512-WCxx1ixHT0GQU9hb0KI/mhgRQhnU+U3GvwY6ZvVjYq8rsihIGoaIOUbY0yMPBxLH5MDtr0kz3fisWGNcbWW7Jw== + dependencies: + object-assign "4.x" + +ajv@^6.12.4: + version "6.12.6" + resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +antd@^5.27.4: + version "5.27.4" + resolved "https://registry.npmjs.org/antd/-/antd-5.27.4.tgz" + integrity sha512-rhArohoAUCxhkPjGI/BXthOrrjaElL4Fb7d4vEHnIR3DpxFXfegd4rN21IgGdiF+Iz4EFuUZu8MdS8NuJHLSVQ== + dependencies: + "@ant-design/colors" "^7.2.1" + "@ant-design/cssinjs" "^1.23.0" + "@ant-design/cssinjs-utils" "^1.1.3" + "@ant-design/fast-color" "^2.0.6" + "@ant-design/icons" "^5.6.1" + "@ant-design/react-slick" "~1.1.2" + "@babel/runtime" "^7.26.0" + "@rc-component/color-picker" "~2.0.1" + "@rc-component/mutate-observer" "^1.1.0" + "@rc-component/qrcode" "~1.0.0" + "@rc-component/tour" "~1.15.1" + "@rc-component/trigger" "^2.3.0" + classnames "^2.5.1" + copy-to-clipboard "^3.3.3" + dayjs "^1.11.11" + rc-cascader "~3.34.0" + rc-checkbox "~3.5.0" + rc-collapse "~3.9.0" + rc-dialog "~9.6.0" + rc-drawer "~7.3.0" + rc-dropdown "~4.2.1" + rc-field-form "~2.7.0" + rc-image "~7.12.0" + rc-input "~1.8.0" + rc-input-number "~9.5.0" + rc-mentions "~2.20.0" + rc-menu "~9.16.1" + rc-motion "^2.9.5" + rc-notification "~5.6.4" + rc-pagination "~5.1.0" + rc-picker "~4.11.3" + rc-progress "~4.0.0" + rc-rate "~2.13.1" + rc-resize-observer "^1.4.3" + rc-segmented "~2.7.0" + rc-select "~14.16.8" + rc-slider "~11.1.9" + rc-steps "~6.0.1" + rc-switch "~4.1.0" + rc-table "~7.53.0" + rc-tabs "~15.7.0" + rc-textarea "~1.10.2" + rc-tooltip "~6.4.0" + rc-tree "~5.13.1" + rc-tree-select "~5.27.0" + rc-upload "~4.9.2" + rc-util "^5.44.4" + scroll-into-view-if-needed "^3.1.0" + throttle-debounce "^5.0.2" + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +baseline-browser-mapping@^2.8.9: + version "2.8.15" + resolved "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.15.tgz" + integrity sha512-qsJ8/X+UypqxHXN75M7dF88jNK37dLBRW7LeUzCPz+TNs37G8cfWy9nWzS+LS//g600zrt2le9KuXt0rWfDz5Q== + +brace-expansion@^1.1.7: + version "1.1.12" + resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz" + integrity sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +brace-expansion@^2.0.1: + version "2.0.2" + resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz" + integrity sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ== + dependencies: + balanced-match "^1.0.0" + +braces@^3.0.3: + version "3.0.3" + resolved "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + +browserslist@^4.24.0: + version "4.26.3" + resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz" + integrity sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w== + dependencies: + baseline-browser-mapping "^2.8.9" + caniuse-lite "^1.0.30001746" + electron-to-chromium "^1.5.227" + node-releases "^2.0.21" + update-browserslist-db "^1.1.3" + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +caniuse-lite@^1.0.30001746: + version "1.0.30001749" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001749.tgz" + integrity sha512-0rw2fJOmLfnzCRbkm8EyHL8SvI2Apu5UbnQuTsJ0ClgrH8hcwFooJ1s5R0EP8o8aVrFu8++ae29Kt9/gZAZp/Q== + +chalk@^4.0.0: + version "4.1.2" + resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chownr@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz" + integrity sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g== + +classnames@2.x, classnames@^2.2.1, classnames@^2.2.3, classnames@^2.2.5, classnames@^2.2.6, classnames@^2.3.1, classnames@^2.3.2, classnames@^2.5.1: + version "2.5.1" + resolved "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz" + integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow== + +clsx@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz" + integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +compute-scroll-into-view@^3.0.2: + version "3.1.1" + resolved "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz" + integrity sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +convert-source-map@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz" + integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== + +cookie@^1.0.1: + version "1.0.2" + resolved "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz" + integrity sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA== + +copy-to-clipboard@^3.3.3: + version "3.3.3" + resolved "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz" + integrity sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA== + dependencies: + toggle-selection "^1.0.6" + +cross-spawn@^7.0.6: + version "7.0.6" + resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz" + integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +csstype@^3.0.2, csstype@^3.1.3: + version "3.1.3" + resolved "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz" + integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== + +"d3-array@2 - 3", "d3-array@2.10.0 - 3", d3-array@^3.1.6: + version "3.2.4" + resolved "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz" + integrity sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg== + dependencies: + internmap "1 - 2" + +"d3-color@1 - 3": + version "3.1.0" + resolved "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz" + integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA== + +d3-ease@^3.0.1: + version "3.0.1" + resolved "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz" + integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w== + +"d3-format@1 - 3": + version "3.1.0" + resolved "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz" + integrity sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA== + +"d3-interpolate@1.2.0 - 3", d3-interpolate@^3.0.1: + version "3.0.1" + resolved "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz" + integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g== + dependencies: + d3-color "1 - 3" + +d3-path@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz" + integrity sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ== + +d3-scale@^4.0.2: + version "4.0.2" + resolved "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz" + integrity sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ== + dependencies: + d3-array "2.10.0 - 3" + d3-format "1 - 3" + d3-interpolate "1.2.0 - 3" + d3-time "2.1.1 - 3" + d3-time-format "2 - 4" + +d3-shape@^3.1.0: + version "3.2.0" + resolved "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz" + integrity sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA== + dependencies: + d3-path "^3.1.0" + +"d3-time-format@2 - 4": + version "4.1.0" + resolved "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz" + integrity sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg== + dependencies: + d3-time "1 - 3" + +"d3-time@1 - 3", "d3-time@2.1.1 - 3", d3-time@^3.0.0: + version "3.1.0" + resolved "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz" + integrity sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q== + dependencies: + d3-array "2 - 3" + +d3-timer@^3.0.1: + version "3.0.1" + resolved "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz" + integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA== + +dayjs@^1.11.10, dayjs@^1.11.11, dayjs@^1.11.18: + version "1.11.18" + resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.11.18.tgz" + integrity sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA== + +debug@^4.1.0, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: + version "4.4.3" + resolved "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz" + integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== + dependencies: + ms "^2.1.3" + +decimal.js-light@^2.5.1: + version "2.5.1" + resolved "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz" + integrity sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg== + +deep-is@^0.1.3: + version "0.1.4" + resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz" + integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== + +dequal@^2.0.3: + version "2.0.3" + resolved "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz" + integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== + +detect-libc@^2.0.3, detect-libc@^2.0.4: + version "2.1.2" + resolved "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz" + integrity sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ== + +electron-to-chromium@^1.5.227: + version "1.5.234" + resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.234.tgz" + integrity sha512-RXfEp2x+VRYn8jbKfQlRImzoJU01kyDvVPBmG39eU2iuRVhuS6vQNocB8J0/8GrIMLnPzgz4eW6WiRnJkTuNWg== + +enhanced-resolve@^5.18.3: + version "5.18.3" + resolved "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz" + integrity sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + +es-toolkit@^1.39.3: + version "1.41.0" + resolved "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.41.0.tgz" + integrity sha512-bDd3oRmbVgqZCJS6WmeQieOrzpl3URcWBUVDXxOELlUW2FuW+0glPOz1n0KnRie+PdyvUZcXz2sOn00c6pPRIA== + +esbuild@^0.25.0: + version "0.25.10" + resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz" + integrity sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ== + optionalDependencies: + "@esbuild/aix-ppc64" "0.25.10" + "@esbuild/android-arm" "0.25.10" + "@esbuild/android-arm64" "0.25.10" + "@esbuild/android-x64" "0.25.10" + "@esbuild/darwin-arm64" "0.25.10" + "@esbuild/darwin-x64" "0.25.10" + "@esbuild/freebsd-arm64" "0.25.10" + "@esbuild/freebsd-x64" "0.25.10" + "@esbuild/linux-arm" "0.25.10" + "@esbuild/linux-arm64" "0.25.10" + "@esbuild/linux-ia32" "0.25.10" + "@esbuild/linux-loong64" "0.25.10" + "@esbuild/linux-mips64el" "0.25.10" + "@esbuild/linux-ppc64" "0.25.10" + "@esbuild/linux-riscv64" "0.25.10" + "@esbuild/linux-s390x" "0.25.10" + "@esbuild/linux-x64" "0.25.10" + "@esbuild/netbsd-arm64" "0.25.10" + "@esbuild/netbsd-x64" "0.25.10" + "@esbuild/openbsd-arm64" "0.25.10" + "@esbuild/openbsd-x64" "0.25.10" + "@esbuild/openharmony-arm64" "0.25.10" + "@esbuild/sunos-x64" "0.25.10" + "@esbuild/win32-arm64" "0.25.10" + "@esbuild/win32-ia32" "0.25.10" + "@esbuild/win32-x64" "0.25.10" + +escalade@^3.2.0: + version "3.2.0" + resolved "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== + +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +eslint-plugin-react-hooks@^5.2.0: + version "5.2.0" + resolved "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz" + integrity sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg== + +eslint-plugin-react-refresh@^0.4.22: + version "0.4.23" + resolved "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.23.tgz" + integrity sha512-G4j+rv0NmbIR45kni5xJOrYvCtyD3/7LjpVH8MPPcudXDcNu8gv+4ATTDXTtbRR8rTCM5HxECvCSsRmxKnWDsA== + +eslint-scope@^8.4.0: + version "8.4.0" + resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz" + integrity sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg== + dependencies: + esrecurse "^4.3.0" + estraverse "^5.2.0" + +eslint-visitor-keys@^3.4.3: + version "3.4.3" + resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz" + integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== + +eslint-visitor-keys@^4.2.1: + version "4.2.1" + resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz" + integrity sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ== + +eslint@^9.36.0: + version "9.37.0" + resolved "https://registry.npmjs.org/eslint/-/eslint-9.37.0.tgz" + integrity sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig== + dependencies: + "@eslint-community/eslint-utils" "^4.8.0" + "@eslint-community/regexpp" "^4.12.1" + "@eslint/config-array" "^0.21.0" + "@eslint/config-helpers" "^0.4.0" + "@eslint/core" "^0.16.0" + "@eslint/eslintrc" "^3.3.1" + "@eslint/js" "9.37.0" + "@eslint/plugin-kit" "^0.4.0" + "@humanfs/node" "^0.16.6" + "@humanwhocodes/module-importer" "^1.0.1" + "@humanwhocodes/retry" "^0.4.2" + "@types/estree" "^1.0.6" + "@types/json-schema" "^7.0.15" + ajv "^6.12.4" + chalk "^4.0.0" + cross-spawn "^7.0.6" + debug "^4.3.2" + escape-string-regexp "^4.0.0" + eslint-scope "^8.4.0" + eslint-visitor-keys "^4.2.1" + espree "^10.4.0" + esquery "^1.5.0" + esutils "^2.0.2" + fast-deep-equal "^3.1.3" + file-entry-cache "^8.0.0" + find-up "^5.0.0" + glob-parent "^6.0.2" + ignore "^5.2.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + json-stable-stringify-without-jsonify "^1.0.1" + lodash.merge "^4.6.2" + minimatch "^3.1.2" + natural-compare "^1.4.0" + optionator "^0.9.3" + +espree@^10.0.1, espree@^10.4.0: + version "10.4.0" + resolved "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz" + integrity sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ== + dependencies: + acorn "^8.15.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^4.2.1" + +esquery@^1.5.0: + version "1.6.0" + resolved "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz" + integrity sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg== + dependencies: + estraverse "^5.1.0" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^5.1.0, estraverse@^5.2.0: + version "5.3.0" + resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +eventemitter3@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz" + integrity sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA== + +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-glob@^3.3.2: + version "3.3.3" + resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz" + integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.8" + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-levenshtein@^2.0.6: + version "2.0.6" + resolved "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz" + integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== + +fastq@^1.6.0: + version "1.19.1" + resolved "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz" + integrity sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ== + dependencies: + reusify "^1.0.4" + +fdir@^6.5.0: + version "6.5.0" + resolved "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz" + integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg== + +file-entry-cache@^8.0.0: + version "8.0.0" + resolved "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz" + integrity sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ== + dependencies: + flat-cache "^4.0.0" + +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +flat-cache@^4.0.0: + version "4.0.1" + resolved "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz" + integrity sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw== + dependencies: + flatted "^3.2.9" + keyv "^4.5.4" + +flatted@^3.2.9: + version "3.3.3" + resolved "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz" + integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg== + +fsevents@~2.3.2, fsevents@~2.3.3: + version "2.3.3" + resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +gensync@^1.0.0-beta.2: + version "1.0.0-beta.2" + resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz" + integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== + +glob-parent@^5.1.2: + version "5.1.2" + resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob-parent@^6.0.2: + version "6.0.2" + resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + +globals@^14.0.0: + version "14.0.0" + resolved "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz" + integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ== + +globals@^16.4.0: + version "16.4.0" + resolved "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz" + integrity sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw== + +graceful-fs@^4.2.4: + version "4.2.11" + resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +graphemer@^1.4.0: + version "1.4.0" + resolved "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz" + integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +ignore@^5.2.0: + version "5.3.2" + resolved "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz" + integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== + +ignore@^7.0.0: + version "7.0.5" + resolved "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz" + integrity sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg== + +immer@^10.1.1, immer@^10.2.0: + version "10.2.0" + resolved "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz" + integrity sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw== + +import-fresh@^3.2.1: + version "3.3.1" + resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz" + integrity sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz" + integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== + +"internmap@1 - 2": + version "2.0.3" + resolved "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz" + integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg== + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3: + version "4.0.3" + resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +jiti@^2.6.0: + version "2.6.1" + resolved "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz" + integrity sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ== + +"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +jsesc@^3.0.2: + version "3.1.0" + resolved "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz" + integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA== + +json-buffer@3.0.1: + version "3.0.1" + resolved "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz" + integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz" + integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== + +json2mq@^0.2.0: + version "0.2.0" + resolved "https://registry.npmjs.org/json2mq/-/json2mq-0.2.0.tgz" + integrity sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA== + dependencies: + string-convert "^0.2.0" + +json5@^2.2.3: + version "2.2.3" + resolved "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== + +keyv@^4.5.4: + version "4.5.4" + resolved "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz" + integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== + dependencies: + json-buffer "3.0.1" + +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== + dependencies: + prelude-ls "^1.2.1" + type-check "~0.4.0" + +lightningcss-darwin-arm64@1.30.1: + version "1.30.1" + resolved "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz" + integrity sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ== + +lightningcss-darwin-x64@1.30.1: + version "1.30.1" + resolved "https://registry.yarnpkg.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz#e81105d3fd6330860c15fe860f64d39cff5fbd22" + integrity sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA== + +lightningcss-freebsd-x64@1.30.1: + version "1.30.1" + resolved "https://registry.yarnpkg.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz#a0e732031083ff9d625c5db021d09eb085af8be4" + integrity sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig== + +lightningcss-linux-arm-gnueabihf@1.30.1: + version "1.30.1" + resolved "https://registry.yarnpkg.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz#1f5ecca6095528ddb649f9304ba2560c72474908" + integrity sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q== + +lightningcss-linux-arm64-gnu@1.30.1: + version "1.30.1" + resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz#eee7799726103bffff1e88993df726f6911ec009" + integrity sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw== + +lightningcss-linux-arm64-musl@1.30.1: + version "1.30.1" + resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz#f2e4b53f42892feeef8f620cbb889f7c064a7dfe" + integrity sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ== + +lightningcss-linux-x64-gnu@1.30.1: + version "1.30.1" + resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz#2fc7096224bc000ebb97eea94aea248c5b0eb157" + integrity sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw== + +lightningcss-linux-x64-musl@1.30.1: + version "1.30.1" + resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz#66dca2b159fd819ea832c44895d07e5b31d75f26" + integrity sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ== + +lightningcss-win32-arm64-msvc@1.30.1: + version "1.30.1" + resolved "https://registry.yarnpkg.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz#7d8110a19d7c2d22bfdf2f2bb8be68e7d1b69039" + integrity sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA== + +lightningcss-win32-x64-msvc@1.30.1: + version "1.30.1" + resolved "https://registry.yarnpkg.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz#fd7dd008ea98494b85d24b4bea016793f2e0e352" + integrity sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg== + +lightningcss@1.30.1: + version "1.30.1" + resolved "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz" + integrity sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg== + dependencies: + detect-libc "^2.0.3" + optionalDependencies: + lightningcss-darwin-arm64 "1.30.1" + lightningcss-darwin-x64 "1.30.1" + lightningcss-freebsd-x64 "1.30.1" + lightningcss-linux-arm-gnueabihf "1.30.1" + lightningcss-linux-arm64-gnu "1.30.1" + lightningcss-linux-arm64-musl "1.30.1" + lightningcss-linux-x64-gnu "1.30.1" + lightningcss-linux-x64-musl "1.30.1" + lightningcss-win32-arm64-msvc "1.30.1" + lightningcss-win32-x64-msvc "1.30.1" + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +lodash-es@^4.17.21: + version "4.17.21" + resolved "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz" + integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== + +lodash.merge@^4.6.2: + version "4.6.2" + resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + +lodash@^4.0.1, lodash@^4.17.21: + version "4.17.21" + resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +loose-envify@^1.0.0, loose-envify@^1.4.0: + version "1.4.0" + resolved "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + +lucide-react@^0.545.0: + version "0.545.0" + resolved "https://registry.npmjs.org/lucide-react/-/lucide-react-0.545.0.tgz" + integrity sha512-7r1/yUuflQDSt4f1bpn5ZAocyIxcTyVyBBChSVtBKn5M+392cPmI5YJMWOJKk/HUWGm5wg83chlAZtCcGbEZtw== + +magic-string@^0.30.19: + version "0.30.19" + resolved "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz" + integrity sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw== + dependencies: + "@jridgewell/sourcemap-codec" "^1.5.5" + +merge2@^1.3.0: + version "1.4.1" + resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +micromatch@^4.0.8: + version "4.0.8" + resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz" + integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== + dependencies: + braces "^3.0.3" + picomatch "^2.3.1" + +minimatch@^3.1.2: + version "3.1.2" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimatch@^9.0.4: + version "9.0.5" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz" + integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== + dependencies: + brace-expansion "^2.0.1" + +minipass@^7.0.4, minipass@^7.1.2: + version "7.1.2" + resolved "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz" + integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== + +minizlib@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz" + integrity sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw== + dependencies: + minipass "^7.1.2" + +ms@^2.1.3: + version "2.1.3" + resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +nanoid@^3.3.11: + version "3.3.11" + resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz" + integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w== + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz" + integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== + +node-releases@^2.0.21: + version "2.0.23" + resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.23.tgz" + integrity sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg== + +object-assign@4.x, object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + +optionator@^0.9.3: + version "0.9.4" + resolved "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz" + integrity sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g== + dependencies: + deep-is "^0.1.3" + fast-levenshtein "^2.0.6" + levn "^0.4.1" + prelude-ls "^1.2.1" + type-check "^0.4.0" + word-wrap "^1.2.5" + +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-to-regexp@8.2.0: + version "8.2.0" + resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz" + integrity sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ== + +picocolors@^1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + +picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +picomatch@^4.0.3: + version "4.0.3" + resolved "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz" + integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q== + +postcss@^8.5.6: + version "8.5.6" + resolved "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz" + integrity sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg== + dependencies: + nanoid "^3.3.11" + picocolors "^1.1.1" + source-map-js "^1.2.1" + +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + +prop-types@^15.5.10: + version "15.8.1" + resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz" + integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.13.1" + +punycode@^2.1.0: + version "2.3.1" + resolved "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== + +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + +rc-cascader@~3.34.0: + version "3.34.0" + resolved "https://registry.npmjs.org/rc-cascader/-/rc-cascader-3.34.0.tgz" + integrity sha512-KpXypcvju9ptjW9FaN2NFcA2QH9E9LHKq169Y0eWtH4e/wHQ5Wh5qZakAgvb8EKZ736WZ3B0zLLOBsrsja5Dag== + dependencies: + "@babel/runtime" "^7.25.7" + classnames "^2.3.1" + rc-select "~14.16.2" + rc-tree "~5.13.0" + rc-util "^5.43.0" + +rc-checkbox@~3.5.0: + version "3.5.0" + resolved "https://registry.npmjs.org/rc-checkbox/-/rc-checkbox-3.5.0.tgz" + integrity sha512-aOAQc3E98HteIIsSqm6Xk2FPKIER6+5vyEFMZfo73TqM+VVAIqOkHoPjgKLqSNtVLWScoaM7vY2ZrGEheI79yg== + dependencies: + "@babel/runtime" "^7.10.1" + classnames "^2.3.2" + rc-util "^5.25.2" + +rc-collapse@~3.9.0: + version "3.9.0" + resolved "https://registry.npmjs.org/rc-collapse/-/rc-collapse-3.9.0.tgz" + integrity sha512-swDdz4QZ4dFTo4RAUMLL50qP0EY62N2kvmk2We5xYdRwcRn8WcYtuetCJpwpaCbUfUt5+huLpVxhvmnK+PHrkA== + dependencies: + "@babel/runtime" "^7.10.1" + classnames "2.x" + rc-motion "^2.3.4" + rc-util "^5.27.0" + +rc-dialog@~9.6.0: + version "9.6.0" + resolved "https://registry.npmjs.org/rc-dialog/-/rc-dialog-9.6.0.tgz" + integrity sha512-ApoVi9Z8PaCQg6FsUzS8yvBEQy0ZL2PkuvAgrmohPkN3okps5WZ5WQWPc1RNuiOKaAYv8B97ACdsFU5LizzCqg== + dependencies: + "@babel/runtime" "^7.10.1" + "@rc-component/portal" "^1.0.0-8" + classnames "^2.2.6" + rc-motion "^2.3.0" + rc-util "^5.21.0" + +rc-drawer@~7.3.0: + version "7.3.0" + resolved "https://registry.npmjs.org/rc-drawer/-/rc-drawer-7.3.0.tgz" + integrity sha512-DX6CIgiBWNpJIMGFO8BAISFkxiuKitoizooj4BDyee8/SnBn0zwO2FHrNDpqqepj0E/TFTDpmEBCyFuTgC7MOg== + dependencies: + "@babel/runtime" "^7.23.9" + "@rc-component/portal" "^1.1.1" + classnames "^2.2.6" + rc-motion "^2.6.1" + rc-util "^5.38.1" + +rc-dropdown@~4.2.0, rc-dropdown@~4.2.1: + version "4.2.1" + resolved "https://registry.npmjs.org/rc-dropdown/-/rc-dropdown-4.2.1.tgz" + integrity sha512-YDAlXsPv3I1n42dv1JpdM7wJ+gSUBfeyPK59ZpBD9jQhK9jVuxpjj3NmWQHOBceA1zEPVX84T2wbdb2SD0UjmA== + dependencies: + "@babel/runtime" "^7.18.3" + "@rc-component/trigger" "^2.0.0" + classnames "^2.2.6" + rc-util "^5.44.1" + +rc-field-form@~2.7.0: + version "2.7.0" + resolved "https://registry.npmjs.org/rc-field-form/-/rc-field-form-2.7.0.tgz" + integrity sha512-hgKsCay2taxzVnBPZl+1n4ZondsV78G++XVsMIJCAoioMjlMQR9YwAp7JZDIECzIu2Z66R+f4SFIRrO2DjDNAA== + dependencies: + "@babel/runtime" "^7.18.0" + "@rc-component/async-validator" "^5.0.3" + rc-util "^5.32.2" + +rc-image@~7.12.0: + version "7.12.0" + resolved "https://registry.npmjs.org/rc-image/-/rc-image-7.12.0.tgz" + integrity sha512-cZ3HTyyckPnNnUb9/DRqduqzLfrQRyi+CdHjdqgsyDpI3Ln5UX1kXnAhPBSJj9pVRzwRFgqkN7p9b6HBDjmu/Q== + dependencies: + "@babel/runtime" "^7.11.2" + "@rc-component/portal" "^1.0.2" + classnames "^2.2.6" + rc-dialog "~9.6.0" + rc-motion "^2.6.2" + rc-util "^5.34.1" + +rc-input-number@~9.5.0: + version "9.5.0" + resolved "https://registry.npmjs.org/rc-input-number/-/rc-input-number-9.5.0.tgz" + integrity sha512-bKaEvB5tHebUURAEXw35LDcnRZLq3x1k7GxfAqBMzmpHkDGzjAtnUL8y4y5N15rIFIg5IJgwr211jInl3cipag== + dependencies: + "@babel/runtime" "^7.10.1" + "@rc-component/mini-decimal" "^1.0.1" + classnames "^2.2.5" + rc-input "~1.8.0" + rc-util "^5.40.1" + +rc-input@~1.8.0: + version "1.8.0" + resolved "https://registry.npmjs.org/rc-input/-/rc-input-1.8.0.tgz" + integrity sha512-KXvaTbX+7ha8a/k+eg6SYRVERK0NddX8QX7a7AnRvUa/rEH0CNMlpcBzBkhI0wp2C8C4HlMoYl8TImSN+fuHKA== + dependencies: + "@babel/runtime" "^7.11.1" + classnames "^2.2.1" + rc-util "^5.18.1" + +rc-mentions@~2.20.0: + version "2.20.0" + resolved "https://registry.npmjs.org/rc-mentions/-/rc-mentions-2.20.0.tgz" + integrity sha512-w8HCMZEh3f0nR8ZEd466ATqmXFCMGMN5UFCzEUL0bM/nGw/wOS2GgRzKBcm19K++jDyuWCOJOdgcKGXU3fXfbQ== + dependencies: + "@babel/runtime" "^7.22.5" + "@rc-component/trigger" "^2.0.0" + classnames "^2.2.6" + rc-input "~1.8.0" + rc-menu "~9.16.0" + rc-textarea "~1.10.0" + rc-util "^5.34.1" + +rc-menu@~9.16.0, rc-menu@~9.16.1: + version "9.16.1" + resolved "https://registry.npmjs.org/rc-menu/-/rc-menu-9.16.1.tgz" + integrity sha512-ghHx6/6Dvp+fw8CJhDUHFHDJ84hJE3BXNCzSgLdmNiFErWSOaZNsihDAsKq9ByTALo/xkNIwtDFGIl6r+RPXBg== + dependencies: + "@babel/runtime" "^7.10.1" + "@rc-component/trigger" "^2.0.0" + classnames "2.x" + rc-motion "^2.4.3" + rc-overflow "^1.3.1" + rc-util "^5.27.0" + +rc-motion@^2.0.0, rc-motion@^2.0.1, rc-motion@^2.3.0, rc-motion@^2.3.4, rc-motion@^2.4.3, rc-motion@^2.4.4, rc-motion@^2.6.1, rc-motion@^2.6.2, rc-motion@^2.9.0, rc-motion@^2.9.5: + version "2.9.5" + resolved "https://registry.npmjs.org/rc-motion/-/rc-motion-2.9.5.tgz" + integrity sha512-w+XTUrfh7ArbYEd2582uDrEhmBHwK1ZENJiSJVb7uRxdE7qJSYjbO2eksRXmndqyKqKoYPc9ClpPh5242mV1vA== + dependencies: + "@babel/runtime" "^7.11.1" + classnames "^2.2.1" + rc-util "^5.44.0" + +rc-notification@~5.6.4: + version "5.6.4" + resolved "https://registry.npmjs.org/rc-notification/-/rc-notification-5.6.4.tgz" + integrity sha512-KcS4O6B4qzM3KH7lkwOB7ooLPZ4b6J+VMmQgT51VZCeEcmghdeR4IrMcFq0LG+RPdnbe/ArT086tGM8Snimgiw== + dependencies: + "@babel/runtime" "^7.10.1" + classnames "2.x" + rc-motion "^2.9.0" + rc-util "^5.20.1" + +rc-overflow@^1.3.1, rc-overflow@^1.3.2: + version "1.4.1" + resolved "https://registry.npmjs.org/rc-overflow/-/rc-overflow-1.4.1.tgz" + integrity sha512-3MoPQQPV1uKyOMVNd6SZfONi+f3st0r8PksexIdBTeIYbMX0Jr+k7pHEDvsXtR4BpCv90/Pv2MovVNhktKrwvw== + dependencies: + "@babel/runtime" "^7.11.1" + classnames "^2.2.1" + rc-resize-observer "^1.0.0" + rc-util "^5.37.0" + +rc-pagination@~5.1.0: + version "5.1.0" + resolved "https://registry.npmjs.org/rc-pagination/-/rc-pagination-5.1.0.tgz" + integrity sha512-8416Yip/+eclTFdHXLKTxZvn70duYVGTvUUWbckCCZoIl3jagqke3GLsFrMs0bsQBikiYpZLD9206Ej4SOdOXQ== + dependencies: + "@babel/runtime" "^7.10.1" + classnames "^2.3.2" + rc-util "^5.38.0" + +rc-picker@~4.11.3: + version "4.11.3" + resolved "https://registry.npmjs.org/rc-picker/-/rc-picker-4.11.3.tgz" + integrity sha512-MJ5teb7FlNE0NFHTncxXQ62Y5lytq6sh5nUw0iH8OkHL/TjARSEvSHpr940pWgjGANpjCwyMdvsEV55l5tYNSg== + dependencies: + "@babel/runtime" "^7.24.7" + "@rc-component/trigger" "^2.0.0" + classnames "^2.2.1" + rc-overflow "^1.3.2" + rc-resize-observer "^1.4.0" + rc-util "^5.43.0" + +rc-progress@~4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/rc-progress/-/rc-progress-4.0.0.tgz" + integrity sha512-oofVMMafOCokIUIBnZLNcOZFsABaUw8PPrf1/y0ZBvKZNpOiu5h4AO9vv11Sw0p4Hb3D0yGWuEattcQGtNJ/aw== + dependencies: + "@babel/runtime" "^7.10.1" + classnames "^2.2.6" + rc-util "^5.16.1" + +rc-rate@~2.13.1: + version "2.13.1" + resolved "https://registry.npmjs.org/rc-rate/-/rc-rate-2.13.1.tgz" + integrity sha512-QUhQ9ivQ8Gy7mtMZPAjLbxBt5y9GRp65VcUyGUMF3N3fhiftivPHdpuDIaWIMOTEprAjZPC08bls1dQB+I1F2Q== + dependencies: + "@babel/runtime" "^7.10.1" + classnames "^2.2.5" + rc-util "^5.0.1" + +rc-resize-observer@^0.2.3: + version "0.2.6" + resolved "https://registry.npmjs.org/rc-resize-observer/-/rc-resize-observer-0.2.6.tgz" + integrity sha512-YX6nYnd6fk7zbuvT6oSDMKiZjyngjHoy+fz+vL3Tez38d/G5iGdaDJa2yE7345G6sc4Mm1IGRUIwclvltddhmA== + dependencies: + "@babel/runtime" "^7.10.1" + classnames "^2.2.1" + rc-util "^5.0.0" + resize-observer-polyfill "^1.5.1" + +rc-resize-observer@^1.0.0, rc-resize-observer@^1.1.0, rc-resize-observer@^1.3.1, rc-resize-observer@^1.4.0, rc-resize-observer@^1.4.3: + version "1.4.3" + resolved "https://registry.npmjs.org/rc-resize-observer/-/rc-resize-observer-1.4.3.tgz" + integrity sha512-YZLjUbyIWox8E9i9C3Tm7ia+W7euPItNWSPX5sCcQTYbnwDb5uNpnLHQCG1f22oZWUhLw4Mv2tFmeWe68CDQRQ== + dependencies: + "@babel/runtime" "^7.20.7" + classnames "^2.2.1" + rc-util "^5.44.1" + resize-observer-polyfill "^1.5.1" + +rc-segmented@~2.7.0: + version "2.7.0" + resolved "https://registry.npmjs.org/rc-segmented/-/rc-segmented-2.7.0.tgz" + integrity sha512-liijAjXz+KnTRVnxxXG2sYDGd6iLL7VpGGdR8gwoxAXy2KglviKCxLWZdjKYJzYzGSUwKDSTdYk8brj54Bn5BA== + dependencies: + "@babel/runtime" "^7.11.1" + classnames "^2.2.1" + rc-motion "^2.4.4" + rc-util "^5.17.0" + +rc-select@~14.16.2, rc-select@~14.16.8: + version "14.16.8" + resolved "https://registry.npmjs.org/rc-select/-/rc-select-14.16.8.tgz" + integrity sha512-NOV5BZa1wZrsdkKaiK7LHRuo5ZjZYMDxPP6/1+09+FB4KoNi8jcG1ZqLE3AVCxEsYMBe65OBx71wFoHRTP3LRg== + dependencies: + "@babel/runtime" "^7.10.1" + "@rc-component/trigger" "^2.1.1" + classnames "2.x" + rc-motion "^2.0.1" + rc-overflow "^1.3.1" + rc-util "^5.16.1" + rc-virtual-list "^3.5.2" + +rc-slider@~11.1.9: + version "11.1.9" + resolved "https://registry.npmjs.org/rc-slider/-/rc-slider-11.1.9.tgz" + integrity sha512-h8IknhzSh3FEM9u8ivkskh+Ef4Yo4JRIY2nj7MrH6GQmrwV6mcpJf5/4KgH5JaVI1H3E52yCdpOlVyGZIeph5A== + dependencies: + "@babel/runtime" "^7.10.1" + classnames "^2.2.5" + rc-util "^5.36.0" + +rc-steps@~6.0.1: + version "6.0.1" + resolved "https://registry.npmjs.org/rc-steps/-/rc-steps-6.0.1.tgz" + integrity sha512-lKHL+Sny0SeHkQKKDJlAjV5oZ8DwCdS2hFhAkIjuQt1/pB81M0cA0ErVFdHq9+jmPmFw1vJB2F5NBzFXLJxV+g== + dependencies: + "@babel/runtime" "^7.16.7" + classnames "^2.2.3" + rc-util "^5.16.1" + +rc-switch@~4.1.0: + version "4.1.0" + resolved "https://registry.npmjs.org/rc-switch/-/rc-switch-4.1.0.tgz" + integrity sha512-TI8ufP2Az9oEbvyCeVE4+90PDSljGyuwix3fV58p7HV2o4wBnVToEyomJRVyTaZeqNPAp+vqeo4Wnj5u0ZZQBg== + dependencies: + "@babel/runtime" "^7.21.0" + classnames "^2.2.1" + rc-util "^5.30.0" + +rc-table@~7.53.0: + version "7.53.1" + resolved "https://registry.npmjs.org/rc-table/-/rc-table-7.53.1.tgz" + integrity sha512-firAd7Z+liqIDS5TubJ1qqcoBd6YcANLKWQDZhFf3rfoOTt/UNPj4n3O+2vhl+z4QMqwPEUVAil661WHA8H8Aw== + dependencies: + "@babel/runtime" "^7.10.1" + "@rc-component/context" "^1.4.0" + classnames "^2.2.5" + rc-resize-observer "^1.1.0" + rc-util "^5.44.3" + rc-virtual-list "^3.14.2" + +rc-tabs@~15.7.0: + version "15.7.0" + resolved "https://registry.npmjs.org/rc-tabs/-/rc-tabs-15.7.0.tgz" + integrity sha512-ZepiE+6fmozYdWf/9gVp7k56PKHB1YYoDsKeQA1CBlJ/POIhjkcYiv0AGP0w2Jhzftd3AVvZP/K+V+Lpi2ankA== + dependencies: + "@babel/runtime" "^7.11.2" + classnames "2.x" + rc-dropdown "~4.2.0" + rc-menu "~9.16.0" + rc-motion "^2.6.2" + rc-resize-observer "^1.0.0" + rc-util "^5.34.1" + +rc-textarea@~1.10.0, rc-textarea@~1.10.2: + version "1.10.2" + resolved "https://registry.npmjs.org/rc-textarea/-/rc-textarea-1.10.2.tgz" + integrity sha512-HfaeXiaSlpiSp0I/pvWpecFEHpVysZ9tpDLNkxQbMvMz6gsr7aVZ7FpWP9kt4t7DB+jJXesYS0us1uPZnlRnwQ== + dependencies: + "@babel/runtime" "^7.10.1" + classnames "^2.2.1" + rc-input "~1.8.0" + rc-resize-observer "^1.0.0" + rc-util "^5.27.0" + +rc-tooltip@~6.4.0: + version "6.4.0" + resolved "https://registry.npmjs.org/rc-tooltip/-/rc-tooltip-6.4.0.tgz" + integrity sha512-kqyivim5cp8I5RkHmpsp1Nn/Wk+1oeloMv9c7LXNgDxUpGm+RbXJGL+OPvDlcRnx9DBeOe4wyOIl4OKUERyH1g== + dependencies: + "@babel/runtime" "^7.11.2" + "@rc-component/trigger" "^2.0.0" + classnames "^2.3.1" + rc-util "^5.44.3" + +rc-tree-select@~5.27.0: + version "5.27.0" + resolved "https://registry.npmjs.org/rc-tree-select/-/rc-tree-select-5.27.0.tgz" + integrity sha512-2qTBTzwIT7LRI1o7zLyrCzmo5tQanmyGbSaGTIf7sYimCklAToVVfpMC6OAldSKolcnjorBYPNSKQqJmN3TCww== + dependencies: + "@babel/runtime" "^7.25.7" + classnames "2.x" + rc-select "~14.16.2" + rc-tree "~5.13.0" + rc-util "^5.43.0" + +rc-tree@~5.13.0, rc-tree@~5.13.1: + version "5.13.1" + resolved "https://registry.npmjs.org/rc-tree/-/rc-tree-5.13.1.tgz" + integrity sha512-FNhIefhftobCdUJshO7M8uZTA9F4OPGVXqGfZkkD/5soDeOhwO06T/aKTrg0WD8gRg/pyfq+ql3aMymLHCTC4A== + dependencies: + "@babel/runtime" "^7.10.1" + classnames "2.x" + rc-motion "^2.0.1" + rc-util "^5.16.1" + rc-virtual-list "^3.5.1" + +rc-upload@~4.9.2: + version "4.9.2" + resolved "https://registry.npmjs.org/rc-upload/-/rc-upload-4.9.2.tgz" + integrity sha512-nHx+9rbd1FKMiMRYsqQ3NkXUv7COHPBo3X1Obwq9SWS6/diF/A0aJ5OHubvwUAIDs+4RMleljV0pcrNUc823GQ== + dependencies: + "@babel/runtime" "^7.18.3" + classnames "^2.2.5" + rc-util "^5.2.0" + +rc-util@^4.19.0: + version "4.21.1" + resolved "https://registry.npmjs.org/rc-util/-/rc-util-4.21.1.tgz" + integrity sha512-Z+vlkSQVc1l8O2UjR3WQ+XdWlhj5q9BMQNLk2iOBch75CqPfrJyGtcWMcnhRlNuDu0Ndtt4kLVO8JI8BrABobg== + dependencies: + add-dom-event-listener "^1.1.0" + prop-types "^15.5.10" + react-is "^16.12.0" + react-lifecycles-compat "^3.0.4" + shallowequal "^1.1.0" + +rc-util@^5.0.0, rc-util@^5.0.1, rc-util@^5.0.6, rc-util@^5.16.1, rc-util@^5.17.0, rc-util@^5.18.1, rc-util@^5.2.0, rc-util@^5.20.1, rc-util@^5.21.0, rc-util@^5.24.4, rc-util@^5.25.2, rc-util@^5.27.0, rc-util@^5.30.0, rc-util@^5.31.1, rc-util@^5.32.2, rc-util@^5.34.1, rc-util@^5.35.0, rc-util@^5.36.0, rc-util@^5.37.0, rc-util@^5.38.0, rc-util@^5.38.1, rc-util@^5.4.0, rc-util@^5.40.1, rc-util@^5.43.0, rc-util@^5.44.0, rc-util@^5.44.1, rc-util@^5.44.3, rc-util@^5.44.4: + version "5.44.4" + resolved "https://registry.npmjs.org/rc-util/-/rc-util-5.44.4.tgz" + integrity sha512-resueRJzmHG9Q6rI/DfK6Kdv9/Lfls05vzMs1Sk3M2P+3cJa+MakaZyWY8IPfehVuhPJFKrIY1IK4GqbiaiY5w== + dependencies: + "@babel/runtime" "^7.18.3" + react-is "^18.2.0" + +rc-virtual-list@^3.14.2, rc-virtual-list@^3.5.1, rc-virtual-list@^3.5.2: + version "3.19.2" + resolved "https://registry.npmjs.org/rc-virtual-list/-/rc-virtual-list-3.19.2.tgz" + integrity sha512-Ys6NcjwGkuwkeaWBDqfI3xWuZ7rDiQXlH1o2zLfFzATfEgXcqpk8CkgMfbJD81McqjcJVez25a3kPxCR807evA== + dependencies: + "@babel/runtime" "^7.20.0" + classnames "^2.2.6" + rc-resize-observer "^1.0.0" + rc-util "^5.36.0" + +react-dom@^19.1.1: + version "19.2.0" + resolved "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz" + integrity sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ== + dependencies: + scheduler "^0.27.0" + +react-is@^16.12.0, react-is@^16.13.1: + version "16.13.1" + resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz" + integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== + +react-is@^18.2.0: + version "18.3.1" + resolved "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz" + integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== + +react-lifecycles-compat@^3.0.4: + version "3.0.4" + resolved "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz" + integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== + +"react-redux@8.x.x || 9.x.x": + version "9.2.0" + resolved "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz" + integrity sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g== + dependencies: + "@types/use-sync-external-store" "^0.0.6" + use-sync-external-store "^1.4.0" + +react-refresh@^0.17.0: + version "0.17.0" + resolved "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz" + integrity sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ== + +react-router-dom@^7.9.4: + version "7.9.4" + resolved "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.9.4.tgz" + integrity sha512-f30P6bIkmYvnHHa5Gcu65deIXoA2+r3Eb6PJIAddvsT9aGlchMatJ51GgpU470aSqRRbFX22T70yQNUGuW3DfA== + dependencies: + react-router "7.9.4" + +react-router@7.9.4: + version "7.9.4" + resolved "https://registry.npmjs.org/react-router/-/react-router-7.9.4.tgz" + integrity sha512-SD3G8HKviFHg9xj7dNODUKDFgpG4xqD5nhyd0mYoB5iISepuZAvzSr8ywxgxKJ52yRzf/HWtVHc9AWwoTbljvA== + dependencies: + cookie "^1.0.1" + set-cookie-parser "^2.6.0" + +react@^19.1.1: + version "19.2.0" + resolved "https://registry.npmjs.org/react/-/react-19.2.0.tgz" + integrity sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ== + +reactcss@^1.2.3: + version "1.2.3" + resolved "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz" + integrity sha512-KiwVUcFu1RErkI97ywr8nvx8dNOpT03rbnma0SSalTYjkrPYaEajR4a/MRt6DZ46K6arDRbWMNHF+xH7G7n/8A== + dependencies: + lodash "^4.0.1" + +recharts@^3.3.0: + version "3.3.0" + resolved "https://registry.npmjs.org/recharts/-/recharts-3.3.0.tgz" + integrity sha512-Vi0qmTB0iz1+/Cz9o5B7irVyUjX2ynvEgImbgMt/3sKRREcUM07QiYjS1QpAVrkmVlXqy5gykq4nGWMz9AS4Rg== + dependencies: + "@reduxjs/toolkit" "1.x.x || 2.x.x" + clsx "^2.1.1" + decimal.js-light "^2.5.1" + es-toolkit "^1.39.3" + eventemitter3 "^5.0.1" + immer "^10.1.1" + react-redux "8.x.x || 9.x.x" + reselect "5.1.1" + tiny-invariant "^1.3.3" + use-sync-external-store "^1.2.2" + victory-vendor "^37.0.2" + +redux-thunk@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz" + integrity sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw== + +redux@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz" + integrity sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w== + +reselect@5.1.1, reselect@^5.1.0: + version "5.1.1" + resolved "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz" + integrity sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w== + +resize-observer-polyfill@^1.5.1: + version "1.5.1" + resolved "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz" + integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg== + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +reusify@^1.0.4: + version "1.1.0" + resolved "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz" + integrity sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw== + +rollup@^4.43.0: + version "4.52.4" + resolved "https://registry.npmjs.org/rollup/-/rollup-4.52.4.tgz" + integrity sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ== + dependencies: + "@types/estree" "1.0.8" + optionalDependencies: + "@rollup/rollup-android-arm-eabi" "4.52.4" + "@rollup/rollup-android-arm64" "4.52.4" + "@rollup/rollup-darwin-arm64" "4.52.4" + "@rollup/rollup-darwin-x64" "4.52.4" + "@rollup/rollup-freebsd-arm64" "4.52.4" + "@rollup/rollup-freebsd-x64" "4.52.4" + "@rollup/rollup-linux-arm-gnueabihf" "4.52.4" + "@rollup/rollup-linux-arm-musleabihf" "4.52.4" + "@rollup/rollup-linux-arm64-gnu" "4.52.4" + "@rollup/rollup-linux-arm64-musl" "4.52.4" + "@rollup/rollup-linux-loong64-gnu" "4.52.4" + "@rollup/rollup-linux-ppc64-gnu" "4.52.4" + "@rollup/rollup-linux-riscv64-gnu" "4.52.4" + "@rollup/rollup-linux-riscv64-musl" "4.52.4" + "@rollup/rollup-linux-s390x-gnu" "4.52.4" + "@rollup/rollup-linux-x64-gnu" "4.52.4" + "@rollup/rollup-linux-x64-musl" "4.52.4" + "@rollup/rollup-openharmony-arm64" "4.52.4" + "@rollup/rollup-win32-arm64-msvc" "4.52.4" + "@rollup/rollup-win32-ia32-msvc" "4.52.4" + "@rollup/rollup-win32-x64-gnu" "4.52.4" + "@rollup/rollup-win32-x64-msvc" "4.52.4" + fsevents "~2.3.2" + +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + +safe-stable-stringify@^2.4.3: + version "2.5.0" + resolved "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz" + integrity sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA== + +scheduler@^0.27.0: + version "0.27.0" + resolved "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz" + integrity sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q== + +scroll-into-view-if-needed@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz" + integrity sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ== + dependencies: + compute-scroll-into-view "^3.0.2" + +semver@^6.3.1: + version "6.3.1" + resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + +semver@^7.6.0: + version "7.7.3" + resolved "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz" + integrity sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q== + +set-cookie-parser@^2.6.0: + version "2.7.1" + resolved "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz" + integrity sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ== + +shallowequal@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz" + integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ== + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +source-map-js@^1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz" + integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== + +string-convert@^0.2.0: + version "0.2.1" + resolved "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz" + integrity sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A== + +strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +stylis@^4.3.4: + version "4.3.6" + resolved "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz" + integrity sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ== + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +swr@^2.0.0: + version "2.3.6" + resolved "https://registry.npmjs.org/swr/-/swr-2.3.6.tgz" + integrity sha512-wfHRmHWk/isGNMwlLGlZX5Gzz/uTgo0o2IRuTMcf4CPuPFJZlq0rDaKUx+ozB5nBOReNV1kiOyzMfj+MBMikLw== + dependencies: + dequal "^2.0.3" + use-sync-external-store "^1.4.0" + +tailwind-merge@^3.3.1: + version "3.3.1" + resolved "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz" + integrity sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g== + +tailwindcss@4.1.14, tailwindcss@^4.1.14: + version "4.1.14" + resolved "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.14.tgz" + integrity sha512-b7pCxjGO98LnxVkKjaZSDeNuljC4ueKUddjENJOADtubtdo8llTaJy7HwBMeLNSSo2N5QIAgklslK1+Ir8r6CA== + +tapable@^2.2.0: + version "2.3.0" + resolved "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz" + integrity sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg== + +tar@^7.5.1: + version "7.5.1" + resolved "https://registry.npmjs.org/tar/-/tar-7.5.1.tgz" + integrity sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g== + dependencies: + "@isaacs/fs-minipass" "^4.0.0" + chownr "^3.0.0" + minipass "^7.1.2" + minizlib "^3.1.0" + yallist "^5.0.0" + +throttle-debounce@^5.0.0, throttle-debounce@^5.0.2: + version "5.0.2" + resolved "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.2.tgz" + integrity sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A== + +tiny-invariant@^1.3.3: + version "1.3.3" + resolved "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz" + integrity sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg== + +tinycolor2@^1.4.2: + version "1.6.0" + resolved "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz" + integrity sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw== + +tinyglobby@^0.2.15: + version "0.2.15" + resolved "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz" + integrity sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ== + dependencies: + fdir "^6.5.0" + picomatch "^4.0.3" + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +toggle-selection@^1.0.6: + version "1.0.6" + resolved "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz" + integrity sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ== + +ts-api-utils@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz" + integrity sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ== + +tslib@^2.0.0, tslib@^2.4.0: + version "2.8.1" + resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== + dependencies: + prelude-ls "^1.2.1" + +typescript-eslint@^8.45.0: + version "8.46.0" + resolved "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.46.0.tgz" + integrity sha512-6+ZrB6y2bT2DX3K+Qd9vn7OFOJR+xSLDj+Aw/N3zBwUt27uTw2sw2TE2+UcY1RiyBZkaGbTkVg9SSdPNUG6aUw== + dependencies: + "@typescript-eslint/eslint-plugin" "8.46.0" + "@typescript-eslint/parser" "8.46.0" + "@typescript-eslint/typescript-estree" "8.46.0" + "@typescript-eslint/utils" "8.46.0" + +typescript@~5.9.3: + version "5.9.3" + resolved "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz" + integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw== + +undici-types@~7.14.0: + version "7.14.0" + resolved "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz" + integrity sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA== + +update-browserslist-db@^1.1.3: + version "1.1.3" + resolved "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz" + integrity sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw== + dependencies: + escalade "^3.2.0" + picocolors "^1.1.1" + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +use-sync-external-store@^1.2.2, use-sync-external-store@^1.4.0: + version "1.6.0" + resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz" + integrity sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w== + +victory-vendor@^37.0.2: + version "37.3.6" + resolved "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz" + integrity sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ== + dependencies: + "@types/d3-array" "^3.0.3" + "@types/d3-ease" "^3.0.0" + "@types/d3-interpolate" "^3.0.1" + "@types/d3-scale" "^4.0.2" + "@types/d3-shape" "^3.1.0" + "@types/d3-time" "^3.0.0" + "@types/d3-timer" "^3.0.0" + d3-array "^3.1.6" + d3-ease "^3.0.1" + d3-interpolate "^3.0.1" + d3-scale "^4.0.2" + d3-shape "^3.1.0" + d3-time "^3.0.0" + d3-timer "^3.0.1" + +vite@^7.1.7: + version "7.1.9" + resolved "https://registry.npmjs.org/vite/-/vite-7.1.9.tgz" + integrity sha512-4nVGliEpxmhCL8DslSAUdxlB6+SMrhB0a1v5ijlh1xB1nEPuy1mxaHxysVucLHuWryAxLWg6a5ei+U4TLn/rFg== + dependencies: + esbuild "^0.25.0" + fdir "^6.5.0" + picomatch "^4.0.3" + postcss "^8.5.6" + rollup "^4.43.0" + tinyglobby "^0.2.15" + optionalDependencies: + fsevents "~2.3.3" + +warning@^4.0.3: + version "4.0.3" + resolved "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz" + integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w== + dependencies: + loose-envify "^1.0.0" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +word-wrap@^1.2.5: + version "1.2.5" + resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz" + integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== + +yallist@^3.0.2: + version "3.1.1" + resolved "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== + +yallist@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz" + integrity sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw== + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==