From 37ba35a2f33213466afa57ac6788799475eb4f0b Mon Sep 17 00:00:00 2001 From: dushixiang Date: Mon, 10 Nov 2025 22:11:46 +0800 Subject: [PATCH] Initial commit --- .dockerignore | 47 + .github/workflows/docker-publish.yml | 137 + .gitignore | 40 + Dockerfile | 27 + Makefile | 88 + README.md | 374 ++ assets.go | 14 + cmd/agent/agent.yaml.example | 48 + cmd/agent/main.go | 545 ++ cmd/serv/main.go | 7 + config.example.yaml | 22 + docker-compose.yml | 28 + go.mod | 80 + go.sum | 214 + internal/app.go | 352 ++ internal/config/config.go | 12 + internal/handler/account_handler.go | 64 + internal/handler/agent_handler.go | 568 ++ internal/handler/alert_handler.go | 163 + internal/handler/api_key_handler.go | 159 + internal/handler/user_handler.go | 182 + internal/models/agent.go | 23 + internal/models/alert.go | 106 + internal/models/api_key.go | 16 + internal/models/audit.go | 17 + internal/models/metric.go | 151 + internal/models/user.go | 15 + internal/protocol/message.go | 243 + internal/providers.go | 65 + internal/repo/agent_repo.go | 142 + internal/repo/alert_repo.go | 183 + internal/repo/api_key_repo.go | 109 + internal/repo/metric_repo.go | 386 ++ internal/repo/user_repo.go | 81 + internal/service/account_service.go | 118 + internal/service/agent_service.go | 670 +++ internal/service/alert_service.go | 341 ++ internal/service/api_key_service.go | 145 + internal/service/notifier.go | 265 + internal/service/user_service.go | 197 + internal/validator.go | 51 + internal/websocket/manager.go | 286 + internal/wire.go | 64 + internal/wire_gen.go | 67 + pkg/agent/audit/audit.go | 2606 +++++++++ pkg/agent/collector/cpu.go | 80 + pkg/agent/collector/disk.go | 164 + pkg/agent/collector/gpu.go | 165 + pkg/agent/collector/manager.go | 154 + pkg/agent/collector/memory.go | 76 + pkg/agent/collector/network.go | 114 + pkg/agent/collector/system.go | 66 + pkg/agent/collector/temperature.go | 125 + pkg/agent/config/config.go | 285 + pkg/agent/id/id.go | 101 + pkg/agent/service/agent.go | 499 ++ pkg/agent/service/service.go | 230 + pkg/agent/updater/updater.go | 268 + pkg/version/version.go | 12 + playground/docker-compose.yml | 16 + web/.gitignore | 24 + web/README.md | 73 + web/assets.go | 14 + web/eslint.config.js | 23 + web/index.html | 13 + web/package-lock.json | 6364 ++++++++++++++++++++++ web/package.json | 41 + web/public/logo.png | Bin 0 -> 142761 bytes web/src/App.css | 6 + web/src/App.tsx | 22 + web/src/api/agent.ts | 187 + web/src/api/alert.ts | 62 + web/src/api/apiKey.ts | 48 + web/src/api/auth.ts | 11 + web/src/api/request.ts | 182 + web/src/api/user.ts | 46 + web/src/assets/github.svg | 1 + web/src/assets/os/apple.png | Bin 0 -> 5588 bytes web/src/assets/os/linux.png | Bin 0 -> 4975 bytes web/src/assets/os/win11.png | Bin 0 -> 1629 bytes web/src/assets/react.svg | 1 + web/src/components/PageHeader.tsx | 45 + web/src/components/PrivateRoute.tsx | 25 + web/src/components/index.ts | 2 + web/src/index.css | 25 + web/src/lib/utils.ts | 29 + web/src/main.tsx | 22 + web/src/pages/AdminLayout/index.tsx | 278 + web/src/pages/Agents/AgentDetail.tsx | 249 + web/src/pages/Agents/AgentInstall.tsx | 241 + web/src/pages/Agents/AgentList.tsx | 294 + web/src/pages/Agents/AuditResult.tsx | 483 ++ web/src/pages/ApiKeys/ApiKeyList.tsx | 382 ++ web/src/pages/Login/index.tsx | 83 + web/src/pages/Public/ServerDetail.tsx | 832 +++ web/src/pages/Public/ServerList.tsx | 541 ++ web/src/pages/Settings/AlertSettings.tsx | 384 ++ web/src/pages/Settings/index.tsx | 34 + web/src/pages/Users/UserList.tsx | 330 ++ web/src/router/index.tsx | 89 + web/src/types/index.ts | 278 + web/tsconfig.app.json | 20 + web/tsconfig.json | 7 + web/tsconfig.node.json | 18 + web/vite.config.ts | 19 + web/yarn.lock | 3172 +++++++++++ 106 files changed, 26873 insertions(+) create mode 100644 .dockerignore create mode 100644 .github/workflows/docker-publish.yml create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 README.md create mode 100644 assets.go create mode 100644 cmd/agent/agent.yaml.example create mode 100644 cmd/agent/main.go create mode 100644 cmd/serv/main.go create mode 100644 config.example.yaml create mode 100644 docker-compose.yml create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/app.go create mode 100644 internal/config/config.go create mode 100644 internal/handler/account_handler.go create mode 100644 internal/handler/agent_handler.go create mode 100644 internal/handler/alert_handler.go create mode 100644 internal/handler/api_key_handler.go create mode 100644 internal/handler/user_handler.go create mode 100644 internal/models/agent.go create mode 100644 internal/models/alert.go create mode 100644 internal/models/api_key.go create mode 100644 internal/models/audit.go create mode 100644 internal/models/metric.go create mode 100644 internal/models/user.go create mode 100644 internal/protocol/message.go create mode 100644 internal/providers.go create mode 100644 internal/repo/agent_repo.go create mode 100644 internal/repo/alert_repo.go create mode 100644 internal/repo/api_key_repo.go create mode 100644 internal/repo/metric_repo.go create mode 100644 internal/repo/user_repo.go create mode 100644 internal/service/account_service.go create mode 100644 internal/service/agent_service.go create mode 100644 internal/service/alert_service.go create mode 100644 internal/service/api_key_service.go create mode 100644 internal/service/notifier.go create mode 100644 internal/service/user_service.go create mode 100644 internal/validator.go create mode 100644 internal/websocket/manager.go create mode 100644 internal/wire.go create mode 100644 internal/wire_gen.go create mode 100644 pkg/agent/audit/audit.go create mode 100644 pkg/agent/collector/cpu.go create mode 100644 pkg/agent/collector/disk.go create mode 100644 pkg/agent/collector/gpu.go create mode 100644 pkg/agent/collector/manager.go create mode 100644 pkg/agent/collector/memory.go create mode 100644 pkg/agent/collector/network.go create mode 100644 pkg/agent/collector/system.go create mode 100644 pkg/agent/collector/temperature.go create mode 100644 pkg/agent/config/config.go create mode 100644 pkg/agent/id/id.go create mode 100644 pkg/agent/service/agent.go create mode 100644 pkg/agent/service/service.go create mode 100644 pkg/agent/updater/updater.go create mode 100644 pkg/version/version.go create mode 100644 playground/docker-compose.yml create mode 100644 web/.gitignore create mode 100644 web/README.md create mode 100644 web/assets.go create mode 100644 web/eslint.config.js create mode 100644 web/index.html create mode 100644 web/package-lock.json create mode 100644 web/package.json create mode 100644 web/public/logo.png create mode 100644 web/src/App.css create mode 100644 web/src/App.tsx create mode 100644 web/src/api/agent.ts create mode 100644 web/src/api/alert.ts create mode 100644 web/src/api/apiKey.ts create mode 100644 web/src/api/auth.ts create mode 100644 web/src/api/request.ts create mode 100644 web/src/api/user.ts create mode 100644 web/src/assets/github.svg create mode 100644 web/src/assets/os/apple.png create mode 100644 web/src/assets/os/linux.png create mode 100644 web/src/assets/os/win11.png create mode 100644 web/src/assets/react.svg create mode 100644 web/src/components/PageHeader.tsx create mode 100644 web/src/components/PrivateRoute.tsx create mode 100644 web/src/components/index.ts create mode 100644 web/src/index.css create mode 100644 web/src/lib/utils.ts create mode 100644 web/src/main.tsx create mode 100644 web/src/pages/AdminLayout/index.tsx create mode 100644 web/src/pages/Agents/AgentDetail.tsx create mode 100644 web/src/pages/Agents/AgentInstall.tsx create mode 100644 web/src/pages/Agents/AgentList.tsx create mode 100644 web/src/pages/Agents/AuditResult.tsx create mode 100644 web/src/pages/ApiKeys/ApiKeyList.tsx create mode 100644 web/src/pages/Login/index.tsx create mode 100644 web/src/pages/Public/ServerDetail.tsx create mode 100644 web/src/pages/Public/ServerList.tsx create mode 100644 web/src/pages/Settings/AlertSettings.tsx create mode 100644 web/src/pages/Settings/index.tsx create mode 100644 web/src/pages/Users/UserList.tsx create mode 100644 web/src/router/index.tsx create mode 100644 web/src/types/index.ts create mode 100644 web/tsconfig.app.json create mode 100644 web/tsconfig.json create mode 100644 web/tsconfig.node.json create mode 100644 web/vite.config.ts create mode 100644 web/yarn.lock 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 0000000000000000000000000000000000000000..59bfd1b60065c8802c557628aa4babfd66923a8c GIT binary patch literal 142761 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�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 GPWV6Bpx2-P literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..8402a0acd85774acaf03c9ef70395f3edf1f4353 GIT binary patch literal 5588 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 NJ+z@#m4;*F{{d?>Cb0kj literal 0 HcmV?d00001 diff --git a/web/src/assets/os/linux.png b/web/src/assets/os/linux.png new file mode 100644 index 0000000000000000000000000000000000000000..aa4fc92626817f3ce29fec4765208299a5045105 GIT binary patch literal 4975 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 literal 0 HcmV?d00001 diff --git a/web/src/assets/os/win11.png b/web/src/assets/os/win11.png new file mode 100644 index 0000000000000000000000000000000000000000..88bc92d945304df856f33abee542f444439699f7 GIT binary patch literal 1629 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k4M?tyST_$yF%}28J29*~C-ahlflb`g#WAEJ z?(NNsxuNb1t`~Q%JF`ylOT!ALT^%00of-$H0MdyCj{4vr0@Nrk1?T_E*e;1eAnu!;``pg~k{Po*snuo-Hl**l3{`#eTeu9GH znbQ~9f`lbJbU!@%$rkh($VgDQ%*1ANcCmtKf8!Mts{ zTSeaUx?j`Y|16SU|N3hG_rK5o?%XYR|MT(q|2g^=_4jy|O!`$~ySMVoXOBNA`X7Gf zuYcpk_l?O~KS9A-EYIi%Fqw8d^1{j0Xa4yh``(df@Bjbo`u;QMe%+5*<_9Y+zgX4Z z$$R*-Z{NAYmAM+{AMddKx7grMc#^`y$Nzt8pK{1*Ha}-RLCW1RtI_4W=)UY#R^*+ntFf&qf(=xL3!GUeSY}u>78@WA1<#s xA6{+sKZbvO-GO}jcjxa`)|}_x@b6|FbJO$^m8D)&{{ZU?22WQ%mvv4FO#s0tg}wj) literal 0 HcmV?d00001 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==