commit c5e51ed0690daa8714585f94f0946ba8a5ebac40 Author: mangomqy Date: Thu Nov 13 02:54:06 2025 +0000 ocdp v1 diff --git a/.github/PROJECT_STRUCTURE.md b/.github/PROJECT_STRUCTURE.md new file mode 100644 index 0000000..0d3f22b --- /dev/null +++ b/.github/PROJECT_STRUCTURE.md @@ -0,0 +1,79 @@ +# Project Structure + +## 📁 Directory Layout + +``` +ocdp-go/ +├── backend/ # Go 后端 +│ ├── cmd/api/ # 主程序入口 +│ ├── internal/ +│ │ ├── handlers/ # HTTP handlers +│ │ ├── graphql/ # GraphQL schema & resolvers +│ │ ├── models/ # 数据模型 +│ │ ├── storage/ # 存储层(JSON) +│ │ └── auth/ # 认证 +│ └── data/ # 数据存储(JSON文件) +│ +└── frontend/ # React 前端 + └── src/ + ├── app/ # 应用层 + │ ├── providers/ # 全局 Provider + │ ├── routes/ # 路由配置 + │ └── constants/ # 导航配置 + │ + ├── features/ # 功能模块(按类别) + │ ├── configuration/ # 🔧 配置类 + │ │ ├── clusters/ + │ │ └── registries/ + │ ├── monitoring/ # 📊 监控类 + │ │ └── clusters/ + │ ├── artifact/ # 📦 制品类 + │ │ ├── registries/ + │ │ └── instances/ + │ ├── auth/ + │ └── home/ + │ + ├── core/ # 核心层 + │ ├── api/ # API 调用 + │ ├── graphql/ # GraphQL 客户端 + │ ├── types/ # 类型定义 + │ └── config/ # 配置 + │ + └── shared/ # 共享层 + ├── components/ # 通用组件 + ├── hooks/ # 通用 hooks + ├── utils/ # 工具函数 + └── services/ # 服务 + +``` + +## 🎯 Features 模块分类 + +### Configuration(配置类) +- `clusters/` - 配置 K8s 集群连接 +- `registries/` - 配置 OCI 仓库连接 + +### Monitoring(监控类) +- `clusters/` - 监控集群状态 + +### Artifact(制品类) +- `registries/` - 浏览制品仓库内容 +- `instances/` - 管理已部署的服务实例 + +## 🔄 API 架构 + +- **GraphQL** - 用于 CRUD 操作(集群、仓库、实例) +- **RESTful** - 用于代理和实时查询(OCI、K8s 状态) +- **Unified API** - 自动根据配置切换 GraphQL/RESTful + +## 🚀 实例管理术语 + +- **Launch** - 启动/创建服务实例 +- **Modify** - 修改实例配置 +- **Terminate** - 终止实例运行 + +## 📝 说明 + +- 所有功能模块按**业务类别**分类(配置、监控、制品) +- Features 模块名使用**单数**形式(configuration, artifact) +- URL 路径保持 RESTful 风格(`/artifact/registries`) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fea6c0e --- /dev/null +++ b/.gitignore @@ -0,0 +1,63 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool +*.out + +# Dependency directories +vendor/ +node_modules/ + +# Go workspace file +go.work + +# IDE specific files +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# OS specific files +.DS_Store +Thumbs.db + +# Build output +bin/ +dist/ +build/ +backend/bin/ +frontend/dist/ + +# Logs +*.log +logs/ +backend/logs/ +backend/*.log + +# Environment files +.env +.env.local +.env.*.local + +# Data directories +backend/data/ +backend/config/bootstrap.json +backend/config/bootstrap.prod.json + +# Docker volumes +postgres_data/ +redis_data/ + +# Temporary files +tmp/ +temp/ +*.tmp + diff --git a/COMMANDS_CHEATSHEET.md b/COMMANDS_CHEATSHEET.md new file mode 100644 index 0000000..e685256 --- /dev/null +++ b/COMMANDS_CHEATSHEET.md @@ -0,0 +1,543 @@ +# OCDP 命令速查表 + +快速查找常用的 Docker 和 Make 命令。 + +--- + +## 🚀 快速启动 + +```bash +# 开发环境(推荐,Mock 模式) +make docker-dev + +# 生产环境(真实数据库) +make docker-prod + +# 测试后端(独立) +make docker-test-backend + +# 测试前端(独立) +make docker-test-frontend +``` + +--- + +## 📋 完整命令列表 + +### Docker 服务管理 + +| 命令 | 说明 | 模式 | +|------|------|------| +| `make docker-dev` | 启动开发环境(前台) | Dev | +| `make docker-dev-bg` | 启动开发环境(后台) | Dev | +| `make docker-prod` | 启动生产环境 | Production | +| `make docker-up` | 同 docker-prod | Production | +| `make docker-down` | 停止所有服务 | - | +| `make docker-down-v` | 停止并删除数据卷 | - | + +### 服务测试 + +| 命令 | 说明 | 端口 | +|------|------|------| +| `make docker-test-backend` | 测试后端(前台) | 8080 | +| `make docker-test-backend-bg` | 测试后端(后台) | 8080 | +| `make docker-test-frontend` | 测试前端(前台) | 3000 | +| `make docker-test-frontend-bg` | 测试前端(后台) | 3000 | + +### 日志查看 + +| 命令 | 说明 | +|------|------| +| `make docker-logs` | 查看所有服务日志 | +| `make docker-logs-backend` | 只看后端日志 | +| `make docker-logs-frontend` | 只看前端日志 | +| `make docker-logs-db` | 只看数据库日志 | + +### 服务状态 + +| 命令 | 说明 | +|------|------| +| `make docker-ps` | 查看服务状态 | +| `make docker-status` | 查看详细状态(含健康检查) | + +### 镜像管理 + +| 命令 | 说明 | +|------|------| +| `make docker-build` | 构建所有镜像 | +| `make docker-build-no-cache` | 无缓存构建 | +| `make docker-restart` | 重启所有服务 | +| `make docker-restart-backend` | 只重启后端 | +| `make docker-restart-frontend` | 只重启前端 | + +### 工具服务 + +| 命令 | 说明 | 端口 | +|------|------|------| +| `make docker-tools` | 启动 pgAdmin + Swagger UI | 5050, 8081 | + +--- + +## 🔧 原始 Docker Compose 命令 + +### 开发模式 + +```bash +# 启动(前台) +docker compose -f docker-compose.yml -f docker-compose.dev.yml up + +# 启动(后台) +docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d + +# 停止 +docker compose -f docker-compose.yml -f docker-compose.dev.yml down + +# 查看日志 +docker compose -f docker-compose.yml -f docker-compose.dev.yml logs -f + +# 重启服务 +docker compose -f docker-compose.yml -f docker-compose.dev.yml restart backend +``` + +### 生产模式 + +```bash +# 启动(前台) +docker compose up + +# 启动(后台) +docker compose up -d + +# 停止 +docker compose down + +# 查看日志 +docker compose logs -f + +# 重启服务 +docker compose restart backend +``` + +### Mock 模式 + +```bash +# 测试后端 +docker compose -f docker-compose.mock.yml up backend + +# 测试前端 +docker compose -f docker-compose.mock.yml up frontend + +# 后台运行 +docker compose -f docker-compose.mock.yml up -d backend + +# 停止 +docker compose -f docker-compose.mock.yml down +``` + +--- + +## 🛠️ 本地开发命令(不使用 Docker) + +### 安装依赖 + +```bash +make install # 安装所有依赖 +make install-tools # 安装 OpenAPI 工具 +make install-deps # 安装项目依赖 +``` + +### 启动服务 + +```bash +make dev-local # 启动完整环境 +make dev-backend-mock # 只启动后端(Mock) +make dev-backend-prod # 只启动后端(生产) +make dev-frontend # 只启动前端 +``` + +### OpenAPI 工作流 + +```bash +make openapi-validate # 验证 OpenAPI 规范 +make openapi-gen # 生成前后端代码 +make openapi-gen-backend # 只生成后端代码 +make openapi-gen-frontend # 只生成前端代码 +make openapi-docs # 生成 HTML 文档 +make openapi-view # 启动文档服务器 +``` + +### 测试 + +```bash +make test # 运行所有测试 +make test-backend # 运行后端测试 +make test-frontend # 运行前端测试 +``` + +### 清理 + +```bash +make clean # 清理构建产物 +make clean-generated # 清理生成的代码 +``` + +--- + +## 📊 常用 Docker 命令 + +### 容器操作 + +```bash +# 进入容器 +docker compose exec backend sh +docker compose exec frontend sh +docker compose exec postgres psql -U postgres -d ocdp + +# 查看容器列表 +docker compose ps + +# 查看容器详情 +docker compose ps -a + +# 删除容器 +docker compose rm backend +docker compose rm -f backend # 强制删除 + +# 停止特定容器 +docker compose stop backend +docker compose stop frontend +``` + +### 日志操作 + +```bash +# 实时日志 +docker compose logs -f + +# 只看特定服务 +docker compose logs -f backend + +# 显示最后 N 行 +docker compose logs --tail=100 backend + +# 显示时间戳 +docker compose logs -f -t backend + +# 不跟随(只显示现有) +docker compose logs backend +``` + +### 镜像操作 + +```bash +# 构建镜像 +docker compose build + +# 无缓存构建 +docker compose build --no-cache + +# 只构建特定服务 +docker compose build backend +docker compose build frontend + +# 查看镜像 +docker images | grep ocdp + +# 删除镜像 +docker rmi ocdp-backend +docker rmi ocdp-frontend +``` + +### 数据卷操作 + +```bash +# 查看数据卷 +docker volume ls + +# 删除特定数据卷 +docker volume rm ocdp_postgres_data +docker volume rm ocdp_redis_data + +# 删除所有未使用的数据卷 +docker volume prune + +# 备份数据卷 +docker run --rm -v ocdp_postgres_data:/data -v $(pwd):/backup alpine tar czf /backup/postgres_backup.tar.gz -C /data . + +# 恢复数据卷 +docker run --rm -v ocdp_postgres_data:/data -v $(pwd):/backup alpine sh -c "cd /data && tar xzf /backup/postgres_backup.tar.gz" +``` + +### 网络操作 + +```bash +# 查看网络 +docker network ls + +# 查看网络详情 +docker network inspect ocdp-network + +# 测试网络连通性 +docker compose exec backend ping frontend +docker compose exec frontend ping backend +``` + +--- + +## 🔍 调试命令 + +### 健康检查 + +```bash +# 后端健康检查 +curl http://localhost:8080/health + +# 前端健康检查 +curl http://localhost:5173/ # Dev 模式 +curl http://localhost:3000/ # Production 模式 + +# 数据库健康检查 +docker compose exec postgres pg_isready -U postgres +``` + +### API 测试 + +```bash +# 登录 +curl -X POST http://localhost:8080/api/v1/auth/login \ + -H "Content-Type: application/json" \ + -d '{"username":"admin","password":"admin123"}' + +# 获取 Registries +curl http://localhost:8080/api/v1/registries + +# 获取 Clusters +curl http://localhost:8080/api/v1/clusters + +# 获取 Repositories +curl http://localhost:8080/api/v1/registries/harbor-prod/repositories + +# 获取 Artifacts(带过滤) +curl "http://localhost:8080/api/v1/registries/harbor-prod/repositories/charts%2Fvllm-serve/artifacts?media_type=chart" +``` + +### 数据库操作 + +```bash +# 连接数据库 +docker compose exec postgres psql -U postgres -d ocdp + +# 查看表 +docker compose exec postgres psql -U postgres -d ocdp -c "\dt" + +# 查询数据 +docker compose exec postgres psql -U postgres -d ocdp -c "SELECT * FROM registries;" + +# 执行 SQL 文件 +docker compose exec -T postgres psql -U postgres -d ocdp < schema.sql +``` + +### 性能监控 + +```bash +# 查看资源使用 +docker stats + +# 只看特定容器 +docker stats ocdp-backend ocdp-frontend + +# 查看容器进程 +docker compose top backend +docker compose top frontend +``` + +--- + +## 🎯 常用组合命令 + +### 完全重置 + +```bash +# 停止并删除所有内容 +docker compose down -v +docker compose -f docker-compose.dev.yml down -v +docker compose -f docker-compose.mock.yml down -v + +# 删除镜像 +docker rmi $(docker images | grep ocdp | awk '{print $3}') + +# 重新构建 +make docker-build + +# 重新启动 +make docker-dev +``` + +### 更新代码后重新部署 + +```bash +# 停止服务 +make docker-down + +# 拉取最新代码 +git pull + +# 重新构建 +make docker-build + +# 启动服务 +make docker-dev +``` + +### 查看完整系统状态 + +```bash +# 查看所有容器 +docker compose ps + +# 查看所有日志 +make docker-logs + +# 查看健康状态 +make docker-status + +# 测试所有服务 +curl http://localhost:8080/health +curl http://localhost:5173/ +curl http://localhost:5432 # 数据库端口 +``` + +--- + +## 📝 环境变量 + +### 后端环境变量 + +```bash +# Mock 模式 +export ADAPTER_MODE=mock +export JWT_SECRET=dev-secret +export ENCRYPTION_KEY=dev-encryption-key-32-bytes-long + +# Production 模式 +export ADAPTER_MODE=production +export DATABASE_URL=postgresql://postgres:postgres@postgres:5432/ocdp?sslmode=disable +export JWT_SECRET=your-production-secret +export ENCRYPTION_KEY=your-production-encryption-key-32-bytes +``` + +### 前端环境变量 + +```bash +export VITE_API_BASE_URL=http://localhost:8080/api/v1 +export VITE_USE_MOCK=false +``` + +--- + +## 🆘 故障排查命令 + +### 问题诊断 + +```bash +# 查看完整日志 +docker compose logs --tail=1000 > debug.log + +# 查看特定时间段日志 +docker compose logs --since 10m backend + +# 查看容器详细信息 +docker compose exec backend env + +# 检查配置 +docker compose config + +# 验证 docker-compose.yml +docker compose -f docker-compose.yml config +docker compose -f docker-compose.dev.yml config +``` + +### 端口冲突检查 + +```bash +# 查看端口占用 +sudo lsof -i :8080 +sudo lsof -i :5173 +sudo lsof -i :3000 +sudo lsof -i :5432 + +# 杀死进程 +sudo kill -9 +``` + +### 磁盘空间清理 + +```bash +# 删除未使用的容器 +docker container prune + +# 删除未使用的镜像 +docker image prune + +# 删除未使用的数据卷 +docker volume prune + +# 删除所有未使用的资源 +docker system prune -a --volumes +``` + +--- + +## 💡 实用技巧 + +### 1. 后台运行并查看日志 + +```bash +make docker-dev-bg && make docker-logs +``` + +### 2. 重启并查看日志 + +```bash +docker compose restart backend && docker compose logs -f backend +``` + +### 3. 构建并启动 + +```bash +make docker-build && make docker-dev +``` + +### 4. 清理并重新开始 + +```bash +make docker-down && docker system prune -f && make docker-dev +``` + +### 5. 快速测试 API + +```bash +# 保存 token +TOKEN=$(curl -s -X POST http://localhost:8080/api/v1/auth/login \ + -H "Content-Type: application/json" \ + -d '{"username":"admin","password":"admin123"}' | jq -r '.token') + +# 使用 token +curl -H "Authorization: Bearer $TOKEN" http://localhost:8080/api/v1/registries +``` + +--- + +## 📚 更多资源 + +- [快速开始](./QUICK_START.md) +- [Docker 服务架构](./DOCKER_SERVICES.md) +- [项目重构总结](./PROJECT_RESTRUCTURE_SUMMARY.md) +- [README](./README.md) + +--- + +
+ 命令速查表 - 最后更新:2025-11-09 +
+ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e181899 --- /dev/null +++ b/Makefile @@ -0,0 +1,56 @@ +# ============================================================ +# OCDP stack orchestration Makefile +# run-2: 构建前端静态资源 + 启动 nginx(统一入口)和 backend 栈 +# clean-2: 清理 run-2 产生的容器 / 卷 / 网络 +# ============================================================ + +SHELL := /bin/bash + +COMPOSE_BIN ?= docker compose + +ROOT_COMPOSE := docker-compose.yml +BACKEND_COMPOSE := backend/docker-compose.yml +BACKEND_PROFILE := backend + +COMPOSE_STACK := $(COMPOSE_BIN) -f $(ROOT_COMPOSE) -f $(BACKEND_COMPOSE) --profile $(BACKEND_PROFILE) +COMPOSE_STACK_ALL := $(COMPOSE_BIN) -f $(ROOT_COMPOSE) -f $(BACKEND_COMPOSE) +STACK_ENV := ADAPTER_MODE=production BACKEND_BUILD_CONTEXT=$(abspath backend) BACKEND_BUILD_DOCKERFILE=$(abspath backend/Dockerfile) BACKEND_MOCK_BUILD_DOCKERFILE=$(abspath backend/Dockerfile.mock) INIT_DB_SQL_PATH=$(abspath backend/scripts/init-db.sql) + +STACK_SERVICES := postgres backend nginx + +.PHONY: run-2 clean-2 build-backend + +run-2: + @echo "═══════════════════════════════════════════════" + @echo "🚀 run-2: rebuild static assets + start web gateway stack" + @echo "═══════════════════════════════════════════════" + @echo "" + @export COMPOSE_PROJECT_NAME=ocdp && \ + export ADAPTER_MODE=production && \ + export BACKEND_BUILD_CONTEXT=$(abspath backend) && \ + export BACKEND_BUILD_DOCKERFILE=$(abspath backend/Dockerfile) && \ + export BACKEND_MOCK_BUILD_DOCKERFILE=$(abspath backend/Dockerfile.mock) && \ + export INIT_DB_SQL_PATH=$(abspath backend/scripts/init-db.sql) && \ + echo "→ Rebuilding frontend static assets" && \ + $(COMPOSE_STACK) run --rm frontend-build && \ + echo "" && \ + echo "→ Rebuilding backend image" && \ + $(COMPOSE_STACK) build backend && \ + echo "" && \ + echo "→ Bringing up backend + nginx services" && \ + $(COMPOSE_STACK) up -d $(STACK_SERVICES) + @echo "" + @echo "✅ Services online:" + @echo "═══════════════════════════════════════════════" + +clean-2: + @echo "═══════════════════════════════════════════════" + @echo "🧹 clean-2: tearing down run-2 stack" + @echo "═══════════════════════════════════════════════" + @$(COMPOSE_STACK_ALL) down --remove-orphans || true + @$(COMPOSE_STACK_ALL) down -v --remove-orphans || true + @$(COMPOSE_BIN) -f $(BACKEND_COMPOSE) down -v --remove-orphans || true + @echo "✅ Environment cleaned" + @echo "═══════════════════════════════════════════════" + + diff --git a/QUICK_START.md b/QUICK_START.md new file mode 100644 index 0000000..4f5a102 --- /dev/null +++ b/QUICK_START.md @@ -0,0 +1,413 @@ +# OCDP 快速开始指南 + +## 🚀 5分钟快速体验 + +### 前置要求 + +- Docker 20.10+ +- Docker Compose 2.0+ +- (可选) Make 工具 + +### 第一步:克隆项目 + +```bash +git clone +cd ocdp-go +``` + +### 第二步:选择运行模式 + +#### 方式 1: 开发模式(推荐用于日常开发) + +**特点**: +- ✅ 后端 Mock 模式(无需数据库) +- ✅ 热重载(代码修改自动生效) +- ✅ 快速启动 +- ✅ 适合快速迭代开发 + +```bash +# 使用 Make +make docker-dev + +# 或直接使用 Docker Compose +docker compose --profile dev up +``` + +**访问服务**: +- 前端:http://localhost:5173 +- 后端:http://localhost:8080 +- API 文档:http://localhost:8080/api/v1 + +**默认账号**: +- 用户名:`admin` +- 密码:`admin123` + +#### 方式 2: 生产模式(用于完整功能测试) + +**特点**: +- ✅ 真实数据库 +- ✅ 完整功能 +- ✅ 生产环境配置 + +```bash +# 使用 Make +make docker-prod + +# 或直接使用 Docker Compose +docker compose up -d +``` + +**访问服务**: +- 前端:http://localhost:3000 +- 后端:http://localhost:8080 +- 数据库:localhost:5432 + +#### 方式 3: 独立测试单个服务 + +**测试后端**: +```bash +make docker-test-backend +# 访问:http://localhost:8080 +``` + +**测试前端**: +```bash +make docker-test-frontend +# 访问:http://localhost:3000 +``` + +### 第三步:验证服务 + +```bash +# 检查后端健康状态 +curl http://localhost:8080/health + +# 登录获取 token +curl -X POST http://localhost:8080/api/v1/auth/login \ + -H "Content-Type: application/json" \ + -d '{"username":"admin","password":"admin123"}' + +# 获取 registries 列表 +curl http://localhost:8080/api/v1/registries +``` + +### 第四步:开始开发 + +#### 修改后端代码 + +```bash +# 编辑任意 Go 文件 +vim backend/cmd/api/main.go + +# Air 会自动检测变化并重新编译(开发模式) +# 查看日志确认重载 +docker compose -f docker-compose.yml -f docker-compose.dev.yml logs -f backend +``` + +#### 修改前端代码 + +```bash +# 编辑任意 React 组件 +vim frontend/src/App.tsx + +# Vite HMR 会自动更新浏览器(开发模式) +# 无需手动刷新页面 +``` + +### 停止服务 + +```bash +# 停止并删除容器 +make docker-down + +# 或 +docker compose down + +# 完全清理(包括数据卷) +docker compose down -v +``` + +--- + +## 📚 常用命令速查 + +### Docker 命令 + +| 命令 | 说明 | +|------|------| +| `make docker-dev` | 启动开发环境 | +| `make docker-prod` | 启动生产环境 | +| `make docker-test-backend` | 测试后端 | +| `make docker-test-frontend` | 测试前端 | +| `make docker-logs` | 查看所有日志 | +| `make docker-down` | 停止所有服务 | +| `make docker-build` | 重新构建镜像 | + +### 查看日志 + +```bash +# 所有服务 +docker compose logs -f + +# 只看后端 +docker compose logs -f backend + +# 只看前端 +docker compose logs -f frontend + +# 只看最后 100 行 +docker compose logs --tail=100 +``` + +### 进入容器 + +```bash +# 进入后端容器 +docker compose exec backend sh + +# 进入前端容器 +docker compose exec frontend sh + +# 进入数据库容器 +docker compose exec postgres psql -U postgres -d ocdp +``` + +### 重启服务 + +```bash +# 重启所有 +docker compose restart + +# 重启后端 +docker compose restart backend + +# 重启前端 +docker compose restart frontend +``` + +--- + +## 🎯 使用场景指南 + +### 场景 1: 我要开发新功能 + +```bash +# 1. 启动开发环境 +make docker-dev + +# 2. 修改代码(自动热重载) + +# 3. 查看日志 +make docker-logs + +# 4. 测试功能 +# 访问 http://localhost:5173 + +# 5. 停止 +make docker-down +``` + +### 场景 2: 我要测试完整功能 + +```bash +# 1. 启动生产环境(包含数据库) +make docker-prod + +# 2. 访问前端 +# http://localhost:3000 + +# 3. 停止 +make docker-down +``` + +### 场景 3: 我只想测试后端 API + +```bash +# 1. 启动后端 Mock +make docker-test-backend-bg + +# 2. 测试 API +curl http://localhost:8080/health +curl http://localhost:8080/api/v1/registries + +# 3. 停止 +docker compose -f docker-compose.mock.yml down +``` + +### 场景 4: 我只想测试前端界面 + +```bash +# 1. 启动前端 Mock +make docker-test-frontend-bg + +# 2. 访问前端 +# http://localhost:3000 + +# 3. 停止 +docker compose -f docker-compose.mock.yml down +``` + +### 场景 5: 代码修改不生效 + +```bash +# 1. 停止所有服务 +make docker-down + +# 2. 重新构建镜像 +make docker-build --no-cache + +# 3. 重新启动 +make docker-dev +``` + +--- + +## 🔍 常见问题 + +### Q1: 端口被占用怎么办? + +**问题**:启动时报错 "port is already allocated" + +**解决方案**: +```bash +# 查看占用端口的进程 +sudo lsof -i :8080 +sudo lsof -i :5173 +sudo lsof -i :3000 + +# 杀掉进程或修改 docker-compose.yml 中的端口映射 +``` + +### Q2: 容器启动失败 + +**查看详细错误**: +```bash +docker compose logs backend +docker compose logs frontend +``` + +**常见原因**: +- 端口冲突 +- 依赖服务未就绪 +- 配置错误 + +**解决方案**: +```bash +# 清理并重新启动 +docker compose down -v +docker compose build --no-cache +docker compose up +``` + +### Q3: 热重载不工作 + +**后端**: +```bash +# 确认 Air 是否运行 +docker compose logs backend | grep "air" + +# 检查文件挂载 +docker compose exec backend ls -la /app +``` + +**前端**: +```bash +# 确认 Vite 是否运行 +docker compose logs frontend | grep "VITE" + +# 重启前端 +docker compose restart frontend +``` + +### Q4: 数据库连接失败 + +**检查**: +```bash +# 数据库是否运行 +docker compose ps postgres + +# 健康检查 +docker compose exec postgres pg_isready +``` + +**解决方案**: +- 确保使用生产模式(`docker compose up`) +- 等待数据库健康检查通过(约 10-20 秒) +- 检查 `DATABASE_URL` 环境变量 + +### Q5: 前端无法连接后端 + +**检查**: +```bash +# 后端是否运行 +curl http://localhost:8080/health + +# 网络连接 +docker network inspect ocdp-network +``` + +**解决方案**: +- 确认后端服务运行正常 +- 检查 `VITE_API_BASE_URL` 环境变量 +- 检查浏览器控制台错误 + +--- + +## 📖 进一步学习 + +### 详细文档 + +- [Docker 服务架构](./DOCKER_SERVICES.md) - 完整的服务说明 +- [开发指南](./docs/development/specification.md) - 开发规范 +- [API 文档](./backend/docs/openapi.yaml) - OpenAPI 规范 +- [部署指南](./docs/deployment/docker-guide.md) - 生产部署 + +### 项目结构 + +``` +ocdp-go/ +├── backend/ # Go 后端服务 +├── frontend/ # React 前端应用 +├── api/ # OpenAPI 规范 +├── docs/ # 项目文档 +├── docker-compose.yml # 生产模式 +├── docker-compose.dev.yml # 开发模式 +├── docker-compose.mock.yml # Mock 模式 +└── Makefile # 便捷命令 +``` + +### 架构说明 + +``` +┌─────────────┐ ┌─────────────┐ +│ Frontend │────▶│ Backend │ +│ (React) │◀────│ (Go API) │ +└─────────────┘ └─────────────┘ + │ + ▼ + ┌─────────────┐ + │ PostgreSQL │ + │ Database │ + └─────────────┘ +``` + +--- + +## 🎉 开始使用 + +现在你已经准备好开始使用 OCDP 了! + +**推荐的开发流程**: +1. ✅ 使用 `make docker-dev` 启动开发环境 +2. ✅ 修改代码(自动热重载) +3. ✅ 使用浏览器测试功能 +4. ✅ 使用 `make docker-test-backend` 测试后端 API +5. ✅ 使用 `make docker-prod` 进行完整测试 + +**需要帮助?** +- 查看 [DOCKER_SERVICES.md](./DOCKER_SERVICES.md) 了解详细配置 +- 查看 [GitHub Issues](https://github.com/your-repo/issues) 报告问题 +- 查看项目文档获取更多信息 + +Happy Coding! 🚀 + diff --git a/README.md b/README.md new file mode 100644 index 0000000..20b9f9c --- /dev/null +++ b/README.md @@ -0,0 +1,336 @@ +# OCDP - Open Cloud Development Platform + +[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) +[![Go Version](https://img.shields.io/badge/go-1.24+-00ADD8?logo=go)](https://go.dev/) +[![Node Version](https://img.shields.io/badge/node-20+-339933?logo=node.js)](https://nodejs.org/) +[![Docker](https://img.shields.io/badge/docker-20.10+-2496ED?logo=docker)](https://www.docker.com/) + +开源云原生开发平台,用于管理 Kubernetes 集群、OCI Registry 和 Helm Charts 部署。 + +--- + +## ✨ 特性 + +- 🎯 **Registry 管理** - 支持 Harbor、Docker Registry、OCI 标准仓库 +- 📦 **Artifact 浏览** - 浏览和管理 Helm Charts、容器镜像 +- 🚀 **一键部署** - 可视化部署 Helm Charts 到 Kubernetes 集群 +- 🔍 **智能过滤** - 按 MediaType 过滤 artifacts(chart、image、other) +- 🎨 **现代 UI** - 响应式设计,基于 React + TypeScript +- 🔐 **安全认证** - JWT 认证,加密存储敏感信息 +- 🐳 **容器化** - 完整的 Docker 支持,多种运行模式 +- 🔄 **热重载** - 开发模式支持代码热重载 + +--- + +## 🚀 快速开始 + +### 前置要求 + +- Docker 20.10+ +- Docker Compose 2.0+ +- (可选) Make 工具 + +### 5分钟快速体验 + +```bash +# 1. 克隆项目 +git clone +cd ocdp-go + +# 2. 启动开发环境(Mock 模式,无需数据库) +make docker-dev + +# 3. 访问应用 +# - 前端:http://localhost:5173 +# - 后端:http://localhost:8080 +# - 默认账号:admin / admin123 +``` + +**详细指南**:查看 [快速开始指南](./QUICK_START.md) + +--- + +## 📚 文档导航 + +### 📖 核心文档(必读) +- 🚀 [快速开始](./QUICK_START.md) - 5分钟快速上手 +- 📋 [使用指南](./USAGE_GUIDE.md) - 详细使用说明(推荐) +- 💡 [命令速查表](./COMMANDS_CHEATSHEET.md) - 常用命令快速参考 +- 📚 [文档中心](./docs/README.md) - 完整文档索引 + +### 🔧 专业文档 +- 📐 [开发规范](./docs/development/specification.md) - 代码规范和架构 +- 🚢 [部署指南](./docs/deployment/docker-guide.md) - 生产环境部署 +- 🔒 [安全实践](./docs/security/security-implementation.md) - 安全配置 +- 🎨 [功能文档](./docs/features/) - 详细功能说明 + +### 🔗 其他资源 +- 📋 [OpenAPI 规范](./backend/docs/openapi.yaml) - RESTful API 定义 +- 📦 [历史文档](./docs/archive/) - 项目演进历史 + +--- + +## 🏗️ 技术架构 + +### 技术栈 + +**后端**: +- Go 1.24+ (Hexagonal Architecture) +- PostgreSQL 16 +- Redis 7 + +**前端**: +- React 18 +- TypeScript 5 +- Vite 6 +- TailwindCSS 3 + +**容器化**: +- Docker +- Docker Compose +- Multi-stage builds + +### 架构图 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Frontend │ +│ React + TypeScript + Vite │ +└──────────────────────────┬──────────────────────────────────┘ + │ HTTP/REST +┌──────────────────────────┼──────────────────────────────────┐ +│ │ Backend API │ +│ ▼ │ +│ ┌─────────────────────┐ │ +│ │ Input Adapters │ │ +│ │ (REST/GraphQL) │ │ +│ └──────────┬──────────┘ │ +│ │ │ +│ ┌──────────▼──────────┐ │ +│ │ Domain Services │ │ +│ │ (Business Logic) │ │ +│ └──────────┬──────────┘ │ +│ │ │ +│ ┌──────────▼──────────┐ │ +│ │ Output Adapters │ │ +│ │ (Repos/Clients) │ │ +│ └──────────┬──────────┘ │ +└───────────────────────┼─┴────────────────────────────────┘ + │ + ┌───────────────┼───────────────┐ + │ │ │ + ┌────▼────┐ ┌────▼────┐ ┌────▼────┐ + │ PG DB │ │ Redis │ │ OCI │ + │ │ │ │ │ Registry│ + └─────────┘ └─────────┘ └─────────┘ +``` + +### 运行模式 + +| 模式 | 特点 | 适用场景 | 命令 | +|------|------|----------|------| +| **开发模式** | Mock 数据,热重载 | 日常开发 | `make docker-dev` | +| **生产模式** | 真实数据库,完整功能 | 生产部署 | `make docker-prod` | +| **Mock 模式** | 独立测试单个服务 | 单元测试 | `make docker-test-backend` | + +--- + +## 🛠️ 开发指南 + +### 项目结构 + +``` +ocdp-go/ +├── backend/ # Go 后端服务 +│ ├── cmd/api/ # 应用入口 +│ ├── internal/ # 内部代码 +│ │ ├── adapter/ # 适配器层 +│ │ ├── domain/ # 领域层 +│ │ └── bootstrap/ # 启动配置 +│ ├── Dockerfile # 生产环境 +│ ├── Dockerfile.dev # 开发环境 +│ └── Dockerfile.mock # Mock 测试 +│ +├── frontend/ # React 前端应用 +│ ├── src/ +│ │ ├── core/ # 核心功能 +│ │ ├── features/ # 功能模块 +│ │ └── shared/ # 共享组件 +│ ├── Dockerfile # 生产环境 +│ ├── Dockerfile.dev # 开发环境 +│ └── Dockerfile.mock # Mock 测试 +│ +├── api/ # API 规范 +│ └── openapi.yaml # OpenAPI 定义 +│ +├── docs/ # 项目文档 +│ ├── features/ # 功能文档 +│ ├── deployment/ # 部署文档 +│ └── development/ # 开发文档 +│ +├── docker-compose.yml # 统一配置(使用 profiles) +└── Makefile # 便捷命令 +``` + +### 常用命令 + +```bash +# Docker 服务(推荐) +make docker-dev # 启动开发环境 +make docker-prod # 启动生产环境 +make docker-test-backend # 测试后端 +make docker-test-frontend # 测试前端 +make docker-logs # 查看日志 +make docker-down # 停止服务 + +# OpenAPI 工作流 +make openapi-validate # 验证 API 规范 +make openapi-gen # 生成代码 +make openapi-docs # 生成文档 + +# 本地开发(不使用 Docker) +make install # 安装依赖 +make dev-local # 启动本地开发 +make test # 运行测试 +``` + +### 开发工作流 + +1. **启动开发环境**: + ```bash + make docker-dev + ``` + +2. **修改代码**(自动热重载): + - 后端:编辑 `backend/` 下的 Go 文件 + - 前端:编辑 `frontend/src/` 下的 React 组件 + +3. **查看日志**: + ```bash + make docker-logs + ``` + +4. **测试功能**: + - 前端:http://localhost:5173 + - 后端:http://localhost:8080 + +5. **提交代码**: + ```bash + git add . + git commit -m "feat: add new feature" + git push + ``` + +--- + +## 🧪 测试 + +### 后端测试 + +```bash +# 启动后端 Mock +make docker-test-backend-bg + +# 测试健康检查 +curl http://localhost:8080/health + +# 测试登录 +curl -X POST http://localhost:8080/api/v1/auth/login \ + -H "Content-Type: application/json" \ + -d '{"username":"admin","password":"admin123"}' + +# 测试 API +curl http://localhost:8080/api/v1/registries +curl http://localhost:8080/api/v1/clusters +``` + +### 前端测试 + +```bash +# 启动前端 Mock +make docker-test-frontend-bg + +# 访问前端 +open http://localhost:3000 +``` + +### 集成测试 + +```bash +# 启动完整环境 +make docker-prod + +# 运行测试套件 +make test +``` + +--- + +## 📦 部署 + +### Docker Compose 部署(推荐) + +```bash +# 1. 配置环境变量 +export JWT_SECRET="your-production-secret" +export ENCRYPTION_KEY="your-32-byte-encryption-key" + +# 2. 启动服务 +docker compose up -d + +# 3. 查看状态 +docker compose ps +``` + +### Kubernetes 部署 + +查看 [Kubernetes 部署指南](./docs/deployment/kubernetes-guide.md) + +--- + +## 🤝 贡献 + +欢迎贡献代码!请遵循以下步骤: + +1. Fork 项目 +2. 创建功能分支 (`git checkout -b feature/amazing-feature`) +3. 提交更改 (`git commit -m 'feat: add amazing feature'`) +4. 推送分支 (`git push origin feature/amazing-feature`) +5. 创建 Pull Request + +### 开发规范 + +- **代码风格**:Go (gofmt),TypeScript (ESLint + Prettier) +- **提交规范**:遵循 [Conventional Commits](https://www.conventionalcommits.org/) +- **测试覆盖**:新功能必须包含测试 + +--- + +## 📄 许可证 + +本项目采用 MIT 许可证 - 查看 [LICENSE](LICENSE) 文件了解详情 + +--- + +## 🙏 致谢 + +- [Go](https://go.dev/) - 后端开发语言 +- [React](https://react.dev/) - 前端框架 +- [Vite](https://vitejs.dev/) - 构建工具 +- [Docker](https://www.docker.com/) - 容器化平台 +- [Kubernetes](https://kubernetes.io/) - 容器编排 +- [Harbor](https://goharbor.io/) - OCI Registry + +--- + +## 📞 联系方式 + +- **项目主页**:https://github.com/your-org/ocdp-go +- **问题反馈**:https://github.com/your-org/ocdp-go/issues +- **文档网站**:https://docs.ocdp.example.com + +--- + +
+ Built with ❤️ by the OCDP Team +
diff --git a/START_BACKEND.md b/START_BACKEND.md new file mode 100644 index 0000000..a81bb4c --- /dev/null +++ b/START_BACKEND.md @@ -0,0 +1,127 @@ +# 启动和更新后端服务指南 + +## 方式一:使用 Makefile(推荐) + +### 启动完整服务(前端 + 后端 + Nginx) +```bash +make run-2 +``` + +这会: +1. 重新构建前端静态资源 +2. 重新构建后端镜像 +3. 启动 PostgreSQL、Backend 和 Nginx 服务 + +### 停止服务 +```bash +make clean-2 +``` + +--- + +## 方式二:只更新后端服务 + +### 1. 停止服务 +```bash +docker compose -f docker-compose.yml -f backend/docker-compose.yml --profile backend down +``` + +### 2. 重新构建并启动后端 +```bash +# 设置环境变量(必须!) +export COMPOSE_PROJECT_NAME=ocdp +export ADAPTER_MODE=production +export BACKEND_BUILD_CONTEXT=$(pwd)/backend +export BACKEND_BUILD_DOCKERFILE=$(pwd)/backend/Dockerfile +export BACKEND_MOCK_BUILD_DOCKERFILE=$(pwd)/backend/Dockerfile.mock +export INIT_DB_SQL_PATH=$(pwd)/backend/scripts/init-db.sql + +# 重新构建后端镜像 +docker compose -f docker-compose.yml -f backend/docker-compose.yml --profile backend build backend + +# 启动服务 +docker compose -f docker-compose.yml -f backend/docker-compose.yml --profile backend up -d postgres backend nginx +``` + +### 3. 或者使用一行命令(强制重建) +```bash +docker compose -f docker-compose.yml -f backend/docker-compose.yml --profile backend up -d --build --force-recreate backend +``` + +--- + +## 方式三:只重启后端容器(不重新构建) + +```bash +# 重启后端容器 +docker compose -f docker-compose.yml -f backend/docker-compose.yml --profile backend restart backend + +# 或者先停止再启动 +docker compose -f docker-compose.yml -f backend/docker-compose.yml --profile backend stop backend +docker compose -f docker-compose.yml -f backend/docker-compose.yml --profile backend start backend +``` + +--- + +## 方式四:查看日志 + +```bash +# 查看所有服务日志 +docker compose -f docker-compose.yml -f backend/docker-compose.yml --profile backend logs -f + +# 只查看后端日志 +docker compose -f docker-compose.yml -f backend/docker-compose.yml --profile backend logs -f backend +``` + +--- + +## 方式五:开发模式(只启动数据库,本地运行后端) + +```bash +# 只启动 PostgreSQL +docker compose -f backend/docker-compose.yml up -d postgres + +# 然后在本地运行后端 +cd backend +go run cmd/api/main.go +``` + +--- + +## 常用命令速查 + +```bash +# 查看运行中的服务 +docker compose -f docker-compose.yml -f backend/docker-compose.yml --profile backend ps + +# 查看后端容器状态 +docker ps | grep ocdp-backend + +# 进入后端容器 +docker exec -it ocdp-backend sh + +# 查看后端健康状态 +curl http://localhost:8080/health + +# 停止所有服务 +docker compose -f docker-compose.yml -f backend/docker-compose.yml --profile backend down + +# 停止并删除数据卷(清理数据) +docker compose -f docker-compose.yml -f backend/docker-compose.yml --profile backend down -v +``` + +--- + +## 快速更新后端(推荐流程) + +```bash +# 1. 停止服务 +docker compose -f docker-compose.yml -f backend/docker-compose.yml --profile backend down + +# 2. 重新构建并启动(使用 Makefile) +make run-2 + +# 或者手动执行 +docker compose -f docker-compose.yml -f backend/docker-compose.yml --profile backend up -d --build +``` + diff --git a/USAGE_GUIDE.md b/USAGE_GUIDE.md new file mode 100644 index 0000000..4578320 --- /dev/null +++ b/USAGE_GUIDE.md @@ -0,0 +1,355 @@ +# OCDP 使用指南 + +## 🎯 统一的 docker-compose.yml + +现在所有配置都整合在一个文件中,使用 **profiles** 区分不同运行模式。 + +--- + +## 🚀 快速开始 + +### 方式 1: 使用 Make(推荐) + +```bash +# 开发模式 +make docker-dev + +# 生产模式 +make docker-prod + +# 测试后端 +make docker-test-backend + +# 测试前端 +make docker-test-frontend +``` + +### 方式 2: 使用 Docker Compose + +```bash +# 开发模式 +docker compose --profile dev up + +# 生产模式 +docker compose --profile production up + +# Mock 测试模式(后端) +docker compose --profile mock up backend-mock + +# Mock 测试模式(前端) +docker compose --profile mock up frontend-mock +``` + +--- + +## 📋 三种运行模式 + +### 1. 开发模式(Dev Profile) + +**特点**: +- ✅ 后端 Mock 模式(无需数据库) +- ✅ 热重载(Air + Vite HMR) +- ✅ 快速启动 + +**启动命令**: +```bash +# 前台运行 +docker compose --profile dev up + +# 后台运行 +docker compose --profile dev up -d + +# 或使用 Make +make docker-dev +make docker-dev-bg +``` + +**访问地址**: +- 前端:http://localhost:5173 +- 后端:http://localhost:8080 + +**服务列表**: +- `backend-dev` - 后端开发服务 +- `frontend-dev` - 前端开发服务 + +### 2. 生产模式(Production Profile) + +**特点**: +- ✅ 真实数据库(PostgreSQL + Redis) +- ✅ 完整功能 +- ✅ 生产环境配置 + +**启动命令**: +```bash +# 后台运行 +docker compose --profile production up -d + +# 或使用 Make +make docker-prod +make docker-up +``` + +**访问地址**: +- 前端:http://localhost:3000 +- 后端:http://localhost:8080 +- 数据库:localhost:5432 + +**服务列表**: +- `postgres` - PostgreSQL 数据库 +- `redis` - Redis 缓存 +- `backend-prod` - 后端生产服务 +- `frontend-prod` - 前端生产服务 + +### 3. Mock 测试模式(Mock Profile) + +**特点**: +- ✅ 独立测试单个服务 +- ✅ 无外部依赖 +- ✅ 快速启动 + +**测试后端**: +```bash +# 前台运行 +docker compose --profile mock up backend-mock + +# 后台运行 +docker compose --profile mock up -d backend-mock + +# 或使用 Make +make docker-test-backend +make docker-test-backend-bg +``` + +**测试前端**: +```bash +# 前台运行 +docker compose --profile mock up frontend-mock + +# 后台运行 +docker compose --profile mock up -d frontend-mock + +# 或使用 Make +make docker-test-frontend +make docker-test-frontend-bg +``` + +--- + +## 🛠️ 常用操作 + +### 查看日志 + +```bash +# 查看所有日志 +docker compose logs -f + +# 查看特定服务日志 +docker compose logs -f backend-dev +docker compose logs -f frontend-dev +docker compose logs -f backend-prod +docker compose logs -f backend-mock + +# 或使用 Make +make docker-logs +make docker-logs-backend +make docker-logs-frontend +``` + +### 停止服务 + +```bash +# 停止所有服务 +docker compose down + +# 停止特定 profile 的服务 +docker compose --profile dev down +docker compose --profile production down +docker compose --profile mock down + +# 或使用 Make +make docker-down +``` + +### 重启服务 + +```bash +# 重启所有运行的服务 +docker compose restart + +# 重启特定服务 +docker compose restart backend-dev +docker compose restart frontend-dev + +# 或使用 Make +make docker-restart +make docker-restart-backend +make docker-restart-frontend +``` + +### 构建镜像 + +```bash +# 构建所有镜像 +docker compose build + +# 无缓存构建 +docker compose build --no-cache + +# 或使用 Make +make docker-build +make docker-build-no-cache +``` + +### 查看状态 + +```bash +# 查看运行的服务 +docker compose ps + +# 查看详细状态 +make docker-status +``` + +--- + +## 🔧 开发工具 + +### 启动 pgAdmin 和 Swagger UI + +```bash +# 需要先启动生产模式 +docker compose --profile production --profile tools up -d + +# 或使用 Make +make docker-tools +``` + +**访问地址**: +- pgAdmin:http://localhost:5050 +- Swagger UI:http://localhost:8081 + +--- + +## 📊 服务对比表 + +| 特性 | 开发模式 | 生产模式 | Mock 模式 | +|------|---------|---------|----------| +| **Profile** | `dev` | `production` | `mock` | +| **后端服务** | `backend-dev` | `backend-prod` | `backend-mock` | +| **前端服务** | `frontend-dev` | `frontend-prod` | `frontend-mock` | +| **数据库** | ❌ Mock | ✅ PostgreSQL | ❌ Mock | +| **前端端口** | 5173 | 3000 | 3000 | +| **热重载** | ✅ | ❌ | ❌ | +| **启动时间** | ~15秒 | ~30秒 | ~5秒 | + +--- + +## 💡 使用场景示例 + +### 场景 1: 日常开发 + +```bash +# 1. 启动开发环境 +make docker-dev + +# 2. 修改代码(自动热重载) + +# 3. 查看日志 +make docker-logs + +# 4. 停止 +make docker-down +``` + +### 场景 2: 测试后端 API + +```bash +# 1. 启动后端 Mock +make docker-test-backend-bg + +# 2. 测试 API +curl http://localhost:8080/health +curl http://localhost:8080/api/v1/registries + +# 3. 停止 +docker compose --profile mock down +``` + +### 场景 3: 生产环境部署 + +```bash +# 1. 配置环境变量 +export JWT_SECRET="your-secret" +export ENCRYPTION_KEY="your-32-byte-key" + +# 2. 启动生产环境 +make docker-prod + +# 3. 检查状态 +make docker-status + +# 4. 启动管理工具 +make docker-tools +``` + +### 场景 4: 完全重置 + +```bash +# 1. 停止并删除所有容器和数据 +docker compose down -v + +# 2. 删除镜像 +docker rmi $(docker images | grep ocdp | awk '{print $3}') + +# 3. 重新构建 +make docker-build + +# 4. 重新启动 +make docker-dev +``` + +--- + +## 🎓 环境变量 + +### 开发模式环境变量 + +后端自动使用: +- `ADAPTER_MODE=mock` +- `JWT_SECRET=dev-secret-key` + +### 生产模式环境变量 + +可通过 `.env` 文件或导出环境变量设置: +```bash +export JWT_SECRET="your-production-secret" +export ENCRYPTION_KEY="your-production-encryption-key-32-bytes" +``` + +--- + +## 📚 相关文档 + +- [README.md](./README.md) - 项目概述 +- [DOCKER_SERVICES.md](./DOCKER_SERVICES.md) - 详细架构说明 +- [COMMANDS_CHEATSHEET.md](./COMMANDS_CHEATSHEET.md) - 命令速查表 +- [QUICK_START.md](./QUICK_START.md) - 快速开始指南 + +--- + +## ✨ 优势 + +通过整合到单个 `docker-compose.yml` 文件: + +- ✅ **更简洁**:只需维护一个配置文件 +- ✅ **更清晰**:所有服务定义在同一处 +- ✅ **更灵活**:通过 profiles 轻松切换模式 +- ✅ **更易维护**:减少配置重复 +- ✅ **向后兼容**:Make 命令保持不变 + +--- + +
+ 简化配置,提升效率!🚀 +
+ diff --git a/backend/.air.toml b/backend/.air.toml new file mode 100644 index 0000000..967d2bb --- /dev/null +++ b/backend/.air.toml @@ -0,0 +1,45 @@ +# Air 配置文件 - 用于开发环境热重载 +root = "." +testdata_dir = "testdata" +tmp_dir = "tmp" + +[build] + args_bin = [] + bin = "./tmp/main" + cmd = "go build -o ./tmp/main ./cmd/api" + delay = 1000 + exclude_dir = ["assets", "tmp", "vendor", "testdata", "node_modules"] + exclude_file = [] + exclude_regex = ["_test.go"] + exclude_unchanged = false + follow_symlink = false + full_bin = "" + include_dir = [] + include_ext = ["go", "tpl", "tmpl", "html", "yaml", "yml"] + include_file = [] + kill_delay = "0s" + log = "build-errors.log" + poll = false + poll_interval = 0 + rerun = false + rerun_delay = 500 + send_interrupt = false + stop_on_error = false + +[color] + app = "" + build = "yellow" + main = "magenta" + runner = "green" + watcher = "cyan" + +[log] + main_only = false + time = false + +[misc] + clean_on_exit = false + +[screen] + clear_on_rebuild = false + keep_scroll = true diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..19796b3 --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,50 @@ +# Binaries +bin/ +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool +*.out + +# Dependency directories +vendor/ + +# Go workspace file +go.work + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log +logs/ +backend.log + +# Data +data/ + +# Docker +Dockerfile +.dockerignore +docker-compose.yml + +# Documentation +*.md + +# Examples +examples/ + diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..3da5686 --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,62 @@ +# Binaries +bin/ +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary +*.test + +# Output of the go coverage tool +*.out + +# Dependency directories +vendor/ + +# Go workspace file +go.work + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log +logs/ +*.log.* + +# Data +data/ +storage.json + +# Production configs (keep example configs) +config/bootstrap.prod.json +config/bootstrap.production.json +config/*.prod.json +config/*.production.json + +# Secrets +.env +.env.* +!.env.example +secrets/ +*.pem +*.key +*.crt + +# Temporary files +tmp/ +temp/ +*.tmp + +# Swagger 生成的文档 (可通过 swag init 重新生成) +docs/swagger/ diff --git a/backend/BOOTSTRAP-DATA.md b/backend/BOOTSTRAP-DATA.md new file mode 100644 index 0000000..c88a7b1 --- /dev/null +++ b/backend/BOOTSTRAP-DATA.md @@ -0,0 +1,452 @@ +# Bootstrap 预注入数据说明 + +## 📋 概述 + +Bootstrap 功能在应用启动时自动预注入初始数据,帮助快速搭建开发/测试环境。 + +**配置文件**: `config/bootstrap.json` + +--- + +## 🔧 预注入数据内容 + +### 1️⃣ 用户 (Users) + +预注入 **1 个管理员账户**: + +| 字段 | 值 | 说明 | +|------|-----|------| +| **username** | `admin` | 管理员用户名 | +| **password** | `admin123` | 初始密码(⚠️ 生产环境请修改) | +| **email** | `admin@example.com` | 邮箱地址 | + +**用途**: +- 登录后台管理系统 +- 测试用户认证功能 +- 管理集群和 Registry + +**密码加密**: 使用 bcrypt 加密存储 + +--- + +### 2️⃣ Registry (OCI 镜像仓库) + +预注入 **1 个 Harbor Registry**: + +| 字段 | 值 | 说明 | +|------|-----|------| +| **name** | `Harbor Production` | Registry 名称 | +| **url** | `https://harbor.example.com` | Registry 地址 | +| **description** | `Production Harbor Registry` | 描述 | +| **username** | `admin` | Registry 用户名 | +| **password** | `Harbor12345` | Registry 密码(加密存储) | +| **insecure** | `false` | 是否跳过 SSL 验证 | + +**用途**: +- 浏览 Helm Chart 制品 +- 拉取 OCI Artifacts +- 测试 Registry 连接 + +**密码加密**: 使用 AES 加密存储(基于 `ENCRYPTION_KEY` 环境变量) + +--- + +### 3️⃣ Kubernetes 集群 (Clusters) + +预注入 **1 个测试集群**: + +| 字段 | 值 | 说明 | +|------|-----|------| +| **name** | `Test Cluster` | 集群名称 | +| **host** | `https://kubernetes.example.com:6443` | Kubernetes API Server 地址 | +| **description** | `Test Kubernetes Cluster` | 描述 | +| **caData** | `LS0tLS1CRUdJTi1D...` | CA 证书(Base64 编码) | +| **certData** | `LS0tLS1CRUdJTi1D...` | 客户端证书(Base64 编码) | +| **keyData** | `LS0tLS1CRUdJTi1S...` | 客户端密钥(Base64 编码) | + +**用途**: +- 部署 Helm Chart 应用 +- 查看集群状态和资源 +- 测试 Kubernetes 集成 + +**证书加密**: CA/Cert/Key 使用 AES 加密存储 + +--- + +## 🎯 Bootstrap 模式 + +### Mock 模式 (run-0) +- ✅ Bootstrap 启用 +- ✅ 数据存储在内存中 +- ✅ 重启后数据重置 + +### 真实模式 (run-1, run-2) +- ✅ Bootstrap 启用 +- ✅ 数据存储在 PostgreSQL +- ✅ 重启后数据持久化 +- ⚠️ **避免重复**: 如果数据已存在会跳过创建 + +--- + +## 📝 配置文件 + +### 完整配置示例 (`config/bootstrap.json`) + +```json +{ + "enabled": true, + "users": [ + { + "username": "admin", + "password": "admin123", + "email": "admin@example.com" + } + ], + "registries": [ + { + "name": "Harbor Production", + "url": "https://harbor.example.com", + "description": "Production Harbor Registry", + "username": "admin", + "password": "Harbor12345", + "insecure": false + } + ], + "clusters": [ + { + "name": "Test Cluster", + "host": "https://kubernetes.example.com:6443", + "description": "Test Kubernetes Cluster", + "caData": "LS0tLS1CRUdJTi1DRVJUSUZJQ0FURS0tLS0t...", + "certData": "LS0tLS1CRUdJTi1DRVJUSUZJQ0FURS0tLS0t...", + "keyData": "LS0tLS1CRUdJTi1SU0EgUFJJVkFURSBLRVktLS0tLQ==" + } + ] +} +``` + +--- + +## ⚙️ 自定义配置 + +### 方式 1: 修改配置文件 + +```bash +# 编辑配置 +vim config/bootstrap.json + +# 重启应用 +make run-1 +``` + +### 方式 2: 通过环境变量 + +```bash +export BOOTSTRAP_CONFIG_JSON='{ + "enabled": true, + "users": [ + {"username": "myuser", "password": "mypass", "email": "user@example.com"} + ], + "registries": [], + "clusters": [] +}' + +make run-1 +``` + +### 方式 3: 指定配置文件路径 + +```bash +export BOOTSTRAP_CONFIG_FILE=/path/to/custom-bootstrap.json +make run-1 +``` + +--- + +## 🔒 安全建议 + +### ⚠️ 生产环境注意事项 + +1. **修改默认密码** + ```json + { + "users": [ + { + "username": "admin", + "password": "YourStrongPasswordHere" // ⚠️ 修改 + } + ] + } + ``` + +2. **设置强加密密钥** + ```bash + # 生成 32 字节随机密钥 + export ENCRYPTION_KEY=$(openssl rand -base64 32) + ``` + +3. **使用真实证书** + - 替换 `caData`, `certData`, `keyData` 为真实集群证书 + - 确保证书有效期和权限正确 + +4. **禁用 Bootstrap(可选)** + ```json + { + "enabled": false + } + ``` + +5. **删除配置文件** + ```bash + # 首次启动后删除(数据已导入) + rm config/bootstrap.json + ``` + +--- + +## 🧪 测试验证 + +### 验证用户 + +```bash +# 登录测试 +curl -X POST http://localhost:8080/api/v1/auth/login \ + -H "Content-Type: application/json" \ + -d '{ + "username": "admin", + "password": "admin123" + }' + +# 预期返回: {"token": "eyJhbGc..."} +``` + +### 验证 Registry + +```bash +# 查看 Registry 列表 +curl http://localhost:8080/api/v1/registries + +# 预期返回: +# [ +# { +# "id": "...", +# "name": "Harbor Production", +# "url": "https://harbor.example.com" +# } +# ] +``` + +### 验证 Cluster + +```bash +# 查看集群列表 +curl http://localhost:8080/api/v1/clusters + +# 预期返回: +# [ +# { +# "id": "...", +# "name": "Test Cluster", +# "host": "https://kubernetes.example.com:6443" +# } +# ] +``` + +--- + +## 📊 启动日志示例 + +### 成功的 Bootstrap 日志 + +``` +🌱 Starting bootstrap seeding... + ↳ Seeding 1 user(s)... + ✓ User 'admin' created + ↳ Seeding 1 registry(ies)... + ✓ Registry 'Harbor Production' created (credentials encrypted) + ↳ Seeding 1 cluster(s)... + ✓ Cluster 'Test Cluster' created (credentials encrypted) +✅ Bootstrap seeding completed +``` + +### 数据已存在的日志 + +``` +🌱 Starting bootstrap seeding... + ↳ Seeding 1 user(s)... + ⊙ User 'admin' already exists, skipping + ↳ Seeding 1 registry(ies)... + ⊙ Registry 'Harbor Production' already exists, skipping + ↳ Seeding 1 cluster(s)... + ⊙ Cluster 'Test Cluster' already exists, skipping +✅ Bootstrap seeding completed +``` + +--- + +## 🔄 重置数据 + +### 重置 Mock 模式数据 + +```bash +# Mock 数据存储在内存,重启即重置 +make run-0 +# Ctrl+C +make run-0 +``` + +### 重置真实数据库数据 + +```bash +# 清理并重新创建 +make clean-1 +make run-1 +``` + +--- + +## 📖 更多示例 + +### 添加多个用户 + +```json +{ + "enabled": true, + "users": [ + { + "username": "admin", + "password": "admin123", + "email": "admin@example.com" + }, + { + "username": "developer", + "password": "dev123", + "email": "dev@example.com" + }, + { + "username": "operator", + "password": "ops123", + "email": "ops@example.com" + } + ] +} +``` + +### 添加多个 Registry + +```json +{ + "registries": [ + { + "name": "Harbor Production", + "url": "https://harbor.example.com", + "username": "admin", + "password": "password1" + }, + { + "name": "Docker Hub", + "url": "https://registry-1.docker.io", + "username": "myuser", + "password": "password2" + }, + { + "name": "GitHub Container Registry", + "url": "https://ghcr.io", + "username": "github-user", + "password": "ghp_token" + } + ] +} +``` + +### 添加多个集群 + +```json +{ + "clusters": [ + { + "name": "Dev Cluster", + "host": "https://dev-k8s.example.com:6443", + "description": "Development Environment", + "caData": "...", + "certData": "...", + "keyData": "..." + }, + { + "name": "Staging Cluster", + "host": "https://staging-k8s.example.com:6443", + "description": "Staging Environment", + "caData": "...", + "certData": "...", + "keyData": "..." + }, + { + "name": "Production Cluster", + "host": "https://prod-k8s.example.com:6443", + "description": "Production Environment", + "caData": "...", + "certData": "...", + "keyData": "..." + } + ] +} +``` + +--- + +## 🛠️ 故障排查 + +### 问题 1: Bootstrap 不生效 + +**症状**: 启动后没有预注入数据 + +**检查**: +```bash +# 1. 检查配置文件是否存在 +ls -la config/bootstrap.json + +# 2. 检查 enabled 是否为 true +cat config/bootstrap.json | jq .enabled + +# 3. 查看启动日志 +# 应该看到 "Starting bootstrap seeding..." +``` + +### 问题 2: 密码不正确 + +**症状**: 无法使用预注入的用户登录 + +**原因**: 密码在配置文件中可能已修改 + +**解决**: +```bash +# 查看配置文件中的密码 +cat config/bootstrap.json | jq '.users[0].password' + +# 使用正确的密码测试 +curl -X POST http://localhost:8080/api/v1/auth/login \ + -d '{"username":"admin","password":"配置文件中的密码"}' +``` + +### 问题 3: 重复创建报错 + +**症状**: 日志显示 "duplicate key" 错误 + +**原因**: 数据库中已存在同名记录 + +**解决**: +- 正常现象,Bootstrap 会自动跳过 +- 如需重新创建,使用 `make clean-1` 清理数据库 + +--- + +## 📚 相关文档 + +- **配置示例**: `config/bootstrap.example.json` +- **代码实现**: `internal/bootstrap/seeder.go` +- **架构文档**: `docs/architecture.md` - Bootstrap 预注入章节 + +--- + +**最后更新**: 2025-11-10 + diff --git a/backend/CODE_FIRST_GUIDE.md b/backend/CODE_FIRST_GUIDE.md new file mode 100644 index 0000000..0052348 --- /dev/null +++ b/backend/CODE_FIRST_GUIDE.md @@ -0,0 +1,324 @@ +# Code First API 开发指南 + +## 🎯 概述 + +本项目现已支持 **Code First** 方式开发 API: +1. **后端**:使用 Swagger 注释从代码生成 OpenAPI 文档 +2. **前端**:使用 Orval 从 OpenAPI 文档生成 TypeScript 客户端 + +--- + +## 🛠️ 工具链 + +### 后端 +- **swaggo/swag**: 从 Go 代码注释生成 OpenAPI/Swagger 文档 +- 文档:https://github.com/swaggo/swag + +### 前端 +- **Orval**: 从 OpenAPI 规范生成 TypeScript API 客户端 +- 文档:https://orval.dev/ + +--- + +## 📝 开发流程 + +### 1. 后端:添加 Swagger 注释 + +#### 主程序注释 (cmd/api/main.go) +```go +// @title OCDP Backend API +// @version 1.0 +// @description OCDP (Open Cloud Development Platform) Backend API +// +// @host localhost:8080 +// @BasePath /api/v1 +// +// @securityDefinitions.apikey BearerAuth +// @in header +// @name Authorization +package main +``` + +#### Handler 方法注释示例 +```go +// ListArtifacts 列出 repository 中的所有 artifacts +// @Summary 列出 Repository 中的所有 Artifacts +// @Description 列出指定 Repository 中的所有 Artifact,支持按类型过滤 +// @Tags Artifacts +// @Accept json +// @Produce json +// @Param registry_id path string true "Registry ID" +// @Param repository_name path string true "Repository Name (URL encoded)" +// @Param media_type query string false "过滤类型 (all, chart, image, other)" default(all) +// @Success 200 {array} dto.TagResponse +// @Failure 500 {object} dto.ErrorResponse +// @Router /registries/{registry_id}/repositories/{repository_name}/artifacts [get] +func (h *ArtifactHandler) ListArtifacts(w http.ResponseWriter, r *http.Request) { + // ... +} +``` + +### 2. 生成 OpenAPI 文档 + +```bash +# 在 backend 目录下运行 +cd backend +swag init -g cmd/api/main.go -o docs/swagger --parseDependency --parseInternal + +# 生成的文件在 docs/swagger/ 目录: +# - swagger.json +# - swagger.yaml +# - docs.go +``` + +### 3. 前端:重新生成 API 客户端 + +```bash +# 在 frontend 目录下运行 +cd frontend +npm run openapi-gen + +# 或者 +orval +``` + +生成的文件在 `src/api/generated-orval/` 目录。 + +--- + +## 🔄 完整工作流 + +### 添加新 API 的步骤 + +#### 1. 后端开发 +```bash +# 1.1 创建 DTO +# internal/adapter/input/http/dto/new_feature_dto.go + +# 1.2 创建 Handler 并添加 Swagger 注释 +# internal/adapter/input/http/rest/new_feature_handler.go + +# 1.3 注册路由 +# cmd/api/main.go + +# 1.4 生成 OpenAPI 文档 +cd backend +swag init -g cmd/api/main.go -o docs/swagger --parseDependency --parseInternal + +# 1.5 复制到主文档 (可选) +cp docs/swagger/swagger.yaml docs/openapi.yaml +``` + +#### 2. 前端开发 +```bash +# 2.1 重新生成 API 客户端 +cd frontend +npm run openapi-gen + +# 2.2 使用生成的客户端 +import { listArtifacts } from '@/api/generated-orval/api'; + +const data = await listArtifacts({ + registryId: 'xxx', + repositoryName: 'charts/nginx' +}); +``` + +--- + +## 📋 Swagger 注释速查 + +### 常用标签 + +| 标签 | 说明 | 示例 | +|------|------|------| +| `@Summary` | 简短描述 | `@Summary 列出所有用户` | +| `@Description` | 详细描述 | `@Description 返回系统中的所有用户列表` | +| `@Tags` | API 分组 | `@Tags Users` | +| `@Accept` | 接受的格式 | `@Accept json` | +| `@Produce` | 返回的格式 | `@Produce json` | +| `@Param` | 参数 | `@Param id path string true "User ID"` | +| `@Success` | 成功响应 | `@Success 200 {object} dto.UserResponse` | +| `@Failure` | 失败响应 | `@Failure 404 {object} dto.ErrorResponse` | +| `@Router` | 路由定义 | `@Router /users/{id} [get]` | +| `@Security` | 安全认证 | `@Security BearerAuth` | + +### 参数类型 + +```go +// 路径参数 (path) +@Param id path string true "User ID" + +// 查询参数 (query) +@Param page query int false "Page number" default(1) +@Param size query int false "Page size" default(20) + +// 请求体 (body) +@Param request body dto.CreateUserRequest true "User data" + +// Header 参数 (header) +@Param X-Request-ID header string false "Request ID" +``` + +### 响应定义 + +```go +// 单个对象 +@Success 200 {object} dto.UserResponse + +// 数组 +@Success 200 {array} dto.UserResponse + +// 自定义响应 +@Success 200 {object} map[string]interface{} + +// 多个可能的响应 +@Success 200 {object} dto.UserResponse "Success" +@Success 201 {object} dto.UserResponse "Created" +@Failure 400 {object} dto.ErrorResponse "Bad Request" +@Failure 404 {object} dto.ErrorResponse "Not Found" +@Failure 500 {object} dto.ErrorResponse "Internal Error" +``` + +--- + +## 🎯 最佳实践 + +### 1. 命名规范 + +| 层级 | 风格 | 示例 | +|------|------|------| +| **路径变量** | snake_case | `{registry_id}`, `{repository_name}` | +| **查询参数** | snake_case | `?media_type=chart` | +| **JSON 字段** | camelCase | `repositoryName`, `mediaType` | +| **Go 结构体** | PascalCase | `RepositoryName`, `MediaType` | + +### 2. DTO 定义示例 + +```go +// ArtifactResponse Artifact 响应 +type ArtifactResponse struct { + RepositoryName string `json:"repositoryName"` // Go: PascalCase, JSON: camelCase + Tag string `json:"tag"` + Type string `json:"type"` + Size int64 `json:"size"` + CreatedAt string `json:"createdAt"` +} +``` + +### 3. 完整的 Handler 示例 + +```go +// GetArtifact 获取 artifact 详情 +// @Summary 获取 Artifact 详情 +// @Description 获取指定 Artifact 的详细信息 +// @Tags Artifacts +// @Accept json +// @Produce json +// @Param registry_id path string true "Registry ID" +// @Param repository_name path string true "Repository Name (URL encoded)" +// @Param reference path string true "Artifact Reference (tag or digest)" +// @Success 200 {object} dto.ArtifactResponse +// @Failure 404 {object} dto.ErrorResponse "Artifact not found" +// @Failure 500 {object} dto.ErrorResponse "Internal server error" +// @Router /registries/{registry_id}/repositories/{repository_name}/artifacts/{reference} [get] +func (h *ArtifactHandler) GetArtifact(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + registryID := vars["registry_id"] + repositoryName := vars["repository_name"] + reference := vars["reference"] + + artifact, err := h.artifactService.GetArtifact(r.Context(), registryID, repositoryName, reference) + if err != nil { + respondError(w, http.StatusNotFound, "Artifact not found", err.Error()) + return + } + + response := &dto.ArtifactResponse{ + RepositoryName: artifact.Repository, + Tag: artifact.Tag, + Digest: artifact.Digest, + Type: string(artifact.Type), + Size: artifact.Size, + CreatedAt: artifact.CreatedAt.Format("2006-01-02T15:04:05Z07:00"), + } + + respondJSON(w, http.StatusOK, response) +} +``` + +--- + +## 🔧 故障排除 + +### 问题 1: swag 命令未找到 +```bash +# 解决方案:安装 swag +go install github.com/swaggo/swag/cmd/swag@latest + +# 确保 $GOPATH/bin 在 PATH 中 +export PATH=$PATH:$(go env GOPATH)/bin +``` + +### 问题 2: 生成的文档不完整 +```bash +# 确保使用了正确的参数 +swag init -g cmd/api/main.go -o docs/swagger --parseDependency --parseInternal + +# 检查注释格式是否正确 +# 注释必须紧邻函数定义,中间不能有空行 +``` + +### 问题 3: 前端客户端生成失败 +```bash +# 检查 OpenAPI 文档是否有效 +cd frontend +npx @apidevtools/swagger-cli validate ../backend/docs/openapi.yaml + +# 清理并重新生成 +rm -rf src/api/generated-orval +npm run openapi-gen +``` + +--- + +## 📚 参考资源 + +- **Swaggo 文档**: https://github.com/swaggo/swag +- **Swaggo 声明式注释**: https://github.com/swaggo/swag#declarative-comments-format +- **Orval 文档**: https://orval.dev/ +- **OpenAPI 规范**: https://swagger.io/specification/ + +--- + +## ✅ 当前状态 + +### 已完成 +- [x] 安装 swag 工具 +- [x] 添加主程序 Swagger 注释 +- [x] 为 Artifact Handler 添加完整注释 +- [x] 生成 OpenAPI 文档 +- [x] 前端配置 Orval +- [x] 重新生成前端客户端 + +### 待完成 (可选) +- [ ] 为其他 Handler 添加 Swagger 注释 + - [ ] Auth Handler + - [ ] Cluster Handler + - [ ] Registry Handler + - [ ] Instance Handler + - [ ] Monitoring Handler +- [ ] 添加更详细的错误响应定义 +- [ ] 添加请求/响应示例 +- [ ] 配置 CI/CD 自动生成文档 + +--- + +## 🎉 总结 + +现在项目支持 Code First 开发流程: +1. **后端开发者**:写代码 + 注释 → 生成 OpenAPI +2. **前端开发者**:使用 OpenAPI → 生成 TypeScript 客户端 +3. **文档自动同步**:代码即文档,保证一致性 + +这确保了 API 文档始终与代码保持同步!✨ diff --git a/backend/DEVELOPMENT.md b/backend/DEVELOPMENT.md new file mode 100644 index 0000000..6311c8f --- /dev/null +++ b/backend/DEVELOPMENT.md @@ -0,0 +1,223 @@ +# 开发环境快速指南 + +## 📋 前置要求 + +- Go 1.21+ +- Docker & Docker Compose +- Air(热加载工具): `go install github.com/cosmtrek/air@latest` + +## 🎯 设计理念 + +本项目使用 **Docker Compose Profile** 机制,通过单一的 `docker-compose.yml` 文件支持三种运行模式,避免配置文件重复。 + +## 🚀 三种运行模式 + +### 模式 0: Mock 模式(最快) + +纯本地运行,无需任何外部依赖,所有服务都是 Mock。 + +```bash +make run-0 +``` + +**特点:** +- ✅ 启动最快(秒启动) +- ✅ 无需 Docker +- ✅ 支持热加载 +- ✅ 适合快速功能开发 + +**停止:** `Ctrl+C` + +--- + +### 模式 1: 开发模式(推荐) + +Docker 提供真实的 PostgreSQL,后端在本地运行并支持热加载。 + +```bash +make run-1 +``` + +**特点:** +- ✅ 真实的数据库 +- ✅ 支持热加载 +- ✅ 代码修改实时生效 +- ✅ 适合日常开发 + +**停止:** `Ctrl+C` 停止后端(数据库容器继续运行) + +**清理:** `make clean-1` 清理数据库和临时文件 + +--- + +### 模式 2: 生产模式 + +所有服务完全容器化,模拟生产环境。 + +```bash +make run-2 +``` + +**特点:** +- ✅ 完全容器化 +- ✅ 接近生产环境 +- ✅ 后台运行 +- ❌ 无热加载 + +**查看日志:** +```bash +docker compose --profile backend logs -f +``` + +**停止:** +```bash +docker compose --profile backend down +``` + +**清理:** `make clean-2` 清理所有容器和构建产物 + +--- + +## 🧹 清理命令 + +```bash +# 清理模式 1 的产物(依赖容器 + 临时文件) +make clean-1 + +# 清理模式 2 的产物(所有容器 + 构建产物) +make clean-2 +``` + +--- + +## 📊 常用工作流 + +### 日常开发 + +```bash +# 启动开发环境 +make run-1 + +# ... 编码、测试 ... +# 代码修改会自动重新编译 + +# 下班停止(Ctrl+C) +# 第二天继续(依赖容器还在运行,直接启动) +make run-1 +``` + +### 重置数据库 + +```bash +# 清理并重新开始 +make clean-1 +make run-1 +``` + +### 测试生产部署 + +```bash +# 启动生产模式 +make run-2 + +# 查看日志 +docker compose -f docker-compose.prod.yml logs -f + +# 测试完成后清理 +make clean-2 +``` + +--- + +## 🔧 环境变量配置 + +复制并修改环境变量配置: + +```bash +cp env.example .env +``` + +主要配置项: + +- `ADAPTER_MODE`: 设置为 `mock` 启用 Mock 模式(模式 0) +- `POSTGRES_*`: 数据库配置(模式 1、2) +- `JWT_SECRET`: JWT 密钥(生产环境必须修改) +- `ENCRYPTION_KEY`: 加密密钥(必须 32 字节) + +--- + +## 📂 文件说明 + +| 文件 | 用途 | +|------|------| +| `Makefile` | 开发命令入口(5个核心命令) | +| `.air.toml` | 热加载配置 | +| `docker-compose.yml` | 统一配置文件(使用 profile 区分模式) | +| `Dockerfile` | 生产镜像构建配置 | +| `DEVELOPMENT.md` | 本文档 | + +### Docker Compose Profile 说明 + +- **无 profile**: 只启动 postgres(用于模式 1) +- **--profile backend**: 启动 postgres + backend(用于模式 2) +- **--profile mock**: 启动 backend-mock(独立的 mock 模式容器) + +--- + +## ❓ 常见问题 + +### Q: 模式 1 启动失败,提示端口被占用? + +A: 检查是否有其他 PostgreSQL 实例在运行: +```bash +docker ps | grep postgres +# 或者修改 .env 中的 POSTGRES_PORT +``` + +### Q: 如何查看依赖服务状态? + +A: +```bash +docker compose ps # 模式 1(只有 postgres) +docker compose --profile backend ps # 模式 2(postgres + backend) +``` + +### Q: 模式 1 和模式 2 的数据库数据会互相影响吗? + +A: 会共享同一个数据库容器和数据卷 `ocdp-postgres-data`。 +- 模式 1: 使用本地 air 连接到 postgres 容器 +- 模式 2: 使用 backend 容器连接到 postgres 容器 +- 数据是持久化的,切换模式不会丢失数据 + +### Q: 如何完全清理所有 Docker 资源? + +A: +```bash +make clean-1 +make clean-2 +docker system prune -a --volumes +``` + +--- + +## 💡 提示 + +- **首次使用**: 推荐从 `make run-0` 开始,快速验证环境 +- **日常开发**: 使用 `make run-1`,享受热加载和真实数据库 +- **部署前测试**: 使用 `make run-2`,确保容器化部署没问题 +- **数据持久化**: 模式 1 和 2 的数据库数据都会持久化,`Ctrl+C` 不会丢失数据 +- **完全重置**: 使用 `clean-*` 命令会删除数据卷,数据会丢失 + +--- + +## 🎯 快速参考 + +```bash +make # 显示帮助 +make run-0 # Mock 模式 +make run-1 # 开发模式 +make run-2 # 生产模式 +make clean-1 # 清理开发环境 +make clean-2 # 清理生产环境 +``` + diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..ae98b1f --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,41 @@ +# ================================================== +# OCDP Backend - Dockerfile +# 构建 backend 服务(连接 PostgreSQL) +# ================================================== +FROM golang:1.24-alpine AS builder + +RUN apk add --no-cache git make + +WORKDIR /build + +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . +RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags="-w -s" -o ocdp-backend cmd/api/main.go + +# ================================================== +# Runtime +# ================================================== +FROM alpine:latest + +RUN apk --no-cache add ca-certificates curl + +RUN addgroup -g 1000 ocdp && \ + adduser -D -u 1000 -G ocdp ocdp + +WORKDIR /app + +COPY --from=builder /build/ocdp-backend . +COPY --from=builder /build/config ./config + +RUN chown -R ocdp:ocdp /app + +USER ocdp + +EXPOSE 8080 + +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:8080/health || exit 1 + +CMD ["./ocdp-backend"] diff --git a/backend/Makefile b/backend/Makefile new file mode 100644 index 0000000..f6ea75a --- /dev/null +++ b/backend/Makefile @@ -0,0 +1,76 @@ +.PHONY: help run-0 run-1 run-2 clean-1 clean-2 + +.DEFAULT_GOAL := help + +help: + @echo "" + @echo "╔════════════════════════════════════════════════════╗" + @echo "║ Backend Development ║" + @echo "╚════════════════════════════════════════════════════╝" + @echo "" + @echo "🚀 Run:" + @echo "" + @echo " make run-0 Hot reload + Mock" + @echo " • No dependencies" + @echo " • Ctrl+C to stop" + @echo "" + @echo " make run-1 Hot reload + Deps in container" + @echo " • Real PostgreSQL (Docker)" + @echo " • Ctrl+C to stop backend" + @echo "" + @echo " make run-2 All in container (background)" + @echo " • Everything in Docker" + @echo " • Use 'docker compose down' to stop" + @echo "" + @echo "🧹 Clean:" + @echo "" + @echo " make clean-1 Clean run-1 artifacts" + @echo " (deps containers + volumes + tmp/)" + @echo "" + @echo " make clean-2 Clean run-2 artifacts" + @echo " (all containers + volumes + bin/)" + @echo "" + +# ============================================ +# Run Commands +# ============================================ + +run-0: + @echo "🎭 Run-0: Hot reload + Mock" + @echo "────────────────────────────────" + ADAPTER_MODE=mock air -c .air.toml + +run-1: + @echo "🔥 Run-1: Hot reload + Deps in container" + @echo "────────────────────────────────" + @docker compose up -d postgres + @sleep 5 + @echo "✅ Dependencies ready, starting backend..." + @echo "" + ADAPTER_MODE=production \ + DATABASE_URL=postgresql://postgres:postgres@localhost:5432/ocdp?sslmode=disable \ + air -c .air.toml + +run-2: + @echo "⚡ Run-2: All in container (background)" + @echo "────────────────────────────────" + @echo "→ Starting backend stack via Docker Compose (postgres + backend)" + ADAPTER_MODE=production docker compose --profile backend up -d + @echo "" + @echo "✅ Backend stack running in Docker (use 'docker compose --profile backend down' to stop)" + +# ============================================ +# Clean Commands +# ============================================ + +clean-1: + @echo "🧹 Cleaning run-1 artifacts..." + @docker compose down -v + @rm -rf tmp/ + @echo "✅ run-1 cleaned" + +clean-2: + @echo "🧹 Cleaning run-2 artifacts..." + @docker compose --profile backend down -v + @rm -rf bin/ dist/ + @echo "✅ run-2 cleaned" diff --git a/backend/QUICK-REFERENCE.md b/backend/QUICK-REFERENCE.md new file mode 100644 index 0000000..1e64064 --- /dev/null +++ b/backend/QUICK-REFERENCE.md @@ -0,0 +1,133 @@ +# 快速参考卡片 + +## 🚀 五个核心命令 + +```bash +make run-0 # Mock 模式(秒启动,无需 Docker) +make run-1 # 开发模式(Docker 依赖 + 本地热加载) +make run-2 # 生产模式(全部容器化) +make clean-1 # 清理开发环境 +make clean-2 # 清理生产环境 +``` + +--- + +## 📋 模式对比 + +| 特性 | run-0 | run-1 | run-2 | +|------|-------|-------|-------| +| **后端位置** | 本地 | 本地 | 容器 | +| **数据库** | Mock | 容器 | 容器 | +| **热加载** | ✅ | ✅ | ❌ | +| **启动速度** | ⚡ 秒启 | 🔥 5秒 | 🐳 30秒 | +| **适用场景** | 快速开发 | 日常开发 | 部署测试 | +| **依赖 Docker** | ❌ | ✅ | ✅ | + +--- + +## 🎯 使用场景 + +### 场景 1: 快速功能开发 +```bash +make run-0 +# 修改代码,自动重新编译 +# Ctrl+C 停止 +``` + +### 场景 2: 日常开发(推荐) +```bash +make run-1 +# 修改代码,自动重新编译 +# Ctrl+C 停止后端(数据库继续运行) + +# 第二天继续 +make run-1 # 数据库还在,直接启动 + +# 重置数据库 +make clean-1 +make run-1 +``` + +### 场景 3: 部署前测试 +```bash +make run-2 +# 查看日志 +docker compose --profile backend logs -f + +# 测试完成 +make clean-2 +``` + +--- + +## 🔧 Docker Compose 直接使用 + +### 启动依赖(run-1 等价) +```bash +docker compose up -d postgres +go run cmd/api/main.go +``` + +### 启动完整环境(run-2 等价) +```bash +docker compose --profile backend up -d +``` + +### 查看状态 +```bash +docker compose ps # run-1 +docker compose --profile backend ps # run-2 +``` + +### 停止 +```bash +docker compose down # run-1 +docker compose --profile backend down # run-2 +``` + +--- + +## 📂 关键文件 + +| 文件 | 说明 | +|------|------| +| `Makefile` | 5个核心命令 | +| `docker-compose.yml` | 使用 profile 的统一配置 | +| `.air.toml` | 热加载配置 | +| `env.example` | 环境变量模板 | + +--- + +## 🐳 Docker Compose Profile 原理 + +```yaml +# docker-compose.yml +services: + postgres: + # 默认启动(无需 profile) + + backend: + profiles: [backend] # 需要 --profile backend +``` + +**使用方式**: +```bash +docker compose up -d # 只启动 postgres +docker compose --profile backend up -d # 启动 postgres + backend +``` + +--- + +## 💡 快速提示 + +- 首次使用运行: `go install github.com/cosmtrek/air@latest` +- 查看帮助: `make` 或 `make help` +- 完整文档: 查看 `DEVELOPMENT.md` +- 审查报告: 查看 `REVIEW.md` + +--- + +## 🎯 一句话总结 + +**三种模式,五个命令,一个 Docker Compose 文件。** + diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..e0d47b2 --- /dev/null +++ b/backend/README.md @@ -0,0 +1,343 @@ +# OCDP Backend + +基于 Go 的 Kubernetes Helm Chart 管理服务后端,提供完整的制品浏览和应用部署能力。 + +## ✨ 特性 + +- 🎪 **Helm Chart 管理** - 完整的 Helm 生命周期支持 +- 📦 **多 Registry 支持** - Harbor、Docker Hub、GHCR 等 +- 🔍 **Artifact 浏览** - 自动识别类型、大小、metadata +- 🚀 **OCI 标准兼容** - 使用 ORAS Go SDK v2 +- 🖥️ **多集群支持** - 管理多个 Kubernetes 集群 +- 🔐 **认证支持** - JWT + 密码加密 +- 📊 **实时状态** - Helm Release 状态监控 +- 🏗️ **六边形架构** - 清晰的分层设计,易于测试和扩展 +- 🔄 **双模式支持** - Mock 模式(开发调试)+ 默认模式(真实 PostgreSQL) + +## 🚀 快速开始 + +### 方式 1: Mock 模式(最快) + +适合快速功能开发和 API 测试,无需数据库。 + +```bash +# 安装 Air(首次) +go install github.com/air-verse/air@latest + +# 启动 Mock 模式 +make dev-mock +``` + +### 方式 2: 本地 Backend + Docker 数据库(推荐日常开发) + +支持数据持久化和热重载。 + +```bash +# 启动数据库 +docker compose up -d postgres + +# 启动 Backend +make dev +``` + +### 方式 3: 完全容器化(生产部署) + +适合生产环境和 CI/CD。 + +```bash +# 启动完整服务 +make prod + +# 或 +docker compose --profile backend up -d +``` + +### 本地运行 + +```bash +# Mock 模式(快速测试) +make run-mock + +# 或 +export ADAPTER_MODE=mock +go run cmd/api/main.go + +# Production 模式(需要数据库) +make run-prod +``` + +### 验证服务 + +```bash +# 健康检查 +curl http://localhost:8080/health + +# 查看 API +curl http://localhost:8080/api/v1/registries | jq + +# 访问 Swagger UI (交互式 API 文档) +open http://localhost:8080/api/docs +``` + +## 📚 文档 + +| 文档 | 说明 | +|------|------| +| [快速开始](QUICK-START.md) | **快速开始指南** - 3分钟上手 ⭐ | +| [命令速查表](COMMANDS.md) | **Make 命令参考** - 所有命令详解 ⭐ | +| [部署指南](DEPLOYMENT-GUIDE.md) | **完整部署指南** - Mock 和默认模式 | +| [架构文档](docs/architecture.md) | 六边形架构、目录结构、开发指南 | +| [OpenAPI 规范](docs/openapi.yaml) | **OpenAPI 3.0 规范** - 标准 API 定义 | +| [Swagger UI](http://localhost:8080/api/docs) | **交互式 API 文档** - 在线测试 API 🚀 | +| [API 与测试](docs/api-and-test.md) | REST API 参考文档 + 测试指南 | + +### 🎯 API 文档使用指南 + +**方式 1: Swagger UI (推荐)** ⭐ + +启动服务后访问:[http://localhost:8080/api/docs](http://localhost:8080/api/docs) + +特性: +- 📖 交互式文档 - 所有 API 可视化展示 +- 🔧 在线测试 - 直接在浏览器中测试 API +- 🔐 认证支持 - 支持 JWT Token 认证 +- 📦 Schema 查看 - 查看所有请求/响应模型 + +**方式 2: OpenAPI 规范文件** + +```bash +# 查看规范文件 +cat docs/openapi.yaml + +# 使用 OpenAPI 工具生成客户端 +openapi-generator-cli generate -i docs/openapi.yaml -g go -o ./client + +# 在线验证 +curl http://localhost:8080/api/docs/openapi.yaml +``` + +**方式 3: Markdown 文档** + +查看 [docs/api-and-test.md](docs/api-and-test.md) - 完整的 API 参考文档 + +## 🏗️ 架构概览 + +采用**六边形架构**(Hexagonal Architecture),实现清晰的分层和依赖倒置: + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Input Adapters │ +│ (HTTP REST API) │ +└──────────────────────┬──────────────────────────────────────┘ + │ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ Domain Layer │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ Entities │ │ Services │ │ Interfaces │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +└──────────────────────┬──────────────────────────────────────┘ + │ + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ Output Adapters │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ Database │ │ OCI Client │ │ Helm Client │ │ +│ │ (Mock/Prod) │ │ (Mock/ORAS) │ │ (Mock/Real) │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +详见 [架构文档](docs/architecture.md) + +## 🎯 核心 API + +| 分类 | 端点 | 说明 | +|------|------|------| +| **认证** | `POST /api/v1/auth/login` | 用户登录 | +| **集群** | `GET /api/v1/clusters` | 列出集群 | +| | `POST /api/v1/clusters` | 创建集群 | +| **Registry** | `GET /api/v1/registries` | 列出 Registry | +| | `POST /api/v1/registries` | 创建 Registry | +| **Artifact** | `GET /api/v1/registries/{id}/repositories` | 列出仓库 | +| | `GET /api/v1/registries/{id}/repositories/{repo}/artifacts` | 列出制品 | +| **实例** | `POST /api/v1/clusters/{id}/instances` | 安装应用 | +| | `GET /api/v1/clusters/{id}/instances` | 列出实例 | +| | `PUT /api/v1/clusters/{id}/instances/{instanceId}` | 升级应用 | +| | `DELETE /api/v1/clusters/{id}/instances/{instanceId}` | 卸载应用 | +| | `GET /api/v1/clusters/{id}/instances/{instanceId}/entries` | 查看实例入口 | +| **监控** | `GET /api/v1/monitoring/summary` | 监控摘要 | + +完整 API 文档: [docs/api.md](docs/api.md) + +## 🔧 开发 + +### 环境要求 + +- Go 1.21+ +- PostgreSQL 15+ (生产模式) +- Docker & Docker Compose (可选) + +### 常用命令 + +```bash +# 查看所有命令 +make help + +# 开发 +make dev # 开发模式(热重载) +make build # 构建 +make run-mock # Mock 模式运行 +make run-prod # Production 模式运行 + +# Docker Compose +make mock # Mock 模式 +make prod # 生产模式 +make logs # 查看日志 +make status # 查看状态 +make stop # 停止服务 + +# 数据库 +make db-up # 启动数据库 +make db-psql # 连接数据库 +make db-backup # 备份数据库 +make pgadmin # 启动 pgAdmin +``` + +### 项目结构 + +``` +backend/ +├── cmd/api/ # 程序入口 +├── internal/ +│ ├── domain/ # 🎯 领域层(核心) +│ │ ├── entity/ # 实体 +│ │ ├── service/ # 业务逻辑 +│ │ └── repository/ # 接口定义 +│ ├── adapter/ +│ │ ├── input/http/ # 📥 REST API +│ │ └── output/ # 📤 数据库、OCI、Helm +│ ├── bootstrap/ # Bootstrap 预注入 +│ └── pkg/ # 🔧 工具包 +├── docs/ # 📚 文档 +├── config/ # ⚙️ 配置 +└── scripts/ # 🛠️ 脚本 +``` + +详见 [架构文档](docs/architecture.md) + +## 🔐 安全配置 + +### 环境变量 + +```bash +# 必需配置 +ADAPTER_MODE=production +JWT_SECRET=your-jwt-secret +ENCRYPTION_KEY=your-32-character-encryption-key +DATABASE_URL=postgresql://user:pass@host:5432/ocdp + +# 生成安全密钥 +openssl rand -base64 32 +``` + +### Bootstrap 预注入 + +在 `config/bootstrap.json` 中配置初始数据: + +```json +{ + "enabled": true, + "users": [ + {"username": "admin", "password": "admin123", "email": "admin@example.com"} + ], + "registries": [ + {"name": "harbor", "url": "https://harbor.example.com", "username": "admin", "password": "secret"} + ], + "clusters": [ + {"name": "prod", "host": "https://k8s.example.com:6443", "caData": "...", "certData": "...", "keyData": "..."} + ] +} +``` + +详见 [架构文档 - Bootstrap 预注入](docs/architecture.md#bootstrap-预注入) + +## 🌐 服务访问 + +| 服务 | 地址 | 说明 | +|------|------|------| +| Backend API | http://localhost:8080/api/v1 | REST API | +| Health Check | http://localhost:8080/health | 健康检查 | +| PostgreSQL | localhost:5432 | 数据库 | +| pgAdmin | http://localhost:5050 | 数据库管理 | + +## 🐛 故障排查 + +### 常见问题 + +**端口被占用**: +```bash +# 修改 .env 中的 BACKEND_PORT +BACKEND_PORT=8081 +``` + +**数据库连接失败**: +```bash +# 检查数据库状态 +docker compose ps postgres +docker compose logs postgres +``` + +**完全重置**: +```bash +# 停止并删除所有数据 +docker compose down -v +docker compose --profile production up -d +``` + +更多问题参见 [部署文档 - 故障排查](docs/deployment.md#故障排查) + +## 📊 技术栈 + +| 组件 | 技术 | +|------|------| +| **语言** | Go 1.21+ | +| **Web 框架** | gorilla/mux | +| **OCI 客户端** | ORAS Go SDK v2 | +| **Helm 集成** | Helm SDK | +| **Kubernetes** | client-go | +| **数据库** | PostgreSQL 15+ | +| **容器化** | Docker, Docker Compose | +| **热重载** | Air | + +## 🔗 相关资源 + +### 规范和文档 +- [OCI Distribution Specification](https://github.com/opencontainers/distribution-spec) +- [OCI Image Specification](https://github.com/opencontainers/image-spec) +- [Helm Documentation](https://helm.sh/docs/) + +### 使用的库 +- [Gorilla Mux](https://github.com/gorilla/mux) - HTTP 路由 +- [ORAS Go SDK](https://oras.land/docs/category/go-library) - OCI Registry 操作 +- [Helm SDK](https://helm.sh/docs/topics/advanced/) - Helm 操作 +- [Kubernetes Client-Go](https://github.com/kubernetes/client-go) - K8s API 客户端 + +## 📝 待办事项 + +- [ ] 添加单元测试和集成测试 +- [ ] 实现 Rate Limiting +- [ ] 添加审计日志 +- [ ] 实现 Webhook 通知 +- [ ] 支持更多 OCI Registry 类型 +- [ ] 添加 Metrics 和 Tracing + +## 📄 License + +MIT License + +--- + +**Version**: 2.2.0 +**Last Updated**: 2025-11-09 +**Port**: 8080 (default) diff --git a/backend/REVIEW.md b/backend/REVIEW.md new file mode 100644 index 0000000..161d416 --- /dev/null +++ b/backend/REVIEW.md @@ -0,0 +1,283 @@ +# 项目实现审查报告 + +## ✅ 要求对照检查 + +### 1. 文档结构 ✅ + +#### 1.1 README.md ✅ +- 位置: `/backend/README.md` +- 状态: ✅ 已存在 +- 内容: 项目概述、快速开始、API文档、架构概览 + +#### 1.2 docs/ ✅ +所有文档齐全: + +##### 2.1 architecture.md ✅ +- 位置: `/backend/docs/architecture.md` +- 状态: ✅ 已存在 +- 内容完整包含: + - ✅ 2.1.1 **需求描述** (Requirement Description) + - 项目背景 + - 核心需求 + - 功能需求 + - 非功能需求 + - ✅ 2.1.2 **业务建模** (Business Modeling) + - 业务领域 + - 核心实体 + - 业务流程 + - 用例场景 + - ✅ 2.1.3 **技术建模** (Technical Modeling) + - ✅ 2.1.3.1 **六边形架构** (Hexagonal Architecture) + - 架构图 + - 层次说明 + - 依赖倒置原则 + - ✅ 2.1.3.2 **技术选型** (Technology Selection) + - Go 语言 + - Web 框架 + - OCI/Helm 客户端 + - 数据库选型 + +##### 2.2 api-and-test.md ✅ +- 位置: `/backend/docs/api-and-test.md` +- 状态: ✅ 已存在 +- 内容: REST API 文档、测试指南 + +##### 2.3 deployment.md ✅ +- 位置: `/backend/docs/deployment.md` +- 状态: ✅ 已存在 +- 内容: 部署指南、环境配置 + +--- + +### 2. 开发模式 ✅ + +#### 3.1 Mode 0: Hot Reload + Mock ✅ +```bash +make run-0 +``` +- ✅ 热加载支持 (Air) +- ✅ Mock 所有依赖 (内存实现) +- ✅ 无需任何容器 +- ✅ 使用环境变量 `ADAPTER_MODE=mock` + +**实现方式**: +```makefile +run-0: + ADAPTER_MODE=mock air -c .air.toml +``` + +#### 3.2 Mode 1: Hot Reload + Real Deps in Container ✅ +```bash +make run-1 +``` +- ✅ 热加载支持 (Air) +- ✅ 真实依赖 (PostgreSQL) 运行在容器中 +- ✅ 后端代码在本地运行 +- ✅ 使用 Docker Compose **无 profile** 启动 postgres + +**实现方式**: +```makefile +run-1: + @docker compose up -d postgres + @sleep 5 + ADAPTER_MODE= \ + DATABASE_URL=postgresql://postgres:postgres@localhost:5432/ocdp?sslmode=disable \ + air -c .air.toml +``` + +#### 3.3 Mode 2: All in Container ✅ +```bash +make run-2 +``` +- ✅ 所有服务容器化 +- ✅ 无热加载(生产模式) +- ✅ 使用 Docker Compose **--profile backend** + +**实现方式**: +```makefile +run-2: + @docker compose --profile backend up --build -d +``` + +--- + +### 3. Makefile 设计 ✅ + +#### 核心要求: 使用 Profile 共享单一 Docker Compose ✅ + +**✅ 已实现**: 使用 `docker-compose.yml` + Profile 机制 + +| 命令 | 功能 | Docker Compose 用法 | +|------|------|---------------------| +| `make run-0` | Mock 模式 | 不使用 Docker | +| `make run-1` | 开发模式 | `docker compose up -d postgres` (无 profile) | +| `make run-2` | 生产模式 | `docker compose --profile backend up -d` | +| `make clean-1` | 清理 run-1 | `docker compose down -v` | +| `make clean-2` | 清理 run-2 | `docker compose --profile backend down -v` | + +#### Docker Compose Profile 配置 ✅ + +文件: `/backend/docker-compose.yml` + +```yaml +services: + postgres: + # 默认启动(无需 profile) + # 用于 run-1 + + backend: + profiles: + - backend # 需要 --profile backend 才启动 + # 用于 run-2 + + backend-mock: + profiles: + - mock # 可选的 mock 容器模式 +``` + +**优点**: +- ✅ 单一配置文件,避免重复 +- ✅ Profile 清晰区分不同模式 +- ✅ 符合 Docker Compose 最佳实践 + +--- + +## 📊 完整流程对照 + +### Process 1: Product Description ✅ + +#### 1.1 Requirement ✅ +- 文档: `docs/architecture.md` - 2.1.1 需求描述 +- 内容: 项目背景、核心需求、功能需求、非功能需求 + +#### 1.2 What API I Need ✅ +- 文档: `docs/api-and-test.md` +- 内容: REST API 端点定义、请求/响应示例 + +#### 1.3 Generator Docs ✅ +所有文档已完整生成: +- ✅ `docs/architecture.md` - 架构文档 + - ✅ Business Modeling - 业务建模 + - ✅ Technical Modeling - 技术建模 + - ✅ Hexagonal Architecture - 六边形架构 + - ✅ Technology Selection - 技术选型 +- ✅ `docs/api-and-test.md` - API 文档 +- ✅ `docs/deployment.md` - 部署文档 + +--- + +### Process 2: Development ✅ + +#### 2.1 Develop Mode 1, 2, 3 ✅ +- ✅ Mode 0 (Mock): `make run-0` +- ✅ Mode 1 (Dev): `make run-1` +- ✅ Mode 2 (Prod): `make run-2` + +--- + +### Process 3: Provide Makefile ✅ + +#### 3.1 Five Commands ✅ +```bash +make run-0 # ✅ Hot reload + Mock (memory) +make run-1 # ✅ Hot reload + Real deps (container) +make run-2 # ✅ All in container +make clean-1 # ✅ Clean run-1 artifacts +make clean-2 # ✅ Clean run-2 artifacts +``` + +#### 3.2 Using Profile to Share Docker Compose ✅ +- ✅ 使用单一的 `docker-compose.yml` +- ✅ 通过 `--profile backend` 区分 mode 1 和 mode 2 +- ✅ Mode 1: `docker compose up -d postgres` (无 profile) +- ✅ Mode 2: `docker compose --profile backend up -d` + +--- + +## 🎯 核心设计亮点 + +### 1. Docker Compose Profile 机制 ⭐⭐⭐⭐⭐ + +**传统做法** (❌ 不推荐): +``` +docker-compose.dev.yml # 重复配置 postgres +docker-compose.prod.yml # 重复配置 postgres +``` + +**本项目做法** (✅ 推荐): +```yaml +# docker-compose.yml (单一文件) +services: + postgres: # 默认启动,run-1 和 run-2 共享 + backend: + profiles: [backend] # 只在 run-2 时启动 +``` + +**优势**: +- ✅ 配置不重复 (DRY 原则) +- ✅ 维护简单 (只需修改一个文件) +- ✅ 数据共享 (run-1 和 run-2 使用同一个数据库) + +### 2. 三种模式的清晰定位 + +| 模式 | 场景 | 启动速度 | 真实依赖 | 热加载 | +|------|------|---------|---------|--------| +| Mode 0 | 快速开发、单元测试 | ⚡ 秒启动 | ❌ Mock | ✅ | +| Mode 1 | 日常开发、集成测试 | 🔥 5秒 | ✅ | ✅ | +| Mode 2 | 部署前测试、生产环境 | 🐳 30秒 | ✅ | ❌ | + +### 3. 命令语义清晰 + +```bash +run-0 → 0 依赖(Mock all) +run-1 → 1 部分容器化(Deps in container) +run-2 → 2 完全容器化(All in container) + +clean-1 → 清理 run-1 的产物 +clean-2 → 清理 run-2 的产物 +``` + +--- + +## ✅ 最终验证 + +### 文档完整性 +- [x] README.md +- [x] docs/architecture.md + - [x] 2.1.1 需求描述 + - [x] 2.1.2 业务建模 + - [x] 2.1.3 技术建模 + - [x] 2.1.3.1 六边形架构 + - [x] 2.1.3.2 技术选型 +- [x] docs/api-and-test.md +- [x] docs/deployment.md + +### 开发模式 +- [x] Mode 0: Hot reload + Mock (memory) +- [x] Mode 1: Hot reload + Real deps (container) +- [x] Mode 2: All in container + +### Makefile +- [x] 5 个核心命令 +- [x] 使用 Profile 共享 Docker Compose +- [x] run-1 和 run-2 使用同一个 docker-compose.yml + +--- + +## 🎉 结论 + +**所有要求均已满足!✅** + +项目完全符合你的设计要求: +1. ✅ 完整的文档结构 (README + docs/) +2. ✅ 三种开发模式 (0/1/2) +3. ✅ 五个核心命令 (run-0/1/2, clean-1/2) +4. ✅ 使用 Profile 共享 Docker Compose(关键设计) + +**特别亮点**: +- 使用 Docker Compose Profile 避免配置重复 +- 命名清晰,语义明确 (run-0/1/2) +- 产物隔离 (clean-1 vs clean-2) + +**可以直接使用!** 🚀 + diff --git a/backend/TEST-REPORT.md b/backend/TEST-REPORT.md new file mode 100644 index 0000000..17893e4 --- /dev/null +++ b/backend/TEST-REPORT.md @@ -0,0 +1,400 @@ +# 测试报告 + +**测试日期**: 2025-11-10 +**测试内容**: Makefile 五个核心命令的功能测试 + +--- + +## ✅ 测试结果总览 + +| 命令 | 状态 | 说明 | +|------|------|------| +| `make run-0` | ✅ 通过 | Mock 模式正常工作 | +| `make run-1` | ✅ 通过 | Docker 依赖 + 热加载正常 | +| `make run-2` | ✅ 通过 | 完全容器化部署成功 | +| `make clean-1` | ✅ 通过 | 清理 run-1 产物完整 | +| `make clean-2` | ✅ 通过 | 清理 run-2 产物完整 | + +--- + +## 📋 详细测试结果 + +### 1️⃣ make run-0 (Mock 模式) + +**测试命令**: +```bash +make run-0 +``` + +**预期行为**: +- ✅ 使用 Mock 适配器(内存存储) +- ✅ 无需任何 Docker 容器 +- ✅ 支持热加载(Air) +- ✅ 环境变量 `ADAPTER_MODE=mock` + +**实际结果**: +``` +✅ 服务启动成功 +✅ Health API 正常: {"status":"healthy"} +✅ Registries API 返回 Mock 数据: 1 条记录 +✅ Clusters API 返回 Mock 数据: 1 条记录 +✅ Bootstrap 数据预注入成功 +``` + +**日志输出**: +``` +📝 Configuration: mode=mock, port=8080 +✅ Output Adapters initialized (mode: mock) + 🌱 Starting bootstrap seeding... + ✓ User 'admin' created + ✓ Registry 'Harbor Production' created + ✓ Cluster 'Test Cluster' created + ✅ Bootstrap seeding completed +🌐 Server starting on :8080 +``` + +**结论**: ✅ **完全符合预期** + +--- + +### 2️⃣ make run-1 (Docker 依赖 + 热加载) + +**测试命令**: +```bash +make run-1 +``` + +**预期行为**: +- ✅ 启动 PostgreSQL 容器(使用 `docker compose up -d postgres`) +- ✅ 后端代码在本地运行 +- ✅ 支持热加载(Air) +- ✅ 连接真实数据库 +- ✅ 环境变量 `ADAPTER_MODE=` (空,表示真实模式) + +**实际结果**: +``` +✅ PostgreSQL 容器启动: ocdp-postgres (healthy) +✅ 后端连接数据库成功 +✅ 数据库 Schema 初始化成功 +✅ Health API 正常 +✅ Registries API 连接真实 PostgreSQL +✅ Clusters API 连接真实 PostgreSQL +``` + +**Docker 容器状态**: +``` +NAME IMAGE STATUS +ocdp-postgres postgres:17-alpine Up (healthy) +``` + +**配置**: +```makefile +run-1: + @docker compose up -d postgres + ADAPTER_MODE= \ + DATABASE_URL=postgresql://postgres:postgres@localhost:5432/ocdp?sslmode=disable \ + air -c .air.toml +``` + +**结论**: ✅ **完全符合预期** + +--- + +### 3️⃣ make run-2 (完全容器化) + +**测试命令**: +```bash +make run-2 +``` + +**预期行为**: +- ✅ 构建后端 Docker 镜像 +- ✅ 启动 PostgreSQL + Backend 容器 +- ✅ 使用 `--profile backend` 区分模式 +- ✅ 后台运行 +- ✅ 无热加载 + +**实际结果**: +``` +✅ Docker 镜像构建成功: backend-backend +✅ 两个容器启动: ocdp-postgres + ocdp-backend +✅ 容器健康检查通过 +✅ Health API 正常 +✅ Registries API 返回真实数据: 1 条记录 +✅ Clusters API 返回真实数据: 1 条记录 +✅ 后端日志正常输出 +``` + +**Docker 容器状态**: +``` +NAME IMAGE STATUS +ocdp-backend backend-backend Up (healthy) +ocdp-postgres postgres:17-alpine Up (healthy) +``` + +**配置**: +```makefile +run-2: + @docker compose --profile backend up --build -d +``` + +**Docker Compose Profile 验证**: +- ✅ 使用单一的 `docker-compose.yml` +- ✅ PostgreSQL 默认启动(无 profile) +- ✅ Backend 需要 `--profile backend` 才启动 +- ✅ 符合设计要求 + +**结论**: ✅ **完全符合预期** + +--- + +### 4️⃣ make clean-1 (清理 run-1 产物) + +**测试命令**: +```bash +make clean-1 +``` + +**预期行为**: +- ✅ 停止并删除 PostgreSQL 容器 +- ✅ 删除 Docker 数据卷 +- ✅ 清空 `tmp/` 目录 +- ✅ 删除 Docker 网络 + +**清理前状态**: +``` +Docker 容器: ocdp-postgres (running) +tmp/ 目录: 包含 main, build-errors.log, test.txt +Docker 卷: ocdp-postgres-data +Docker 网络: ocdp-network +``` + +**清理后状态**: +``` +✅ Docker 容器: 0 个 +✅ tmp/ 目录: 已清空 +✅ Docker 卷: ocdp-postgres-data 已删除 +✅ Docker 网络: ocdp-network 已删除 +``` + +**执行输出**: +``` +🧹 Cleaning run-1 artifacts... + Container ocdp-postgres Stopping + Container ocdp-postgres Stopped + Container ocdp-postgres Removing + Container ocdp-postgres Removed + Volume ocdp-postgres-data Removing + Volume ocdp-postgres-data Removed + Network ocdp-network Removed +✅ run-1 cleaned +``` + +**配置**: +```makefile +clean-1: + @docker compose down -v + @rm -rf tmp/ +``` + +**结论**: ✅ **清理完整,无残留** + +--- + +### 5️⃣ make clean-2 (清理 run-2 产物) + +**测试命令**: +```bash +make clean-2 +``` + +**预期行为**: +- ✅ 停止并删除 Backend + PostgreSQL 容器 +- ✅ 删除 Docker 数据卷 +- ✅ 清空 `bin/` 和 `dist/` 目录 +- ✅ 删除 Docker 网络 + +**清理前状态**: +``` +Docker 容器: ocdp-backend (running), ocdp-postgres (running) +bin/ 目录: 存在 +dist/ 目录: 包含 test.tar.gz +Docker 卷: ocdp-postgres-data +Docker 网络: ocdp-network +``` + +**清理后状态**: +``` +✅ Docker 容器: 0 个 +✅ bin/ 目录: 0 个文件 +✅ dist/ 目录: 0 个文件 +✅ Docker 卷: ocdp-postgres-data 已删除 +✅ Docker 网络: ocdp-network 已删除 +``` + +**执行输出**: +``` +🧹 Cleaning run-2 artifacts... + Container ocdp-backend Stopping + Container ocdp-backend Stopped + Container ocdp-backend Removing + Container ocdp-backend Removed + Container ocdp-postgres Stopping + Container ocdp-postgres Stopped + Container ocdp-postgres Removing + Container ocdp-postgres Removed + Volume ocdp-postgres-data Removed + Network ocdp-network Removed +✅ run-2 cleaned +``` + +**配置**: +```makefile +clean-2: + @docker compose --profile backend down -v + @rm -rf bin/ dist/ +``` + +**结论**: ✅ **清理完整,无残留** + +--- + +## 🎯 核心设计验证 + +### Docker Compose Profile 机制 ✅ + +**验证内容**: 使用单一 `docker-compose.yml` + Profile 实现三种模式 + +**验证结果**: + +1. **run-1**: `docker compose up -d postgres` + - ✅ 只启动 postgres 服务(无 profile) + - ✅ backend 服务不启动(需要 profile) + +2. **run-2**: `docker compose --profile backend up -d` + - ✅ 启动 postgres + backend 两个服务 + - ✅ 使用 `--profile backend` 激活 backend + +3. **共享配置**: + - ✅ 两种模式使用同一个 `docker-compose.yml` + - ✅ postgres 配置不重复 + - ✅ 符合 DRY 原则 + +**配置文件**: +```yaml +services: + postgres: + # 默认启动(无需 profile) + + backend: + profiles: + - backend # 需要 --profile backend + depends_on: + - postgres +``` + +**结论**: ✅ **设计正确,实现完美** + +--- + +## 📊 API 功能测试 + +### Health Check API +```bash +curl http://localhost:8080/health +``` + +**结果**: ✅ 三种模式均正常 +```json +{"status":"healthy"} +``` + +--- + +### Registries API +```bash +curl http://localhost:8080/api/v1/registries +``` + +**run-0 (Mock)**: ✅ 返回 1 条 Mock 数据 +**run-1 (PostgreSQL)**: ✅ 返回真实数据 +**run-2 (容器)**: ✅ 返回真实数据 + +--- + +### Clusters API +```bash +curl http://localhost:8080/api/v1/clusters +``` + +**run-0 (Mock)**: ✅ 返回 1 条 Mock 数据 +**run-1 (PostgreSQL)**: ✅ 返回真实数据 +**run-2 (容器)**: ✅ 返回真实数据 + +--- + +## 🎉 总结 + +### ✅ 所有测试通过 + +- ✅ **run-0**: Mock 模式工作正常 +- ✅ **run-1**: Docker 依赖 + 热加载正常 +- ✅ **run-2**: 完全容器化部署成功 +- ✅ **clean-1**: 清理 run-1 产物完整 +- ✅ **clean-2**: 清理 run-2 产物完整 + +### ✅ 核心设计验证 + +- ✅ **Docker Compose Profile** 机制正确 +- ✅ **单一配置文件** 实现多模式 +- ✅ **产物隔离** 清晰 +- ✅ **命名语义** 明确 + +### 🎯 符合所有设计要求 + +1. ✅ 三种运行模式(0/1/2) +2. ✅ 五个核心命令 +3. ✅ 使用 Profile 共享 Docker Compose +4. ✅ 清理命令分离且完整 + +--- + +## 💡 使用建议 + +### 日常开发流程 + +```bash +# 快速开发 +make run-0 + +# 需要真实数据库 +make run-1 + +# 测试部署 +make run-2 + +# 清理环境 +make clean-1 # 或 make clean-2 +``` + +### 注意事项 + +1. **run-0** 和 **run-1** 是前台运行,`Ctrl+C` 停止 +2. **run-2** 是后台运行,使用 `docker compose --profile backend down` 停止 +3. **clean-1** 和 **clean-2** 会删除数据卷,数据会丢失 +4. run-1 和 run-2 共享同一个 PostgreSQL 容器配置和数据 + +--- + +## 📝 测试环境 + +- **操作系统**: Linux 5.15.0-160-generic +- **Docker**: 已安装 +- **Docker Compose**: 已安装 +- **Go**: 1.24+ +- **Air**: 已安装 + +--- + +**测试结论**: 🎉 **所有功能正常,可以投入使用!** + diff --git a/backend/api b/backend/api new file mode 100755 index 0000000..6f51168 Binary files /dev/null and b/backend/api differ diff --git a/backend/cmd/api/main.go b/backend/cmd/api/main.go new file mode 100644 index 0000000..d32c592 --- /dev/null +++ b/backend/cmd/api/main.go @@ -0,0 +1,309 @@ +// @title OCDP Backend API +// @version 1.0 +// @description OCDP (Open Cloud Development Platform) Backend API +// @description +// @description RESTful API for managing Kubernetes clusters, OCI registries, and Helm deployments. +// +// @contact.name API Support +// @contact.email support@ocdp.io +// +// @license.name Apache 2.0 +// @license.url http://www.apache.org/licenses/LICENSE-2.0.html +// +// @host localhost:8080 +// @BasePath /api/v1 +// +// @schemes http https +// +// @securityDefinitions.apikey BearerAuth +// @in header +// @name Authorization +// @description Type "Bearer" followed by a space and JWT token. +package main + +import ( + "context" + "fmt" + "log" + "net/http" + "os" + "time" + + "github.com/gorilla/mux" + + "github.com/ocdp/cluster-service/internal/adapter/input/http/rest" + "github.com/ocdp/cluster-service/internal/adapter/output" + "github.com/ocdp/cluster-service/internal/bootstrap" + "github.com/ocdp/cluster-service/internal/domain/service" + "github.com/ocdp/cluster-service/internal/pkg/crypto" + "github.com/ocdp/cluster-service/internal/pkg/jwt" + "github.com/ocdp/cluster-service/internal/pkg/password" +) + +func main() { + log.Println("🚀 Starting OCDP Backend (Hexagonal Architecture)") + + // ===== 1. 读取配置 ===== + config := loadConfig() + log.Printf("📝 Configuration: mode=%s, port=%s", config.AdapterMode, config.Port) + + // ===== 2. 创建加密器(用于敏感数据加密存储) ===== + encryptor := crypto.NewAESEncryptor(config.EncryptionKey) + log.Println("✅ Encryption enabled for sensitive data") + + // ===== 3. 创建 Output Adapters(通过工厂) ===== + factory := output.NewAdapterFactory( + output.AdapterMode(config.AdapterMode), + encryptor, + config.DatabaseURL, + ) + + repos, err := factory.CreateAllRepositories() + if err != nil { + log.Fatalf("❌ Failed to create adapters: %v", err) + } + log.Printf("✅ Output Adapters initialized (mode: %s)", config.AdapterMode) + + // ===== 4. 创建工具包实例 ===== + passwordHasher := password.NewHasher() + tokenGenerator := jwt.NewJWTManager(config.JWTSecret) + log.Println("✅ Utilities initialized") + + // ===== 5. 创建 Domain Services ===== + authService := service.NewAuthService( + repos.UserRepo, + passwordHasher, + tokenGenerator, + ) + + clusterService := service.NewClusterService( + repos.ClusterRepo, + ) + + registryService := service.NewRegistryService( + repos.RegistryRepo, + repos.OCIClient, + ) + + artifactService := service.NewArtifactService( + repos.RegistryRepo, + repos.OCIClient, + ) + + instanceService := service.NewInstanceService( + repos.InstanceRepo, + repos.ClusterRepo, + repos.RegistryRepo, + repos.HelmClient, + repos.OCIClient, + repos.EntryClient, + ) + + monitoringService := service.NewMonitoringService( + repos.ClusterRepo, + repos.MetricsClient, + ) + + log.Println("✅ Domain Services initialized") + + // ===== 6. 加载并执行 Bootstrap 预注入 ===== + bootstrapConfig, err := bootstrap.LoadBootstrapConfig() + if err != nil { + log.Printf("⚠️ Warning: Failed to load bootstrap config: %v", err) + // 使用默认配置 + bootstrapConfig = bootstrap.GetDefaultBootstrapConfig() + } + + seeder := bootstrap.NewSeeder(repos, passwordHasher, bootstrapConfig) + if err := seeder.SeedAll(context.Background()); err != nil { + log.Printf("⚠️ Warning: Failed to seed data: %v", err) + } + + // ===== 7. 创建 Input Adapters (REST Handlers) ===== + authHandler := rest.NewAuthHandler(authService) + clusterHandler := rest.NewClusterHandler(clusterService) + registryHandler := rest.NewRegistryHandler(registryService) + artifactHandler := rest.NewArtifactHandler(artifactService) + instanceHandler := rest.NewInstanceHandler(instanceService) + monitoringHandler := rest.NewMonitoringHandler(monitoringService) + swaggerHandler := rest.NewSwaggerHandler() + + log.Println("✅ Input Adapters (REST handlers) initialized") + + // ===== 8. 设置路由 ===== + router := setupRouter( + authHandler, + clusterHandler, + registryHandler, + artifactHandler, + instanceHandler, + monitoringHandler, + swaggerHandler, + ) + + // ===== 9. 启动服务器 ===== + addr := fmt.Sprintf(":%s", config.Port) + log.Printf("🌐 Server starting on %s", addr) + log.Println("") + log.Println("📍 Available endpoints:") + log.Printf(" - REST API: http://localhost:%s/api/v1", config.Port) + log.Printf(" - Swagger UI: http://localhost:%s/api/docs", config.Port) + log.Printf(" - OpenAPI Spec: http://localhost:%s/api/docs/openapi.yaml", config.Port) + log.Printf(" - Health: http://localhost:%s/health", config.Port) + log.Println("") + log.Println("✨ Press Ctrl+C to stop") + log.Println("") + + if err := http.ListenAndServe(addr, router); err != nil { + log.Fatalf("❌ Server failed: %v", err) + } +} + +// Config 应用配置 +type Config struct { + AdapterMode string + Port string + JWTSecret string + EncryptionKey string + DatabaseURL string +} + +// loadConfig 加载配置 +func loadConfig() *Config { + return &Config{ + AdapterMode: getEnv("ADAPTER_MODE", ""), // 默认为空字符串(真实模式) + Port: getEnv("PORT", "8080"), + JWTSecret: getEnv("JWT_SECRET", "your-secret-key-change-in-production"), + EncryptionKey: getEnv("ENCRYPTION_KEY", "default-encryption-key-change-in-production"), + DatabaseURL: getEnv("DATABASE_URL", ""), + } +} + +// getEnv 获取环境变量,如果不存在则返回默认值 +func getEnv(key, defaultValue string) string { + value := os.Getenv(key) + if value == "" { + return defaultValue + } + return value +} + +// setupRouter 设置路由 +func setupRouter( + authHandler *rest.AuthHandler, + clusterHandler *rest.ClusterHandler, + registryHandler *rest.RegistryHandler, + artifactHandler *rest.ArtifactHandler, + instanceHandler *rest.InstanceHandler, + monitoringHandler *rest.MonitoringHandler, + swaggerHandler *rest.SwaggerHandler, +) *mux.Router { + router := mux.NewRouter().StrictSlash(true) + + // 全局中间件 + router.Use(loggingMiddleware) + router.Use(corsMiddleware) + + // 健康检查 + router.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"status":"healthy"}`)) + }) + + // ===== Swagger UI ===== + router.HandleFunc("/api/docs", swaggerHandler.ServeSwaggerUI).Methods(http.MethodGet) + router.HandleFunc("/api/docs/assets/swagger-ui.css", swaggerHandler.ServeSwaggerCSS).Methods(http.MethodGet) + router.HandleFunc("/api/docs/assets/swagger-ui-bundle.js", swaggerHandler.ServeSwaggerBundle).Methods(http.MethodGet) + router.HandleFunc("/api/docs/assets/swagger-ui-standalone-preset.js", swaggerHandler.ServeSwaggerStandalonePreset).Methods(http.MethodGet) + router.HandleFunc("/api/docs/openapi.yaml", swaggerHandler.ServeOpenAPISpec).Methods(http.MethodGet) + + // API v1 + api := router.PathPrefix("/api/v1").Subrouter() + + // ===== 认证路由 ===== + api.HandleFunc("/auth/register", authHandler.Register) + api.HandleFunc("/auth/login", authHandler.Login) + api.HandleFunc("/auth/refresh", authHandler.RefreshToken) + + // ===== 集群路由 ===== + api.HandleFunc("/clusters", clusterHandler.CreateCluster).Methods(http.MethodPost) + api.HandleFunc("/clusters", clusterHandler.GetAllClusters).Methods(http.MethodGet) + api.HandleFunc("/clusters/{cluster_id}", clusterHandler.GetCluster).Methods(http.MethodGet) + api.HandleFunc("/clusters/{cluster_id}", clusterHandler.UpdateCluster).Methods(http.MethodPut) + api.HandleFunc("/clusters/{cluster_id}", clusterHandler.DeleteCluster).Methods(http.MethodDelete) + api.HandleFunc("/clusters/{cluster_id}/health", clusterHandler.GetClusterHealth).Methods(http.MethodGet) + + // ===== Registry 路由 ===== + api.HandleFunc("/registries", registryHandler.CreateRegistry).Methods(http.MethodPost) + api.HandleFunc("/registries", registryHandler.GetAllRegistries).Methods(http.MethodGet) + api.HandleFunc("/registries/{registry_id}", registryHandler.GetRegistry).Methods(http.MethodGet) + api.HandleFunc("/registries/{registry_id}", registryHandler.UpdateRegistry).Methods(http.MethodPut) + api.HandleFunc("/registries/{registry_id}", registryHandler.DeleteRegistry).Methods(http.MethodDelete) + api.HandleFunc("/registries/{registry_id}/health", registryHandler.GetRegistryHealth).Methods(http.MethodGet) + + // ===== Artifact 路由 ===== + api.HandleFunc("/registries/{registry_id}/repositories", artifactHandler.ListRepositories).Methods(http.MethodGet) + api.HandleFunc("/registries/{registry_id}/repositories/{repository_name:.+}/artifacts", artifactHandler.ListArtifacts).Methods(http.MethodGet) + api.HandleFunc("/registries/{registry_id}/repositories/{repository_name:.+}/artifacts/{reference}", artifactHandler.GetArtifact).Methods(http.MethodGet) + api.HandleFunc("/registries/{registry_id}/repositories/{repository_name:.+}/artifacts/{reference}/values-schema", artifactHandler.GetArtifactValuesSchema).Methods(http.MethodGet) + + // ===== Instance 路由 ===== + api.HandleFunc("/clusters/{cluster_id}/instances", instanceHandler.CreateInstance).Methods(http.MethodPost) + api.HandleFunc("/clusters/{cluster_id}/instances", instanceHandler.ListInstances).Methods(http.MethodGet) + api.HandleFunc("/clusters/{cluster_id}/instances/{instance_id}", instanceHandler.GetInstance).Methods(http.MethodGet) + api.HandleFunc("/clusters/{cluster_id}/instances/{instance_id}", instanceHandler.UpdateInstance).Methods(http.MethodPut) + api.HandleFunc("/clusters/{cluster_id}/instances/{instance_id}", instanceHandler.DeleteInstance).Methods(http.MethodDelete) + api.HandleFunc("/clusters/{cluster_id}/instances/{instance_id}/entries", instanceHandler.ListInstanceEntries).Methods(http.MethodGet) + + // ===== Monitoring 路由 ===== + api.HandleFunc("/monitoring/clusters", monitoringHandler.ListClusterMonitoring).Methods(http.MethodGet) + api.HandleFunc("/monitoring/clusters/{cluster_id}", monitoringHandler.GetClusterMonitoring).Methods(http.MethodGet) + api.HandleFunc("/monitoring/clusters/{cluster_id}/nodes", monitoringHandler.GetNodeMetrics).Methods(http.MethodGet) + api.HandleFunc("/monitoring/summary", monitoringHandler.GetMonitoringSummary).Methods(http.MethodGet) + + // 处理 MethodNotAllowed 错误(OPTIONS 请求会触发) + router.MethodNotAllowedHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodOptions { + // CORS 预检已在中间件处理,这里直接返回 + w.WriteHeader(http.StatusNoContent) + return + } + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + }) + + return router +} + +// loggingMiddleware 日志中间件 +func loggingMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + next.ServeHTTP(w, r) + log.Printf("%s %s %s %v", r.Method, r.RequestURI, r.RemoteAddr, time.Since(start)) + }) +} + +// corsMiddleware CORS 中间件 +func corsMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // 设置 CORS 头 + origin := r.Header.Get("Origin") + if origin == "" { + origin = "*" + } + w.Header().Set("Access-Control-Allow-Origin", origin) + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With") + w.Header().Set("Access-Control-Allow-Credentials", "true") + w.Header().Set("Access-Control-Max-Age", "86400") + + // 处理 OPTIONS 预检请求 + if r.Method == http.MethodOptions { + w.WriteHeader(http.StatusNoContent) + return + } + + next.ServeHTTP(w, r) + }) +} diff --git a/backend/config/bootstrap.example.json b/backend/config/bootstrap.example.json new file mode 100644 index 0000000..fab493e --- /dev/null +++ b/backend/config/bootstrap.example.json @@ -0,0 +1,31 @@ +{ + "enabled": true, + "users": [ + { + "username": "admin", + "password": "change-me-in-production", + "email": "admin@example.com" + } + ], + "registries": [ + { + "name": "my-harbor", + "url": "https://harbor.example.com", + "description": "Harbor Registry", + "username": "admin", + "password": "change-me", + "insecure": false + } + ], + "clusters": [ + { + "name": "my-cluster", + "host": "https://kubernetes.example.com:6443", + "description": "Production Kubernetes Cluster", + "caData": "LS0tLS1CRUdJTi...(base64-encoded-ca-cert)...", + "certData": "LS0tLS1CRUdJTi...(base64-encoded-client-cert)...", + "keyData": "LS0tLS1CRUdJTi...(base64-encoded-client-key)..." + } + ] +} + diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml new file mode 100644 index 0000000..4dab3fd --- /dev/null +++ b/backend/docker-compose.yml @@ -0,0 +1,157 @@ +# ================================================== +# OCDP Backend - Docker Compose 统一配置 +# ================================================== +# 使用方式: +# +# 1. 只启动依赖服务 (PostgreSQL) - 开发模式 +# docker compose up -d +# 然后在本地运行: go run cmd/api/main.go +# +# 2. 启动完整服务 (PostgreSQL + Backend) +# docker compose --profile backend up -d +# +# 3. 启动 Mock 模式 (无需数据库) +# docker compose --profile mock up -d +# +# 4. 启动数据库管理工具 (可选) +# docker compose --profile tools up -d +# +# 停止服务: +# docker compose down +# +# 查看日志: +# docker compose logs -f +# ================================================== + +services: + # ================================================== + # PostgreSQL 数据库 (默认启动) + # ================================================== + postgres: + image: postgres:17-alpine + container_name: ocdp-postgres + restart: unless-stopped + environment: + POSTGRES_DB: ${POSTGRES_DB:-ocdp} + POSTGRES_USER: ${POSTGRES_USER:-postgres} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres} + POSTGRES_INITDB_ARGS: "--encoding=UTF8 --lc-collate=C --lc-ctype=C" + ports: + - "${POSTGRES_PORT:-5432}:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + - ${INIT_DB_SQL_PATH:-./scripts/init-db.sql}:/docker-entrypoint-initdb.d/01-init.sql:ro + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres} -d ${POSTGRES_DB:-ocdp}"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s + networks: + - ocdp-network + + # ================================================== + # Backend - 真实模式 (连接 PostgreSQL) + # 使用: docker compose --profile backend up -d + # ================================================== + backend: + build: + context: ${BACKEND_BUILD_CONTEXT:-.} + dockerfile: ${BACKEND_BUILD_DOCKERFILE:-Dockerfile} + image: ocdp-backend:latest + container_name: ocdp-backend + restart: unless-stopped + environment: + ADAPTER_MODE: ${ADAPTER_MODE:-production} + PORT: 8080 + JWT_SECRET: ${JWT_SECRET:-change-me-in-production} + ENCRYPTION_KEY: ${ENCRYPTION_KEY:-change-me-32-bytes-long-key-here} + DATABASE_URL: postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@postgres:5432/${POSTGRES_DB:-ocdp}?sslmode=disable + ports: + - "${BACKEND_PORT:-8080}:8080" + volumes: + - ./config:/app/config:ro + - ./data:/app/data + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 15s + networks: + - ocdp-network + depends_on: + postgres: + condition: service_healthy + profiles: + - backend + + # ================================================== + # Backend - Mock 模式 (无需数据库,适合快速测试) + # 使用: docker compose --profile mock up -d + # ================================================== + backend-mock: + build: + context: ${BACKEND_BUILD_CONTEXT:-.} + dockerfile: ${BACKEND_MOCK_BUILD_DOCKERFILE:-Dockerfile.mock} + container_name: ocdp-backend-mock + restart: unless-stopped + environment: + ADAPTER_MODE: mock + PORT: 8080 + JWT_SECRET: ${JWT_SECRET:-test-jwt-secret-key} + ENCRYPTION_KEY: ${ENCRYPTION_KEY:-test-encryption-key-32-bytes-long} + ports: + - "${BACKEND_PORT:-8080}:8080" + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 5s + networks: + - ocdp-network + profiles: + - mock + + # ================================================== + # pgAdmin - 数据库管理界面 (可选) + # 使用: docker compose --profile tools up -d + # ================================================== + pgadmin: + image: dpage/pgadmin4:latest + container_name: ocdp-pgadmin + restart: unless-stopped + environment: + PGADMIN_DEFAULT_EMAIL: ${PGADMIN_EMAIL:-admin@ocdp.local} + PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_PASSWORD:-admin} + PGADMIN_CONFIG_SERVER_MODE: "False" + PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED: "False" + ports: + - "${PGADMIN_PORT:-5050}:80" + volumes: + - pgadmin_data:/var/lib/pgadmin + networks: + - ocdp-network + depends_on: + postgres: + condition: service_healthy + profiles: + - tools + +# ================================================== +# 网络配置 +# ================================================== +networks: + ocdp-network: + driver: bridge + name: ocdp-network + +# ================================================== +# 数据卷配置 +# ================================================== +volumes: + postgres_data: + name: ocdp-postgres-data + pgadmin_data: + name: ocdp-pgadmin-data diff --git a/backend/docs/api-and-test.md b/backend/docs/api-and-test.md new file mode 100644 index 0000000..4eec6a0 --- /dev/null +++ b/backend/docs/api-and-test.md @@ -0,0 +1,1802 @@ +# 📚 API 与测试文档 + +> **💡 推荐使用 OpenAPI 规范** +> +> 本项目现在提供标准的 **OpenAPI 3.0 规范**! +> +> - 📖 **交互式 API 文档**: [http://localhost:8080/api/docs](http://localhost:8080/api/docs) (Swagger UI) +> - 📄 **OpenAPI 规范文件**: [openapi.yaml](./openapi.yaml) +> - 🔧 **在线测试**: 在 Swagger UI 中直接测试所有 API +> - 🚀 **客户端生成**: 使用 OpenAPI 规范自动生成客户端代码 +> +> **本文档 (Markdown 版本)** 作为参考文档保留,内容与 OpenAPI 规范保持一致。 +> 如需最新的 API 定义,请参考 [openapi.yaml](./openapi.yaml)。 + +--- + +## 目录 + +### Part 1: API 文档 +- [基础信息](#基础信息) +- [认证 API](#认证-api) +- [集群管理 API](#集群管理-api) +- [Registry 管理 API](#registry-管理-api) +- [Artifact 浏览 API](#artifact-浏览-api) +- [实例管理 API](#实例管理-api) +- [监控 API](#监控-api) +- [响应格式](#响应格式) +- [错误处理](#错误处理) + +### Part 2: 测试文档 +- [测试策略](#测试策略) +- [单元测试](#单元测试) +- [集成测试](#集成测试) +- [API 测试](#api-测试) +- [E2E 测试](#e2e-测试) +- [Mock 测试](#mock-测试) +- [测试工具](#测试工具) +- [测试最佳实践](#测试最佳实践) + +--- + +# Part 1: API 文档 + +## 基础信息 + +### Base URL + +``` +http://localhost:8080/api/v1 +``` + +### 健康检查 + +```bash +GET /health + +# 响应 +{ + "status": "healthy" +} +``` + +### 通用请求头 + +``` +Content-Type: application/json +Authorization: Bearer # 部分接口需要 +``` + +--- + +## 认证 API + +### 用户注册 + +```bash +POST /api/v1/auth/register +Content-Type: application/json + +{ + "username": "john", + "password": "secret123", + "email": "john@example.com" +} + +# 响应 201 +{ + "id": "user-123", + "username": "john", + "email": "john@example.com", + "createdAt": "2025-11-09T10:00:00Z" +} +``` + +### 用户登录 + +```bash +POST /api/v1/auth/login +Content-Type: application/json + +{ + "username": "john", + "password": "secret123" +} + +# 响应 200 +{ + "accessToken": "eyJhbGciOiJIUzI1NiIs...", + "refreshToken": "eyJhbGciOiJIUzI1NiIs...", + "userId": "user-123", + "username": "john" +} +``` + +### 刷新 Token + +```bash +POST /api/v1/auth/refresh +Content-Type: application/json + +{ + "refreshToken": "eyJhbGciOiJIUzI1NiIs..." +} + +# 响应 200 +{ + "accessToken": "eyJhbGciOiJIUzI1NiIs...", + "refreshToken": "eyJhbGciOiJIUzI1NiIs...", + "userId": "user-123", + "username": "john" +} +``` + +--- + +## 集群管理 API + +### 创建集群 + +```bash +POST /api/v1/clusters +Content-Type: application/json + +{ + "name": "Production Cluster", + "host": "https://k8s.example.com:6443", + "description": "生产环境集群", + "caData": "LS0tLS1CRUdJTi0...", # Base64 编码的 CA 证书 + "certData": "LS0tLS1CRUdJTi0...", # Base64 编码的客户端证书 + "keyData": "LS0tLS1CRUdJTi0..." # Base64 编码的客户端密钥 +} + +# 响应 201 +{ + "id": "cluster-abc123", + "name": "Production Cluster", + "host": "https://k8s.example.com:6443", + "description": "生产环境集群", + "status": "healthy", + "createdAt": "2025-11-09T10:00:00Z", + "updatedAt": "2025-11-09T10:00:00Z" +} +``` + +### 列出所有集群 + +```bash +GET /api/v1/clusters + +# 响应 200 +[ + { + "id": "cluster-abc123", + "name": "Production Cluster", + "host": "https://k8s.example.com:6443", + "description": "生产环境集群", + "status": "healthy", + "createdAt": "2025-11-09T10:00:00Z" + } +] +``` + +### 获取集群详情 + +```bash +GET /api/v1/clusters/{clusterId} + +# 响应 200 +{ + "id": "cluster-abc123", + "name": "Production Cluster", + "host": "https://k8s.example.com:6443", + "description": "生产环境集群", + "status": "healthy", + "version": "v1.28.0", + "nodeCount": 5, + "createdAt": "2025-11-09T10:00:00Z", + "updatedAt": "2025-11-09T10:00:00Z" +} +``` + +### 更新集群 + +```bash +PUT /api/v1/clusters/{clusterId} +Content-Type: application/json + +{ + "name": "Production Cluster (Updated)", + "description": "更新后的描述" +} + +# 响应 200 +{ + "id": "cluster-abc123", + "name": "Production Cluster (Updated)", + "description": "更新后的描述", + "updatedAt": "2025-11-09T11:00:00Z" +} +``` + +### 删除集群 + +```bash +DELETE /api/v1/clusters/{clusterId} + +# 响应 204 No Content +``` + +### 集群健康检查 + +```bash +GET /api/v1/clusters/{clusterId}/health + +# 响应 200 +{ + "clusterId": "cluster-abc123", + "status": "healthy", + "version": "v1.28.0", + "nodeCount": 5, + "readyNodes": 5, + "cpuCapacity": "40 cores", + "memoryCapacity": "160Gi", + "checkedAt": "2025-11-09T12:00:00Z" +} +``` + +--- + +## Registry 管理 API + +> **OCI 标准**: 所有 Registry 都遵循 OCI (Open Container Initiative) 标准,支持 Harbor, Docker Hub, GHCR, Nexus, 以及任何兼容 OCI Distribution Spec 的 Registry。 + +### 创建 Registry + +```bash +POST /api/v1/registries +Content-Type: application/json + +{ + "name": "Harbor Production", + "url": "https://harbor.example.com", + "description": "生产环境 Harbor 仓库", + "username": "admin", + "password": "secret", + "insecure": false +} + +# 响应 201 +{ + "id": "registry-123", + "name": "Harbor Production", + "url": "https://harbor.example.com", + "description": "生产环境 Harbor 仓库", + "username": "admin", + "insecure": false, + "createdAt": "2025-11-09T10:00:00Z", + "updatedAt": "2025-11-09T10:00:00Z" +} +``` + +**字段说明**: +- `url`: Registry URL,所有 Registry 都使用 OCI Distribution API +- `username/password`: 可选,用于私有 Registry 认证 +- `insecure`: 是否跳过 TLS 验证(开发环境可用) + +> **注意**: 密码不会在响应中返回,存储时会自动加密。 + +### 列出所有 Registries + +```bash +GET /api/v1/registries + +# 响应 200 +[ + { + "id": "registry-123", + "name": "Harbor Production", + "url": "https://harbor.example.com", + "description": "生产环境 Harbor 仓库", + "username": "admin", + "insecure": false, + "createdAt": "2025-11-09T10:00:00Z" + } +] +``` + +### 获取 Registry 详情 + +```bash +GET /api/v1/registries/{registryId} + +# 响应 200 +{ + "id": "registry-123", + "name": "Harbor Production", + "url": "https://harbor.example.com", + "description": "生产环境 Harbor 仓库", + "username": "admin", + "insecure": false, + "createdAt": "2025-11-09T10:00:00Z", + "updatedAt": "2025-11-09T10:00:00Z" +} +``` + +### 更新 Registry + +```bash +PUT /api/v1/registries/{registryId} +Content-Type: application/json + +{ + "name": "Harbor Production (Updated)", + "url": "https://new-harbor.example.com", + "password": "new-secret" # 可选,只在需要更新密码时提供 +} + +# 响应 200 +{ + "id": "registry-123", + "name": "Harbor Production (Updated)", + "url": "https://new-harbor.example.com", + "updatedAt": "2025-11-09T11:00:00Z" +} +``` + +### 删除 Registry + +```bash +DELETE /api/v1/registries/{registryId} + +# 响应 204 No Content +``` + +### Registry 健康检查 + +```bash +GET /api/v1/registries/{registryId}/health + +# 响应 200 +{ + "registryId": "registry-123", + "status": "healthy", + "url": "https://harbor.example.com", + "reachable": true, + "authenticated": true, + "responseTime": 125, # 毫秒 + "checkedAt": "2025-11-09T12:00:00Z" +} + +# 响应 503 (不健康) +{ + "registryId": "registry-123", + "status": "unhealthy", + "url": "https://harbor.example.com", + "reachable": false, + "error": "connection timeout", + "checkedAt": "2025-11-09T12:00:00Z" +} +``` + +--- + +## Artifact 浏览 API + +### 列出 Repositories + +```bash +GET /api/v1/registries/{registryId}/repositories + +# 响应 200 +{ + "registryId": "registry-123", + "registryUrl": "https://harbor.example.com", + "repositories": [ + "charts/nginx", + "charts/redis", + "charts/vllm-serve", + "library/alpine" + ], + "total": 4, + "catalogSupported": true, + "source": "catalog" +} +``` + +> **注意**: 需要 Registry 支持 `_catalog` API(OCI Distribution Spec)。 + +### 列出 Artifacts + +```bash +GET /api/v1/registries/{registryId}/repositories/{repository_name}/artifacts + +# 示例(需要 URL 编码) +GET /api/v1/registries/registry-123/repositories/charts%2Fnginx/artifacts + +# 响应 200 +{ + "repositoryName": "charts/nginx", + "tags": [ + { + "name": "1.0.0", + "digest": "sha256:abc123def456...", + "type": "chart", + "size": 12345678, + "createdAt": "2025-11-01T10:00:00Z" + } + ], + "total": 1 +} +``` + +**Artifact 类型识别**: +- `chart`: Helm Chart +- `image`: Docker Image / OCI Image +- `other`: 其他类型 + +### 获取 Artifact 详情 + +```bash +GET /api/v1/registries/{registryId}/repositories/{repository_name}/artifacts/{reference} + +# reference 可以是 tag 或 digest +GET /api/v1/registries/registry-123/repositories/charts%2Fnginx/artifacts/1.0.0 + +# 响应 200 +{ + "repositoryName": "charts/nginx", + "tag": "1.0.0", + "digest": "sha256:abc123def456...", + "type": "chart", + "size": 12345678, + "createdAt": "2025-11-01T10:00:00Z" +} +``` + +**字段说明**: +- `repositoryName`: 仓库名称 +- `tag`: 标签名称(如果使用 tag 引用) +- `digest`: SHA256 摘要 +- `type`: 制品类型,从 mediaType 自动识别: + - `chart`: Helm Chart(mediaType 包含 `helm.config` 或 `helm.chart`) + - `image`: Docker/OCI Image(mediaType 包含 `docker.container.image` 或 `oci.image`) + - `other`: 其他类型 +- `size`: 总大小(字节) +- `createdAt`: 创建时间(ISO 8601 格式) + +### 获取 Helm Chart Values Schema + +```bash +GET /api/v1/registries/{registryId}/repositories/{repository_name}/artifacts/{reference}/values-schema + +# 仅支持 Helm Chart 类型 +GET /api/v1/registries/registry-123/repositories/charts%2Fnginx/artifacts/1.0.0/values-schema + +# 响应 200 +{ + "repositoryName": "charts/nginx", + "tag": "1.0.0", + "valuesSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "replicaCount": { + "type": "integer", + "default": 1, + "description": "Number of replicas" + }, + "image": { + "type": "object", + "properties": { + "repository": { + "type": "string", + "default": "nginx" + }, + "tag": { + "type": "string", + "default": "latest" + } + } + } + } + } +} +``` + +> **URL 编码提示**: Repository 名称包含斜杠时(如 `charts/nginx`),需要编码为 `charts%2Fnginx`。 + +--- + +## 实例管理 API + +### 安装应用 + +```bash +POST /api/v1/clusters/{clusterId}/instances +Content-Type: application/json + +# 方式 1: 使用 JSON values +{ + "name": "my-nginx", + "namespace": "default", + "registryId": "registry-123", + "repository": "charts/nginx", + "chart": "nginx", + "version": "1.0.0", + "description": "My NGINX deployment", + "values": { + "replicaCount": 2, + "image": { + "tag": "1.21.0" + } + } +} + +# 响应 201 +{ + "id": "instance-xyz789", + "name": "my-nginx", + "namespace": "default", + "clusterId": "cluster-abc123", + "registryId": "registry-123", + "chart": "nginx", + "version": "1.0.0", + "status": "deployed", + "revision": 1, + "description": "My NGINX deployment", + "createdAt": "2025-11-09T10:00:00Z", + "updatedAt": "2025-11-09T10:00:00Z" +} +``` + +### 列出应用实例 + +```bash +GET /api/v1/clusters/{clusterId}/instances + +# 响应 200 +[ + { + "id": "instance-xyz789", + "name": "my-nginx", + "namespace": "default", + "chart": "nginx", + "version": "1.0.0", + "status": "deployed", + "revision": 1, + "createdAt": "2025-11-09T10:00:00Z" + } +] +``` + +### 升级应用 + +```bash +PUT /api/v1/clusters/{clusterId}/instances/{instanceId} +Content-Type: application/json + +{ + "version": "1.1.0", + "values": { + "replicaCount": 3 + }, + "description": "Upgrade to v1.1.0" +} + +# 响应 200 +{ + "id": "instance-xyz789", + "name": "my-nginx", + "version": "1.1.0", + "status": "deployed", + "revision": 4, + "updatedAt": "2025-11-09T12:00:00Z" +} +``` + +### 卸载应用 + +```bash +DELETE /api/v1/clusters/{clusterId}/instances/{instanceId} + +# 响应 204 No Content +``` + +### 获取实例访问入口 + +```bash +GET /api/v1/clusters/{clusterId}/instances/{instanceId}/entries + +# 响应 200 +[ + { + "kind": "Service", + "name": "test-nginx", + "namespace": "default", + "type": "ClusterIP", + "clusterIP": "10.43.120.15", + "ports": [ + { + "name": "http", + "protocol": "TCP", + "port": 80, + "targetPort": "http" + } + ] + }, + { + "kind": "Ingress", + "name": "test-nginx", + "namespace": "default", + "type": "nginx", + "hosts": [ + { + "host": "nginx.example.com", + "paths": [ + { + "path": "/", + "serviceName": "test-nginx", + "servicePort": "http" + } + ] + } + ], + "loadBalancerIngress": [ + "34.120.0.12" + ] + } +] +``` + +--- + +## 监控 API + +### 列出集群监控信息 + +```bash +GET /api/v1/monitoring/clusters + +# 响应 200 +[ + { + "clusterId": "cluster-abc123", + "clusterName": "Production Cluster", + "status": "healthy", + "cpuUsage": 45.2, + "memoryUsage": 62.8, + "nodeCount": 5, + "readyNodes": 5, + "podCount": 120, + "timestamp": "2025-11-09T12:00:00Z" + } +] +``` + +### 获取监控摘要 + +```bash +GET /api/v1/monitoring/summary + +# 响应 200 +{ + "totalClusters": 3, + "healthyClusters": 2, + "unhealthyClusters": 1, + "totalNodes": 15, + "totalInstances": 45, + "averageCpuUsage": 42.3, + "averageMemoryUsage": 58.6, + "timestamp": "2025-11-09T12:00:00Z" +} +``` + +--- + +## 响应格式 + +### 成功响应 + +**单个资源**: + +```json +{ + "id": "resource-123", + "name": "Resource Name", + "status": "active", + "createdAt": "2025-11-09T10:00:00Z" +} +``` + +**资源列表**: + +```json +[ + { + "id": "resource-123", + "name": "Resource 1" + }, + { + "id": "resource-456", + "name": "Resource 2" + } +] +``` + +### HTTP 状态码 + +| 状态码 | 说明 | 使用场景 | +|--------|------|----------| +| 200 | OK | 请求成功 | +| 201 | Created | 资源创建成功 | +| 204 | No Content | 删除成功(无返回内容) | +| 400 | Bad Request | 请求参数错误 | +| 401 | Unauthorized | 未认证或 Token 无效 | +| 403 | Forbidden | 无权限访问 | +| 404 | Not Found | 资源不存在 | +| 409 | Conflict | 资源冲突(如重复创建) | +| 500 | Internal Server Error | 服务器内部错误 | +| 503 | Service Unavailable | 服务不可用 | + +--- + +## 错误处理 + +### 错误响应格式 + +```json +{ + "error": "Error Title", + "message": "Detailed error message", + "code": "ERROR_CODE", + "details": {...} +} +``` + +### 常见错误示例 + +#### 400 Bad Request + +```json +{ + "error": "Invalid Request", + "message": "Field 'name' is required" +} +``` + +#### 401 Unauthorized + +```json +{ + "error": "Unauthorized", + "message": "Invalid or expired token" +} +``` + +#### 404 Not Found + +```json +{ + "error": "Not Found", + "message": "Cluster with ID 'cluster-123' not found" +} +``` + +#### 500 Internal Server Error + +```json +{ + "error": "Internal Server Error", + "message": "Failed to connect to database" +} +``` + +--- + +# Part 2: 测试文档 + +## 测试策略 + +OCDP Backend 采用多层测试策略,确保代码质量和系统稳定性。 + +### 测试金字塔 + +``` + ┌────────────┐ + ╱ E2E ╱│ 少量(慢速、昂贵) + ╱ 集成测试 ╱ │ 中等(中速、适度) + ╱ 单元测试 ╱ │ 大量(快速、便宜) + └──────────┘ │ + │ │ + └────────────┘ +``` + +### 测试类型 + +| 测试类型 | 范围 | 速度 | 依赖 | 执行频率 | +|---------|------|------|------|----------| +| **单元测试** | Domain Layer | 快 | 无 | 每次提交 | +| **集成测试** | 跨层交互 | 中 | Mock | 每次提交 | +| **API 测试** | HTTP 接口 | 中 | Mock | 每次 PR | +| **E2E 测试** | 完整流程 | 慢 | 真实环境 | 发布前 | + +--- + +## 单元测试 + +### 测试 Domain Service + +单元测试专注于业务逻辑,使用 Mock Repository。 + +#### 示例:测试 ClusterService + +```go +// internal/domain/service/cluster_service_test.go +package service_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "ocdp-backend/internal/adapter/output/persistence/mock" + "ocdp-backend/internal/domain/service" +) + +func TestClusterService_CreateCluster(t *testing.T) { + // Arrange + repo := mock.NewClusterRepositoryMock() + svc := service.NewClusterService(repo) + + ctx := context.Background() + name := "Test Cluster" + host := "https://k8s.test:6443" + + // Act + cluster, err := svc.CreateCluster(ctx, name, host, "desc", "ca", "cert", "key") + + // Assert + require.NoError(t, err) + assert.NotEmpty(t, cluster.ID) + assert.Equal(t, name, cluster.Name) + assert.Equal(t, host, cluster.Host) +} + +func TestClusterService_GetCluster(t *testing.T) { + // Arrange + repo := mock.NewClusterRepositoryMock() + svc := service.NewClusterService(repo) + ctx := context.Background() + + // 先创建集群 + created, _ := svc.CreateCluster(ctx, "Test", "https://k8s.test:6443", "", "", "", "") + + // Act + cluster, err := svc.GetCluster(ctx, created.ID) + + // Assert + require.NoError(t, err) + assert.Equal(t, created.ID, cluster.ID) + assert.Equal(t, created.Name, cluster.Name) +} + +func TestClusterService_GetCluster_NotFound(t *testing.T) { + // Arrange + repo := mock.NewClusterRepositoryMock() + svc := service.NewClusterService(repo) + ctx := context.Background() + + // Act + cluster, err := svc.GetCluster(ctx, "non-existent-id") + + // Assert + assert.Error(t, err) + assert.Nil(t, cluster) +} + +func TestClusterService_ListClusters(t *testing.T) { + // Arrange + repo := mock.NewClusterRepositoryMock() + svc := service.NewClusterService(repo) + ctx := context.Background() + + // 创建多个集群 + svc.CreateCluster(ctx, "Cluster 1", "https://k8s1.test:6443", "", "", "", "") + svc.CreateCluster(ctx, "Cluster 2", "https://k8s2.test:6443", "", "", "", "") + + // Act + clusters, err := svc.ListClusters(ctx) + + // Assert + require.NoError(t, err) + assert.Len(t, clusters, 2) +} + +func TestClusterService_DeleteCluster(t *testing.T) { + // Arrange + repo := mock.NewClusterRepositoryMock() + svc := service.NewClusterService(repo) + ctx := context.Background() + + cluster, _ := svc.CreateCluster(ctx, "Test", "https://k8s.test:6443", "", "", "", "") + + // Act + err := svc.DeleteCluster(ctx, cluster.ID) + + // Assert + require.NoError(t, err) + + // 验证已删除 + _, err = svc.GetCluster(ctx, cluster.ID) + assert.Error(t, err) +} +``` + +#### 示例:测试 AuthService + +```go +// internal/domain/service/auth_service_test.go +package service_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "ocdp-backend/internal/adapter/output/persistence/mock" + "ocdp-backend/internal/domain/service" + "ocdp-backend/internal/pkg/password" + "ocdp-backend/internal/pkg/jwt" +) + +func TestAuthService_Register(t *testing.T) { + // Arrange + userRepo := mock.NewUserRepositoryMock() + hasher := password.NewBcryptHasher() + jwtGen := jwt.NewJWTGenerator("test-secret") + svc := service.NewAuthService(userRepo, hasher, jwtGen) + + ctx := context.Background() + + // Act + user, err := svc.Register(ctx, "testuser", "password123", "test@example.com") + + // Assert + require.NoError(t, err) + assert.NotEmpty(t, user.ID) + assert.Equal(t, "testuser", user.Username) + assert.NotEqual(t, "password123", user.PasswordHash) // 密码已哈希 +} + +func TestAuthService_Login(t *testing.T) { + // Arrange + userRepo := mock.NewUserRepositoryMock() + hasher := password.NewBcryptHasher() + jwtGen := jwt.NewJWTGenerator("test-secret") + svc := service.NewAuthService(userRepo, hasher, jwtGen) + + ctx := context.Background() + + // 先注册用户 + svc.Register(ctx, "testuser", "password123", "test@example.com") + + // Act + response, err := svc.Login(ctx, "testuser", "password123") + + // Assert + require.NoError(t, err) + assert.NotEmpty(t, response.AccessToken) + assert.NotEmpty(t, response.RefreshToken) + assert.Equal(t, "testuser", response.User.Username) +} + +func TestAuthService_Login_WrongPassword(t *testing.T) { + // Arrange + userRepo := mock.NewUserRepositoryMock() + hasher := password.NewBcryptHasher() + jwtGen := jwt.NewJWTGenerator("test-secret") + svc := service.NewAuthService(userRepo, hasher, jwtGen) + + ctx := context.Background() + svc.Register(ctx, "testuser", "password123", "test@example.com") + + // Act + response, err := svc.Login(ctx, "testuser", "wrongpassword") + + // Assert + assert.Error(t, err) + assert.Nil(t, response) +} +``` + +### 运行单元测试 + +```bash +# 运行所有单元测试 +go test ./internal/domain/... + +# 运行特定包 +go test ./internal/domain/service + +# 带覆盖率 +go test -cover ./internal/domain/... + +# 详细输出 +go test -v ./internal/domain/... + +# 生成覆盖率报告 +go test -coverprofile=coverage.out ./internal/domain/... +go tool cover -html=coverage.out +``` + +--- + +## 集成测试 + +集成测试验证跨层交互,使用 Mock 适配器。 + +### 示例:测试 REST Handler + Service + +```go +// internal/adapter/input/http/rest/cluster_handler_test.go +package rest_test + +import ( + "bytes" + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gorilla/mux" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "ocdp-backend/internal/adapter/input/http/dto" + "ocdp-backend/internal/adapter/input/http/rest" + "ocdp-backend/internal/adapter/output/persistence/mock" + "ocdp-backend/internal/domain/service" +) + +func setupClusterHandler() (*rest.ClusterHandler, *service.ClusterService) { + repo := mock.NewClusterRepositoryMock() + svc := service.NewClusterService(repo) + handler := rest.NewClusterHandler(svc) + return handler, svc +} + +func TestClusterHandler_CreateCluster(t *testing.T) { + // Arrange + handler, _ := setupClusterHandler() + + reqBody := dto.CreateClusterRequest{ + Name: "Test Cluster", + Host: "https://k8s.test:6443", + Description: "Test cluster", + CAData: "ca-data", + CertData: "cert-data", + KeyData: "key-data", + } + + body, _ := json.Marshal(reqBody) + req := httptest.NewRequest(http.MethodPost, "/api/v1/clusters", bytes.NewBuffer(body)) + req.Header.Set("Content-Type", "application/json") + rec := httptest.NewRecorder() + + // Act + handler.CreateCluster(rec, req) + + // Assert + assert.Equal(t, http.StatusCreated, rec.Code) + + var response map[string]interface{} + json.Unmarshal(rec.Body.Bytes(), &response) + + assert.NotEmpty(t, response["id"]) + assert.Equal(t, "Test Cluster", response["name"]) +} + +func TestClusterHandler_GetAllClusters(t *testing.T) { + // Arrange + handler, svc := setupClusterHandler() + + // 创建测试数据 + ctx := context.Background() + svc.CreateCluster(ctx, "Cluster 1", "https://k8s1.test:6443", "", "", "", "") + svc.CreateCluster(ctx, "Cluster 2", "https://k8s2.test:6443", "", "", "", "") + + req := httptest.NewRequest(http.MethodGet, "/api/v1/clusters", nil) + rec := httptest.NewRecorder() + + // Act + handler.GetAllClusters(rec, req) + + // Assert + assert.Equal(t, http.StatusOK, rec.Code) + + var clusters []map[string]interface{} + json.Unmarshal(rec.Body.Bytes(), &clusters) + + assert.Len(t, clusters, 2) +} + +func TestClusterHandler_GetCluster(t *testing.T) { + // Arrange + handler, svc := setupClusterHandler() + ctx := context.Background() + + cluster, _ := svc.CreateCluster(ctx, "Test", "https://k8s.test:6443", "", "", "", "") + + req := httptest.NewRequest(http.MethodGet, "/api/v1/clusters/"+cluster.ID, nil) + req = mux.SetURLVars(req, map[string]string{"clusterId": cluster.ID}) + rec := httptest.NewRecorder() + + // Act + handler.GetCluster(rec, req) + + // Assert + assert.Equal(t, http.StatusOK, rec.Code) + + var response map[string]interface{} + json.Unmarshal(rec.Body.Bytes(), &response) + + assert.Equal(t, cluster.ID, response["id"]) +} +``` + +### 运行集成测试 + +```bash +# 使用 Mock 模式运行所有测试 +ADAPTER_MODE=mock go test ./... + +# 测试特定模块 +go test ./internal/adapter/input/http/rest/... + +# 并行测试 +go test -parallel 4 ./... +``` + +--- + +## API 测试 + +使用 HTTP 客户端测试完整的 API 流程。 + +### 使用 cURL 测试 + +```bash +#!/bin/bash +# scripts/test-api.sh + +BASE_URL="http://localhost:8080/api/v1" + +# 健康检查 +echo "=== Health Check ===" +curl -X GET http://localhost:8080/health + +# 注册用户 +echo "\n=== Register User ===" +curl -X POST $BASE_URL/auth/register \ + -H "Content-Type: application/json" \ + -d '{ + "username": "testuser", + "password": "test123", + "email": "test@example.com" + }' + +# 登录 +echo "\n=== Login ===" +LOGIN_RESPONSE=$(curl -X POST $BASE_URL/auth/login \ + -H "Content-Type: application/json" \ + -d '{ + "username": "testuser", + "password": "test123" + }') + +TOKEN=$(echo $LOGIN_RESPONSE | jq -r '.accessToken') + +# 创建集群 +echo "\n=== Create Cluster ===" +curl -X POST $BASE_URL/clusters \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $TOKEN" \ + -d '{ + "name": "Test Cluster", + "host": "https://k8s.test:6443", + "description": "Test cluster", + "caData": "LS0tLS...", + "certData": "LS0tLS...", + "keyData": "LS0tLS..." + }' + +# 列出集群 +echo "\n=== List Clusters ===" +curl -X GET $BASE_URL/clusters \ + -H "Authorization: Bearer $TOKEN" + +# 创建 Registry +echo "\n=== Create Registry ===" +curl -X POST $BASE_URL/registries \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $TOKEN" \ + -d '{ + "name": "Test Harbor", + "url": "https://harbor.test.com", + "username": "admin", + "password": "secret" + }' + +# 列出 Registries +echo "\n=== List Registries ===" +curl -X GET $BASE_URL/registries \ + -H "Authorization: Bearer $TOKEN" +``` + +### 使用 Go 测试 HTTP 接口 + +```go +// test/api/api_test.go +package api_test + +import ( + "bytes" + "encoding/json" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const baseURL = "http://localhost:8080/api/v1" + +func TestAPI_HealthCheck(t *testing.T) { + resp, err := http.Get("http://localhost:8080/health") + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) +} + +func TestAPI_AuthFlow(t *testing.T) { + // 1. 注册 + registerBody := map[string]string{ + "username": "apitest", + "password": "test123", + "email": "apitest@example.com", + } + + body, _ := json.Marshal(registerBody) + resp, err := http.Post(baseURL+"/auth/register", "application/json", bytes.NewBuffer(body)) + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusCreated, resp.StatusCode) + + // 2. 登录 + loginBody := map[string]string{ + "username": "apitest", + "password": "test123", + } + + body, _ = json.Marshal(loginBody) + resp, err = http.Post(baseURL+"/auth/login", "application/json", bytes.NewBuffer(body)) + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var loginResponse map[string]interface{} + json.NewDecoder(resp.Body).Decode(&loginResponse) + + assert.NotEmpty(t, loginResponse["accessToken"]) + assert.NotEmpty(t, loginResponse["refreshToken"]) +} + +func TestAPI_ClusterCRUD(t *testing.T) { + // 先获取 token + token := getAuthToken(t) + + // 1. 创建集群 + clusterBody := map[string]string{ + "name": "API Test Cluster", + "host": "https://k8s.test:6443", + "description": "Created by API test", + "caData": "test-ca", + "certData": "test-cert", + "keyData": "test-key", + } + + body, _ := json.Marshal(clusterBody) + req, _ := http.NewRequest(http.MethodPost, baseURL+"/clusters", bytes.NewBuffer(body)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+token) + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusCreated, resp.StatusCode) + + var cluster map[string]interface{} + json.NewDecoder(resp.Body).Decode(&cluster) + clusterID := cluster["id"].(string) + + // 2. 获取集群 + req, _ = http.NewRequest(http.MethodGet, baseURL+"/clusters/"+clusterID, nil) + req.Header.Set("Authorization", "Bearer "+token) + + resp, err = http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + + // 3. 删除集群 + req, _ = http.NewRequest(http.MethodDelete, baseURL+"/clusters/"+clusterID, nil) + req.Header.Set("Authorization", "Bearer "+token) + + resp, err = http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusNoContent, resp.StatusCode) +} + +func getAuthToken(t *testing.T) string { + // 登录并返回 token + loginBody := map[string]string{ + "username": "admin", + "password": "admin123", + } + + body, _ := json.Marshal(loginBody) + resp, _ := http.Post(baseURL+"/auth/login", "application/json", bytes.NewBuffer(body)) + defer resp.Body.Close() + + var response map[string]interface{} + json.NewDecoder(resp.Body).Decode(&response) + + return response["accessToken"].(string) +} +``` + +--- + +## E2E 测试 + +端到端测试验证完整的用户场景。 + +### E2E 测试示例 + +```go +// test/e2e/deployment_test.go +package e2e_test + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestE2E_CompleteDeploymentFlow(t *testing.T) { + if testing.Short() { + t.Skip("Skipping E2E test in short mode") + } + + ctx := context.Background() + + // 1. 注册用户 + t.Log("Step 1: Register user") + user := registerUser(t, "e2euser", "password123") + assert.NotEmpty(t, user.ID) + + // 2. 登录获取 token + t.Log("Step 2: Login") + token := login(t, "e2euser", "password123") + assert.NotEmpty(t, token) + + // 3. 创建 Registry + t.Log("Step 3: Create registry") + registry := createRegistry(t, token, "Test Harbor", "https://harbor.test.com") + assert.NotEmpty(t, registry.ID) + + // 4. 创建 Cluster + t.Log("Step 4: Create cluster") + cluster := createCluster(t, token, "Test K8s", "https://k8s.test:6443") + assert.NotEmpty(t, cluster.ID) + + // 5. 部署应用 + t.Log("Step 5: Deploy application") + instance := deployApp(t, token, cluster.ID, registry.ID, "nginx", "1.0.0") + assert.Equal(t, "deployed", instance.Status) + + // 6. 等待应用就绪 + t.Log("Step 6: Wait for application ready") + time.Sleep(10 * time.Second) + + // 7. 检查应用状态 + t.Log("Step 7: Check application status") + status := getInstanceStatus(t, token, cluster.ID, instance.ID) + assert.Equal(t, "deployed", status) + + // 8. 升级应用 + t.Log("Step 8: Upgrade application") + upgraded := upgradeApp(t, token, cluster.ID, instance.ID, "1.1.0") + assert.Equal(t, 2, upgraded.Revision) + + // 9. 卸载应用 + t.Log("Step 9: Uninstall application") + err := uninstallApp(t, token, cluster.ID, instance.ID) + assert.NoError(t, err) + + // 10. 清理 + t.Log("Step 10: Cleanup") + deleteCluster(t, token, cluster.ID) + deleteRegistry(t, token, registry.ID) +} +``` + +### 运行 E2E 测试 + +```bash +# 使用 Production 模式运行 E2E 测试 +ADAPTER_MODE=production DATABASE_URL="..." go test ./test/e2e/... -timeout 30m + +# 跳过 E2E 测试 +go test -short ./... +``` + +--- + +## Mock 测试 + +### Mock Repository 示例 + +所有 Repository 都有 Mock 实现,位于 `internal/adapter/output/persistence/mock/`。 + +```go +// internal/adapter/output/persistence/mock/cluster_repository_mock.go +package mock + +import ( + "context" + "fmt" + "sync" + + "ocdp-backend/internal/domain/entity" + "ocdp-backend/internal/domain/repository" +) + +type ClusterRepositoryMock struct { + clusters map[string]*entity.Cluster + mu sync.RWMutex +} + +func NewClusterRepositoryMock() repository.ClusterRepository { + return &ClusterRepositoryMock{ + clusters: make(map[string]*entity.Cluster), + } +} + +func (r *ClusterRepositoryMock) Create(ctx context.Context, cluster *entity.Cluster) error { + r.mu.Lock() + defer r.mu.Unlock() + + if _, exists := r.clusters[cluster.ID]; exists { + return fmt.Errorf("cluster already exists") + } + + r.clusters[cluster.ID] = cluster + return nil +} + +func (r *ClusterRepositoryMock) GetByID(ctx context.Context, id string) (*entity.Cluster, error) { + r.mu.RLock() + defer r.mu.RUnlock() + + cluster, exists := r.clusters[id] + if !exists { + return nil, fmt.Errorf("cluster not found") + } + + return cluster, nil +} + +func (r *ClusterRepositoryMock) List(ctx context.Context) ([]*entity.Cluster, error) { + r.mu.RLock() + defer r.mu.RUnlock() + + clusters := make([]*entity.Cluster, 0, len(r.clusters)) + for _, cluster := range r.clusters { + clusters = append(clusters, cluster) + } + + return clusters, nil +} + +func (r *ClusterRepositoryMock) Delete(ctx context.Context, id string) error { + r.mu.Lock() + defer r.mu.Unlock() + + if _, exists := r.clusters[id]; !exists { + return fmt.Errorf("cluster not found") + } + + delete(r.clusters, id) + return nil +} +``` + +--- + +## 测试工具 + +### 推荐的测试库 + +```go +import ( + "testing" + + "github.com/stretchr/testify/assert" // 断言 + "github.com/stretchr/testify/require" // 必需条件 + "github.com/stretchr/testify/mock" // Mock 对象 + "github.com/stretchr/testify/suite" // 测试套件 +) +``` + +### 安装测试依赖 + +```bash +go get github.com/stretchr/testify +``` + +### 测试覆盖率 + +```bash +# 生成覆盖率报告 +go test -coverprofile=coverage.out ./... + +# 查看覆盖率 +go tool cover -func=coverage.out + +# HTML 报告 +go tool cover -html=coverage.out -o coverage.html +``` + +### 性能测试 + +```go +func BenchmarkClusterService_CreateCluster(b *testing.B) { + repo := mock.NewClusterRepositoryMock() + svc := service.NewClusterService(repo) + ctx := context.Background() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + svc.CreateCluster(ctx, "Test", "https://k8s.test:6443", "", "", "", "") + } +} + +// 运行性能测试 +// go test -bench=. -benchmem ./internal/domain/service/ +``` + +--- + +## 测试最佳实践 + +### 1. 测试命名规范 + +```go +// ✅ 好的命名 +func TestClusterService_CreateCluster(t *testing.T) +func TestClusterService_CreateCluster_DuplicateName(t *testing.T) +func TestClusterHandler_GetCluster_NotFound(t *testing.T) + +// ❌ 不好的命名 +func TestCreate(t *testing.T) +func Test1(t *testing.T) +``` + +### 2. AAA 模式 (Arrange-Act-Assert) + +```go +func TestExample(t *testing.T) { + // Arrange - 准备测试数据 + repo := mock.NewClusterRepositoryMock() + svc := service.NewClusterService(repo) + + // Act - 执行操作 + cluster, err := svc.CreateCluster(ctx, "Test", "https://k8s.test:6443", "", "", "", "") + + // Assert - 验证结果 + require.NoError(t, err) + assert.NotEmpty(t, cluster.ID) +} +``` + +### 3. 表驱动测试 + +```go +func TestClusterValidation(t *testing.T) { + tests := []struct { + name string + input string + want bool + wantErr bool + }{ + {"valid URL", "https://k8s.example.com:6443", true, false}, + {"invalid URL", "not-a-url", false, true}, + {"empty URL", "", false, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := validateClusterURL(tt.input) + + if tt.wantErr { + assert.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, tt.want, got) + } + }) + } +} +``` + +### 4. 使用测试辅助函数 + +```go +// test/helpers/helpers.go +package helpers + +func CreateTestCluster(t *testing.T, svc *service.ClusterService) *entity.Cluster { + t.Helper() + + cluster, err := svc.CreateCluster( + context.Background(), + "Test Cluster", + "https://k8s.test:6443", + "", "", "", "", + ) + + require.NoError(t, err) + return cluster +} + +// 在测试中使用 +func TestSomething(t *testing.T) { + cluster := helpers.CreateTestCluster(t, svc) + // ... +} +``` + +### 5. 清理测试数据 + +```go +func TestWithCleanup(t *testing.T) { + repo := mock.NewClusterRepositoryMock() + svc := service.NewClusterService(repo) + ctx := context.Background() + + cluster, _ := svc.CreateCluster(ctx, "Test", "https://k8s.test:6443", "", "", "", "") + + // 确保清理 + t.Cleanup(func() { + svc.DeleteCluster(ctx, cluster.ID) + }) + + // 测试逻辑 + // ... +} +``` + +### 6. 并行测试 + +```go +func TestParallel(t *testing.T) { + t.Parallel() // 标记为可并行 + + // 测试逻辑 +} +``` + +### 7. 跳过测试 + +```go +func TestSomething(t *testing.T) { + if testing.Short() { + t.Skip("Skipping test in short mode") + } + + // 长时间运行的测试 +} + +// 运行: go test -short +``` + +--- + +## CI/CD 集成 + +### GitHub Actions 示例 + +```yaml +# .github/workflows/test.yml +name: Tests + +on: [push, pull_request] + +jobs: + test: + runs-on: ubuntu-latest + + services: + postgres: + image: postgres:15 + env: + POSTGRES_PASSWORD: postgres + POSTGRES_DB: ocdp_test + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + + steps: + - uses: actions/checkout@v3 + + - name: Setup Go + uses: actions/setup-go@v4 + with: + go-version: '1.21' + + - name: Install dependencies + run: go mod download + + - name: Run unit tests + run: go test -short -cover ./internal/domain/... + + - name: Run integration tests (Mock) + env: + ADAPTER_MODE: mock + run: go test -short ./... + + - name: Run integration tests (Production) + env: + ADAPTER_MODE: production + DATABASE_URL: postgresql://postgres:postgres@localhost:5432/ocdp_test?sslmode=disable + run: go test ./... + + - name: Generate coverage report + run: | + go test -coverprofile=coverage.out ./... + go tool cover -func=coverage.out +``` + +--- + +## 相关文档 + +- [架构文档](architecture.md) +- [部署文档](deployment.md) +- [主 README](../README.md) + +--- + +**Last Updated**: 2025-11-09 +**API Version**: v1 diff --git a/backend/docs/architecture.md b/backend/docs/architecture.md new file mode 100644 index 0000000..601aa4c --- /dev/null +++ b/backend/docs/architecture.md @@ -0,0 +1,1305 @@ +# 🏗️ 架构文档 + +## 目录 + +- [2.1.1 需求描述](#211-需求描述) + - [项目背景](#项目背景) + - [核心需求](#核心需求) + - [功能需求](#功能需求) + - [非功能需求](#非功能需求) +- [2.1.2 业务建模](#212-业务建模) + - [业务领域](#业务领域) + - [核心实体](#核心实体) + - [业务流程](#业务流程) + - [用例场景](#用例场景) +- [2.1.3 技术建模](#213-技术建模) + - [2.1.3.1 六边形架构](#2131-六边形架构) + - [2.1.3.2 技术选型](#2132-技术选型) + +--- + +# 2.1.1 需求描述 + +## 项目背景 + +### 问题陈述 + +在云原生时代,Kubernetes 已成为容器编排的事实标准,Helm 是 Kubernetes 的包管理工具。然而,当前企业在使用 Helm 进行应用部署时面临以下挑战: + +1. **多集群管理复杂** - 企业通常有多个 Kubernetes 集群(开发、测试、生产),需要统一的管理界面 +2. **制品仓库分散** - Helm Chart 可能分布在多个 OCI Registry(Harbor、Docker Hub、GHCR)中,难以统一浏览 +3. **部署流程繁琐** - 需要手动编写 `helm install` 命令,配置复杂的 values.yaml +4. **缺乏可视化** - 命令行操作对非技术人员不友好,缺少直观的 UI +5. **版本管理困难** - 应用升级需要记住历史版本,容易出错 +6. **监控信息分散** - 需要单独访问 Kubernetes Dashboard 查看应用状态 + +### 解决方案 + +OCDP Backend 提供统一的后端 API 服务,实现: + +- ✅ **统一管理** - 集中管理多个 Kubernetes 集群和 OCI Registry +- ✅ **可视化部署** - 通过 API 简化 Helm Chart 的浏览和部署流程 +- ✅ **版本控制** - 完整的应用生命周期管理(安装、升级、卸载) +- ✅ **实时监控** - 集成 Kubernetes API,实时获取应用状态和资源使用情况 +- ✅ **安全认证** - 支持用户认证和敏感数据加密存储 + +--- + +## 核心需求 + +### 业务需求 + +| 需求 ID | 需求描述 | 优先级 | +|---------|---------|--------| +| BR-001 | 支持管理多个 Kubernetes 集群 | P0 | +| BR-002 | 支持管理多个 OCI Registry | P0 | +| BR-003 | 浏览和搜索 Helm Chart 制品 | P0 | +| BR-004 | 部署 Helm Chart 到 Kubernetes 集群 | P0 | +| BR-005 | 升级已部署的应用 | P0 | +| BR-006 | 查看应用实时状态和资源使用 | P1 | +| BR-007 | 用户认证和权限管理 | P1 | +| BR-008 | 审计日志记录 | P2 | + +### 用户角色 + +1. **平台管理员** - 管理集群、Registry、用户 +2. **开发者** - 部署和管理自己的应用 +3. **运维人员** - 监控和维护应用状态 +4. **访客** - 只读查看应用列表和状态 + +--- + +## 功能需求 + +### F1. 集群管理 + +**描述**: 管理多个 Kubernetes 集群的连接配置 + +**功能点**: +- 添加集群(配置 API Server 地址、证书) +- 查看集群列表和详情 +- 测试集群连接健康状态 +- 更新和删除集群配置 +- 查看集群资源使用情况(CPU、内存、节点数) + +**验收标准**: +- ✅ 支持证书认证和 Token 认证 +- ✅ 敏感信息(证书、密钥)加密存储 +- ✅ 连接失败时给出清晰的错误提示 +- ✅ 支持测试连接功能 + +--- + +### F2. Registry 管理 + +**描述**: 管理多个 OCI Registry 的连接配置 + +**功能点**: +- 添加 Registry(Harbor、Docker Hub、GHCR 等) +- 配置认证信息(用户名/密码) +- 查看 Registry 列表和详情 +- 测试 Registry 连接健康状态 +- 更新和删除 Registry 配置 + +**验收标准**: +- ✅ 支持 Basic Auth 和 Bearer Token +- ✅ 密码加密存储 +- ✅ 支持 HTTP/HTTPS 和自签名证书 +- ✅ 连接测试返回响应时间 + +--- + +### F3. Artifact 浏览 + +**描述**: 浏览和搜索 OCI Registry 中的 Helm Chart + +**功能点**: +- 列出 Registry 中的所有仓库 +- 列出仓库中的所有制品(tags) +- 查看制品详情(大小、创建时间、annotations) +- 自动识别制品类型(Helm Chart、Docker Image) +- 获取 Helm Chart 的 values schema + +**验收标准**: +- ✅ 符合 OCI Distribution Specification +- ✅ 支持 URL 编码的仓库名称(如 `charts/app`) +- ✅ 正确解析 manifest 和 config +- ✅ 计算制品总大小(包含所有 layers) + +--- + +### F4. 应用部署 + +**描述**: 部署 Helm Chart 到 Kubernetes 集群 + +**功能点**: +- 选择集群、Registry、Chart 和版本 +- 配置 values(JSON 或 YAML) +- 安装应用到指定 namespace +- 查看安装进度和状态 +- 获取应用访问端点 + +**验收标准**: +- ✅ 支持自定义 Release 名称 +- ✅ 支持 JSON 和 YAML 格式的 values +- ✅ 记录部署历史 + +--- + +### F5. 应用生命周期管理 + +**描述**: 管理已部署应用的完整生命周期 + +**功能点**: +- 查看应用列表和详情 +- 升级应用到新版本 +- 查看部署历史 +- 卸载应用 + +**验收标准**: +- ✅ 升级时保留配置 +- ✅ 卸载时可选保留历史 +- ✅ 显示每次部署的描述信息 + +--- + +### F6. 监控和状态 + +**描述**: 实时监控应用和集群状态 + +**功能点**: +- 查看应用实时状态(Running、Failed 等) +- 查看应用资源使用(CPU、内存) +- 查看集群整体监控 +- 查看节点资源使用 +- 监控摘要统计 + +**验收标准**: +- ✅ 实时获取 Kubernetes 资源状态 +- ✅ 支持 Prometheus 指标集成 +- ✅ 显示 Pod 状态和事件 +- ✅ 资源使用百分比显示 + +--- + +### F7. 认证和授权 + +**描述**: 用户身份认证和访问控制 + +**功能点**: +- 用户注册和登录 +- JWT Token 认证 +- Token 刷新机制 +- 密码加密存储 + +**验收标准**: +- ✅ 使用 bcrypt 哈希密码 +- ✅ JWT Token 有效期配置 +- ✅ Refresh Token 支持 +- ✅ 密码强度验证 + +--- + +## 非功能需求 + +### NFR1. 性能要求 + +| 指标 | 要求 | +|------|------| +| API 响应时间 | P95 < 500ms | +| 并发用户数 | 支持 100+ 并发 | +| 数据库连接池 | 25 个连接 | +| Helm 操作超时 | 5 分钟 | + +### NFR2. 可用性 + +- **系统可用性**: 99.5% +- **故障恢复时间**: < 5 分钟 +- **数据备份**: 每日备份 +- **健康检查**: 提供 `/health` 端点 + +### NFR3. 安全性 + +- **数据加密**: AES-256 加密敏感数据 +- **传输安全**: 支持 HTTPS +- **认证方式**: JWT Token +- **密码策略**: 最小长度 8 位 +- **审计日志**: 记录所有操作(未来) + +### NFR4. 可扩展性 + +- **水平扩展**: 支持多实例部署 +- **数据库**: PostgreSQL 支持主从复制 +- **无状态设计**: API 服务无状态 +- **缓存策略**: 支持 Redis(未来) + +### NFR5. 可维护性 + +- **代码质量**: 遵循 Go 最佳实践 +- **测试覆盖**: > 70% +- **文档完整**: API 文档、架构文档、部署文档 +- **日志记录**: 结构化日志 +- **监控指标**: Prometheus 指标(未来) + +### NFR6. 兼容性 + +- **Kubernetes 版本**: 1.24+ +- **Helm 版本**: 3.x +- **OCI Registry**: 符合 OCI Distribution Spec +- **数据库**: PostgreSQL 15+ +- **Go 版本**: 1.21+ + +--- + +# 2.1.2 业务建模 + +## 业务领域 + +OCDP Backend 属于**云原生应用管理**领域,主要涉及以下子域: + +1. **制品管理域** - OCI Registry、Helm Chart、Docker Image +2. **集群管理域** - Kubernetes Cluster、Node、Namespace +3. **应用部署域** - Helm Release、Application Instance +4. **监控运维域** - Metrics、Logs、Events + +--- + +## 核心实体 + +### 领域模型图 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Core Domain Entities │ +└─────────────────────────────────────────────────────────────┘ + +┌──────────┐ ┌──────────┐ ┌──────────┐ +│ User │─────────│ Cluster │─────────│ Instance │ +└──────────┘ └──────────┘ └──────────┘ + │ │ │ + │ │ │ + ▼ ▼ ▼ +┌──────────┐ ┌──────────┐ ┌──────────┐ +│ Auth │ │ Health │ │ Status │ +│ Token │ │ Check │ │ Resource │ +└──────────┘ └──────────┘ └──────────┘ + +┌──────────┐ ┌──────────┐ ┌──────────┐ +│ Registry │─────────│Artifact │─────────│ Chart │ +└──────────┘ └──────────┘ └──────────┘ + │ │ │ + │ │ │ + ▼ ▼ ▼ +┌──────────┐ ┌──────────┐ ┌──────────┐ +│ Health │ │Repository│ │ Values │ +│ Check │ │ │ │ Schema │ +└──────────┘ └──────────┘ └──────────┘ +``` + +### 实体详解 + +#### 1. User(用户) + +**职责**: 表示系统用户,负责身份认证 + +**属性**: +- `ID`: 唯一标识符 +- `Username`: 用户名(唯一) +- `Email`: 邮箱 +- `PasswordHash`: 密码哈希(bcrypt) +- `CreatedAt`: 创建时间 +- `UpdatedAt`: 更新时间 + +**业务规则**: +- 用户名唯一 +- 邮箱格式验证 +- 密码最小长度 8 位 + +--- + +#### 2. Cluster(集群) + +**职责**: 表示 Kubernetes 集群连接配置 + +**属性**: +- `ID`: 唯一标识符 +- `Name`: 集群名称 +- `Host`: API Server 地址 +- `Description`: 描述 +- `CAData`: CA 证书(加密存储) +- `CertData`: 客户端证书(加密存储) +- `KeyData`: 客户端密钥(加密存储) +- `Token`: Bearer Token(可选) +- `CreatedAt`: 创建时间 +- `UpdatedAt`: 更新时间 + +**业务规则**: +- 集群名称唯一 +- 必须提供证书或 Token +- Host 必须是有效的 HTTPS URL + +--- + +#### 3. Registry(镜像仓库) + +**职责**: 表示 OCI Registry 连接配置 + +**属性**: +- `ID`: 唯一标识符 +- `Name`: Registry 名称 +- `URL`: Registry URL +- `Description`: 描述 +- `Username`: 用户名 +- `Password`: 密码(加密存储) +- `Insecure`: 是否跳过 SSL 验证 +- `CreatedAt`: 创建时间 +- `UpdatedAt`: 更新时间 + +**业务规则**: +- Registry 名称唯一 +- URL 必须是有效的 HTTP/HTTPS URL +- 密码加密存储 + +--- + +#### 4. Instance(应用实例) + +**职责**: 表示部署在 Kubernetes 中的 Helm Release + +**属性**: +- `ID`: 唯一标识符 +- `Name`: Release 名称 +- `Namespace`: Kubernetes namespace +- `ClusterID`: 所属集群 +- `RegistryID`: Chart 来源 Registry +- `Repository`: Chart 仓库名 +- `Chart`: Chart 名称 +- `Version`: Chart 版本 +- `Status`: 部署状态 +- `Revision`: 当前版本号 +- `Values`: 配置值(JSON) +- `Description`: 描述 +- `CreatedAt`: 创建时间 +- `UpdatedAt`: 更新时间 + +**业务规则**: +- 同一集群和 namespace 下 Release 名称唯一 +- 必须关联有效的 Cluster 和 Registry +- Values 必须是有效的 JSON 或 YAML + +--- + +#### 5. Artifact(制品) + +**职责**: 表示 OCI Registry 中的制品(Helm Chart、Docker Image 等) + +**属性**: +- `RepositoryName`: 仓库名称 +- `Tag`: 标签 +- `Digest`: SHA256 摘要 +- `Type`: 制品类型(helm、docker、oci) +- `Size`: 总大小 +- `MediaType`: 媒体类型 +- `Annotations`: 元数据 +- `CreatedAt`: 创建时间 + +**业务规则**: +- Tag 或 Digest 至少提供一个 +- 自动识别制品类型 +- 计算所有 layers 的总大小 + +--- + +## 业务流程 + +### 流程 1: 用户注册和登录 + +``` +┌──────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ +│ User │ │ Handler │ │ Service │ │ Repo │ +└──┬───┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ + │ │ │ │ + │ Register │ │ │ + ├────────────>│ │ │ + │ │ Create User │ │ + │ ├──────────────>│ │ + │ │ │ Hash Password │ + │ │ ├──────┐ │ + │ │ │<─────┘ │ + │ │ │ Save User │ + │ │ ├──────────────>│ + │ │ │<──────────────┤ + │ │<──────────────┤ │ + │<────────────┤ │ │ + │ │ │ │ + │ Login │ │ │ + ├────────────>│ │ │ + │ │ Authenticate │ │ + │ ├──────────────>│ │ + │ │ │ Verify Pwd │ + │ │ ├──────┐ │ + │ │ │<─────┘ │ + │ │ │ Gen JWT │ + │ │ ├──────┐ │ + │ │ │<─────┘ │ + │ │<──────────────┤ │ + │<────────────┤ Return Token │ │ + │ │ │ │ +``` + +--- + +### 流程 2: 部署应用 + +``` +┌──────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ +│ User │ │ Handler │ │ Service │ │HelmClient│ │Kubernetes│ +└──┬───┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ + │ │ │ │ │ + │ Deploy App │ │ │ │ + ├────────────>│ │ │ │ + │ │ Create Inst │ │ │ + │ ├──────────────>│ │ │ + │ │ │ Validate │ │ + │ │ ├──────┐ │ │ + │ │ │<─────┘ │ │ + │ │ │ Pull Chart │ │ + │ │ ├──────────────>│ │ + │ │ │<──────────────┤ │ + │ │ │ Install Chart │ │ + │ │ ├──────────────>│ │ + │ │ │ │ Apply K8s │ + │ │ │ ├──────────────>│ + │ │ │ │<──────────────┤ + │ │ │<──────────────┤ │ + │ │ │ Save Instance │ │ + │ │ ├──────┐ │ │ + │ │ │<─────┘ │ │ + │ │<──────────────┤ │ │ + │<────────────┤ Return Status │ │ │ + │ │ │ │ │ +``` + +--- + +### 流程 3: 浏览制品 + +``` +┌──────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ +│ User │ │ Handler │ │ Service │ │OCIClient │ │ Registry │ +└──┬───┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ + │ │ │ │ │ + │List Repos │ │ │ │ + ├────────────>│ │ │ │ + │ │ Get Repos │ │ │ + │ ├──────────────>│ │ │ + │ │ │ List Catalog │ │ + │ │ ├──────────────>│ │ + │ │ │ │ GET _catalog │ + │ │ │ ├──────────────>│ + │ │ │ │<──────────────┤ + │ │ │<──────────────┤ │ + │ │<──────────────┤ │ │ + │<────────────┤ │ │ │ + │ │ │ │ │ + │List Tags │ │ │ │ + ├────────────>│ │ │ │ + │ │ Get Artifacts │ │ │ + │ ├──────────────>│ │ │ + │ │ │ List Tags │ │ + │ │ ├──────────────>│ │ + │ │ │ │ GET tags/list │ + │ │ │ ├──────────────>│ + │ │ │ │<──────────────┤ + │ │ │ Get Manifest │ │ + │ │ ├──────────────>│ │ + │ │ │ │ GET manifest │ + │ │ │ ├──────────────>│ + │ │ │ │<──────────────┤ + │ │ │<──────────────┤ │ + │ │<──────────────┤ │ │ + │<────────────┤ │ │ │ + │ │ │ │ │ +``` + +--- + +## 用例场景 + +### UC1: 开发者部署测试应用 + +**主角**: 开发者 Alice + +**前置条件**: +- Alice 已登录系统 +- 系统中已配置开发环境集群 +- 系统中已配置 Harbor Registry + +**基本流程**: +1. Alice 选择"开发集群" +2. Alice 浏览 Harbor 中的"charts/nginx"仓库 +3. Alice 选择 nginx 1.0.0 版本 +4. Alice 配置 values: `{"replicaCount": 2}` +5. Alice 点击"部署" +6. 系统显示部署进度 +7. 部署成功,显示应用访问地址 + +**后置条件**: +- nginx 应用成功部署到开发集群 +- 应用状态为 "deployed" +- Alice 可以访问应用 + +**异常流程**: +- 3a. Chart 版本不存在 → 显示错误提示 +- 5a. 部署失败 → 显示错误日志 + +--- + +### UC2: 运维人员升级生产应用 + +**主角**: 运维 Bob + +**前置条件**: +- Bob 已登录系统 +- 生产环境有运行中的 nginx 应用(版本 1.0.0) + +**基本流程**: +1. Bob 进入"生产集群"应用列表 +2. Bob 选择 nginx 应用 +3. Bob 查看当前版本和配置 +4. Bob 点击"升级" +5. Bob 选择新版本 1.1.0 +6. Bob 更新配置: `{"replicaCount": 3}` +7. Bob 添加升级说明 +8. Bob 确认升级 +9. 系统执行滚动升级 +10. 升级成功,Revision 增加到 2 + +**后置条件**: +- nginx 应用升级到 1.1.0 +- 副本数增加到 3 +- 历史记录中保留了 Revision 1 + +**异常流程**: +- 9a. 升级失败 → 显示错误信息,保持原状态 +- 9b. 超时 → 取消升级,保持原状态 + +--- + +### UC3: 管理员添加新集群 + +**主角**: 管理员 Charlie + +**前置条件**: +- Charlie 已登录系统 +- Charlie 有集群的 kubeconfig 文件 + +**基本流程**: +1. Charlie 进入"集群管理"页面 +2. Charlie 点击"添加集群" +3. Charlie 填写集群信息: + - 名称: "Production Cluster" + - API Server: "https://k8s.prod.com:6443" + - 描述: "生产环境集群" +4. Charlie 从 kubeconfig 提取证书数据 +5. Charlie 粘贴 CA、Cert、Key 数据 +6. Charlie 点击"测试连接" +7. 系统显示"连接成功" +8. Charlie 保存配置 + +**后置条件**: +- 新集群添加到系统 +- 集群证书加密存储 +- 其他用户可以使用此集群 + +**异常流程**: +- 6a. 连接失败 → 显示具体错误信息 +- 6b. 证书格式错误 → 提示正确的格式 + +--- + +# 2.1.3 技术建模 + +## 2.1.3.1 六边形架构 + +### 架构概述 + +OCDP Backend 采用**六边形架构**(Hexagonal Architecture,也称为端口和适配器架构),这是一种分层架构模式,强调业务逻辑与外部依赖的分离。 + +### 核心原则 + +1. **依赖倒置** - 所有层依赖 Domain,Domain 无外部依赖 +2. **端口和适配器** - 通过接口(Port)定义交互协议,通过适配器(Adapter)实现具体技术 +3. **可测试性** - 业务逻辑可独立测试,无需外部依赖 +4. **可替换性** - 适配器可轻松替换(Mock ↔ Production) + +### 架构分层图 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Input Adapters │ +│ (HTTP REST API) │ +│ │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ Auth Handler │ │Cluster Handl │ │Instance Handl│ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +│ │ +└──────────────────────┬──────────────────────────────────────┘ + │ DTO / Request + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ Domain Layer │ +│ (Business Logic) │ +│ │ +│ ┌──────────────────────────────────────────────────────┐ │ +│ │ Entities │ │ +│ │ User | Cluster | Registry | Instance | Artifact │ │ +│ └──────────────────────────────────────────────────────┘ │ +│ │ +│ ┌──────────────────────────────────────────────────────┐ │ +│ │ Services │ │ +│ │ AuthService | ClusterService | InstanceService │ │ +│ └──────────────────────────────────────────────────────┘ │ +│ │ +│ ┌──────────────────────────────────────────────────────┐ │ +│ │ Repository Interfaces (Ports) │ │ +│ │ UserRepo | ClusterRepo | OCIClient | HelmClient │ │ +│ └──────────────────────────────────────────────────────┘ │ +│ │ +└──────────────────────┬──────────────────────────────────────┘ + │ Interface Contract + ↓ +┌─────────────────────────────────────────────────────────────┐ +│ Output Adapters │ +│ (Infrastructure Implementations) │ +│ │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ Database │ │ OCI Client │ │ Helm Client │ │ +│ │ │ │ │ │ │ │ +│ ├──────────────┤ ├──────────────┤ ├──────────────┤ │ +│ │ Mock: Memory │ │ Mock: Static │ │ Mock: Fake │ │ +│ │ Prod:Postgres│ │ Prod: ORAS │ │ Prod: Helm │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────┘ +``` + +### 目录结构 + +``` +internal/ +├── domain/ # 🎯 领域层(核心) +│ ├── entity/ # 领域实体 +│ │ ├── user.go +│ │ ├── cluster.go +│ │ ├── registry.go +│ │ ├── instance.go +│ │ └── artifact.go +│ │ +│ ├── service/ # 业务逻辑服务 +│ │ ├── auth_service.go +│ │ ├── cluster_service.go +│ │ ├── registry_service.go +│ │ ├── artifact_service.go +│ │ ├── instance_service.go +│ │ └── monitoring_service.go +│ │ +│ └── repository/ # 接口定义(Output Ports) +│ ├── user_repository.go +│ ├── cluster_repository.go +│ ├── registry_repository.go +│ ├── instance_repository.go +│ ├── oci_client.go +│ ├── helm_client.go +│ └── metrics_client.go +│ +├── adapter/ +│ ├── input/ # 📥 输入适配器 +│ │ └── http/ +│ │ ├── rest/ # REST API Handlers +│ │ │ ├── auth_handler.go +│ │ │ ├── cluster_handler.go +│ │ │ ├── registry_handler.go +│ │ │ ├── artifact_handler.go +│ │ │ ├── instance_handler.go +│ │ │ ├── monitoring_handler.go +│ │ │ └── utils.go +│ │ │ +│ │ └── dto/ # 数据传输对象 +│ │ ├── auth_dto.go +│ │ ├── cluster_dto.go +│ │ ├── registry_dto.go +│ │ └── instance_dto.go +│ │ +│ └── output/ # 📤 输出适配器 +│ ├── persistence/ +│ │ ├── mock/ # ✅ Mock 实现 +│ │ │ ├── user_repository_mock.go +│ │ │ ├── cluster_repository_mock.go +│ │ │ ├── registry_repository_mock.go +│ │ │ └── instance_repository_mock.go +│ │ │ +│ │ └── postgres/ # 🐘 PostgreSQL 实现 +│ │ ├── user_repository.go +│ │ ├── cluster_repository.go +│ │ ├── registry_repository.go +│ │ └── instance_repository.go +│ │ +│ ├── oci/ +│ │ ├── mock/ # ✅ Mock OCI Client +│ │ │ └── oci_client_mock.go +│ │ └── oras_client.go # ORAS SDK 实现 +│ │ +│ ├── helm/ +│ │ ├── mock/ # ✅ Mock Helm Client +│ │ │ └── helm_client_mock.go +│ │ └── helm_client.go # Helm SDK 实现 +│ │ +│ ├── metrics/ +│ │ ├── mock/ # ✅ Mock Metrics Client +│ │ │ └── metrics_client_mock.go +│ │ └── prometheus_client.go +│ │ +│ └── factory.go # 🏭 适配器工厂 +│ +├── bootstrap/ # Bootstrap 预注入模块 +│ ├── config.go +│ └── seeder.go +│ +└── pkg/ # 🔧 工具包 + ├── jwt/ # JWT 工具 + ├── password/ # 密码哈希 + └── encryption/ # AES 加密 +``` + +### 数据流 + +``` +1. HTTP Request + ↓ +2. [REST Handler] (Input Adapter) + - 验证请求参数 + - 转换为 Domain 对象(Entity) + ↓ +3. [Domain Service] (Business Logic) + - 执行业务逻辑 + - 调用 Repository 接口(Port) + ↓ +4. [Repository Implementation] (Output Adapter) + - Mock: 操作内存数据 + - PostgreSQL: 操作数据库 + - ORAS: 与 OCI Registry 交互 + - Helm: 与 Kubernetes 交互 + ↓ +5. Response + - 返回结果到 Service + - Service 返回到 Handler + - Handler 转换为 HTTP 响应 +``` + +### 依赖注入 + +在 `cmd/api/main.go` 中组装所有组件: + +```go +func main() { + // 1. 加载配置 + config := loadConfig() + + // 2. 创建适配器工厂 + factory := output.NewAdapterFactory( + config.AdapterMode, + config.DatabaseURL, + ) + + // 3. 创建 Output Adapters + repos, _ := factory.CreateAllRepositories() + ociClient, _ := factory.CreateOCIClient() + helmClient, _ := factory.CreateHelmClient() + + // 4. 创建工具类 + hasher := password.NewBcryptHasher() + jwtGen := jwt.NewJWTGenerator(config.JWTSecret) + + // 5. 创建 Domain Services + authService := service.NewAuthService(repos.UserRepo, hasher, jwtGen) + clusterService := service.NewClusterService(repos.ClusterRepo) + registryService := service.NewRegistryService(repos.RegistryRepo) + instanceService := service.NewInstanceService( + repos.InstanceRepo, + repos.ClusterRepo, + repos.RegistryRepo, + helmClient, + ) + + // 6. 创建 Input Adapters (REST Handlers) + authHandler := rest.NewAuthHandler(authService) + clusterHandler := rest.NewClusterHandler(clusterService) + instanceHandler := rest.NewInstanceHandler(instanceService) + + // 7. 设置路由 + router := setupRouter( + authHandler, + clusterHandler, + instanceHandler, + ) + + // 8. 启动服务器 + http.ListenAndServe(":8080", router) +} +``` + +### 适配器模式 + +#### 适配器工厂 + +```go +// internal/adapter/output/factory.go +type AdapterMode string + +const ( + ModeMock AdapterMode = "mock" + ModeProduction AdapterMode = "production" +) + +type AdapterFactory struct { + mode AdapterMode + dbConnString string +} + +func (f *AdapterFactory) CreateUserRepository() (repository.UserRepository, error) { + switch f.mode { + case ModeMock: + return mock.NewUserRepositoryMock(), nil + case ModeProduction: + return postgres.NewUserRepository(f.dbConnString) + default: + return nil, fmt.Errorf("unknown adapter mode: %s", f.mode) + } +} +``` + +#### Mock vs Production + +| 接口 | Mock 模式 | Production 模式 | +|------|----------|----------------| +| UserRepository | ✅ 内存 map | 🐘 PostgreSQL | +| ClusterRepository | ✅ 内存 map | 🐘 PostgreSQL | +| RegistryRepository | ✅ 内存 map | 🐘 PostgreSQL | +| InstanceRepository | ✅ 内存 map | 🐘 PostgreSQL | +| OCIClient | ✅ 静态数据 | 🌐 ORAS SDK v2 | +| HelmClient | ✅ 模拟部署 | ☸️ Helm SDK | +| MetricsClient | ✅ 假数据 | 📊 Prometheus | + +--- + +## 2.1.3.2 技术选型 + +### 编程语言 + +**Go 1.21+** + +**选型理由**: +- ✅ 高性能,原生并发支持 +- ✅ 静态类型,编译时检查 +- ✅ 丰富的云原生生态(Kubernetes client-go、Helm SDK、ORAS) +- ✅ 简单易维护,部署方便(单一二进制文件) +- ✅ 广泛应用于云原生领域 + +**替代方案**: Java/Spring Boot, Python/FastAPI + +--- + +### Web 框架 + +**gorilla/mux** + +**选型理由**: +- ✅ 轻量级,性能好 +- ✅ 灵活的路由匹配(支持正则表达式) +- ✅ 与标准库 `net/http` 完美兼容 +- ✅ 活跃的社区支持 + +**使用示例**: +```go +router := mux.NewRouter() +api := router.PathPrefix("/api/v1").Subrouter() +api.HandleFunc("/clusters", handler.CreateCluster).Methods(http.MethodPost) +api.HandleFunc("/clusters/{clusterId}", handler.GetCluster).Methods(http.MethodGet) +``` + +**替代方案**: Gin, Echo, Fiber + +--- + +### 数据库 + +**PostgreSQL 15+** + +**选型理由**: +- ✅ 开源、成熟、可靠 +- ✅ 支持复杂查询和事务 +- ✅ JSONB 类型适合存储动态配置 +- ✅ 主从复制、高可用支持 +- ✅ 与 Go 生态集成良好 + +**数据库驱动**: `lib/pq` 或 `pgx` + +**替代方案**: MySQL, MongoDB + +--- + +### OCI Registry 客户端 + +**ORAS Go SDK v2** + +**选型理由**: +- ✅ 符合 OCI Distribution Specification +- ✅ 官方维护,质量可靠 +- ✅ 支持所有 OCI 兼容的 Registry +- ✅ 完整的 manifest、blob 操作 + +**使用示例**: +```go +repo, err := remote.NewRepository("harbor.example.com/charts/nginx") +repo.Client = &auth.Client{ + Credential: auth.StaticCredential("username", "password"), +} + +tags, err := repo.Tags(ctx) +manifest, err := repo.FetchReference(ctx, "1.0.0") +``` + +**官网**: https://oras.land/ + +**替代方案**: containerd, go-containerregistry + +--- + +### Kubernetes 客户端 + +**client-go** + +**选型理由**: +- ✅ Kubernetes 官方 Go 客户端 +- ✅ 完整的 API 支持 +- ✅ 动态客户端支持 +- ✅ 与 Helm SDK 集成 + +**使用示例**: +```go +config, err := clientcmd.BuildConfigFromFlags("", kubeconfig) +clientset, err := kubernetes.NewForConfig(config) + +pods, err := clientset.CoreV1().Pods("default").List(ctx, metav1.ListOptions{}) +``` + +**替代方案**: 无(标准库) + +--- + +### Helm 客户端 + +**Helm SDK (helm.sh/helm/v3)** + +**选型理由**: +- ✅ Helm 官方 SDK +- ✅ 完整的 Helm 操作支持 +- ✅ Chart 解析和渲染 +- ✅ Release 生命周期管理 + +**使用示例**: +```go +actionConfig := new(action.Configuration) +actionConfig.Init(settings.RESTClientGetter(), namespace, os.Getenv("HELM_DRIVER"), log.Printf) + +install := action.NewInstall(actionConfig) +install.ReleaseName = "my-release" +install.Namespace = "default" + +chart, err := loader.Load(chartPath) +release, err := install.Run(chart, values) +``` + +**替代方案**: 命令行调用 helm(不推荐) + +--- + +### 认证和授权 + +**JWT (golang-jwt/jwt)** + +**选型理由**: +- ✅ 无状态,易扩展 +- ✅ 标准化(RFC 7519) +- ✅ 支持过期时间和刷新 +- ✅ 广泛应用 + +**使用示例**: +```go +token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ + "user_id": user.ID, + "exp": time.Now().Add(time.Hour * 24).Unix(), +}) + +tokenString, err := token.SignedString([]byte(jwtSecret)) +``` + +**替代方案**: OAuth 2.0, Session + +--- + +### 密码哈希 + +**bcrypt (golang.org/x/crypto/bcrypt)** + +**选型理由**: +- ✅ 安全、抗暴力破解 +- ✅ 自动加盐 +- ✅ 计算成本可调 +- ✅ Go 标准扩展库 + +**使用示例**: +```go +hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) +err = bcrypt.CompareHashAndPassword(hash, []byte(password)) +``` + +**替代方案**: argon2, scrypt + +--- + +### 数据加密 + +**AES-256 (crypto/aes + crypto/cipher)** + +**选型理由**: +- ✅ 对称加密,性能好 +- ✅ 256 位密钥,安全性高 +- ✅ Go 标准库支持 +- ✅ 适合敏感数据加密(证书、密码) + +**使用模式**: GCM(Galois/Counter Mode) + +**使用示例**: +```go +block, err := aes.NewCipher(key) // 32 bytes key +gcm, err := cipher.NewGCM(block) +nonce := make([]byte, gcm.NonceSize()) +ciphertext := gcm.Seal(nonce, nonce, plaintext, nil) +``` + +**替代方案**: RSA (非对称加密,性能较差) + +--- + +### 容器化 + +**Docker + Docker Compose** + +**选型理由**: +- ✅ 标准化容器技术 +- ✅ 简化部署和环境一致性 +- ✅ 丰富的镜像生态 +- ✅ Docker Compose 简化多容器编排 + +**Dockerfile 示例**: +```dockerfile +FROM golang:1.21-alpine AS builder +WORKDIR /app +COPY . . +RUN go build -o ocdp-backend cmd/api/main.go + +FROM alpine:latest +RUN apk --no-cache add ca-certificates +WORKDIR /root/ +COPY --from=builder /app/ocdp-backend . +EXPOSE 8080 +CMD ["./ocdp-backend"] +``` + +**替代方案**: Podman, containerd + +--- + +### 日志记录 + +**标准库 log + 结构化日志(未来:zerolog/zap)** + +**当前实现**: +```go +log.Printf("✅ User created: %s", user.Username) +log.Printf("⚠️ Warning: %v", err) +log.Printf("❌ Error: %v", err) +``` + +**未来优化**: +```go +logger.Info(). + Str("user_id", user.ID). + Str("username", user.Username). + Msg("User created") +``` + +--- + +### 测试框架 + +**testify** + +**选型理由**: +- ✅ 丰富的断言函数 +- ✅ Mock 支持 +- ✅ 测试套件支持 +- ✅ 活跃的社区 + +**使用示例**: +```go +import ( + "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCreateUser(t *testing.T) { + user, err := service.CreateUser(ctx, "test", "password") + require.NoError(t, err) + assert.NotEmpty(t, user.ID) + assert.Equal(t, "test", user.Username) +} +``` + +--- + +### 热重载(开发) + +**Air** + +**选型理由**: +- ✅ Go 项目热重载 +- ✅ 监听文件变化自动重启 +- ✅ 配置简单 + +**配置文件**: `.air.toml` + +```toml +[build] + cmd = "go build -o ./tmp/main cmd/api/main.go" + bin = "./tmp/main" + include_ext = ["go", "yaml", "json"] + exclude_dir = ["tmp", "vendor"] +``` + +**启动**: `air -c .air.toml` + +--- + +### 技术栈总结表 + +| 组件 | 技术 | 版本 | 用途 | +|------|------|------|------| +| **语言** | Go | 1.21+ | 主要编程语言 | +| **Web 框架** | gorilla/mux | latest | HTTP 路由 | +| **数据库** | PostgreSQL | 15+ | 持久化存储 | +| **数据库驱动** | lib/pq 或 pgx | latest | Go 数据库驱动 | +| **OCI 客户端** | ORAS Go SDK | v2 | OCI Registry 操作 | +| **Kubernetes** | client-go | latest | K8s API 交互 | +| **Helm** | Helm SDK | v3 | Helm 操作 | +| **JWT** | golang-jwt/jwt | v5 | 认证 Token | +| **密码哈希** | bcrypt | latest | 密码加密 | +| **数据加密** | AES-256 | stdlib | 敏感数据加密 | +| **容器化** | Docker | latest | 应用容器化 | +| **编排** | Docker Compose | latest | 多容器编排 | +| **测试** | testify | latest | 单元测试 | +| **热重载** | Air | latest | 开发环境 | + +--- + +### Bootstrap 预注入 + +**概述**: 在应用启动时自动初始化用户、Registry、Cluster 等数据。 + +**配置文件**: `config/bootstrap.json` + +```json +{ + "enabled": true, + "users": [ + { + "username": "admin", + "password": "admin123", + "email": "admin@example.com" + } + ], + "registries": [ + { + "name": "Harbor Production", + "url": "https://harbor.example.com", + "username": "admin", + "password": "secret" + } + ], + "clusters": [ + { + "name": "Production Cluster", + "host": "https://k8s.example.com:6443", + "caData": "LS0tLS...", + "certData": "LS0tLS...", + "keyData": "LS0tLS..." + } + ] +} +``` + +**特性**: +- ✅ 自动加密敏感数据 +- ✅ 幂等性(重复启动不会重复创建) +- ✅ 可通过环境变量配置 +- ✅ 支持禁用 + +**从 kubeconfig 提取证书**: +```bash +kubectl config view --raw -o jsonpath='{.clusters[0].cluster.certificate-authority-data}' +kubectl config view --raw -o jsonpath='{.users[0].user.client-certificate-data}' +kubectl config view --raw -o jsonpath='{.users[0].user.client-key-data}' +``` + +--- + +### 开发指南 + +#### 添加新功能的步骤 + +1. **定义 Entity** (`internal/domain/entity/`) +2. **定义 Repository 接口** (`internal/domain/repository/`) +3. **实现 Domain Service** (`internal/domain/service/`) +4. **实现 Mock Adapter** (`internal/adapter/output/persistence/mock/`) +5. **实现 Production Adapter** (`internal/adapter/output/persistence/postgres/`) +6. **添加到 Factory** (`internal/adapter/output/factory.go`) +7. **创建 REST Handler** (`internal/adapter/input/http/rest/`) +8. **注册路由** (`cmd/api/main.go`) + +#### 依赖方向规则 + +- ✅ Input Adapters → Domain Layer +- ✅ Domain Layer → Repository Interfaces +- ✅ Output Adapters → Domain Layer (实现接口) +- ❌ Domain Layer → Output Adapters(禁止) + +#### 测试策略 + +- **单元测试**: 测试 Domain Service,使用 Mock Repository +- **集成测试**: 测试 Handler + Service,使用 Mock Adapters +- **E2E 测试**: 完整流程测试,使用真实环境 + +--- + +## 相关文档 + +- [API 与测试](api-and-test.md) +- [部署文档](deployment.md) +- [主 README](../README.md) + +--- + +**Last Updated**: 2025-11-09 diff --git a/backend/docs/deployment.md b/backend/docs/deployment.md new file mode 100644 index 0000000..7bd2ea1 --- /dev/null +++ b/backend/docs/deployment.md @@ -0,0 +1,1546 @@ +# 🚀 部署文档 + +## 目录 + +- [快速开始](#快速开始) +- [2.4.1 Mock 模式](#241-mock-模式) +- [2.4.2 Dev 模式](#242-dev-模式) +- [2.4.3 Up 模式](#243-up-模式) +- [环境变量配置](#环境变量配置) +- [故障排查](#故障排查) + +--- + +## 快速开始 + +### 三种部署模式 + +| 模式 | 命令 | Backend | 数据库 | 热重载 | 适用场景 | +|------|------|---------|--------|--------|----------| +| **Mock** | `make mock` | 本地 | 内存 | ✅ | 快速开发、API 测试 | +| **Dev** | `make db-up`
`make dev` | 本地 | Docker | ✅ | 日常开发 ⭐ 推荐 | +| **Up** | `make up` | Docker | Docker | ❌ | 集成测试、生产预演 | + +### 使用 Makefile + +```bash +# 查看所有命令 +make help + +# Mock 模式(零依赖,最快启动) +make mock + +# Dev 模式(推荐日常开发) +make db-up # 启动数据库 +make dev # 启动 Backend(热重载) + +# Up 模式(完全容器化) +make up + +# 管理服务 +make logs # 查看日志 +make stop # 停止服务 +``` + +### 验证服务 + +```bash +# 健康检查 +curl http://localhost:8080/health + +# 测试 API +curl http://localhost:8080/api/v1/registries | jq + +# 访问 Swagger UI +open http://localhost:8080/api/docs +``` + +--- + +# 2.4.1 Mock 模式 + +## 概述 + +Mock 模式使用内存存储,无需任何外部依赖,适合快速开发和测试。 + +**特点**: +- ✅ **零依赖** - 无需数据库、Docker 等 +- ✅ **快速启动** - 3 秒内启动 +- ✅ **热重载** - 支持 Air 自动重载 +- ✅ **自带测试数据** - Bootstrap 预注入 +- ✅ **无需容器** - 本地直接运行 +- ❌ **数据不持久** - 重启后数据丢失 + +**适用场景**: +- 🚀 快速功能开发 +- 🧪 API 接口测试 +- 🎨 前端开发联调 +- 📝 编写单元测试 + +**架构**: + +``` +┌─────────────────────────────────────────┐ +│ Mock 模式(本地直接运行) │ +│ │ +│ ┌──────────────────────────────────┐ │ +│ │ OCDP Backend (Go Process) │ │ +│ │ Port: 8080 │ │ +│ │ │ │ +│ │ ┌─────────────────────────┐ │ │ +│ │ │ Air (Hot Reload) │ │ │ +│ │ │ 监听文件变化自动重载 │ │ │ +│ │ └─────────────────────────┘ │ │ +│ │ │ │ +│ │ Adapter: Mock │ │ +│ │ Storage: Memory │ │ +│ └──────────────────────────────────┘ │ +│ │ +│ 无外部依赖 ✨ │ +└─────────────────────────────────────────┘ +``` + +--- + +## 环境准备 + +```bash +# 1. 确保已安装 Go +go version # 需要 Go 1.21+ + +# 2. 安装 Air(热重载工具) +go install github.com/air-verse/air@latest + +# 3. 验证安装 +air -v +``` + +--- + +## 运行方式 + +### 方式 1: 使用 Makefile(推荐) + +```bash +make mock +``` + +### 方式 2: 使用 Air + +```bash +# 1. 设置环境变量 +export ADAPTER_MODE=mock +export PORT=8080 +export JWT_SECRET=dev-secret-key + +# 2. 启动 Air +air -c .air.toml + +# 输出示例: +# __ _ ___ +# / /\ | | | |_) +# /_/--\ |_| |_| \_ v1.49.0 +# +# watching . +# building... +# running... +# 🚀 Starting OCDP Backend (mode=mock) +# 🌐 Server starting on :8080 +``` + +### 方式 3: 直接运行 Go + +```bash +# 设置环境变量 +export ADAPTER_MODE=mock +export PORT=8080 +export JWT_SECRET=dev-secret-key + +# 运行 +go run cmd/api/main.go +``` + +--- + +## 热重载演示 + +修改任何 `.go` 文件后,Air 会自动检测并重启: + +``` +main.go has changed +building... +running... +🚀 Starting OCDP Backend (mode=mock) +🌐 Server starting on :8080 +``` + +**支持的文件类型**: +- `.go` - Go 源文件 +- `.yaml`, `.json` - 配置文件 +- `.toml` - Air 配置文件 + +--- + +## 配置文件 + +Air 配置文件 `.air.toml` 已包含在项目中: + +```toml +root = "." +testdata_dir = "testdata" +tmp_dir = "tmp" + +[build] + cmd = "go build -o ./tmp/main cmd/api/main.go" + bin = "./tmp/main" + include_ext = ["go", "tpl", "tmpl", "html", "yaml", "json"] + exclude_dir = ["assets", "tmp", "vendor", "testdata"] + exclude_regex = ["_test.go"] + delay = 1000 +``` + +--- + +## 测试数据 + +Mock 模式自动加载 `config/bootstrap.json` 中的测试数据: + +```json +{ + "enabled": true, + "users": [ + { + "username": "admin", + "password": "admin123", + "email": "admin@example.com" + } + ], + "registries": [ + { + "name": "Harbor Production", + "url": "https://harbor.example.com", + "username": "admin", + "password": "secret" + } + ], + "clusters": [ + { + "name": "Production Cluster", + "host": "https://k8s.example.com:6443", + "description": "生产环境集群" + } + ] +} +``` + +--- + +## 测试 API + +```bash +# 登录 +curl -X POST http://localhost:8080/api/v1/auth/login \ + -H "Content-Type: application/json" \ + -d '{"username":"admin","password":"admin123"}' | jq + +# 响应: +# { +# "accessToken": "eyJhbGciOiJIUzI1NiIs...", +# "userId": "user-123", +# "username": "admin" +# } + +# 查看 Registries +curl http://localhost:8080/api/v1/registries | jq + +# 查看 Clusters +curl http://localhost:8080/api/v1/clusters | jq +``` + +--- + +## 环境变量 + +| 变量 | 说明 | 默认值 | 必需 | +|------|------|--------|------| +| `ADAPTER_MODE` | 适配器模式 | `mock` | ✅ | +| `PORT` | 服务端口 | `8080` | ❌ | +| `JWT_SECRET` | JWT 密钥 | - | ✅ | + +--- + +## 优势与限制 + +### ✅ 优势 + +1. **启动极快** - 3 秒内启动,无需等待数据库 +2. **零配置** - 无需配置数据库连接、证书等 +3. **自带数据** - Bootstrap 自动注入测试数据 +4. **热重载** - 代码修改立即生效 +5. **易于调试** - 本地运行,支持 IDE 断点调试 +6. **完美测试** - 适合 API 测试和前端联调 + +### ❌ 限制 + +1. **数据不持久** - 重启后数据丢失 +2. **功能简化** - 某些功能(如真实 K8s 操作)被模拟 +3. **不适合生产** - 仅用于开发调试 + +--- + +# 2.4.2 Dev 模式 + +## 概述 + +Dev 模式使用 Docker 运行 PostgreSQL 数据库,Backend 在本地热重载运行,适合日常开发。 + +**特点**: +- ✅ **隔离的依赖服务** - 数据库在容器中,避免污染本地环境 +- ✅ **本地热重载** - Backend 支持 Air 自动重载 +- ✅ **完整的 IDE 支持** - 断点调试、代码补全、性能分析 +- ✅ **快速迭代** - 代码修改立即生效,无需重新构建镜像 +- ✅ **数据持久化** - PostgreSQL 数据持久保存 +- ✅ **最佳开发体验** - 兼顾隔离性和开发效率 + +**适用场景**: +- 📝 **日常功能开发** ⭐ 最推荐 +- 🐛 **代码调试** - 支持 IDE 断点 +- 🔄 **快速迭代** - 热重载提升效率 +- 🧪 **数据持久化测试** - 验证数据库操作 + +**架构**: + +``` +┌──────────────────────────────────────────────────────────┐ +│ Dev 模式(部分容器化,推荐日常开发) │ +│ │ +│ ┌────────────────────────┐ ┌────────────────────┐ │ +│ │ OCDP Backend │ │ PostgreSQL │ │ +│ │ (本地进程) │───▶│ (Docker 容器) │ │ +│ │ Port: 8080 │ │ Port: 5432 │ │ +│ │ │ │ │ │ +│ │ ┌─────────────────┐ │ │ Volume: │ │ +│ │ │ Air (Hot Reload) │ │ │ postgres_data │ │ +│ │ └─────────────────┘ │ └────────────────────┘ │ +│ └────────────────────────┘ │ +│ │ +│ Backend: 本地运行(热重载) │ +│ Database: Docker 容器(隔离环境) │ +└──────────────────────────────────────────────────────────┘ +``` + +--- + +## 环境准备 + +```bash +# 1. 确保已安装 Docker 和 Docker Compose +docker --version +docker compose version + +# 2. 安装 Air(热重载工具) +go install github.com/air-verse/air@latest + +# 3. 验证安装 +air -v +``` + +--- + +## 运行步骤 + +### 步骤 1: 启动数据库 + +```bash +# 使用 Makefile(推荐) +make db-up + +# 或手动启动 +docker compose up -d postgres + +# 等待数据库启动(查看健康状态) +docker compose ps + +# 预期输出: +# NAME IMAGE STATUS PORTS +# ocdp-postgres postgres:17-alpine Up 10s (healthy) 0.0.0.0:5432->5432/tcp +``` + +**验证数据库连接**: + +```bash +# 方式 1: 使用 psql +psql -h localhost -U postgres -d ocdp + +# 方式 2: 使用 Docker +docker exec -it ocdp-postgres psql -U postgres -d ocdp + +# 测试查询 +SELECT current_database(); +\q +``` + +--- + +### 步骤 2: 启动 Backend(热重载) + +**选项 A: 使用 Makefile(推荐)** + +```bash +# 设置环境变量(首次) +export DATABASE_URL="postgresql://postgres:postgres@localhost:5432/ocdp?sslmode=disable" +export ENCRYPTION_KEY="12345678901234567890123456789012" + +# 启动 Backend +make dev + +# 输出: +# 🚀 启动 Dev 模式 - Backend(本地热重载)... +# 🔌 Connecting to database... +# ✅ Database connected +# 🌱 Bootstrap seeding... +# 🌐 Server starting on :8080 +``` + +**选项 B: 使用 Air** + +```bash +# 设置环境变量 +export ADAPTER_MODE=production +export PORT=8080 +export JWT_SECRET=dev-secret-key +export ENCRYPTION_KEY=12345678901234567890123456789012 +export DATABASE_URL="postgresql://postgres:postgres@localhost:5432/ocdp?sslmode=disable" + +# 启动 Air +air -c .air.toml +``` + +**选项 C: 直接运行 Go** + +```bash +export ADAPTER_MODE=production +export DATABASE_URL="postgresql://postgres:postgres@localhost:5432/ocdp?sslmode=disable" +export JWT_SECRET=dev-secret-key +export ENCRYPTION_KEY=12345678901234567890123456789012 + +go run cmd/api/main.go +``` + +--- + +## 验证服务 + +```bash +# 健康检查 +curl http://localhost:8080/health +# 输出: {"status":"healthy"} + +# 注册用户 +curl -X POST http://localhost:8080/api/v1/auth/register \ + -H "Content-Type: application/json" \ + -d '{ + "username": "testuser", + "password": "test123", + "email": "test@example.com" + }' + +# 登录 +curl -X POST http://localhost:8080/api/v1/auth/login \ + -H "Content-Type: application/json" \ + -d '{ + "username": "testuser", + "password": "test123" + }' | jq +``` + +--- + +## 热重载演示 + +修改代码后,Air 会自动检测并重启: + +```bash +# 终端 1: Air 运行中 +# 修改文件: internal/domain/service/cluster_service.go + +# Air 输出: +# cluster_service.go has changed +# building... +# running... +# 🚀 Starting OCDP Backend (mode=production) +# 🔌 Connecting to database... +# ✅ Database connected +# 🌐 Server starting on :8080 + +# 终端 2: 测试 +curl http://localhost:8080/api/v1/clusters | jq +``` + +--- + +## 开发工作流 + +```bash +# 1. 启动数据库(首次或停止后) +make db-up + +# 2. 启动 Backend(热重载) +make dev + +# 3. 修改代码 +vim internal/domain/service/cluster_service.go +# Air 自动检测并重启 + +# 4. 测试 API +curl http://localhost:8080/api/v1/clusters | jq + +# 5. 查看数据库 +make db +SELECT * FROM users; +\q + +# 6. 停止服务 +# - Backend: Ctrl+C +# - Database: make stop +``` + +--- + +## 管理服务 + +**查看服务状态**: + +```bash +# 查看所有容器 +docker compose ps + +# 查看 PostgreSQL 日志 +docker compose logs -f postgres + +# 查看 Backend 日志(在 Air 终端中) +``` + +**停止服务**: + +```bash +# 停止 Backend: 在 Air 终端按 Ctrl+C + +# 停止数据库 +docker compose stop postgres + +# 停止并删除容器(数据保留) +docker compose down + +# 停止并删除容器和数据卷(⚠️ 数据丢失) +docker compose down -v +``` + +**重启服务**: + +```bash +# 重启数据库 +docker compose restart postgres + +# 重启 Backend: 在 Air 终端按 Ctrl+C 后重新运行 make dev +``` + +--- + +## 数据库管理 + +**连接数据库**: + +```bash +# 使用 Makefile +make db + +# 使用 psql +psql -h localhost -U postgres -d ocdp + +# 或使用 Docker +docker exec -it ocdp-postgres psql -U postgres -d ocdp +``` + +**常用查询**: + +```sql +-- 查看所有表 +\dt + +-- 查看用户 +SELECT id, username, email, created_at FROM users; + +-- 查看 Registry +SELECT id, name, url, created_at FROM registries; + +-- 查看集群 +SELECT id, name, host, created_at FROM clusters; + +-- 清空测试数据 +TRUNCATE users, registries, clusters, instances CASCADE; + +-- 退出 +\q +``` + +**数据库备份与恢复**: + +```bash +# 备份 +docker exec ocdp-postgres pg_dump -U postgres ocdp > backup-$(date +%Y%m%d).sql + +# 恢复 +docker exec -i ocdp-postgres psql -U postgres ocdp < backup.sql + +# 压缩备份 +docker exec ocdp-postgres pg_dump -U postgres ocdp | gzip > backup.sql.gz + +# 从压缩文件恢复 +gunzip -c backup.sql.gz | docker exec -i ocdp-postgres psql -U postgres ocdp +``` + +--- + +## IDE 调试配置 + +### VS Code (launch.json) + +```json +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Launch OCDP Backend (Dev Mode)", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${workspaceFolder}/cmd/api/main.go", + "env": { + "ADAPTER_MODE": "production", + "PORT": "8080", + "JWT_SECRET": "dev-secret-key", + "ENCRYPTION_KEY": "12345678901234567890123456789012", + "DATABASE_URL": "postgresql://postgres:postgres@localhost:5432/ocdp?sslmode=disable" + }, + "args": [] + } + ] +} +``` + +### GoLand / IntelliJ IDEA + +1. Run → Edit Configurations +2. Add New → Go Build +3. 配置: + - **Files**: `cmd/api/main.go` + - **Working directory**: 项目根目录 + - **Environment**: + ``` + ADAPTER_MODE=production + PORT=8080 + JWT_SECRET=dev-secret-key + ENCRYPTION_KEY=12345678901234567890123456789012 + DATABASE_URL=postgresql://postgres:postgres@localhost:5432/ocdp?sslmode=disable + ``` + +--- + +## 环境变量 + +| 变量 | 说明 | 示例 | 必需 | +|------|------|------|------| +| `ADAPTER_MODE` | 适配器模式 | `production` | ✅ | +| `PORT` | 服务端口 | `8080` | ❌ | +| `JWT_SECRET` | JWT 密钥 | `dev-secret` | ✅ | +| `ENCRYPTION_KEY` | 加密密钥(32字节) | `12345678901234567890123456789012` | ✅ | +| `DATABASE_URL` | 数据库连接 | `postgresql://postgres:postgres@localhost:5432/ocdp?sslmode=disable` | ✅ | + +**生成密钥**: + +```bash +# JWT Secret +openssl rand -base64 32 + +# Encryption Key(32 字节) +openssl rand -base64 32 +``` + +--- + +## 故障排查 + +**问题 1: 数据库连接失败** + +```bash +# 错误: connection refused + +# 检查数据库是否启动 +docker compose ps postgres + +# 检查端口 +netstat -tuln | grep 5432 +# 或 +lsof -i :5432 + +# 测试连接 +psql -h localhost -U postgres -d ocdp -c "SELECT 1;" +``` + +**问题 2: 数据库未初始化** + +```bash +# 重新初始化数据库 +docker compose down postgres +docker compose up -d postgres + +# 等待健康检查通过 +docker compose ps postgres +``` + +**问题 3: 端口冲突** + +```bash +# 修改 docker-compose.yml 中的端口映射 +# ports: +# - "5433:5432" # 使用 5433 而不是 5432 + +# 更新 DATABASE_URL +export DATABASE_URL="postgresql://postgres:postgres@localhost:5433/ocdp?sslmode=disable" +``` + +--- + +# 2.4.3 Up 模式 + +## 概述 + +Up 模式使用 Docker Compose 完全容器化部署,PostgreSQL 和 Backend 都在容器中运行。 + +**特点**: +- ✅ **完全容器化** - 一致的运行环境 +- ✅ **一键部署** - 简单快速 +- ✅ **接近生产** - 与生产环境高度一致 +- ✅ **易于分享** - 团队成员快速启动相同环境 +- ❌ **无热重载** - 代码修改需重新构建镜像 +- ❌ **调试受限** - 需要通过日志调试 + +**适用场景**: +- 🧪 **集成测试** - 测试完整系统 +- 🎯 **生产预演** - 验证部署流程 +- 👥 **团队协作** - 快速搭建统一环境 +- 📦 **交付演示** - 向客户展示系统 + +**架构**: + +``` +┌──────────────────────────────────────────────────────────┐ +│ Up 模式(完全容器化) │ +│ │ +│ ┌────────────────────────────────────────────────────┐ │ +│ │ Docker Compose Network │ │ +│ │ │ │ +│ │ ┌──────────────────┐ ┌──────────────────┐ │ │ +│ │ │ OCDP Backend │ │ PostgreSQL │ │ │ +│ │ │ (Docker 容器) │───▶│ (Docker 容器) │ │ │ +│ │ │ Port: 8080 │ │ Port: 5432 │ │ │ +│ │ └──────────────────┘ └──────────────────┘ │ │ +│ │ │ │ +│ └────────────────────────────────────────────────────┘ │ +│ │ +│ ┌────────────────────────────────────────────────────┐ │ +│ │ Persistent Volumes │ │ +│ │ postgres_data: /var/lib/postgresql/data │ │ +│ └────────────────────────────────────────────────────┘ │ +└──────────────────────────────────────────────────────────┘ +``` + +--- + +## 环境准备 + +```bash +# 确保已安装 Docker 和 Docker Compose +docker --version +docker compose version +``` + +--- + +## 目录结构 + +``` +backend/ +├── docker-compose.yml # Docker Compose 配置 +├── Dockerfile # Backend 镜像 +├── .env # 环境变量 +├── env.example # 环境变量示例 +└── config/ + └── bootstrap.json # 初始数据(可选) +``` + +--- + +## 部署步骤 + +### 步骤 1: 准备环境变量 + +```bash +# 复制示例文件 +cp env.example .env + +# 编辑环境变量 +nano .env +``` + +**.env 文件内容**: + +```bash +# 适配器模式 +ADAPTER_MODE=production + +# 端口配置 +BACKEND_PORT=8080 +POSTGRES_PORT=5432 + +# 安全密钥(⚠️ 生产环境必须修改) +JWT_SECRET=your-jwt-secret-change-in-production +ENCRYPTION_KEY=your-32-character-encryption-key-here + +# 数据库配置 +POSTGRES_DB=ocdp +POSTGRES_USER=postgres +POSTGRES_PASSWORD=your-postgres-password-change-it + +# 数据库连接 URL(容器内部使用) +DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}?sslmode=disable +``` + +**生成安全密钥**: + +```bash +# 生成随机 JWT Secret +openssl rand -base64 32 + +# 生成随机 Encryption Key(32 字节) +openssl rand -base64 32 + +# 生成随机数据库密码 +openssl rand -base64 16 +``` + +--- + +### 步骤 2: 启动服务 + +**使用 Makefile(推荐)**: + +```bash +make up + +# 查看服务状态 +docker compose ps + +# 预期输出: +# NAME IMAGE STATUS PORTS +# ocdp-backend backend:latest Up 30s (healthy) 0.0.0.0:8080->8080/tcp +# ocdp-postgres postgres:17-alpine Up 40s (healthy) 0.0.0.0:5432->5432/tcp +``` + +**手动启动**: + +```bash +# 使用 profile 启动完整服务 +docker compose --profile backend up -d + +# 查看日志 +docker compose logs -f backend postgres +``` + +--- + +### 步骤 3: 验证部署 + +```bash +# 健康检查 +curl http://localhost:8080/health +# 输出: {"status":"healthy"} + +# 测试 API +curl http://localhost:8080/api/v1/registries | jq + +# 访问 Swagger UI +open http://localhost:8080/api/docs + +# 查看容器状态 +docker compose ps + +# 进入 Backend 容器 +docker compose exec backend sh + +# 连接数据库 +make db +``` + +--- + +## 管理服务 + +**停止服务**: + +```bash +# 停止所有服务 +make stop + +# 或 +docker compose down + +# 停止并删除数据卷(⚠️ 会删除数据) +docker compose down -v + +# 仅停止 Backend +docker compose stop backend +``` + +**重启服务**: + +```bash +# 重启所有服务 +make restart + +# 或 +docker compose restart + +# 重启 Backend +docker compose restart backend + +# 重启数据库 +docker compose restart postgres +``` + +**查看日志**: + +```bash +# 查看所有日志 +make logs + +# 或 +docker compose logs -f + +# 查看 Backend 日志 +docker compose logs -f backend + +# 查看最近 100 行 +docker compose logs --tail=100 backend +``` + +--- + +## 重新构建镜像 + +**代码修改后需要重新构建**: + +```bash +# 方式 1: 使用 Makefile +make up-build + +# 方式 2: 手动构建 +docker compose build backend +docker compose --profile backend up -d + +# 方式 3: 强制重新构建(不使用缓存) +make up-rebuild + +# 或 +docker compose build --no-cache backend +docker compose --profile backend up -d +``` + +--- + +## 数据备份和恢复 + +```bash +# 备份数据库 +docker compose exec postgres pg_dump -U postgres ocdp > backup-$(date +%Y%m%d).sql + +# 恢复数据库 +docker compose exec -T postgres psql -U postgres ocdp < backup.sql + +# 导出为压缩文件 +docker compose exec postgres pg_dump -U postgres ocdp | gzip > backup-$(date +%Y%m%d).sql.gz + +# 从压缩文件恢复 +gunzip -c backup.sql.gz | docker compose exec -T postgres psql -U postgres ocdp +``` + +--- + +## docker-compose.yml 配置 + +```yaml +version: '3.8' + +services: + # PostgreSQL 数据库 + postgres: + image: postgres:17-alpine + container_name: ocdp-postgres + environment: + - POSTGRES_DB=${POSTGRES_DB:-ocdp} + - POSTGRES_USER=${POSTGRES_USER:-postgres} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + ports: + - "${POSTGRES_PORT:-5432}:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + - ${INIT_DB_SQL_PATH:-./scripts/init-db.sql}:/docker-entrypoint-initdb.d/01-init.sql:ro + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres} -d ${POSTGRES_DB:-ocdp}"] + interval: 10s + timeout: 5s + retries: 5 + restart: unless-stopped + networks: + - ocdp-network + + # Backend 服务(需要 --profile backend 才启动) + backend: + profiles: ["backend"] + build: + context: . + dockerfile: Dockerfile + container_name: ocdp-backend + ports: + - "${BACKEND_PORT:-8080}:8080" + environment: + - ADAPTER_MODE=${ADAPTER_MODE:-production} + - PORT=8080 + - JWT_SECRET=${JWT_SECRET} + - ENCRYPTION_KEY=${ENCRYPTION_KEY} + - DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}?sslmode=disable + depends_on: + postgres: + condition: service_healthy + restart: unless-stopped + networks: + - ocdp-network + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/health"] + interval: 30s + timeout: 3s + retries: 3 + start_period: 40s + +networks: + ocdp-network: + driver: bridge + +volumes: + postgres_data: + driver: local +``` + +> 💡 **提示** +> 如果与仓库根目录下的 `docker-compose.yml` 联合使用(例如通过 `docker compose -f docker-compose.yml -f backend/docker-compose.yml --profile backend ...`),请确保设置环境变量 `INIT_DB_SQL_PATH=./backend/scripts/init-db.sql`(或绝对路径),以便 PostgreSQL 容器挂载到正确的初始化脚本。 + +--- + +## Dockerfile 配置 + +```dockerfile +# 多阶段构建,优化镜像大小 + +# 构建阶段 +FROM golang:1.21-alpine AS builder + +WORKDIR /app + +# 复制依赖文件 +COPY go.mod go.sum ./ +RUN go mod download + +# 复制源代码 +COPY . . + +# 编译(静态链接) +RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags '-w -s' -o ocdp-backend cmd/api/main.go + +# 运行阶段 +FROM alpine:latest + +# 安装 CA 证书 +RUN apk --no-cache add ca-certificates tzdata wget + +# 设置时区 +ENV TZ=Asia/Shanghai + +WORKDIR /root/ + +# 复制二进制文件 +COPY --from=builder /app/ocdp-backend . + +# 复制配置文件(如果有) +COPY config/ config/ + +# 暴露端口 +EXPOSE 8080 + +# 健康检查 +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1 + +# 运行 +CMD ["./ocdp-backend"] +``` + +--- + +## 环境变量 + +| 变量 | 说明 | 示例 | 必需 | +|------|------|------|------| +| `ADAPTER_MODE` | 适配器模式 | `production` | ✅ | +| `BACKEND_PORT` | Backend 端口 | `8080` | ❌ | +| `POSTGRES_PORT` | PostgreSQL 端口 | `5432` | ❌ | +| `JWT_SECRET` | JWT 密钥 | `随机字符串` | ✅ | +| `ENCRYPTION_KEY` | 加密密钥(32字节) | `随机字符串` | ✅ | +| `POSTGRES_DB` | 数据库名 | `ocdp` | ✅ | +| `POSTGRES_USER` | 数据库用户 | `postgres` | ✅ | +| `POSTGRES_PASSWORD` | 数据库密码 | `随机字符串` | ✅ | +| `DATABASE_URL` | 数据库连接 | `postgresql://...` | ✅ | + +--- + +## 故障排查 + +**问题 1: Backend 容器无法启动** + +```bash +# 查看容器日志 +docker compose logs backend + +# 查看容器状态 +docker compose ps + +# 重新构建镜像 +docker compose build --no-cache backend +docker compose --profile backend up -d +``` + +**问题 2: 数据库连接失败** + +```bash +# 检查数据库健康状态 +docker compose exec postgres pg_isready -U postgres + +# 查看数据库日志 +docker compose logs postgres + +# 检查网络连接 +docker compose exec backend ping postgres + +# 等待数据库完全启动后重启 Backend +sleep 10 +docker compose restart backend +``` + +**问题 3: 端口冲突** + +```bash +# 修改 .env 文件中的端口 +BACKEND_PORT=8081 +POSTGRES_PORT=5433 + +# 重新启动 +docker compose down +docker compose --profile backend up -d +``` + +**问题 4: 完全重置** + +```bash +# 停止所有服务 +docker compose down + +# 删除数据卷(⚠️ 会删除数据) +docker compose down -v + +# 清理 Docker 系统 +docker system prune -a --volumes + +# 重新部署 +make up +``` + +--- + +# 环境变量配置 + +## 环境变量对比 + +| 变量 | Mock | Dev | Up | 说明 | +|------|------|-----|-----|------| +| `ADAPTER_MODE` | `mock` | `production` | `production` | 适配器模式 | +| `PORT` | `8080` | `8080` | `8080` | 服务端口 | +| `JWT_SECRET` | 简单值 | 开发值 | 强随机值 | JWT 密钥 | +| `ENCRYPTION_KEY` | - | 开发值 | 强随机值 | 加密密钥 | +| `DATABASE_URL` | - | 本地 PostgreSQL | 容器内 PostgreSQL | 数据库连接 | + +--- + +## 模式对比 + +| 特性 | Mock | Dev | Up | +|------|------|-----|-----| +| **Backend 运行位置** | 本地进程 | 本地进程 | Docker 容器 | +| **依赖服务** | 无 | Docker (PostgreSQL) | Docker (PostgreSQL) | +| **热重载** | ✅ Air | ✅ Air | ❌ 需重新构建 | +| **数据库** | ❌ 内存 | ✅ PostgreSQL | ✅ PostgreSQL | +| **数据持久化** | ❌ | ✅ | ✅ | +| **启动时间** | 3 秒 | 5-8 秒 | 15-30 秒 | +| **调试能力** | ✅ IDE 断点 | ✅ IDE 断点 | ⚠️ 日志调试 | +| **适用场景** | 快速开发、API 测试 | 日常开发 ⭐ 推荐 | 集成测试、生产预演 | +| **环境隔离** | ✅ 完全隔离 | ⚠️ 部分隔离 | ✅ 完全隔离 | +| **生产一致性** | ❌ 低 | ⚠️ 中 | ✅ 高 | + +--- + +## 推荐使用场景 + +### 🚀 Mock 模式 +- ✅ 快速功能开发 +- ✅ API 接口测试 +- ✅ 前端联调 +- ✅ 单元测试 + +### ⭐ Dev 模式(推荐日常开发) +- ✅ 日常功能开发 +- ✅ 代码调试 +- ✅ 数据持久化测试 +- ✅ 快速迭代 + +### 🎯 Up 模式 +- ✅ 集成测试 +- ✅ 生产环境预演 +- ✅ 团队协作环境搭建 +- ✅ 交付演示 + +--- + +# 故障排查 + +## 通用问题 + +### 1. 端口被占用 + +```bash +# 查找占用端口的进程 +lsof -i :8080 +# 或 +netstat -tuln | grep 8080 + +# 关闭进程 +kill -9 + +# 或使用其他端口 +export PORT=8081 +``` + +### 2. Go 环境问题 + +```bash +# 检查 Go 版本 +go version # 需要 Go 1.21+ + +# 更新 Go 模块 +go mod download +go mod tidy + +# 清理 Go 缓存 +go clean -cache -modcache +``` + +### 3. Air 找不到命令 + +```bash +# 确保 GOPATH/bin 在 PATH 中 +export PATH=$PATH:$(go env GOPATH)/bin + +# 重新安装 Air +go install github.com/air-verse/air@latest + +# 验证安装 +air -v +``` + +--- + +## Mock 模式问题 + +### 1. 服务无法启动 + +```bash +# 检查环境变量 +echo $ADAPTER_MODE # 应该是 mock +echo $PORT # 应该是 8080 +echo $JWT_SECRET # 不应为空 + +# 手动运行查看详细错误 +export ADAPTER_MODE=mock +export JWT_SECRET=test-secret +go run cmd/api/main.go +``` + +### 2. Bootstrap 数据未加载 + +```bash +# 检查 bootstrap.json 文件 +cat config/bootstrap.json + +# 确保 enabled 为 true +# { +# "enabled": true, +# ... +# } +``` + +--- + +## Dev 模式问题 + +### 1. 数据库连接失败 + +```bash +# 检查 PostgreSQL 是否运行 +docker compose ps postgres + +# 检查端口 +lsof -i :5432 + +# 测试连接 +psql -h localhost -U postgres -d ocdp -c "SELECT 1;" + +# 重启数据库 +docker compose restart postgres +``` + +### 2. 数据库未初始化 + +```bash +# 重新初始化数据库 +docker compose down postgres +docker compose up -d postgres + +# 等待健康检查通过 +docker compose ps postgres +``` + +### 3. Air 热重载失败 + +```bash +# 清理临时文件 +rm -rf tmp/ + +# 重新启动 Air +air -c .air.toml + +# 检查 .air.toml 配置 +cat .air.toml +``` + +--- + +## Up 模式问题 + +### 1. Backend 容器无法启动 + +```bash +# 查看容器日志 +docker compose logs backend + +# 查看容器状态 +docker compose ps + +# 重新构建镜像 +make up-rebuild +``` + +### 2. 镜像构建失败 + +```bash +# 清理 Docker 缓存 +docker builder prune -a + +# 重新构建 +docker compose build --no-cache backend + +# 检查 Dockerfile +cat Dockerfile +``` + +### 3. 容器健康检查失败 + +```bash +# 查看容器日志 +docker compose logs backend + +# 手动执行健康检查 +docker compose exec backend wget --no-verbose --tries=1 --spider http://localhost:8080/health + +# 检查 healthcheck 配置 +docker compose config | grep -A 5 healthcheck +``` + +--- + +## 数据库问题 + +### 1. 数据库连接超时 + +```bash +# 检查数据库是否启动完成 +docker compose ps postgres + +# 查看数据库日志 +docker compose logs postgres + +# 手动测试连接 +docker compose exec postgres pg_isready -U postgres + +# 等待更长时间 +sleep 10 +docker compose restart backend +``` + +### 2. 数据丢失 + +```bash +# 检查数据卷 +docker volume ls | grep postgres + +# 备份当前数据 +docker compose exec postgres pg_dump -U postgres ocdp > backup.sql + +# 恢复数据 +docker compose exec -T postgres psql -U postgres ocdp < backup.sql +``` + +### 3. 权限问题 + +```bash +# 检查数据卷权限 +docker volume inspect ocdp_postgres_data + +# 重新创建数据卷 +docker compose down -v +docker compose up -d postgres +``` + +--- + +## 健康检查 + +```bash +# 服务健康检查 +curl http://localhost:8080/health +# 预期: {"status":"healthy"} + +# 数据库健康检查 +# Docker 方式 +docker compose exec postgres pg_isready -U postgres + +# 本地方式 +pg_isready -h localhost -p 5432 -U postgres + +# 检查所有服务状态 +docker compose ps +``` + +--- + +## 资源监控 + +```bash +# 查看容器资源使用 +docker stats + +# 查看磁盘使用 +docker system df + +# 查看日志大小 +du -sh $(docker inspect --format='{{.LogPath}}' ocdp-backend) +du -sh $(docker inspect --format='{{.LogPath}}' ocdp-postgres) + +# 清理 Docker 资源 +docker system prune -a --volumes +``` + +--- + +## 完全重置 + +### Mock 模式 + +```bash +# 清理临时文件 +rm -rf tmp/ + +# 重新启动 +make mock +``` + +### Dev 模式 + +```bash +# 停止服务 +docker compose down + +# 删除数据卷(⚠️ 数据丢失) +docker compose down -v + +# 重新启动 +make db-up +make dev +``` + +### Up 模式 + +```bash +# 停止所有服务 +docker compose down + +# 删除数据卷(⚠️ 数据丢失) +docker compose down -v + +# 清理 Docker 系统 +docker system prune -a --volumes + +# 重新部署 +make up +``` + +--- + +## 服务访问地址 + +| 服务 | Mock | Dev | Up | 说明 | +|------|------|-----|-----|------| +| **Backend API** | http://localhost:8080/api/v1 | http://localhost:8080/api/v1 | http://localhost:8080/api/v1 | REST API | +| **Health Check** | http://localhost:8080/health | http://localhost:8080/health | http://localhost:8080/health | 健康检查 | +| **Swagger UI** | http://localhost:8080/api/docs | http://localhost:8080/api/docs | http://localhost:8080/api/docs | API 文档 | +| **PostgreSQL** | - | localhost:5432 | localhost:5432 | 数据库 | + +--- + +## 相关文档 + +- [架构文档](architecture.md) - 六边形架构、技术选型 +- [API 与测试](api-and-test.md) - API 文档、测试指南 +- [主 README](../README.md) - 项目概览 + +--- + +**Last Updated**: 2025-11-09 +**Version**: v3.0.0 diff --git a/backend/docs/embed.go b/backend/docs/embed.go new file mode 100644 index 0000000..3db4f4e --- /dev/null +++ b/backend/docs/embed.go @@ -0,0 +1,8 @@ +package docs + +import _ "embed" + +var ( + //go:embed openapi.yaml + OpenAPISpec []byte +) diff --git a/backend/docs/openapi.json b/backend/docs/openapi.json new file mode 100644 index 0000000..44109aa --- /dev/null +++ b/backend/docs/openapi.json @@ -0,0 +1,2055 @@ +{ + "schemes": [ + "http", + "https" + ], + "swagger": "2.0", + "info": { + "description": "OCDP (Open Cloud Development Platform) Backend API\n\nRESTful API for managing Kubernetes clusters, OCI registries, and Helm deployments.", + "title": "OCDP Backend API", + "contact": { + "name": "API Support", + "email": "support@ocdp.io" + }, + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.0" + }, + "host": "localhost:8080", + "basePath": "/api/v1", + "paths": { + "/auth/login": { + "post": { + "description": "使用用户名和密码获取访问令牌", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Auth" + ], + "summary": "用户登录", + "parameters": [ + { + "description": "登录信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.LoginRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.AuthResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + } + }, + "/auth/refresh": { + "post": { + "description": "使用刷新令牌获取新的访问令牌", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Auth" + ], + "summary": "刷新访问令牌", + "parameters": [ + { + "description": "刷新令牌", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.RefreshTokenRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.AuthResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + } + }, + "/auth/register": { + "post": { + "description": "创建一个新的后台用户", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Auth" + ], + "summary": "用户注册", + "parameters": [ + { + "description": "注册信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.RegisterRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UserResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + } + }, + "/clusters": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Clusters" + ], + "summary": "列出所有集群", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ClusterResponse" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "创建一个新的 Kubernetes 集群配置", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Clusters" + ], + "summary": "创建集群", + "parameters": [ + { + "description": "集群信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.CreateClusterRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ClusterResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + } + }, + "/clusters/{cluster_id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Clusters" + ], + "summary": "获取集群详情", + "parameters": [ + { + "type": "string", + "description": "集群 ID", + "name": "cluster_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ClusterResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Clusters" + ], + "summary": "更新集群", + "parameters": [ + { + "type": "string", + "description": "集群 ID", + "name": "cluster_id", + "in": "path", + "required": true + }, + { + "description": "更新内容", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UpdateClusterRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ClusterResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Clusters" + ], + "summary": "删除集群", + "parameters": [ + { + "type": "string", + "description": "集群 ID", + "name": "cluster_id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content", + "schema": { + "type": "string" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + } + }, + "/clusters/{cluster_id}/health": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Clusters" + ], + "summary": "获取集群健康状态", + "parameters": [ + { + "type": "string", + "description": "集群 ID", + "name": "cluster_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ClusterHealthResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + } + }, + "/clusters/{cluster_id}/instances": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Instances" + ], + "summary": "列出实例", + "parameters": [ + { + "type": "string", + "description": "集群 ID", + "name": "cluster_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceListResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "在指定集群上部署一个 artifact", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Instances" + ], + "summary": "创建实例", + "parameters": [ + { + "type": "string", + "description": "集群 ID", + "name": "cluster_id", + "in": "path", + "required": true + }, + { + "description": "实例配置", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.CreateInstanceRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + } + }, + "/clusters/{cluster_id}/instances/{instance_id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Instances" + ], + "summary": "获取实例详情", + "parameters": [ + { + "type": "string", + "description": "集群 ID", + "name": "cluster_id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "实例 ID", + "name": "instance_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Instances" + ], + "summary": "更新实例", + "parameters": [ + { + "type": "string", + "description": "集群 ID", + "name": "cluster_id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "实例 ID", + "name": "instance_id", + "in": "path", + "required": true + }, + { + "description": "更新内容", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UpdateInstanceRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Instances" + ], + "summary": "删除实例", + "parameters": [ + { + "type": "string", + "description": "集群 ID", + "name": "cluster_id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "实例 ID", + "name": "instance_id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content", + "schema": { + "type": "string" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + } + }, + "/clusters/{cluster_id}/instances/{instance_id}/entries": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Instances" + ], + "summary": "获取实例 Service/Ingress 入口", + "parameters": [ + { + "type": "string", + "description": "集群 ID", + "name": "cluster_id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "实例 ID", + "name": "instance_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceEntryResponse" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + } + }, + "/monitoring/clusters": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Monitoring" + ], + "summary": "列出集群监控", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ClusterMetricsResponse" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + } + }, + "/monitoring/clusters/{cluster_id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Monitoring" + ], + "summary": "获取集群监控", + "parameters": [ + { + "type": "string", + "description": "集群 ID", + "name": "cluster_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ClusterMetricsResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + } + }, + "/monitoring/clusters/{cluster_id}/nodes": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Monitoring" + ], + "summary": "获取节点指标", + "parameters": [ + { + "type": "string", + "description": "集群 ID", + "name": "cluster_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.NodeMetricsResponse" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + } + }, + "/monitoring/summary": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Monitoring" + ], + "summary": "获取监控汇总", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.MonitoringSummaryResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + } + }, + "/registries": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Registries" + ], + "summary": "列出所有 Registries", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.RegistryResponse" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "新增 OCI Registry 配置", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Registries" + ], + "summary": "创建 Registry", + "parameters": [ + { + "description": "Registry 信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.CreateRegistryRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.RegistryResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + } + }, + "/registries/{registry_id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Registries" + ], + "summary": "获取 Registry", + "parameters": [ + { + "type": "string", + "description": "Registry ID", + "name": "registry_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.RegistryResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Registries" + ], + "summary": "更新 Registry", + "parameters": [ + { + "type": "string", + "description": "Registry ID", + "name": "registry_id", + "in": "path", + "required": true + }, + { + "description": "更新内容", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UpdateRegistryRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.RegistryResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Registries" + ], + "summary": "删除 Registry", + "parameters": [ + { + "type": "string", + "description": "Registry ID", + "name": "registry_id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content", + "schema": { + "type": "string" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + } + }, + "/registries/{registry_id}/health": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "produces": [ + "application/json" + ], + "tags": [ + "Registries" + ], + "summary": "检查 Registry 健康", + "parameters": [ + { + "type": "string", + "description": "Registry ID", + "name": "registry_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.RegistryHealthResponse" + } + } + } + } + }, + "/registries/{registry_id}/repositories": { + "get": { + "description": "列出指定 Registry 中的所有 Repository", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Artifacts" + ], + "summary": "列出 Registry 中的所有 Repositories", + "parameters": [ + { + "type": "string", + "description": "Registry ID", + "name": "registry_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.RepositoryListResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + } + }, + "/registries/{registry_id}/repositories/{repository_name}/artifacts": { + "get": { + "description": "列出指定 Repository 中的所有 Artifact,支持按类型过滤", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Artifacts" + ], + "summary": "列出 Repository 中的所有 Artifacts", + "parameters": [ + { + "type": "string", + "description": "Registry ID", + "name": "registry_id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Repository Name (URL encoded, e.g. charts%2Fnginx)", + "name": "repository_name", + "in": "path", + "required": true + }, + { + "type": "string", + "default": "all", + "description": "过滤 Artifact 类型 (all, chart, image, other)", + "name": "media_type", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.TagResponse" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + } + }, + "/registries/{registry_id}/repositories/{repository_name}/artifacts/{reference}": { + "get": { + "description": "获取指定 Artifact 的详细信息", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Artifacts" + ], + "summary": "获取 Artifact 详情", + "parameters": [ + { + "type": "string", + "description": "Registry ID", + "name": "registry_id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Repository Name (URL encoded)", + "name": "repository_name", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Artifact Reference (tag or digest)", + "name": "reference", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ArtifactResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + } + }, + "/registries/{registry_id}/repositories/{repository_name}/artifacts/{reference}/values-schema": { + "get": { + "description": "获取 Helm Chart 的 values.schema.json (仅支持 Chart 类型)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Artifacts" + ], + "summary": "获取 Helm Chart Values Schema", + "parameters": [ + { + "type": "string", + "description": "Registry ID", + "name": "registry_id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Repository Name (URL encoded)", + "name": "repository_name", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Artifact Reference (tag or digest)", + "name": "reference", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ValuesSchemaResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse" + } + } + } + } + } + }, + "definitions": { + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ArtifactResponse": { + "type": "object", + "properties": { + "createdAt": { + "type": "string" + }, + "digest": { + "type": "string" + }, + "repositoryName": { + "type": "string" + }, + "size": { + "type": "integer" + }, + "tag": { + "type": "string" + }, + "type": { + "description": "chart | image | other", + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.AuthResponse": { + "type": "object", + "properties": { + "accessToken": { + "type": "string" + }, + "refreshToken": { + "type": "string" + }, + "userId": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ClusterHealthResponse": { + "type": "object", + "properties": { + "healthy": { + "type": "boolean" + }, + "message": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ClusterMetricsResponse": { + "type": "object", + "properties": { + "clusterId": { + "type": "string" + }, + "clusterName": { + "type": "string" + }, + "cpuUsage": { + "type": "number" + }, + "gpuUsage": { + "type": "number" + }, + "lastCheck": { + "type": "string" + }, + "maxNodeCpu": { + "type": "string" + }, + "maxNodeCpuUsage": { + "type": "number" + }, + "maxNodeGpu": { + "type": "integer" + }, + "maxNodeGpuUsage": { + "type": "number" + }, + "maxNodeMemUsage": { + "type": "number" + }, + "maxNodeMemory": { + "type": "string" + }, + "memoryUsage": { + "type": "number" + }, + "nodeCount": { + "type": "integer" + }, + "nodes": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.NodeMetricsResponse" + } + }, + "podCount": { + "type": "integer" + }, + "status": { + "type": "string" + }, + "totalCpu": { + "type": "string" + }, + "totalGpu": { + "type": "integer" + }, + "totalMemory": { + "type": "string" + }, + "uptime": { + "type": "string" + }, + "usedCpu": { + "type": "string" + }, + "usedGpu": { + "type": "integer" + }, + "usedMemory": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ClusterResponse": { + "type": "object", + "properties": { + "caData": { + "description": "脱敏数据(仅用于前端显示,实际值为掩码)", + "type": "string" + }, + "certData": { + "description": "脱敏显示(••••••••)", + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "description": { + "type": "string" + }, + "hasCaData": { + "description": "认证配置状态(不返回实际证书数据,仅返回是否已配置)", + "type": "boolean" + }, + "hasCertData": { + "type": "boolean" + }, + "hasKeyData": { + "type": "boolean" + }, + "hasToken": { + "type": "boolean" + }, + "host": { + "type": "string" + }, + "id": { + "type": "string" + }, + "keyData": { + "description": "脱敏显示(••••••••)", + "type": "string" + }, + "name": { + "type": "string" + }, + "token": { + "description": "脱敏显示(••••••••)", + "type": "string" + }, + "updatedAt": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.CreateClusterRequest": { + "type": "object", + "required": [ + "host", + "name" + ], + "properties": { + "caData": { + "type": "string" + }, + "certData": { + "type": "string" + }, + "description": { + "type": "string" + }, + "host": { + "type": "string" + }, + "keyData": { + "type": "string" + }, + "name": { + "type": "string" + }, + "token": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.CreateInstanceRequest": { + "type": "object", + "required": [ + "name", + "namespace", + "registryId", + "repository", + "tag" + ], + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "registryId": { + "type": "string" + }, + "repository": { + "type": "string" + }, + "tag": { + "type": "string" + }, + "values": { + "type": "object", + "additionalProperties": true + }, + "valuesYaml": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.CreateRegistryRequest": { + "type": "object", + "required": [ + "name", + "url" + ], + "properties": { + "description": { + "type": "string" + }, + "insecure": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "password": { + "type": "string" + }, + "url": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse": { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "error": { + "type": "string" + }, + "message": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceEntryHostResponse": { + "type": "object", + "properties": { + "host": { + "type": "string" + }, + "paths": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceEntryPathResponse" + } + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceEntryPathResponse": { + "type": "object", + "properties": { + "path": { + "type": "string" + }, + "serviceName": { + "type": "string" + }, + "servicePort": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceEntryPortResponse": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "nodePort": { + "type": "integer" + }, + "port": { + "type": "integer" + }, + "protocol": { + "type": "string" + }, + "targetPort": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceEntryResponse": { + "type": "object", + "properties": { + "clusterIP": { + "type": "string" + }, + "externalIPs": { + "type": "array", + "items": { + "type": "string" + } + }, + "hosts": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceEntryHostResponse" + } + }, + "kind": { + "type": "string" + }, + "loadBalancerIngress": { + "type": "array", + "items": { + "type": "string" + } + }, + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "ports": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceEntryPortResponse" + } + }, + "tls": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceEntryTLSResponse" + } + }, + "type": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceEntryTLSResponse": { + "type": "object", + "properties": { + "hosts": { + "type": "array", + "items": { + "type": "string" + } + }, + "secretName": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceListResponse": { + "type": "object", + "properties": { + "instances": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceResponse" + } + }, + "total": { + "type": "integer" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceResponse": { + "type": "object", + "properties": { + "chart": { + "type": "string" + }, + "clusterId": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "description": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "registryId": { + "type": "string" + }, + "repository": { + "type": "string" + }, + "revision": { + "type": "integer" + }, + "status": { + "type": "string" + }, + "updatedAt": { + "type": "string" + }, + "values": { + "type": "object", + "additionalProperties": true + }, + "version": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.LoginRequest": { + "type": "object", + "required": [ + "password", + "username" + ], + "properties": { + "password": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.MonitoringSummaryResponse": { + "type": "object", + "properties": { + "errorClusters": { + "type": "integer" + }, + "healthyClusters": { + "type": "integer" + }, + "lastUpdate": { + "type": "string" + }, + "totalClusters": { + "type": "integer" + }, + "totalNodes": { + "type": "integer" + }, + "totalPods": { + "type": "integer" + }, + "warningClusters": { + "type": "integer" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.NodeMetricsResponse": { + "type": "object", + "properties": { + "age": { + "type": "string" + }, + "containerRuntime": { + "type": "string" + }, + "cpuAllocatable": { + "type": "string" + }, + "cpuCapacity": { + "type": "string" + }, + "cpuPercent": { + "type": "number" + }, + "cpuUsage": { + "type": "string" + }, + "gpuCapacity": { + "type": "integer" + }, + "gpuPercent": { + "type": "number" + }, + "gpuType": { + "type": "string" + }, + "gpuUsage": { + "type": "integer" + }, + "kernelVersion": { + "type": "string" + }, + "kubeletVersion": { + "type": "string" + }, + "memoryAllocatable": { + "type": "string" + }, + "memoryCapacity": { + "type": "string" + }, + "memoryPercent": { + "type": "number" + }, + "memoryUsage": { + "type": "string" + }, + "nodeName": { + "type": "string" + }, + "osImage": { + "type": "string" + }, + "podCount": { + "type": "integer" + }, + "role": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.RefreshTokenRequest": { + "type": "object", + "required": [ + "refreshToken" + ], + "properties": { + "refreshToken": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.RegisterRequest": { + "type": "object", + "required": [ + "password", + "username" + ], + "properties": { + "password": { + "type": "string", + "minLength": 6 + }, + "username": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.RegistryHealthResponse": { + "type": "object", + "properties": { + "healthy": { + "type": "boolean" + }, + "message": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.RegistryResponse": { + "type": "object", + "properties": { + "createdAt": { + "type": "string" + }, + "description": { + "type": "string" + }, + "hasPassword": { + "description": "是否已设置密码", + "type": "boolean" + }, + "id": { + "type": "string" + }, + "insecure": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "password": { + "description": "脱敏显示(••••••••)", + "type": "string" + }, + "updatedAt": { + "type": "string" + }, + "url": { + "type": "string" + }, + "username": { + "description": "明文返回用户名(不敏感)", + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.RepositoryListResponse": { + "type": "object", + "properties": { + "catalogSupported": { + "description": "Whether _catalog API is supported", + "type": "boolean" + }, + "message": { + "description": "User-friendly message", + "type": "string" + }, + "registryId": { + "type": "string" + }, + "registryUrl": { + "type": "string" + }, + "repositories": { + "type": "array", + "items": { + "type": "string" + } + }, + "source": { + "description": "Data source: \"catalog\" | \"preconfigured\" | \"unavailable\"", + "type": "string" + }, + "total": { + "type": "integer" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.TagResponse": { + "type": "object", + "properties": { + "mediaType": { + "type": "string" + }, + "repositoryName": { + "description": "Repository name", + "type": "string" + }, + "size": { + "description": "Artifact size (bytes)", + "type": "integer" + }, + "tag": { + "description": "Tag name (e.g. \"1.0.0\", \"latest\")", + "type": "string" + }, + "type": { + "description": "Artifact type: chart, image, other", + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UpdateClusterRequest": { + "type": "object", + "properties": { + "caData": { + "type": "string" + }, + "certData": { + "type": "string" + }, + "description": { + "type": "string" + }, + "host": { + "type": "string" + }, + "keyData": { + "type": "string" + }, + "name": { + "type": "string" + }, + "token": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UpdateInstanceRequest": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "values": { + "type": "object", + "additionalProperties": true + }, + "valuesYaml": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UpdateRegistryRequest": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "insecure": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "password": { + "type": "string" + }, + "url": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UserResponse": { + "type": "object", + "properties": { + "createdAt": { + "type": "string" + }, + "email": { + "type": "string" + }, + "id": { + "type": "string" + }, + "updatedAt": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ValuesSchemaResponse": { + "type": "object", + "properties": { + "schema": { + "type": "string" + } + } + } + }, + "securityDefinitions": { + "BearerAuth": { + "description": "Type \"Bearer\" followed by a space and JWT token.", + "type": "apiKey", + "name": "Authorization", + "in": "header" + } + } +} \ No newline at end of file diff --git a/backend/docs/openapi.yaml b/backend/docs/openapi.yaml new file mode 100644 index 0000000..457ae7e --- /dev/null +++ b/backend/docs/openapi.yaml @@ -0,0 +1,1367 @@ +basePath: /api/v1 +definitions: + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ArtifactResponse: + properties: + createdAt: + type: string + digest: + type: string + repositoryName: + type: string + size: + type: integer + tag: + type: string + type: + description: chart | image | other + type: string + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.AuthResponse: + properties: + accessToken: + type: string + refreshToken: + type: string + userId: + type: string + username: + type: string + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ClusterHealthResponse: + properties: + healthy: + type: boolean + message: + type: string + version: + type: string + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ClusterMetricsResponse: + properties: + clusterId: + type: string + clusterName: + type: string + cpuUsage: + type: number + gpuUsage: + type: number + lastCheck: + type: string + maxNodeCpu: + type: string + maxNodeCpuUsage: + type: number + maxNodeGpu: + type: integer + maxNodeGpuUsage: + type: number + maxNodeMemUsage: + type: number + maxNodeMemory: + type: string + memoryUsage: + type: number + nodeCount: + type: integer + nodes: + items: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.NodeMetricsResponse' + type: array + podCount: + type: integer + status: + type: string + totalCpu: + type: string + totalGpu: + type: integer + totalMemory: + type: string + uptime: + type: string + usedCpu: + type: string + usedGpu: + type: integer + usedMemory: + type: string + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ClusterResponse: + properties: + caData: + description: 脱敏数据(仅用于前端显示,实际值为掩码) + type: string + certData: + description: 脱敏显示(••••••••) + type: string + createdAt: + type: string + description: + type: string + hasCaData: + description: 认证配置状态(不返回实际证书数据,仅返回是否已配置) + type: boolean + hasCertData: + type: boolean + hasKeyData: + type: boolean + hasToken: + type: boolean + host: + type: string + id: + type: string + keyData: + description: 脱敏显示(••••••••) + type: string + name: + type: string + token: + description: 脱敏显示(••••••••) + type: string + updatedAt: + type: string + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.CreateClusterRequest: + properties: + caData: + description: Base64 CA data (also accepts legacy field "ca_data") + type: string + certData: + description: Base64 client certificate (also accepts legacy field "cert_data") + type: string + description: + type: string + host: + type: string + keyData: + description: Base64 client key (also accepts legacy field "key_data") + type: string + name: + type: string + token: + type: string + required: + - host + - name + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.CreateInstanceRequest: + properties: + description: + type: string + name: + type: string + namespace: + type: string + registryId: + description: Registry identifier (also accepts legacy field "registry_id") + type: string + repository: + type: string + tag: + type: string + values: + additionalProperties: true + type: object + valuesYaml: + type: string + required: + - name + - namespace + - registryId + - repository + - tag + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.CreateRegistryRequest: + properties: + description: + type: string + insecure: + type: boolean + name: + type: string + password: + type: string + url: + type: string + username: + type: string + required: + - name + - url + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse: + properties: + code: + type: integer + error: + type: string + message: + type: string + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceEntryHostResponse: + properties: + host: + type: string + paths: + items: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceEntryPathResponse' + type: array + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceEntryPathResponse: + properties: + path: + type: string + serviceName: + type: string + servicePort: + type: string + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceEntryPortResponse: + properties: + name: + type: string + nodePort: + type: integer + port: + type: integer + protocol: + type: string + targetPort: + type: string + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceEntryResponse: + properties: + clusterIP: + type: string + externalIPs: + items: + type: string + type: array + hosts: + items: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceEntryHostResponse' + type: array + kind: + type: string + loadBalancerIngress: + items: + type: string + type: array + name: + type: string + namespace: + type: string + ports: + items: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceEntryPortResponse' + type: array + tls: + items: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceEntryTLSResponse' + type: array + type: + type: string + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceEntryTLSResponse: + properties: + hosts: + items: + type: string + type: array + secretName: + type: string + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceListResponse: + properties: + instances: + items: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceResponse' + type: array + total: + type: integer + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceResponse: + properties: + chart: + type: string + clusterId: + type: string + createdAt: + type: string + description: + type: string + id: + type: string + name: + type: string + namespace: + type: string + registryId: + type: string + repository: + type: string + revision: + type: integer + status: + description: 实例当前状态 + enum: + - deployed + - uninstalled + - superseded + - failed + - pending-install + - pending-upgrade + - pending-rollback + - pending-delete + - unknown + type: string + statusReason: + description: 状态说明 + type: string + lastOperation: + description: 最后一次操作类型 + enum: + - "" + - install + - upgrade + - rollback + - delete + - sync + type: string + lastError: + description: 最近一次错误信息 + type: string + updatedAt: + type: string + values: + additionalProperties: true + type: object + version: + type: string + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.LoginRequest: + properties: + password: + type: string + username: + type: string + required: + - password + - username + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.MonitoringSummaryResponse: + properties: + errorClusters: + type: integer + healthyClusters: + type: integer + lastUpdate: + type: string + totalClusters: + type: integer + totalNodes: + type: integer + totalPods: + type: integer + warningClusters: + type: integer + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.NodeMetricsResponse: + properties: + age: + type: string + containerRuntime: + type: string + cpuAllocatable: + type: string + cpuCapacity: + type: string + cpuPercent: + type: number + cpuUsage: + type: string + gpuCapacity: + type: integer + gpuPercent: + type: number + gpuType: + type: string + gpuUsage: + type: integer + kernelVersion: + type: string + kubeletVersion: + type: string + memoryAllocatable: + type: string + memoryCapacity: + type: string + memoryPercent: + type: number + memoryUsage: + type: string + nodeName: + type: string + osImage: + type: string + podCount: + type: integer + role: + type: string + status: + type: string + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.RefreshTokenRequest: + properties: + refreshToken: + type: string + required: + - refreshToken + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.RegisterRequest: + properties: + password: + minLength: 6 + type: string + username: + type: string + required: + - password + - username + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.RegistryHealthResponse: + properties: + healthy: + type: boolean + message: + type: string + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.RegistryResponse: + properties: + createdAt: + type: string + description: + type: string + hasPassword: + description: 是否已设置密码 + type: boolean + id: + type: string + insecure: + type: boolean + name: + type: string + password: + description: 脱敏显示(••••••••) + type: string + updatedAt: + type: string + url: + type: string + username: + description: 明文返回用户名(不敏感) + type: string + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.RepositoryListResponse: + properties: + catalogSupported: + description: Whether _catalog API is supported + type: boolean + message: + description: User-friendly message + type: string + registryId: + type: string + registryUrl: + type: string + repositories: + items: + type: string + type: array + source: + description: 'Data source: "catalog" | "preconfigured" | "unavailable"' + type: string + total: + type: integer + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.TagResponse: + properties: + mediaType: + type: string + repositoryName: + description: Repository name + type: string + size: + description: Artifact size (bytes) + type: integer + tag: + description: Tag name (e.g. "1.0.0", "latest") + type: string + type: + description: 'Artifact type: chart, image, other' + type: string + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UpdateClusterRequest: + properties: + caData: + description: Base64 CA data (also accepts legacy field "ca_data") + type: string + certData: + description: Base64 client certificate (also accepts legacy field "cert_data") + type: string + description: + type: string + host: + type: string + keyData: + description: Base64 client key (also accepts legacy field "key_data") + type: string + name: + type: string + token: + type: string + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UpdateInstanceRequest: + properties: + description: + type: string + values: + additionalProperties: true + type: object + valuesYaml: + type: string + version: + type: string + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UpdateRegistryRequest: + properties: + description: + type: string + insecure: + type: boolean + name: + type: string + password: + type: string + url: + type: string + username: + type: string + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UserResponse: + properties: + createdAt: + type: string + email: + type: string + id: + type: string + updatedAt: + type: string + username: + type: string + type: object + github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ValuesSchemaResponse: + properties: + schema: + type: string + type: object +host: localhost:8080 +info: + contact: + email: support@ocdp.io + name: API Support + description: |- + OCDP (Open Cloud Development Platform) Backend API + + RESTful API for managing Kubernetes clusters, OCI registries, and Helm deployments. + license: + name: Apache 2.0 + url: http://www.apache.org/licenses/LICENSE-2.0.html + title: OCDP Backend API + version: "1.0" +paths: + /auth/login: + post: + consumes: + - application/json + description: 使用用户名和密码获取访问令牌 + parameters: + - description: 登录信息 + in: body + name: request + required: true + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.LoginRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.AuthResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse' + summary: 用户登录 + tags: + - Auth + /auth/refresh: + post: + consumes: + - application/json + description: 使用刷新令牌获取新的访问令牌 + parameters: + - description: 刷新令牌 + in: body + name: request + required: true + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.RefreshTokenRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.AuthResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse' + summary: 刷新访问令牌 + tags: + - Auth + /auth/register: + post: + consumes: + - application/json + description: 创建一个新的后台用户 + parameters: + - description: 注册信息 + in: body + name: request + required: true + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.RegisterRequest' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UserResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse' + summary: 用户注册 + tags: + - Auth + /clusters: + get: + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ClusterResponse' + type: array + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse' + security: + - BearerAuth: [] + summary: 列出所有集群 + tags: + - Clusters + post: + consumes: + - application/json + description: 创建一个新的 Kubernetes 集群配置 + parameters: + - description: 集群信息 + in: body + name: request + required: true + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.CreateClusterRequest' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ClusterResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse' + security: + - BearerAuth: [] + summary: 创建集群 + tags: + - Clusters + /clusters/{cluster_id}: + delete: + parameters: + - description: 集群 ID + in: path + name: cluster_id + required: true + type: string + produces: + - application/json + responses: + "204": + description: No Content + schema: + type: string + "404": + description: Not Found + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse' + security: + - BearerAuth: [] + summary: 删除集群 + tags: + - Clusters + get: + parameters: + - description: 集群 ID + in: path + name: cluster_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ClusterResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse' + security: + - BearerAuth: [] + summary: 获取集群详情 + tags: + - Clusters + put: + consumes: + - application/json + parameters: + - description: 集群 ID + in: path + name: cluster_id + required: true + type: string + - description: 更新内容 + in: body + name: request + required: true + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UpdateClusterRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ClusterResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse' + security: + - BearerAuth: [] + summary: 更新集群 + tags: + - Clusters + /clusters/{cluster_id}/health: + get: + parameters: + - description: 集群 ID + in: path + name: cluster_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ClusterHealthResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse' + security: + - BearerAuth: [] + summary: 获取集群健康状态 + tags: + - Clusters + /clusters/{cluster_id}/instances: + get: + parameters: + - description: 集群 ID + in: path + name: cluster_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceListResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse' + security: + - BearerAuth: [] + summary: 列出实例 + tags: + - Instances + post: + consumes: + - application/json + description: 在指定集群上部署一个 artifact + parameters: + - description: 集群 ID + in: path + name: cluster_id + required: true + type: string + - description: 实例配置 + in: body + name: request + required: true + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.CreateInstanceRequest' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse' + security: + - BearerAuth: [] + summary: 创建实例 + tags: + - Instances + /clusters/{cluster_id}/instances/{instance_id}: + delete: + parameters: + - description: 集群 ID + in: path + name: cluster_id + required: true + type: string + - description: 实例 ID + in: path + name: instance_id + required: true + type: string + produces: + - application/json + responses: + "204": + description: No Content + schema: + type: string + "404": + description: Not Found + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse' + security: + - BearerAuth: [] + summary: 删除实例 + tags: + - Instances + get: + parameters: + - description: 集群 ID + in: path + name: cluster_id + required: true + type: string + - description: 实例 ID + in: path + name: instance_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse' + security: + - BearerAuth: [] + summary: 获取实例详情 + tags: + - Instances + put: + consumes: + - application/json + parameters: + - description: 集群 ID + in: path + name: cluster_id + required: true + type: string + - description: 实例 ID + in: path + name: instance_id + required: true + type: string + - description: 更新内容 + in: body + name: request + required: true + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UpdateInstanceRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse' + security: + - BearerAuth: [] + summary: 更新实例 + tags: + - Instances + /clusters/{cluster_id}/instances/{instance_id}/entries: + get: + parameters: + - description: 集群 ID + in: path + name: cluster_id + required: true + type: string + - description: 实例 ID + in: path + name: instance_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.InstanceEntryResponse' + type: array + "404": + description: Not Found + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse' + security: + - BearerAuth: [] + summary: 获取实例 Service/Ingress 入口 + tags: + - Instances + /monitoring/clusters: + get: + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ClusterMetricsResponse' + type: array + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse' + security: + - BearerAuth: [] + summary: 列出集群监控 + tags: + - Monitoring + /monitoring/clusters/{cluster_id}: + get: + parameters: + - description: 集群 ID + in: path + name: cluster_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ClusterMetricsResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse' + security: + - BearerAuth: [] + summary: 获取集群监控 + tags: + - Monitoring + /monitoring/clusters/{cluster_id}/nodes: + get: + parameters: + - description: 集群 ID + in: path + name: cluster_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.NodeMetricsResponse' + type: array + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse' + security: + - BearerAuth: [] + summary: 获取节点指标 + tags: + - Monitoring + /monitoring/summary: + get: + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.MonitoringSummaryResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse' + security: + - BearerAuth: [] + summary: 获取监控汇总 + tags: + - Monitoring + /registries: + get: + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.RegistryResponse' + type: array + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse' + security: + - BearerAuth: [] + summary: 列出所有 Registries + tags: + - Registries + post: + consumes: + - application/json + description: 新增 OCI Registry 配置 + parameters: + - description: Registry 信息 + in: body + name: request + required: true + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.CreateRegistryRequest' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.RegistryResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse' + security: + - BearerAuth: [] + summary: 创建 Registry + tags: + - Registries + /registries/{registry_id}: + delete: + parameters: + - description: Registry ID + in: path + name: registry_id + required: true + type: string + produces: + - application/json + responses: + "204": + description: No Content + schema: + type: string + "404": + description: Not Found + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse' + security: + - BearerAuth: [] + summary: 删除 Registry + tags: + - Registries + get: + parameters: + - description: Registry ID + in: path + name: registry_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.RegistryResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse' + security: + - BearerAuth: [] + summary: 获取 Registry + tags: + - Registries + put: + consumes: + - application/json + parameters: + - description: Registry ID + in: path + name: registry_id + required: true + type: string + - description: 更新内容 + in: body + name: request + required: true + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.UpdateRegistryRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.RegistryResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse' + security: + - BearerAuth: [] + summary: 更新 Registry + tags: + - Registries + /registries/{registry_id}/health: + get: + parameters: + - description: Registry ID + in: path + name: registry_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.RegistryHealthResponse' + security: + - BearerAuth: [] + summary: 检查 Registry 健康 + tags: + - Registries + /registries/{registry_id}/repositories: + get: + consumes: + - application/json + description: 列出指定 Registry 中的所有 Repository + parameters: + - description: Registry ID + in: path + name: registry_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.RepositoryListResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse' + summary: 列出 Registry 中的所有 Repositories + tags: + - Artifacts + /registries/{registry_id}/repositories/{repository_name}/artifacts: + get: + consumes: + - application/json + description: 列出指定 Repository 中的所有 Artifact,支持按类型过滤 + parameters: + - description: Registry ID + in: path + name: registry_id + required: true + type: string + - description: Repository Name (URL encoded, e.g. charts%2Fnginx) + in: path + name: repository_name + required: true + type: string + - default: all + description: 过滤 Artifact 类型 (all, chart, image, other) + in: query + name: media_type + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.TagResponse' + type: array + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse' + summary: 列出 Repository 中的所有 Artifacts + tags: + - Artifacts + /registries/{registry_id}/repositories/{repository_name}/artifacts/{reference}: + get: + consumes: + - application/json + description: 获取指定 Artifact 的详细信息 + parameters: + - description: Registry ID + in: path + name: registry_id + required: true + type: string + - description: Repository Name (URL encoded) + in: path + name: repository_name + required: true + type: string + - description: Artifact Reference (tag or digest) + in: path + name: reference + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ArtifactResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse' + summary: 获取 Artifact 详情 + tags: + - Artifacts + /registries/{registry_id}/repositories/{repository_name}/artifacts/{reference}/values-schema: + get: + consumes: + - application/json + description: 获取 Helm Chart 的 values.schema.json (仅支持 Chart 类型) + parameters: + - description: Registry ID + in: path + name: registry_id + required: true + type: string + - description: Repository Name (URL encoded) + in: path + name: repository_name + required: true + type: string + - description: Artifact Reference (tag or digest) + in: path + name: reference + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ValuesSchemaResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/github_com_ocdp_cluster-service_internal_adapter_input_http_dto.ErrorResponse' + summary: 获取 Helm Chart Values Schema + tags: + - Artifacts +schemes: +- http +- https +securityDefinitions: + BearerAuth: + description: Type "Bearer" followed by a space and JWT token. + in: header + name: Authorization + type: apiKey +swagger: "2.0" diff --git a/backend/env.example b/backend/env.example new file mode 100644 index 0000000..136c0a3 --- /dev/null +++ b/backend/env.example @@ -0,0 +1,43 @@ +# ================================================== +# OCDP Backend - 环境变量配置示例 +# ================================================== +# 使用方式: +# 1. 复制此文件为 .env: cp env.example .env +# 2. 修改相应的配置值 +# 3. 启动服务: docker compose up +# ================================================== + +# ================================================== +# 应用配置 +# ================================================== +# 适配器模式: mock (开发调试) 或留空/任意值 (真实模式,连接 PostgreSQL) +# ADAPTER_MODE=mock +ADAPTER_MODE= + +# 后端服务端口 +BACKEND_PORT=8080 + +# JWT 密钥 (生产环境必须修改) +JWT_SECRET=change-me-in-production-use-strong-secret + +# 加密密钥 (必须是 32 字节,生产环境必须修改) +ENCRYPTION_KEY=change-me-32-bytes-long-key-here + +# ================================================== +# PostgreSQL 数据库配置 +# ================================================== +POSTGRES_DB=ocdp +POSTGRES_USER=postgres +POSTGRES_PASSWORD=postgres +POSTGRES_PORT=5432 + +# 完整的数据库连接 URL (可选,会自动从上面的配置生成) +# DATABASE_URL=postgresql://postgres:postgres@localhost:5432/ocdp?sslmode=disable + +# ================================================== +# pgAdmin 配置 (可选) +# ================================================== +PGADMIN_EMAIL=admin@ocdp.local +PGADMIN_PASSWORD=admin +PGADMIN_PORT=5050 + diff --git a/backend/go.mod b/backend/go.mod new file mode 100644 index 0000000..c13c94c --- /dev/null +++ b/backend/go.mod @@ -0,0 +1,127 @@ +module github.com/ocdp/cluster-service + +go 1.24.0 + +toolchain go1.24.6 + +require ( + github.com/golang-jwt/jwt/v5 v5.3.0 + github.com/google/uuid v1.6.0 + github.com/gorilla/mux v1.8.1 + github.com/lib/pq v1.10.9 + golang.org/x/crypto v0.43.0 + helm.sh/helm/v3 v3.19.0 + k8s.io/api v0.34.1 + k8s.io/apimachinery v0.34.1 + k8s.io/client-go v0.34.1 + k8s.io/metrics v0.34.1 + oras.land/oras-go/v2 v2.6.0 +) + +require ( + dario.cat/mergo v1.0.1 // indirect + github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect + github.com/BurntSushi/toml v1.5.0 // indirect + github.com/MakeNowJust/heredoc v1.0.0 // indirect + github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/semver/v3 v3.4.0 // indirect + github.com/Masterminds/sprig/v3 v3.3.0 // indirect + github.com/Masterminds/squirrel v1.5.4 // indirect + github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect + github.com/blang/semver/v4 v4.0.0 // indirect + github.com/chai2010/gettext-go v1.0.2 // indirect + github.com/containerd/containerd v1.7.28 // indirect + github.com/containerd/errdefs v0.3.0 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/containerd/platforms v0.2.1 // indirect + github.com/cyphar/filepath-securejoin v0.4.1 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/evanphx/json-patch v5.9.11+incompatible // indirect + github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect + github.com/fatih/color v1.13.0 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect + github.com/go-errors/errors v1.4.2 // indirect + github.com/go-gorp/gorp/v3 v3.1.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/gobwas/glob v0.2.3 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/btree v1.1.3 // indirect + github.com/google/gnostic-models v0.7.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect + github.com/gosuri/uitable v0.0.4 // indirect + github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/huandu/xstrings v1.5.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jmoiron/sqlx v1.4.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect + github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect + github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.17 // indirect + github.com/mattn/go-runewidth v0.0.9 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/go-wordwrap v1.0.1 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/moby/spdystream v0.5.0 // indirect + github.com/moby/term v0.5.2 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect + github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/peterbourgon/diskv v2.0.1+incompatible // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/rubenv/sql-migrate v1.8.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect + github.com/shopspring/decimal v1.4.0 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/spf13/cast v1.7.0 // indirect + github.com/spf13/cobra v1.10.1 // indirect + github.com/spf13/pflag v1.0.9 // indirect + github.com/x448/float16 v0.8.4 // indirect + github.com/xlab/treeprint v1.2.0 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/net v0.45.0 // indirect + golang.org/x/oauth2 v0.30.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/term v0.36.0 // indirect + golang.org/x/text v0.30.0 // indirect + golang.org/x/time v0.12.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect + google.golang.org/grpc v1.72.1 // indirect + google.golang.org/protobuf v1.36.9 // indirect + gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/apiextensions-apiserver v0.34.0 // indirect + k8s.io/apiserver v0.34.0 // indirect + k8s.io/cli-runtime v0.34.0 // indirect + k8s.io/component-base v0.34.0 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect + k8s.io/kubectl v0.34.0 // indirect + k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect + sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect + sigs.k8s.io/kustomize/api v0.20.1 // indirect + sigs.k8s.io/kustomize/kyaml v0.20.1 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect +) diff --git a/backend/go.sum b/backend/go.sum new file mode 100644 index 0000000..edc1b29 --- /dev/null +++ b/backend/go.sum @@ -0,0 +1,452 @@ +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= +github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= +github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= +github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= +github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= +github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70= +github.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk= +github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= +github.com/containerd/containerd v1.7.28 h1:Nsgm1AtcmEh4AHAJ4gGlNSaKgXiNccU270Dnf81FQ3c= +github.com/containerd/containerd v1.7.28/go.mod h1:azUkWcOvHrWvaiUjSQH0fjzuHIwSPg1WL5PshGP4Szs= +github.com/containerd/errdefs v0.3.0 h1:FSZgGOeK4yuT/+DnF07/Olde/q4KBoMsaamhXxIMDp4= +github.com/containerd/errdefs v0.3.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= +github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/distribution/distribution/v3 v3.0.0 h1:q4R8wemdRQDClzoNNStftB2ZAfqOiN6UX90KJc4HjyM= +github.com/distribution/distribution/v3 v3.0.0/go.mod h1:tRNuFoZsUdyRVegq8xGNeds4KLjwLCRin/tTo6i1DhU= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= +github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= +github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= +github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8= +github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= +github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= +github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= +github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= +github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= +github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4= +github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc= +github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI= +github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= +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/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= +github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs= +github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +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/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +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/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= +github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= +github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY= +github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo= +github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= +github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/golang-lru/arc/v2 v2.0.5 h1:l2zaLDubNhW4XO3LnliVj0GXO3+/CGNJAg1dcN2Fpfw= +github.com/hashicorp/golang-lru/arc/v2 v2.0.5/go.mod h1:ny6zBSQZi2JxIeYcv7kt2sH2PXJtirBN7RDhRpxPkxU= +github.com/hashicorp/golang-lru/v2 v2.0.5 h1:wW7h1TG88eUIJ2i69gaE3uNVtEPIagzhGvHgwfx2Vm4= +github.com/hashicorp/golang-lru/v2 v2.0.5/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= +github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= +github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= +github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM= +github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= +github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU= +github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= +github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= +github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= +github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI= +github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY= +github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg= +github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= +github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= +github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 h1:EaDatTxkdHG+U3Bk4EUr+DZ7fOGwTfezUiUJMaIcaho= +github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5/go.mod h1:fyalQWdtzDBECAQFBJuQe5bzQ02jGd5Qcbgb97Flm7U= +github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 h1:EfpWLLCyXw8PSM2/XNJLjI3Pb27yVE+gIAfeqp8LUCc= +github.com/redis/go-redis/extra/redisotel/v9 v9.0.5/go.mod h1:WZjPDy7VNzn77AAfnAfVjZNvfJTYfPetfZk5yoSTLaQ= +github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM= +github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/rubenv/sql-migrate v1.8.0 h1:dXnYiJk9k3wetp7GfQbKJcPHjVJL6YK19tKj8t2Ns0o= +github.com/rubenv/sql-migrate v1.8.0/go.mod h1:F2bGFBwCU+pnmbtNYDeKvSuvL6lBVtXDXUUv5t+u1qw= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= +github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= +github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= +github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +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 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +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/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= +github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/bridges/prometheus v0.57.0 h1:UW0+QyeyBVhn+COBec3nGhfnFe5lwB0ic1JBVjzhk0w= +go.opentelemetry.io/contrib/bridges/prometheus v0.57.0/go.mod h1:ppciCHRLsyCio54qbzQv0E4Jyth/fLWDTJYfvWpcSVk= +go.opentelemetry.io/contrib/exporters/autoexport v0.57.0 h1:jmTVJ86dP60C01K3slFQa2NQ/Aoi7zA+wy7vMOKD9H4= +go.opentelemetry.io/contrib/exporters/autoexport v0.57.0/go.mod h1:EJBheUMttD/lABFyLXhce47Wr6DPWYReCzaZiXadH7g= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0 h1:WzNab7hOOLzdDF/EoWCt4glhrbMPVMOO5JYTmpz36Ls= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0/go.mod h1:hKvJwTzJdp90Vh7p6q/9PAOd55dI6WA6sWj62a/JvSs= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0 h1:S+LdBGiQXtJdowoJoQPEtI52syEP/JYBUpjO49EQhV8= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0/go.mod h1:5KXybFvPGds3QinJWQT7pmXf+TN5YIa7CNYObWRkj50= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0 h1:j7ZSD+5yn+lo3sGV69nW04rRR0jhYnBwjuX3r0HvnK0= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0/go.mod h1:WXbYJTUaZXAbYd8lbgGuvih0yuCfOFC5RJoYnoLcGz8= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0 h1:t/Qur3vKSkUCcDVaSumWF2PKHt85pc7fRvFuoVT8qFU= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0/go.mod h1:Rl61tySSdcOJWoEgYZVtmnKdA0GeKrSqkHC1t+91CH8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0 h1:cMyu9O88joYEaI47CnQkxO1XZdpoTF9fEnW2duIddhw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0/go.mod h1:6Am3rn7P9TVVeXYG+wtcGE7IE1tsQ+bP3AuWcKt/gOI= +go.opentelemetry.io/otel/exporters/prometheus v0.54.0 h1:rFwzp68QMgtzu9PgP3jm9XaMICI6TsofWWPcBDKwlsU= +go.opentelemetry.io/otel/exporters/prometheus v0.54.0/go.mod h1:QyjcV9qDP6VeK5qPyKETvNjmaaEc7+gqjh4SS0ZYzDU= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.8.0 h1:CHXNXwfKWfzS65yrlB2PVds1IBZcdsX8Vepy9of0iRU= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.8.0/go.mod h1:zKU4zUgKiaRxrdovSS2amdM5gOc59slmo/zJwGX+YBg= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.32.0 h1:SZmDnHcgp3zwlPBS2JX2urGYe/jBKEIT6ZedHRUyCz8= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.32.0/go.mod h1:fdWW0HtZJ7+jNpTKUR0GpMEDP69nR8YBJQxNiVCE3jk= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0 h1:cC2yDI3IQd0Udsux7Qmq8ToKAx1XCilTQECZ0KDZyTw= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0/go.mod h1:2PD5Ex6z8CFzDbTdOlwyNIUywRr1DN0ospafJM1wJ+s= +go.opentelemetry.io/otel/log v0.8.0 h1:egZ8vV5atrUWUbnSsHn6vB8R21G2wrKqNiDt3iWertk= +go.opentelemetry.io/otel/log v0.8.0/go.mod h1:M9qvDdUTRCopJcGRKg57+JSQ9LgLBrwwfC32epk5NX8= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= +go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= +go.opentelemetry.io/otel/sdk/log v0.8.0 h1:zg7GUYXqxk1jnGF/dTdLPrK06xJdrXgqgFLnI4Crxvs= +go.opentelemetry.io/otel/sdk/log v0.8.0/go.mod h1:50iXr0UVwQrYS45KbruFrEt4LvAdCaWWgIrsN3ZQggo= +go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= +go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= +go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +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.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= +golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM= +golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +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.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= +golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3 h1:1hfbdAfFbkmpg41000wDVqr7jUpK/Yo+LPnIxxGzmkg= +google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950= +google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb h1:TLPQVbx1GJ8VKZxz52VAxl1EBgKXXbTiU9Fc5fZeLn4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I= +google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA= +google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= +google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= +google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +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/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +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= +helm.sh/helm/v3 v3.19.0 h1:krVyCGa8fa/wzTZgqw0DUiXuRT5BPdeqE/sQXujQ22k= +helm.sh/helm/v3 v3.19.0/go.mod h1:Lk/SfzN0w3a3C3o+TdAKrLwJ0wcZ//t1/SDXAvfgDdc= +k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM= +k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk= +k8s.io/apiextensions-apiserver v0.34.0 h1:B3hiB32jV7BcyKcMU5fDaDxk882YrJ1KU+ZSkA9Qxoc= +k8s.io/apiextensions-apiserver v0.34.0/go.mod h1:hLI4GxE1BDBy9adJKxUxCEHBGZtGfIg98Q+JmTD7+g0= +k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4= +k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/apiserver v0.34.0 h1:Z51fw1iGMqN7uJ1kEaynf2Aec1Y774PqU+FVWCFV3Jg= +k8s.io/apiserver v0.34.0/go.mod h1:52ti5YhxAvewmmpVRqlASvaqxt0gKJxvCeW7ZrwgazQ= +k8s.io/cli-runtime v0.34.0 h1:N2/rUlJg6TMEBgtQ3SDRJwa8XyKUizwjlOknT1mB2Cw= +k8s.io/cli-runtime v0.34.0/go.mod h1:t/skRecS73Piv+J+FmWIQA2N2/rDjdYSQzEE67LUUs8= +k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY= +k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8= +k8s.io/component-base v0.34.0 h1:bS8Ua3zlJzapklsB1dZgjEJuJEeHjj8yTu1gxE2zQX8= +k8s.io/component-base v0.34.0/go.mod h1:RSCqUdvIjjrEm81epPcjQ/DS+49fADvGSCkIP3IC6vg= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA= +k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts= +k8s.io/kubectl v0.34.0 h1:NcXz4TPTaUwhiX4LU+6r6udrlm0NsVnSkP3R9t0dmxs= +k8s.io/kubectl v0.34.0/go.mod h1:bmd0W5i+HuG7/p5sqicr0Li0rR2iIhXL0oUyLF3OjR4= +k8s.io/metrics v0.34.1 h1:374Rexmp1xxgRt64Bi0TsjAM8cA/Y8skwCoPdjtIslE= +k8s.io/metrics v0.34.1/go.mod h1:Drf5kPfk2NJrlpcNdSiAAHn/7Y9KqxpRNagByM7Ei80= +k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= +k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc= +oras.land/oras-go/v2 v2.6.0/go.mod h1:magiQDfG6H1O9APp+rOsvCPcW1GD2MM7vgnKY0Y+u1o= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/kustomize/api v0.20.1 h1:iWP1Ydh3/lmldBnH/S5RXgT98vWYMaTUL1ADcr+Sv7I= +sigs.k8s.io/kustomize/api v0.20.1/go.mod h1:t6hUFxO+Ph0VxIk1sKp1WS0dOjbPCtLJ4p8aADLwqjM= +sigs.k8s.io/kustomize/kyaml v0.20.1 h1:PCMnA2mrVbRP3NIB6v9kYCAc38uvFLVs8j/CD567A78= +sigs.k8s.io/kustomize/kyaml v0.20.1/go.mod h1:0EmkQHRUsJxY8Ug9Niig1pUMSCGHxQ5RklbpV/Ri6po= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/backend/internal/adapter/input/http/dto/artifact_dto.go b/backend/internal/adapter/input/http/dto/artifact_dto.go new file mode 100644 index 0000000..cd71cca --- /dev/null +++ b/backend/internal/adapter/input/http/dto/artifact_dto.go @@ -0,0 +1,44 @@ +package dto + +// RepositoryListResponse Repository 列表响应 +type RepositoryListResponse struct { + RegistryID string `json:"registryId"` + RegistryURL string `json:"registryUrl"` + Repositories []string `json:"repositories"` + Total int `json:"total"` + CatalogSupported bool `json:"catalogSupported"` // Whether _catalog API is supported + Source string `json:"source"` // Data source: "catalog" | "preconfigured" | "unavailable" + Message string `json:"message,omitempty"` // User-friendly message +} + +// ArtifactResponse Artifact 响应(简化版本,只包含核心字段) +type ArtifactResponse struct { + RepositoryName string `json:"repositoryName"` + Tag string `json:"tag"` + Digest string `json:"digest"` + Type string `json:"type"` // chart | image | other + Size int64 `json:"size"` + CreatedAt string `json:"createdAt"` +} + +// TagResponse Tag 响应(前端期望的扁平化结构) +type TagResponse struct { + RepositoryName string `json:"repositoryName"` // Repository name + Tag string `json:"tag"` // Tag name (e.g. "1.0.0", "latest") + Type string `json:"type"` // Artifact type: chart, image, other + MediaType string `json:"mediaType,omitempty"` + Size int64 `json:"size"` // Artifact size (bytes) +} + +// ArtifactListResponse Artifact 列表响应(包装格式,用于详细接口) +type ArtifactListResponse struct { + RepositoryName string `json:"repositoryName"` + Artifacts []*ArtifactResponse `json:"artifacts"` + Total int `json:"total"` +} + +// ValuesSchemaResponse Values Schema 响应 +type ValuesSchemaResponse struct { + Schema string `json:"schema"` +} + diff --git a/backend/internal/adapter/input/http/dto/auth_dto.go b/backend/internal/adapter/input/http/dto/auth_dto.go new file mode 100644 index 0000000..cb2c823 --- /dev/null +++ b/backend/internal/adapter/input/http/dto/auth_dto.go @@ -0,0 +1,35 @@ +package dto + +// RegisterRequest 用户注册请求 +type RegisterRequest struct { + Username string `json:"username" binding:"required"` + Password string `json:"password" binding:"required,min=6"` +} + +// LoginRequest 用户登录请求 +type LoginRequest struct { + Username string `json:"username" binding:"required"` + Password string `json:"password" binding:"required"` +} + +// RefreshTokenRequest 刷新 Token 请求 +type RefreshTokenRequest struct { + RefreshToken string `json:"refreshToken" binding:"required"` +} + +// AuthResponse 认证响应 +type AuthResponse struct { + AccessToken string `json:"accessToken"` + RefreshToken string `json:"refreshToken"` + UserID string `json:"userId"` + Username string `json:"username"` +} + +// UserResponse 用户信息响应 +type UserResponse struct { + ID string `json:"id"` + Username string `json:"username"` + Email string `json:"email"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` +} diff --git a/backend/internal/adapter/input/http/dto/cluster_dto.go b/backend/internal/adapter/input/http/dto/cluster_dto.go new file mode 100644 index 0000000..d84a816 --- /dev/null +++ b/backend/internal/adapter/input/http/dto/cluster_dto.go @@ -0,0 +1,82 @@ +package dto + +// CreateClusterRequest 创建集群请求 +type CreateClusterRequest struct { + Name string `json:"name" binding:"required"` + Host string `json:"host" binding:"required"` + CAData string `json:"caData"` + CADataAlt string `json:"ca_data"` + CertData string `json:"certData"` + CertDataAlt string `json:"cert_data"` + KeyData string `json:"keyData"` + KeyDataAlt string `json:"key_data"` + Token string `json:"token"` + Description string `json:"description"` +} + +// UpdateClusterRequest 更新集群请求 +type UpdateClusterRequest struct { + Name string `json:"name"` + Host string `json:"host"` + CAData string `json:"caData"` + CADataAlt string `json:"ca_data"` + CertData string `json:"certData"` + CertDataAlt string `json:"cert_data"` + KeyData string `json:"keyData"` + KeyDataAlt string `json:"key_data"` + Token string `json:"token"` + Description string `json:"description"` +} + +// Normalize 将多种命名风格的字段合并到统一字段 +func (r *CreateClusterRequest) Normalize() { + if r.CAData == "" { + r.CAData = r.CADataAlt + } + if r.CertData == "" { + r.CertData = r.CertDataAlt + } + if r.KeyData == "" { + r.KeyData = r.KeyDataAlt + } +} + +// Normalize 将多种命名风格的字段合并到统一字段 +func (r *UpdateClusterRequest) Normalize() { + if r.CAData == "" { + r.CAData = r.CADataAlt + } + if r.CertData == "" { + r.CertData = r.CertDataAlt + } + if r.KeyData == "" { + r.KeyData = r.KeyDataAlt + } +} + +// ClusterResponse 集群响应(敏感数据已脱敏) +type ClusterResponse struct { + ID string `json:"id"` + Name string `json:"name"` + Host string `json:"host"` + Description string `json:"description"` + // 认证配置状态(不返回实际证书数据,仅返回是否已配置) + HasCAData bool `json:"hasCaData"` + HasCertData bool `json:"hasCertData"` + HasKeyData bool `json:"hasKeyData"` + HasToken bool `json:"hasToken"` + // 脱敏数据(仅用于前端显示,实际值为掩码) + CAData string `json:"caData,omitempty"` // 脱敏显示(••••••••) + CertData string `json:"certData,omitempty"` // 脱敏显示(••••••••) + KeyData string `json:"keyData,omitempty"` // 脱敏显示(••••••••) + Token string `json:"token,omitempty"` // 脱敏显示(••••••••) + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` +} + +// ClusterHealthResponse 集群健康状态响应 +type ClusterHealthResponse struct { + Healthy bool `json:"healthy"` + Message string `json:"message,omitempty"` + Version string `json:"version,omitempty"` +} diff --git a/backend/internal/adapter/input/http/dto/converter.go b/backend/internal/adapter/input/http/dto/converter.go new file mode 100644 index 0000000..40fa9b6 --- /dev/null +++ b/backend/internal/adapter/input/http/dto/converter.go @@ -0,0 +1,63 @@ +package dto + +import ( + "github.com/ocdp/cluster-service/internal/domain/entity" + "github.com/ocdp/cluster-service/internal/pkg/crypto" +) + +// ToRegistryResponse 转换 Registry 实体为响应 DTO(脱敏) +func ToRegistryResponse(registry *entity.Registry) *RegistryResponse { + response := &RegistryResponse{ + ID: registry.ID, + Name: registry.Name, + URL: registry.URL, + Description: registry.Description, + Username: registry.Username, + Insecure: registry.Insecure, + CreatedAt: registry.CreatedAt.Format("2006-01-02T15:04:05Z07:00"), + UpdatedAt: registry.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"), + } + + // 脱敏处理密码 + if registry.Password != "" { + response.HasPassword = true + response.Password = crypto.MaskSensitiveData(registry.Password) + } + + return response +} + +// ToClusterResponse 转换 Cluster 实体为响应 DTO(脱敏) +func ToClusterResponse(cluster *entity.Cluster) *ClusterResponse { + response := &ClusterResponse{ + ID: cluster.ID, + Name: cluster.Name, + Host: cluster.Host, + Description: cluster.Description, + CreatedAt: cluster.CreatedAt.Format("2006-01-02T15:04:05Z07:00"), + UpdatedAt: cluster.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"), + } + + // 设置认证配置状态标志 + response.HasCAData = cluster.CAData != "" + response.HasCertData = cluster.CertData != "" + response.HasKeyData = cluster.KeyData != "" + response.HasToken = cluster.Token != "" + + // 脱敏处理敏感数据(仅显示掩码) + if cluster.CAData != "" { + response.CAData = crypto.MaskSensitiveData(cluster.CAData) + } + if cluster.CertData != "" { + response.CertData = crypto.MaskSensitiveData(cluster.CertData) + } + if cluster.KeyData != "" { + response.KeyData = crypto.MaskSensitiveData(cluster.KeyData) + } + if cluster.Token != "" { + response.Token = crypto.MaskSensitiveData(cluster.Token) + } + + return response +} + diff --git a/backend/internal/adapter/input/http/dto/error_dto.go b/backend/internal/adapter/input/http/dto/error_dto.go new file mode 100644 index 0000000..6b33da6 --- /dev/null +++ b/backend/internal/adapter/input/http/dto/error_dto.go @@ -0,0 +1,15 @@ +package dto + +// ErrorResponse 错误响应 +type ErrorResponse struct { + Error string `json:"error"` + Message string `json:"message,omitempty"` + Code int `json:"code,omitempty"` +} + +// SuccessResponse 成功响应 +type SuccessResponse struct { + Message string `json:"message"` + Data interface{} `json:"data,omitempty"` +} + diff --git a/backend/internal/adapter/input/http/dto/instance_dto.go b/backend/internal/adapter/input/http/dto/instance_dto.go new file mode 100644 index 0000000..76aca25 --- /dev/null +++ b/backend/internal/adapter/input/http/dto/instance_dto.go @@ -0,0 +1,133 @@ +package dto + +// CreateInstanceRequest 创建实例请求 +type CreateInstanceRequest struct { + Name string `json:"name" binding:"required"` + Namespace string `json:"namespace" binding:"required"` + RegistryID string `json:"registryId" binding:"required"` + RegistryIDAlt string `json:"registry_id"` + Repository string `json:"repository" binding:"required"` + Tag string `json:"tag" binding:"required"` + Description string `json:"description"` + Values map[string]interface{} `json:"values"` + ValuesYAML string `json:"valuesYaml"` +} + +// UpdateInstanceRequest 更新实例请求 +type UpdateInstanceRequest struct { + Version string `json:"version"` + Description string `json:"description"` + Values map[string]interface{} `json:"values"` + ValuesYAML string `json:"valuesYaml"` +} + +// Normalize 将多种命名风格的字段合并到统一字段 +func (r *CreateInstanceRequest) Normalize() { + if r.RegistryID == "" { + r.RegistryID = r.RegistryIDAlt + } +} + +// RollbackInstanceRequest 回滚实例请求 +type RollbackInstanceRequest struct { + Revision int `json:"revision" binding:"required"` + Wait bool `json:"wait"` + Timeout int `json:"timeout"` // seconds +} + +// DeleteInstanceRequest 删除实例请求 +type DeleteInstanceRequest struct { + KeepHistory bool `json:"keepHistory"` + Timeout int `json:"timeout"` // seconds +} + +// InstanceResponse 实例响应 +type InstanceResponse struct { + ID string `json:"id"` + ClusterID string `json:"clusterId"` + Name string `json:"name"` + Namespace string `json:"namespace"` + RegistryID string `json:"registryId"` + Repository string `json:"repository"` + Chart string `json:"chart"` + Version string `json:"version"` + Description string `json:"description"` + Status string `json:"status"` + StatusReason string `json:"statusReason,omitempty"` + LastOperation string `json:"lastOperation,omitempty"` + LastError string `json:"lastError,omitempty"` + Revision int `json:"revision"` + Values map[string]interface{} `json:"values,omitempty"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` +} + +// InstanceStatusResponse 实例状态响应 +type InstanceStatusResponse struct { + ID string `json:"id"` + Name string `json:"name"` + Namespace string `json:"namespace"` + Status string `json:"status"` + Revision int `json:"revision"` + Chart string `json:"chart"` + Version string `json:"version"` + UpdatedAt string `json:"updatedAt"` +} + +// ReleaseHistoryResponse Release 历史响应 +type ReleaseHistoryResponse struct { + Revision int `json:"revision"` + Updated string `json:"updated"` + Status string `json:"status"` + Chart string `json:"chart"` + AppVersion string `json:"appVersion"` + Description string `json:"description"` +} + +// InstanceListResponse 实例列表响应 +type InstanceListResponse struct { + Instances []*InstanceResponse `json:"instances"` + Total int `json:"total"` +} + +// InstanceEntryPortResponse Service 端口响应 +type InstanceEntryPortResponse struct { + Name string `json:"name,omitempty"` + Protocol string `json:"protocol"` + Port int32 `json:"port"` + TargetPort string `json:"targetPort,omitempty"` + NodePort int32 `json:"nodePort,omitempty"` +} + +// InstanceEntryPathResponse Ingress path 响应 +type InstanceEntryPathResponse struct { + Path string `json:"path"` + ServiceName string `json:"serviceName,omitempty"` + ServicePort string `json:"servicePort,omitempty"` +} + +// InstanceEntryHostResponse Ingress host 响应 +type InstanceEntryHostResponse struct { + Host string `json:"host"` + Paths []InstanceEntryPathResponse `json:"paths,omitempty"` +} + +// InstanceEntryTLSResponse Ingress TLS 响应 +type InstanceEntryTLSResponse struct { + Hosts []string `json:"hosts,omitempty"` + SecretName string `json:"secretName,omitempty"` +} + +// InstanceEntryResponse 实例入口响应 +type InstanceEntryResponse struct { + Kind string `json:"kind"` + Name string `json:"name"` + Namespace string `json:"namespace"` + Type string `json:"type,omitempty"` + ClusterIP string `json:"clusterIP,omitempty"` + ExternalIPs []string `json:"externalIPs,omitempty"` + LoadBalancerIngress []string `json:"loadBalancerIngress,omitempty"` + Ports []InstanceEntryPortResponse `json:"ports,omitempty"` + Hosts []InstanceEntryHostResponse `json:"hosts,omitempty"` + TLS []InstanceEntryTLSResponse `json:"tls,omitempty"` +} diff --git a/backend/internal/adapter/input/http/dto/monitoring_dto.go b/backend/internal/adapter/input/http/dto/monitoring_dto.go new file mode 100644 index 0000000..bcb3496 --- /dev/null +++ b/backend/internal/adapter/input/http/dto/monitoring_dto.go @@ -0,0 +1,143 @@ +package dto + +import ( + "time" + + "github.com/ocdp/cluster-service/internal/domain/entity" +) + +// ClusterMetricsResponse 集群监控响应 +type ClusterMetricsResponse struct { + ClusterID string `json:"clusterId"` + ClusterName string `json:"clusterName"` + Status string `json:"status"` + Uptime string `json:"uptime"` + NodeCount int `json:"nodeCount"` + PodCount int `json:"podCount"` + LastCheck time.Time `json:"lastCheck"` + TotalCPU string `json:"totalCpu"` + TotalMemory string `json:"totalMemory"` + TotalGPU int `json:"totalGpu"` + UsedCPU string `json:"usedCpu"` + UsedMemory string `json:"usedMemory"` + UsedGPU int `json:"usedGpu"` + CPUUsage float64 `json:"cpuUsage"` + MemoryUsage float64 `json:"memoryUsage"` + GPUUsage float64 `json:"gpuUsage"` + MaxNodeCPU string `json:"maxNodeCpu"` + MaxNodeMemory string `json:"maxNodeMemory"` + MaxNodeGPU int `json:"maxNodeGpu"` + MaxNodeCPUUsage float64 `json:"maxNodeCpuUsage"` + MaxNodeMemUsage float64 `json:"maxNodeMemUsage"` + MaxNodeGPUUsage float64 `json:"maxNodeGpuUsage"` + Nodes []NodeMetricsResponse `json:"nodes,omitempty"` +} + +// NodeMetricsResponse 节点监控响应 +type NodeMetricsResponse struct { + NodeName string `json:"nodeName"` + Status string `json:"status"` + Role string `json:"role"` + Age string `json:"age"` + PodCount int `json:"podCount"` + CPUCapacity string `json:"cpuCapacity"` + CPUAllocatable string `json:"cpuAllocatable"` + CPUUsage string `json:"cpuUsage"` + CPUPercent float64 `json:"cpuPercent"` + MemoryCapacity string `json:"memoryCapacity"` + MemoryAllocatable string `json:"memoryAllocatable"` + MemoryUsage string `json:"memoryUsage"` + MemoryPercent float64 `json:"memoryPercent"` + GPUCapacity int `json:"gpuCapacity"` + GPUUsage int `json:"gpuUsage"` + GPUPercent float64 `json:"gpuPercent"` + GPUType string `json:"gpuType,omitempty"` + OSImage string `json:"osImage,omitempty"` + KernelVersion string `json:"kernelVersion,omitempty"` + ContainerRuntime string `json:"containerRuntime,omitempty"` + KubeletVersion string `json:"kubeletVersion,omitempty"` +} + +// MonitoringSummaryResponse 监控汇总响应 +type MonitoringSummaryResponse struct { + TotalClusters int `json:"totalClusters"` + HealthyClusters int `json:"healthyClusters"` + WarningClusters int `json:"warningClusters"` + ErrorClusters int `json:"errorClusters"` + TotalNodes int `json:"totalNodes"` + TotalPods int `json:"totalPods"` + LastUpdate time.Time `json:"lastUpdate"` +} + +// ToClusterMetricsResponse 转换为响应 +func ToClusterMetricsResponse(m *entity.ClusterMetrics) *ClusterMetricsResponse { + resp := &ClusterMetricsResponse{ + ClusterID: m.ClusterID, + ClusterName: m.ClusterName, + Status: m.Status, + Uptime: m.Uptime, + NodeCount: m.NodeCount, + PodCount: m.PodCount, + LastCheck: m.LastCheck, + TotalCPU: m.TotalCPU, + TotalMemory: m.TotalMemory, + TotalGPU: m.TotalGPU, + UsedCPU: m.UsedCPU, + UsedMemory: m.UsedMemory, + UsedGPU: m.UsedGPU, + CPUUsage: m.CPUUsage, + MemoryUsage: m.MemoryUsage, + GPUUsage: m.GPUUsage, + MaxNodeCPU: m.MaxNodeCPU, + MaxNodeMemory: m.MaxNodeMemory, + MaxNodeGPU: m.MaxNodeGPU, + MaxNodeCPUUsage: m.MaxNodeCPUUsage, + MaxNodeMemUsage: m.MaxNodeMemUsage, + MaxNodeGPUUsage: m.MaxNodeGPUUsage, + } + + if len(m.Nodes) > 0 { + resp.Nodes = make([]NodeMetricsResponse, len(m.Nodes)) + for i, node := range m.Nodes { + resp.Nodes[i] = NodeMetricsResponse{ + NodeName: node.NodeName, + Status: node.Status, + Role: node.Role, + Age: node.Age, + PodCount: node.PodCount, + CPUCapacity: node.CPUCapacity, + CPUAllocatable: node.CPUAllocatable, + CPUUsage: node.CPUUsage, + CPUPercent: node.CPUPercent, + MemoryCapacity: node.MemoryCapacity, + MemoryAllocatable: node.MemoryAllocatable, + MemoryUsage: node.MemoryUsage, + MemoryPercent: node.MemoryPercent, + GPUCapacity: node.GPUCapacity, + GPUUsage: node.GPUUsage, + GPUPercent: node.GPUPercent, + GPUType: node.GPUType, + OSImage: node.OSImage, + KernelVersion: node.KernelVersion, + ContainerRuntime: node.ContainerRuntime, + KubeletVersion: node.KubeletVersion, + } + } + } + + return resp +} + +// ToMonitoringSummaryResponse 转换为汇总响应 +func ToMonitoringSummaryResponse(s *entity.MonitoringSummary) *MonitoringSummaryResponse { + return &MonitoringSummaryResponse{ + TotalClusters: s.TotalClusters, + HealthyClusters: s.HealthyClusters, + WarningClusters: s.WarningClusters, + ErrorClusters: s.ErrorClusters, + TotalNodes: s.TotalNodes, + TotalPods: s.TotalPods, + LastUpdate: s.LastUpdate, + } +} + diff --git a/backend/internal/adapter/input/http/dto/registry_dto.go b/backend/internal/adapter/input/http/dto/registry_dto.go new file mode 100644 index 0000000..25de40a --- /dev/null +++ b/backend/internal/adapter/input/http/dto/registry_dto.go @@ -0,0 +1,42 @@ +package dto + +// CreateRegistryRequest 创建 Registry 请求 +type CreateRegistryRequest struct { + Name string `json:"name" binding:"required"` + URL string `json:"url" binding:"required"` + Username string `json:"username"` + Password string `json:"password"` + Description string `json:"description"` + Insecure bool `json:"insecure"` +} + +// UpdateRegistryRequest 更新 Registry 请求 +type UpdateRegistryRequest struct { + Name string `json:"name"` + URL string `json:"url"` + Username string `json:"username"` + Password string `json:"password"` + Description string `json:"description"` + Insecure bool `json:"insecure"` +} + +// RegistryResponse Registry 响应(敏感数据已脱敏) +type RegistryResponse struct { + ID string `json:"id"` + Name string `json:"name"` + URL string `json:"url"` + Description string `json:"description"` + Username string `json:"username,omitempty"` // 明文返回用户名(不敏感) + Password string `json:"password,omitempty"` // 脱敏显示(••••••••) + HasPassword bool `json:"hasPassword"` // 是否已设置密码 + Insecure bool `json:"insecure"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` +} + +// RegistryHealthResponse Registry 健康状态响应 +type RegistryHealthResponse struct { + Healthy bool `json:"healthy"` + Message string `json:"message,omitempty"` +} + diff --git a/backend/internal/adapter/input/http/rest/artifact_handler.go b/backend/internal/adapter/input/http/rest/artifact_handler.go new file mode 100644 index 0000000..d04bdbc --- /dev/null +++ b/backend/internal/adapter/input/http/rest/artifact_handler.go @@ -0,0 +1,193 @@ +package rest + +import ( + "errors" + "net/http" + + "github.com/gorilla/mux" + "github.com/ocdp/cluster-service/internal/adapter/input/http/dto" + "github.com/ocdp/cluster-service/internal/domain/entity" + "github.com/ocdp/cluster-service/internal/domain/service" +) + +// ArtifactHandler Artifact Handler +type ArtifactHandler struct { + artifactService *service.ArtifactService +} + +// NewArtifactHandler 创建 Artifact Handler +func NewArtifactHandler(artifactService *service.ArtifactService) *ArtifactHandler { + return &ArtifactHandler{ + artifactService: artifactService, + } +} + +// ListRepositories 列出 Registry 中的所有 repositories +// @Summary 列出 Registry 中的所有 Repositories +// @Description 列出指定 Registry 中的所有 Repository +// @Tags Artifacts +// @Accept json +// @Produce json +// @Param registry_id path string true "Registry ID" +// @Success 200 {object} dto.RepositoryListResponse +// @Failure 500 {object} dto.ErrorResponse +// @Router /registries/{registry_id}/repositories [get] +func (h *ArtifactHandler) ListRepositories(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + registryID := vars["registry_id"] + + repositories, err := h.artifactService.ListRepositories(r.Context(), registryID) + if err != nil { + respondError(w, http.StatusInternalServerError, "Failed to list repositories", err.Error()) + return + } + + // Get registry info for URL + registry, err := h.artifactService.GetRegistry(r.Context(), registryID) + registryURL := "" + if err == nil && registry != nil { + registryURL = registry.URL + } + + // Determine source and message based on repository count + source := "catalog" + catalogSupported := true + message := "" + + if len(repositories) == 0 { + source = "unavailable" + message = "No repositories found in this registry" + } + + response := &dto.RepositoryListResponse{ + RegistryID: registryID, + RegistryURL: registryURL, + Repositories: repositories, + Total: len(repositories), + CatalogSupported: catalogSupported, + Source: source, + Message: message, + } + + respondJSON(w, http.StatusOK, response) +} + +// ListArtifacts 列出 repository 中的所有 artifacts(返回扁平化的 Tag 数组) +// @Summary 列出 Repository 中的所有 Artifacts +// @Description 列出指定 Repository 中的所有 Artifact,支持按类型过滤 +// @Tags Artifacts +// @Accept json +// @Produce json +// @Param registry_id path string true "Registry ID" +// @Param repository_name path string true "Repository Name (URL encoded, e.g. charts%2Fnginx)" +// @Param media_type query string false "过滤 Artifact 类型 (all, chart, image, other)" default(all) +// @Success 200 {array} dto.TagResponse +// @Failure 500 {object} dto.ErrorResponse +// @Router /registries/{registry_id}/repositories/{repository_name}/artifacts [get] +func (h *ArtifactHandler) ListArtifacts(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + registryID := vars["registry_id"] + repositoryName := vars["repository_name"] + + // 获取 mediaType 过滤参数(默认为 "all") + mediaTypeFilter := r.URL.Query().Get("media_type") + if mediaTypeFilter == "" { + mediaTypeFilter = "all" + } + + artifacts, err := h.artifactService.ListArtifacts(r.Context(), registryID, repositoryName, mediaTypeFilter) + if err != nil { + respondError(w, http.StatusInternalServerError, "Failed to list artifacts", err.Error()) + return + } + + // 转换为前端期望的扁平化 Tag 数组 + tagResponses := make([]*dto.TagResponse, 0, len(artifacts)) + for _, artifact := range artifacts { + tagResponses = append(tagResponses, &dto.TagResponse{ + RepositoryName: artifact.Repository, + Tag: artifact.Tag, + Type: string(artifact.Type), + MediaType: artifact.MediaType, + Size: artifact.Size, + }) + } + + // 直接返回数组,不包装 + respondJSON(w, http.StatusOK, tagResponses) +} + +// GetArtifact 获取 artifact 详情 +// @Summary 获取 Artifact 详情 +// @Description 获取指定 Artifact 的详细信息 +// @Tags Artifacts +// @Accept json +// @Produce json +// @Param registry_id path string true "Registry ID" +// @Param repository_name path string true "Repository Name (URL encoded)" +// @Param reference path string true "Artifact Reference (tag or digest)" +// @Success 200 {object} dto.ArtifactResponse +// @Failure 404 {object} dto.ErrorResponse +// @Router /registries/{registry_id}/repositories/{repository_name}/artifacts/{reference} [get] +func (h *ArtifactHandler) GetArtifact(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + registryID := vars["registry_id"] + repositoryName := vars["repository_name"] + reference := vars["reference"] + + artifact, err := h.artifactService.GetArtifact(r.Context(), registryID, repositoryName, reference) + if err != nil { + respondError(w, http.StatusNotFound, "Artifact not found", err.Error()) + return + } + + response := &dto.ArtifactResponse{ + RepositoryName: artifact.Repository, + Tag: artifact.Tag, + Digest: artifact.Digest, + Type: string(artifact.Type), + Size: artifact.Size, + CreatedAt: artifact.CreatedAt.Format("2006-01-02T15:04:05Z07:00"), + } + + respondJSON(w, http.StatusOK, response) +} + +// GetArtifactValuesSchema 获取 Helm Chart 的 values schema +// @Summary 获取 Helm Chart Values Schema +// @Description 获取 Helm Chart 的 values.schema.json (仅支持 Chart 类型) +// @Tags Artifacts +// @Accept json +// @Produce json +// @Param registry_id path string true "Registry ID" +// @Param repository_name path string true "Repository Name (URL encoded)" +// @Param reference path string true "Artifact Reference (tag or digest)" +// @Success 200 {object} dto.ValuesSchemaResponse +// @Failure 500 {object} dto.ErrorResponse +// @Router /registries/{registry_id}/repositories/{repository_name}/artifacts/{reference}/values-schema [get] +func (h *ArtifactHandler) GetArtifactValuesSchema(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + registryID := vars["registry_id"] + repositoryName := vars["repository_name"] + reference := vars["reference"] + + schema, err := h.artifactService.GetValuesSchema(r.Context(), registryID, repositoryName, reference) + if err != nil { + switch { + case errors.Is(err, entity.ErrRegistryNotFound), + errors.Is(err, entity.ErrRepositoryNotFound), + errors.Is(err, entity.ErrArtifactNotFound), + errors.Is(err, entity.ErrValuesSchemaNotFound): + respondError(w, http.StatusNotFound, "Values schema not found", err.Error()) + default: + respondError(w, http.StatusInternalServerError, "Failed to get values schema", err.Error()) + } + return + } + + response := &dto.ValuesSchemaResponse{ + Schema: schema, + } + + respondJSON(w, http.StatusOK, response) +} diff --git a/backend/internal/adapter/input/http/rest/auth_handler.go b/backend/internal/adapter/input/http/rest/auth_handler.go new file mode 100644 index 0000000..f67acda --- /dev/null +++ b/backend/internal/adapter/input/http/rest/auth_handler.go @@ -0,0 +1,127 @@ +package rest + +import ( + "encoding/json" + "net/http" + + "github.com/ocdp/cluster-service/internal/adapter/input/http/dto" + "github.com/ocdp/cluster-service/internal/domain/service" +) + +// AuthHandler 认证 Handler +type AuthHandler struct { + authService *service.AuthService +} + +// NewAuthHandler 创建认证 Handler +func NewAuthHandler(authService *service.AuthService) *AuthHandler { + return &AuthHandler{ + authService: authService, + } +} + +// Register 用户注册 +// @Summary 用户注册 +// @Description 创建一个新的后台用户 +// @Tags Auth +// @Accept json +// @Produce json +// @Param request body dto.RegisterRequest true "注册信息" +// @Success 201 {object} dto.UserResponse +// @Failure 400 {object} dto.ErrorResponse +// @Router /auth/register [post] +func (h *AuthHandler) Register(w http.ResponseWriter, r *http.Request) { + var req dto.RegisterRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + respondError(w, http.StatusBadRequest, "Invalid request body", err.Error()) + return + } + + // 调用领域服务 + user, err := h.authService.Register(r.Context(), req.Username, req.Password) + if err != nil { + respondError(w, http.StatusBadRequest, "Registration failed", err.Error()) + return + } + + // 返回响应 + response := &dto.UserResponse{ + ID: user.ID, + Username: user.Username, + Email: user.Email, + CreatedAt: user.CreatedAt.Format("2006-01-02T15:04:05Z07:00"), + UpdatedAt: user.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"), + } + + respondJSON(w, http.StatusCreated, response) +} + +// Login 用户登录 +// @Summary 用户登录 +// @Description 使用用户名和密码获取访问令牌 +// @Tags Auth +// @Accept json +// @Produce json +// @Param request body dto.LoginRequest true "登录信息" +// @Success 200 {object} dto.AuthResponse +// @Failure 401 {object} dto.ErrorResponse +// @Router /auth/login [post] +func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) { + var req dto.LoginRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + respondError(w, http.StatusBadRequest, "Invalid request body", err.Error()) + return + } + + // 调用领域服务 + accessToken, refreshToken, err := h.authService.Login(r.Context(), req.Username, req.Password) + if err != nil { + respondError(w, http.StatusUnauthorized, "Login failed", err.Error()) + return + } + + // 获取用户信息 + // TODO: 从 token 解析用户信息或从服务获取 + + // 返回响应 + response := &dto.AuthResponse{ + AccessToken: accessToken, + RefreshToken: refreshToken, + Username: req.Username, + } + + respondJSON(w, http.StatusOK, response) +} + +// RefreshToken 刷新 Token +// @Summary 刷新访问令牌 +// @Description 使用刷新令牌获取新的访问令牌 +// @Tags Auth +// @Accept json +// @Produce json +// @Param request body dto.RefreshTokenRequest true "刷新令牌" +// @Success 200 {object} dto.AuthResponse +// @Failure 401 {object} dto.ErrorResponse +// @Router /auth/refresh [post] +func (h *AuthHandler) RefreshToken(w http.ResponseWriter, r *http.Request) { + var req dto.RefreshTokenRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + respondError(w, http.StatusBadRequest, "Invalid request body", err.Error()) + return + } + + // 调用领域服务 + newAccessToken, err := h.authService.RefreshToken(r.Context(), req.RefreshToken) + if err != nil { + respondError(w, http.StatusUnauthorized, "Token refresh failed", err.Error()) + return + } + + // 返回响应 + response := &dto.AuthResponse{ + AccessToken: newAccessToken, + RefreshToken: req.RefreshToken, + } + + respondJSON(w, http.StatusOK, response) +} diff --git a/backend/internal/adapter/input/http/rest/cluster_handler.go b/backend/internal/adapter/input/http/rest/cluster_handler.go new file mode 100644 index 0000000..c887f8e --- /dev/null +++ b/backend/internal/adapter/input/http/rest/cluster_handler.go @@ -0,0 +1,221 @@ +package rest + +import ( + "encoding/json" + "net/http" + "os" + + "github.com/gorilla/mux" + "github.com/ocdp/cluster-service/internal/adapter/input/http/dto" + "github.com/ocdp/cluster-service/internal/domain/entity" + "github.com/ocdp/cluster-service/internal/domain/service" +) + +// ClusterHandler 集群 Handler +type ClusterHandler struct { + clusterService *service.ClusterService +} + +// NewClusterHandler 创建集群 Handler +func NewClusterHandler(clusterService *service.ClusterService) *ClusterHandler { + return &ClusterHandler{ + clusterService: clusterService, + } +} + +// CreateCluster 创建集群 +// @Summary 创建集群 +// @Description 创建一个新的 Kubernetes 集群配置 +// @Tags Clusters +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param request body dto.CreateClusterRequest true "集群信息" +// @Success 201 {object} dto.ClusterResponse +// @Failure 400 {object} dto.ErrorResponse +// @Router /clusters [post] +func (h *ClusterHandler) CreateCluster(w http.ResponseWriter, r *http.Request) { + var req dto.CreateClusterRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + respondError(w, http.StatusBadRequest, "Invalid request body", err.Error()) + return + } + req.Normalize() + + // 创建实体 + cluster := entity.NewCluster(req.Name, req.Host) + cluster.Description = req.Description + + if req.CertData != "" && req.KeyData != "" { + cluster.SetCertAuth(req.CAData, req.CertData, req.KeyData) + } else if req.Token != "" { + cluster.SetTokenAuth(req.Token) + } else if os.Getenv("ADAPTER_MODE") == "mock" { + // Mock 模式:如果没有提供认证信息,使用默认的 Mock 证书 + cluster.SetCertAuth( + "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1vY2sgQ0EgQ2VydGlmaWNhdGUKLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQ==", + "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1vY2sgQ2xpZW50IENlcnRpZmljYXRlCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0=", + "LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNb2NrIFByaXZhdGUgS2V5Ci0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0t", + ) + } + + // 调用领域服务 + if err := h.clusterService.CreateCluster(r.Context(), cluster); err != nil { + respondError(w, http.StatusBadRequest, "Failed to create cluster", err.Error()) + return + } + + // 返回响应 + response := h.toClusterResponse(cluster) + respondJSON(w, http.StatusCreated, response) +} + +// GetCluster 获取集群详情 +// @Summary 获取集群详情 +// @Tags Clusters +// @Produce json +// @Security BearerAuth +// @Param cluster_id path string true "集群 ID" +// @Success 200 {object} dto.ClusterResponse +// @Failure 404 {object} dto.ErrorResponse +// @Router /clusters/{cluster_id} [get] +func (h *ClusterHandler) GetCluster(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + clusterID := vars["cluster_id"] + + cluster, err := h.clusterService.GetCluster(r.Context(), clusterID) + if err != nil { + respondError(w, http.StatusNotFound, "Cluster not found", err.Error()) + return + } + + response := h.toClusterResponse(cluster) + respondJSON(w, http.StatusOK, response) +} + +// GetAllClusters 获取所有集群 +// @Summary 列出所有集群 +// @Tags Clusters +// @Produce json +// @Security BearerAuth +// @Success 200 {array} dto.ClusterResponse +// @Failure 500 {object} dto.ErrorResponse +// @Router /clusters [get] +func (h *ClusterHandler) GetAllClusters(w http.ResponseWriter, r *http.Request) { + clusters, err := h.clusterService.ListClusters(r.Context()) + if err != nil { + respondError(w, http.StatusInternalServerError, "Failed to list clusters", err.Error()) + return + } + + responses := make([]*dto.ClusterResponse, 0, len(clusters)) + for _, cluster := range clusters { + responses = append(responses, h.toClusterResponse(cluster)) + } + + respondJSON(w, http.StatusOK, responses) +} + +// UpdateCluster 更新集群 +// @Summary 更新集群 +// @Tags Clusters +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param cluster_id path string true "集群 ID" +// @Param request body dto.UpdateClusterRequest true "更新内容" +// @Success 200 {object} dto.ClusterResponse +// @Failure 404 {object} dto.ErrorResponse +// @Router /clusters/{cluster_id} [put] +func (h *ClusterHandler) UpdateCluster(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + clusterID := vars["cluster_id"] + + var req dto.UpdateClusterRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + respondError(w, http.StatusBadRequest, "Invalid request body", err.Error()) + return + } + req.Normalize() + + // 获取现有集群 + cluster, err := h.clusterService.GetCluster(r.Context(), clusterID) + if err != nil { + respondError(w, http.StatusNotFound, "Cluster not found", err.Error()) + return + } + + // 更新字段 + cluster.Update(req.Name, req.Host, req.Description) + + if req.CertData != "" && req.KeyData != "" { + cluster.SetCertAuth(req.CAData, req.CertData, req.KeyData) + } else if req.Token != "" { + cluster.SetTokenAuth(req.Token) + } + + // 调用领域服务 + if err := h.clusterService.UpdateCluster(r.Context(), cluster); err != nil { + respondError(w, http.StatusBadRequest, "Failed to update cluster", err.Error()) + return + } + + response := h.toClusterResponse(cluster) + respondJSON(w, http.StatusOK, response) +} + +// DeleteCluster 删除集群 +// @Summary 删除集群 +// @Tags Clusters +// @Produce json +// @Security BearerAuth +// @Param cluster_id path string true "集群 ID" +// @Success 204 {string} string "No Content" +// @Failure 404 {object} dto.ErrorResponse +// @Router /clusters/{cluster_id} [delete] +func (h *ClusterHandler) DeleteCluster(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + clusterID := vars["cluster_id"] + + if err := h.clusterService.DeleteCluster(r.Context(), clusterID); err != nil { + respondError(w, http.StatusNotFound, "Failed to delete cluster", err.Error()) + return + } + + w.WriteHeader(http.StatusNoContent) +} + +// GetClusterHealth 获取集群健康状态 +// @Summary 获取集群健康状态 +// @Tags Clusters +// @Produce json +// @Security BearerAuth +// @Param cluster_id path string true "集群 ID" +// @Success 200 {object} dto.ClusterHealthResponse +// @Failure 404 {object} dto.ErrorResponse +// @Router /clusters/{cluster_id}/health [get] +func (h *ClusterHandler) GetClusterHealth(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + clusterID := vars["cluster_id"] + + // 检查集群是否存在 + _, err := h.clusterService.GetCluster(r.Context(), clusterID) + if err != nil { + respondError(w, http.StatusNotFound, "Cluster not found", err.Error()) + return + } + + // TODO: 实现真实的健康检查 + response := &dto.ClusterHealthResponse{ + Healthy: true, + Message: "Cluster is healthy", + Version: "v1.28.0", + } + + respondJSON(w, http.StatusOK, response) +} + +// toClusterResponse 将 Cluster 实体转换为响应 DTO(脱敏) +func (h *ClusterHandler) toClusterResponse(cluster *entity.Cluster) *dto.ClusterResponse { + return dto.ToClusterResponse(cluster) +} diff --git a/backend/internal/adapter/input/http/rest/instance_handler.go b/backend/internal/adapter/input/http/rest/instance_handler.go new file mode 100644 index 0000000..777e965 --- /dev/null +++ b/backend/internal/adapter/input/http/rest/instance_handler.go @@ -0,0 +1,371 @@ +package rest + +import ( + "encoding/json" + "net/http" + "strings" + + "github.com/gorilla/mux" + "github.com/ocdp/cluster-service/internal/adapter/input/http/dto" + "github.com/ocdp/cluster-service/internal/domain/entity" + "github.com/ocdp/cluster-service/internal/domain/service" +) + +// InstanceHandler 实例 Handler +type InstanceHandler struct { + instanceService *service.InstanceService +} + +// NewInstanceHandler 创建实例 Handler +func NewInstanceHandler(instanceService *service.InstanceService) *InstanceHandler { + return &InstanceHandler{ + instanceService: instanceService, + } +} + +// CreateInstance 创建实例 +// @Summary 创建实例 +// @Description 在指定集群上部署一个 artifact +// @Tags Instances +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param cluster_id path string true "集群 ID" +// @Param request body dto.CreateInstanceRequest true "实例配置" +// @Success 201 {object} dto.InstanceResponse +// @Failure 400 {object} dto.ErrorResponse +// @Router /clusters/{cluster_id}/instances [post] +func (h *InstanceHandler) CreateInstance(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + clusterID := vars["cluster_id"] + + var req dto.CreateInstanceRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + respondError(w, http.StatusBadRequest, "Invalid request body", err.Error()) + return + } + req.Normalize() + + // Extract chart name from repository (e.g., "charts/nginx" -> "nginx") + chart := req.Repository + if lastSlash := strings.LastIndex(req.Repository, "/"); lastSlash != -1 { + chart = req.Repository[lastSlash+1:] + } + + // 创建实体 + instance := entity.NewInstance( + clusterID, + req.Name, + req.Namespace, + req.RegistryID, + req.Repository, + chart, // Extracted chart name + req.Tag, // Tag mapped to version + ) + instance.Description = req.Description + + if req.Values != nil { + instance.SetValues(req.Values) + } + if req.ValuesYAML != "" { + instance.SetValuesYAML(req.ValuesYAML) + } + + // 调用领域服务 + if err := h.instanceService.CreateInstance(r.Context(), instance); err != nil { + respondError(w, http.StatusBadRequest, "Failed to create instance", err.Error()) + return + } + + // 返回响应 + response := &dto.InstanceResponse{ + ID: instance.ID, + ClusterID: instance.ClusterID, + Name: instance.Name, + Namespace: instance.Namespace, + RegistryID: instance.RegistryID, + Repository: instance.Repository, + Chart: instance.Chart, + Version: instance.Version, + Description: instance.Description, + Status: string(instance.Status), + StatusReason: instance.StatusReason, + LastOperation: string(instance.LastOperation), + LastError: instance.LastError, + Revision: instance.Revision, + Values: instance.Values, + CreatedAt: instance.CreatedAt.Format("2006-01-02T15:04:05Z07:00"), + UpdatedAt: instance.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"), + } + + respondJSON(w, http.StatusCreated, response) +} + +// GetInstance 获取实例详情 +// @Summary 获取实例详情 +// @Tags Instances +// @Produce json +// @Security BearerAuth +// @Param cluster_id path string true "集群 ID" +// @Param instance_id path string true "实例 ID" +// @Success 200 {object} dto.InstanceResponse +// @Failure 404 {object} dto.ErrorResponse +// @Router /clusters/{cluster_id}/instances/{instance_id} [get] +func (h *InstanceHandler) GetInstance(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + instanceID := vars["instance_id"] + + instance, err := h.instanceService.GetInstance(r.Context(), instanceID) + if err != nil { + respondError(w, http.StatusNotFound, "Instance not found", err.Error()) + return + } + + response := &dto.InstanceResponse{ + ID: instance.ID, + ClusterID: instance.ClusterID, + Name: instance.Name, + Namespace: instance.Namespace, + RegistryID: instance.RegistryID, + Repository: instance.Repository, + Chart: instance.Chart, + Version: instance.Version, + Description: instance.Description, + Status: string(instance.Status), + StatusReason: instance.StatusReason, + LastOperation: string(instance.LastOperation), + LastError: instance.LastError, + Revision: instance.Revision, + Values: instance.Values, + CreatedAt: instance.CreatedAt.Format("2006-01-02T15:04:05Z07:00"), + UpdatedAt: instance.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"), + } + + respondJSON(w, http.StatusOK, response) +} + +// ListInstances 列出集群的所有实例 +// @Summary 列出实例 +// @Tags Instances +// @Produce json +// @Security BearerAuth +// @Param cluster_id path string true "集群 ID" +// @Success 200 {object} dto.InstanceListResponse +// @Failure 500 {object} dto.ErrorResponse +// @Router /clusters/{cluster_id}/instances [get] +func (h *InstanceHandler) ListInstances(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + clusterID := vars["cluster_id"] + + instances, err := h.instanceService.ListInstancesByCluster(r.Context(), clusterID) + if err != nil { + respondError(w, http.StatusInternalServerError, "Failed to list instances", err.Error()) + return + } + + responses := make([]*dto.InstanceResponse, 0, len(instances)) + for _, instance := range instances { + responses = append(responses, &dto.InstanceResponse{ + ID: instance.ID, + ClusterID: instance.ClusterID, + Name: instance.Name, + Namespace: instance.Namespace, + RegistryID: instance.RegistryID, + Repository: instance.Repository, + Chart: instance.Chart, + Version: instance.Version, + Description: instance.Description, + Status: string(instance.Status), + StatusReason: instance.StatusReason, + LastOperation: string(instance.LastOperation), + LastError: instance.LastError, + Revision: instance.Revision, + CreatedAt: instance.CreatedAt.Format("2006-01-02T15:04:05Z07:00"), + UpdatedAt: instance.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"), + }) + } + + response := &dto.InstanceListResponse{ + Instances: responses, + Total: len(responses), + } + + respondJSON(w, http.StatusOK, response) +} + +// UpdateInstance 更新实例 +// @Summary 更新实例 +// @Tags Instances +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param cluster_id path string true "集群 ID" +// @Param instance_id path string true "实例 ID" +// @Param request body dto.UpdateInstanceRequest true "更新内容" +// @Success 200 {object} dto.InstanceResponse +// @Failure 404 {object} dto.ErrorResponse +// @Router /clusters/{cluster_id}/instances/{instance_id} [put] +func (h *InstanceHandler) UpdateInstance(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + instanceID := vars["instance_id"] + + var req dto.UpdateInstanceRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + respondError(w, http.StatusBadRequest, "Invalid request body", err.Error()) + return + } + + // 获取现有实例 + instance, err := h.instanceService.GetInstance(r.Context(), instanceID) + if err != nil { + respondError(w, http.StatusNotFound, "Instance not found", err.Error()) + return + } + + // 更新字段 + if req.Version != "" { + instance.Upgrade(req.Version, req.Values) + } + if req.Description != "" { + instance.Description = req.Description + } + if req.ValuesYAML != "" { + instance.SetValuesYAML(req.ValuesYAML) + } + + // 调用领域服务 + if err := h.instanceService.UpdateInstance(r.Context(), instance); err != nil { + respondError(w, http.StatusBadRequest, "Failed to update instance", err.Error()) + return + } + + response := &dto.InstanceResponse{ + ID: instance.ID, + ClusterID: instance.ClusterID, + Name: instance.Name, + Namespace: instance.Namespace, + RegistryID: instance.RegistryID, + Repository: instance.Repository, + Chart: instance.Chart, + Version: instance.Version, + Description: instance.Description, + Status: string(instance.Status), + StatusReason: instance.StatusReason, + LastOperation: string(instance.LastOperation), + LastError: instance.LastError, + Revision: instance.Revision, + Values: instance.Values, + CreatedAt: instance.CreatedAt.Format("2006-01-02T15:04:05Z07:00"), + UpdatedAt: instance.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"), + } + + respondJSON(w, http.StatusOK, response) +} + +// DeleteInstance 删除实例 +// @Summary 删除实例 +// @Tags Instances +// @Produce json +// @Security BearerAuth +// @Param cluster_id path string true "集群 ID" +// @Param instance_id path string true "实例 ID" +// @Success 204 {string} string "No Content" +// @Failure 404 {object} dto.ErrorResponse +// @Router /clusters/{cluster_id}/instances/{instance_id} [delete] +func (h *InstanceHandler) DeleteInstance(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + instanceID := vars["instance_id"] + + if err := h.instanceService.DeleteInstance(r.Context(), instanceID); err != nil { + respondError(w, http.StatusNotFound, "Failed to delete instance", err.Error()) + return + } + + w.WriteHeader(http.StatusNoContent) +} + +// ListInstanceEntries 获取实例入口 +// @Summary 获取实例 Service/Ingress 入口 +// @Tags Instances +// @Produce json +// @Security BearerAuth +// @Param cluster_id path string true "集群 ID" +// @Param instance_id path string true "实例 ID" +// @Success 200 {array} dto.InstanceEntryResponse +// @Failure 404 {object} dto.ErrorResponse +// @Router /clusters/{cluster_id}/instances/{instance_id}/entries [get] +func (h *InstanceHandler) ListInstanceEntries(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + clusterID := vars["cluster_id"] + instanceID := vars["instance_id"] + + entries, err := h.instanceService.ListInstanceEntries(r.Context(), clusterID, instanceID) + if err != nil { + status := http.StatusInternalServerError + switch err { + case entity.ErrInstanceNotFound: + status = http.StatusNotFound + case entity.ErrClusterNotFound: + status = http.StatusNotFound + } + respondError(w, status, "Failed to list instance entries", err.Error()) + return + } + + responses := make([]*dto.InstanceEntryResponse, 0, len(entries)) + for _, entry := range entries { + responses = append(responses, convertInstanceEntry(entry)) + } + + respondJSON(w, http.StatusOK, responses) +} + +func convertInstanceEntry(entry *entity.InstanceEntry) *dto.InstanceEntryResponse { + portResponses := make([]dto.InstanceEntryPortResponse, 0, len(entry.Ports)) + for _, port := range entry.Ports { + portResponses = append(portResponses, dto.InstanceEntryPortResponse{ + Name: port.Name, + Protocol: port.Protocol, + Port: port.Port, + TargetPort: port.TargetPort, + NodePort: port.NodePort, + }) + } + + hostResponses := make([]dto.InstanceEntryHostResponse, 0, len(entry.Hosts)) + for _, host := range entry.Hosts { + pathResponses := make([]dto.InstanceEntryPathResponse, 0, len(host.Paths)) + for _, path := range host.Paths { + pathResponses = append(pathResponses, dto.InstanceEntryPathResponse{ + Path: path.Path, + ServiceName: path.ServiceName, + ServicePort: path.ServicePort, + }) + } + hostResponses = append(hostResponses, dto.InstanceEntryHostResponse{ + Host: host.Host, + Paths: pathResponses, + }) + } + + tlsResponses := make([]dto.InstanceEntryTLSResponse, 0, len(entry.TLS)) + for _, tls := range entry.TLS { + tlsResponses = append(tlsResponses, dto.InstanceEntryTLSResponse{ + Hosts: tls.Hosts, + SecretName: tls.SecretName, + }) + } + + return &dto.InstanceEntryResponse{ + Kind: entry.Kind, + Name: entry.Name, + Namespace: entry.Namespace, + Type: entry.Type, + ClusterIP: entry.ClusterIP, + ExternalIPs: entry.ExternalIPs, + LoadBalancerIngress: entry.LoadBalancerIngress, + Ports: portResponses, + Hosts: hostResponses, + TLS: tlsResponses, + } +} diff --git a/backend/internal/adapter/input/http/rest/monitoring_handler.go b/backend/internal/adapter/input/http/rest/monitoring_handler.go new file mode 100644 index 0000000..2834168 --- /dev/null +++ b/backend/internal/adapter/input/http/rest/monitoring_handler.go @@ -0,0 +1,137 @@ +package rest + +import ( + "net/http" + + "github.com/gorilla/mux" + "github.com/ocdp/cluster-service/internal/adapter/input/http/dto" + "github.com/ocdp/cluster-service/internal/domain/service" +) + +// MonitoringHandler 监控处理器 +type MonitoringHandler struct { + monitoringService *service.MonitoringService +} + +// NewMonitoringHandler 创建监控处理器 +func NewMonitoringHandler(monitoringService *service.MonitoringService) *MonitoringHandler { + return &MonitoringHandler{ + monitoringService: monitoringService, + } +} + +// GetClusterMonitoring 获取单个集群的监控信息 +// @Summary 获取集群监控 +// @Tags Monitoring +// @Produce json +// @Security BearerAuth +// @Param cluster_id path string true "集群 ID" +// @Success 200 {object} dto.ClusterMetricsResponse +// @Failure 500 {object} dto.ErrorResponse +// @Router /monitoring/clusters/{cluster_id} [get] +func (h *MonitoringHandler) GetClusterMonitoring(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + clusterID := vars["cluster_id"] + + metrics, err := h.monitoringService.GetClusterMonitoring(r.Context(), clusterID) + if err != nil { + respondError(w, http.StatusInternalServerError, "MONITORING_ERROR", err.Error()) + return + } + + response := dto.ToClusterMetricsResponse(metrics) + respondJSON(w, http.StatusOK, response) +} + +// ListClusterMonitoring 获取所有集群的监控信息 +// @Summary 列出集群监控 +// @Tags Monitoring +// @Produce json +// @Security BearerAuth +// @Success 200 {array} dto.ClusterMetricsResponse +// @Failure 500 {object} dto.ErrorResponse +// @Router /monitoring/clusters [get] +func (h *MonitoringHandler) ListClusterMonitoring(w http.ResponseWriter, r *http.Request) { + monitoringList, err := h.monitoringService.ListClusterMonitoring(r.Context()) + if err != nil { + respondError(w, http.StatusInternalServerError, "MONITORING_ERROR", err.Error()) + return + } + + // 转换为响应格式 + response := make([]*dto.ClusterMetricsResponse, len(monitoringList)) + for i, m := range monitoringList { + response[i] = dto.ToClusterMetricsResponse(m) + } + + respondJSON(w, http.StatusOK, response) +} + +// GetMonitoringSummary 获取监控汇总信息 +// @Summary 获取监控汇总 +// @Tags Monitoring +// @Produce json +// @Security BearerAuth +// @Success 200 {object} dto.MonitoringSummaryResponse +// @Failure 500 {object} dto.ErrorResponse +// @Router /monitoring/summary [get] +func (h *MonitoringHandler) GetMonitoringSummary(w http.ResponseWriter, r *http.Request) { + summary, err := h.monitoringService.GetMonitoringSummary(r.Context()) + if err != nil { + respondError(w, http.StatusInternalServerError, "MONITORING_ERROR", err.Error()) + return + } + + response := dto.ToMonitoringSummaryResponse(summary) + respondJSON(w, http.StatusOK, response) +} + +// GetNodeMetrics 获取集群的节点指标 +// @Summary 获取节点指标 +// @Tags Monitoring +// @Produce json +// @Security BearerAuth +// @Param cluster_id path string true "集群 ID" +// @Success 200 {array} dto.NodeMetricsResponse +// @Failure 500 {object} dto.ErrorResponse +// @Router /monitoring/clusters/{cluster_id}/nodes [get] +func (h *MonitoringHandler) GetNodeMetrics(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + clusterID := vars["cluster_id"] + + nodes, err := h.monitoringService.GetNodeMetrics(r.Context(), clusterID) + if err != nil { + respondError(w, http.StatusInternalServerError, "MONITORING_ERROR", err.Error()) + return + } + + // 转换为响应格式 + response := make([]dto.NodeMetricsResponse, len(nodes)) + for i, node := range nodes { + response[i] = dto.NodeMetricsResponse{ + NodeName: node.NodeName, + Status: node.Status, + Role: node.Role, + Age: node.Age, + PodCount: node.PodCount, + CPUCapacity: node.CPUCapacity, + CPUAllocatable: node.CPUAllocatable, + CPUUsage: node.CPUUsage, + CPUPercent: node.CPUPercent, + MemoryCapacity: node.MemoryCapacity, + MemoryAllocatable: node.MemoryAllocatable, + MemoryUsage: node.MemoryUsage, + MemoryPercent: node.MemoryPercent, + GPUCapacity: node.GPUCapacity, + GPUUsage: node.GPUUsage, + GPUPercent: node.GPUPercent, + GPUType: node.GPUType, + OSImage: node.OSImage, + KernelVersion: node.KernelVersion, + ContainerRuntime: node.ContainerRuntime, + KubeletVersion: node.KubeletVersion, + } + } + + respondJSON(w, http.StatusOK, response) +} diff --git a/backend/internal/adapter/input/http/rest/registry_handler.go b/backend/internal/adapter/input/http/rest/registry_handler.go new file mode 100644 index 0000000..f6d1a03 --- /dev/null +++ b/backend/internal/adapter/input/http/rest/registry_handler.go @@ -0,0 +1,201 @@ +package rest + +import ( + "encoding/json" + "net/http" + + "github.com/gorilla/mux" + "github.com/ocdp/cluster-service/internal/adapter/input/http/dto" + "github.com/ocdp/cluster-service/internal/domain/entity" + "github.com/ocdp/cluster-service/internal/domain/service" +) + +// RegistryHandler Registry Handler +type RegistryHandler struct { + registryService *service.RegistryService +} + +// NewRegistryHandler 创建 Registry Handler +func NewRegistryHandler(registryService *service.RegistryService) *RegistryHandler { + return &RegistryHandler{ + registryService: registryService, + } +} + +// CreateRegistry 创建 Registry +// @Summary 创建 Registry +// @Description 新增 OCI Registry 配置 +// @Tags Registries +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param request body dto.CreateRegistryRequest true "Registry 信息" +// @Success 201 {object} dto.RegistryResponse +// @Failure 400 {object} dto.ErrorResponse +// @Router /registries [post] +func (h *RegistryHandler) CreateRegistry(w http.ResponseWriter, r *http.Request) { + var req dto.CreateRegistryRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + respondError(w, http.StatusBadRequest, "Invalid request body", err.Error()) + return + } + + // 创建实体 + registry := entity.NewRegistry(req.Name, req.URL) + registry.Description = req.Description + registry.Insecure = req.Insecure + registry.SetCredentials(req.Username, req.Password) + + // 调用领域服务 + if err := h.registryService.CreateRegistry(r.Context(), registry); err != nil { + respondError(w, http.StatusBadRequest, "Failed to create registry", err.Error()) + return + } + + // 返回响应(脱敏) + response := dto.ToRegistryResponse(registry) + respondJSON(w, http.StatusCreated, response) +} + +// GetRegistry 获取 Registry 详情 +// @Summary 获取 Registry +// @Tags Registries +// @Produce json +// @Security BearerAuth +// @Param registry_id path string true "Registry ID" +// @Success 200 {object} dto.RegistryResponse +// @Failure 404 {object} dto.ErrorResponse +// @Router /registries/{registry_id} [get] +func (h *RegistryHandler) GetRegistry(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + registryID := vars["registry_id"] + + registry, err := h.registryService.GetRegistry(r.Context(), registryID) + if err != nil { + respondError(w, http.StatusNotFound, "Registry not found", err.Error()) + return + } + + // 返回响应(脱敏) + response := dto.ToRegistryResponse(registry) + respondJSON(w, http.StatusOK, response) +} + +// GetAllRegistries 获取所有 Registries +// @Summary 列出所有 Registries +// @Tags Registries +// @Produce json +// @Security BearerAuth +// @Success 200 {array} dto.RegistryResponse +// @Failure 500 {object} dto.ErrorResponse +// @Router /registries [get] +func (h *RegistryHandler) GetAllRegistries(w http.ResponseWriter, r *http.Request) { + registries, err := h.registryService.ListRegistries(r.Context()) + if err != nil { + respondError(w, http.StatusInternalServerError, "Failed to list registries", err.Error()) + return + } + + // 转换为响应(脱敏) + responses := make([]*dto.RegistryResponse, 0, len(registries)) + for _, registry := range registries { + responses = append(responses, dto.ToRegistryResponse(registry)) + } + + respondJSON(w, http.StatusOK, responses) +} + +// UpdateRegistry 更新 Registry +// @Summary 更新 Registry +// @Tags Registries +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param registry_id path string true "Registry ID" +// @Param request body dto.UpdateRegistryRequest true "更新内容" +// @Success 200 {object} dto.RegistryResponse +// @Failure 404 {object} dto.ErrorResponse +// @Router /registries/{registry_id} [put] +func (h *RegistryHandler) UpdateRegistry(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + registryID := vars["registry_id"] + + var req dto.UpdateRegistryRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + respondError(w, http.StatusBadRequest, "Invalid request body", err.Error()) + return + } + + // 获取现有 Registry + registry, err := h.registryService.GetRegistry(r.Context(), registryID) + if err != nil { + respondError(w, http.StatusNotFound, "Registry not found", err.Error()) + return + } + + // 更新字段 + registry.Update(req.Name, req.URL, req.Description) + registry.Insecure = req.Insecure + if req.Username != "" || req.Password != "" { + registry.SetCredentials(req.Username, req.Password) + } + + // 调用领域服务 + if err := h.registryService.UpdateRegistry(r.Context(), registry); err != nil { + respondError(w, http.StatusBadRequest, "Failed to update registry", err.Error()) + return + } + + // 返回响应(脱敏) + response := dto.ToRegistryResponse(registry) + respondJSON(w, http.StatusOK, response) +} + +// DeleteRegistry 删除 Registry +// @Summary 删除 Registry +// @Tags Registries +// @Produce json +// @Security BearerAuth +// @Param registry_id path string true "Registry ID" +// @Success 204 {string} string "No Content" +// @Failure 404 {object} dto.ErrorResponse +// @Router /registries/{registry_id} [delete] +func (h *RegistryHandler) DeleteRegistry(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + registryID := vars["registry_id"] + + if err := h.registryService.DeleteRegistry(r.Context(), registryID); err != nil { + respondError(w, http.StatusNotFound, "Failed to delete registry", err.Error()) + return + } + + w.WriteHeader(http.StatusNoContent) +} + +// GetRegistryHealth 获取 Registry 健康状态 +// @Summary 检查 Registry 健康 +// @Tags Registries +// @Produce json +// @Security BearerAuth +// @Param registry_id path string true "Registry ID" +// @Success 200 {object} dto.RegistryHealthResponse +// @Router /registries/{registry_id}/health [get] +func (h *RegistryHandler) GetRegistryHealth(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + registryID := vars["registry_id"] + + // 调用领域服务检查健康状态 + err := h.registryService.CheckHealth(r.Context(), registryID) + + response := &dto.RegistryHealthResponse{ + Healthy: err == nil, + } + + if err != nil { + response.Message = err.Error() + } else { + response.Message = "Registry is healthy" + } + + respondJSON(w, http.StatusOK, response) +} diff --git a/backend/internal/adapter/input/http/rest/swagger-ui.html b/backend/internal/adapter/input/http/rest/swagger-ui.html new file mode 100644 index 0000000..f9bed5e --- /dev/null +++ b/backend/internal/adapter/input/http/rest/swagger-ui.html @@ -0,0 +1,89 @@ + + + + + + OCDP Backend API - Swagger UI + + + + +
+ + + + + diff --git a/backend/internal/adapter/input/http/rest/swagger_handler.go b/backend/internal/adapter/input/http/rest/swagger_handler.go new file mode 100644 index 0000000..60b2198 --- /dev/null +++ b/backend/internal/adapter/input/http/rest/swagger_handler.go @@ -0,0 +1,68 @@ +package rest + +import ( + _ "embed" + "net/http" + + repoDocs "github.com/ocdp/cluster-service/docs" +) + +var ( + //go:embed swagger-ui.html + swaggerHTML []byte + + //go:embed swaggerui/swagger-ui.css + swaggerCSS []byte + + //go:embed swaggerui/swagger-ui-bundle.js + swaggerBundleJS []byte + + //go:embed swaggerui/swagger-ui-standalone-preset.js + swaggerStandalonePresetJS []byte +) + +// SwaggerHandler Swagger UI Handler +type SwaggerHandler struct{} + +// NewSwaggerHandler 创建 Swagger Handler +func NewSwaggerHandler() *SwaggerHandler { + return &SwaggerHandler{} +} + +// ServeSwaggerUI 提供 Swagger UI 页面 +func (h *SwaggerHandler) ServeSwaggerUI(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.WriteHeader(http.StatusOK) + w.Write(swaggerHTML) +} + +// ServeSwaggerCSS 提供 Swagger UI 样式 +func (h *SwaggerHandler) ServeSwaggerCSS(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/css; charset=utf-8") + w.Header().Set("Cache-Control", "public, max-age=86400") + w.WriteHeader(http.StatusOK) + w.Write(swaggerCSS) +} + +// ServeSwaggerBundle 提供 Swagger UI 主脚本 +func (h *SwaggerHandler) ServeSwaggerBundle(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/javascript; charset=utf-8") + w.Header().Set("Cache-Control", "public, max-age=86400") + w.WriteHeader(http.StatusOK) + w.Write(swaggerBundleJS) +} + +// ServeSwaggerStandalonePreset 提供 Swagger UI 预设脚本 +func (h *SwaggerHandler) ServeSwaggerStandalonePreset(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/javascript; charset=utf-8") + w.Header().Set("Cache-Control", "public, max-age=86400") + w.WriteHeader(http.StatusOK) + w.Write(swaggerStandalonePresetJS) +} + +// ServeOpenAPISpec 提供 OpenAPI 规范文件 +func (h *SwaggerHandler) ServeOpenAPISpec(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/yaml; charset=utf-8") + w.WriteHeader(http.StatusOK) + w.Write(repoDocs.OpenAPISpec) +} diff --git a/backend/internal/adapter/input/http/rest/swaggerui/swagger-ui-bundle.js b/backend/internal/adapter/input/http/rest/swaggerui/swagger-ui-bundle.js new file mode 100644 index 0000000..afe03d9 --- /dev/null +++ b/backend/internal/adapter/input/http/rest/swaggerui/swagger-ui-bundle.js @@ -0,0 +1,3 @@ +/*! For license information please see swagger-ui-bundle.js.LICENSE.txt */ +!function webpackUniversalModuleDefinition(i,s){"object"==typeof exports&&"object"==typeof module?module.exports=s():"function"==typeof define&&define.amd?define([],s):"object"==typeof exports?exports.SwaggerUIBundle=s():i.SwaggerUIBundle=s()}(this,(()=>(()=>{var i={17967:(i,s)=>{"use strict";s.Nm=s.Rq=void 0;var u=/^([^\w]*)(javascript|data|vbscript)/im,m=/&#(\w+)(^\w|;)?/g,v=/&(newline|tab);/gi,_=/[\u0000-\u001F\u007F-\u009F\u2000-\u200D\uFEFF]/gim,j=/^.+(:|:)/gim,M=[".","/"];s.Rq="about:blank",s.Nm=function sanitizeUrl(i){if(!i)return s.Rq;var $=function decodeHtmlCharacters(i){return i.replace(_,"").replace(m,(function(i,s){return String.fromCharCode(s)}))}(i).replace(v,"").replace(_,"").trim();if(!$)return s.Rq;if(function isRelativeUrlWithoutProtocol(i){return M.indexOf(i[0])>-1}($))return $;var W=$.match(j);if(!W)return $;var X=W[0];return u.test(X)?s.Rq:$}},79742:(i,s)=>{"use strict";s.byteLength=function byteLength(i){var s=getLens(i),u=s[0],m=s[1];return 3*(u+m)/4-m},s.toByteArray=function toByteArray(i){var s,u,_=getLens(i),j=_[0],M=_[1],$=new v(function _byteLength(i,s,u){return 3*(s+u)/4-u}(0,j,M)),W=0,X=M>0?j-4:j;for(u=0;u>16&255,$[W++]=s>>8&255,$[W++]=255&s;2===M&&(s=m[i.charCodeAt(u)]<<2|m[i.charCodeAt(u+1)]>>4,$[W++]=255&s);1===M&&(s=m[i.charCodeAt(u)]<<10|m[i.charCodeAt(u+1)]<<4|m[i.charCodeAt(u+2)]>>2,$[W++]=s>>8&255,$[W++]=255&s);return $},s.fromByteArray=function fromByteArray(i){for(var s,m=i.length,v=m%3,_=[],j=16383,M=0,$=m-v;M<$;M+=j)_.push(encodeChunk(i,M,M+j>$?$:M+j));1===v?(s=i[m-1],_.push(u[s>>2]+u[s<<4&63]+"==")):2===v&&(s=(i[m-2]<<8)+i[m-1],_.push(u[s>>10]+u[s>>4&63]+u[s<<2&63]+"="));return _.join("")};for(var u=[],m=[],v="undefined"!=typeof Uint8Array?Uint8Array:Array,_="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",j=0;j<64;++j)u[j]=_[j],m[_.charCodeAt(j)]=j;function getLens(i){var s=i.length;if(s%4>0)throw new Error("Invalid string. Length must be a multiple of 4");var u=i.indexOf("=");return-1===u&&(u=s),[u,u===s?0:4-u%4]}function encodeChunk(i,s,m){for(var v,_,j=[],M=s;M>18&63]+u[_>>12&63]+u[_>>6&63]+u[63&_]);return j.join("")}m["-".charCodeAt(0)]=62,m["_".charCodeAt(0)]=63},48764:(i,s,u)=>{"use strict";const m=u(79742),v=u(80645),_="function"==typeof Symbol&&"function"==typeof Symbol.for?Symbol.for("nodejs.util.inspect.custom"):null;s.Buffer=Buffer,s.SlowBuffer=function SlowBuffer(i){+i!=i&&(i=0);return Buffer.alloc(+i)},s.INSPECT_MAX_BYTES=50;const j=2147483647;function createBuffer(i){if(i>j)throw new RangeError('The value "'+i+'" is invalid for option "size"');const s=new Uint8Array(i);return Object.setPrototypeOf(s,Buffer.prototype),s}function Buffer(i,s,u){if("number"==typeof i){if("string"==typeof s)throw new TypeError('The "string" argument must be of type string. Received type number');return allocUnsafe(i)}return from(i,s,u)}function from(i,s,u){if("string"==typeof i)return function fromString(i,s){"string"==typeof s&&""!==s||(s="utf8");if(!Buffer.isEncoding(s))throw new TypeError("Unknown encoding: "+s);const u=0|byteLength(i,s);let m=createBuffer(u);const v=m.write(i,s);v!==u&&(m=m.slice(0,v));return m}(i,s);if(ArrayBuffer.isView(i))return function fromArrayView(i){if(isInstance(i,Uint8Array)){const s=new Uint8Array(i);return fromArrayBuffer(s.buffer,s.byteOffset,s.byteLength)}return fromArrayLike(i)}(i);if(null==i)throw new TypeError("The first argument must be one of type string, Buffer, ArrayBuffer, Array, or Array-like Object. Received type "+typeof i);if(isInstance(i,ArrayBuffer)||i&&isInstance(i.buffer,ArrayBuffer))return fromArrayBuffer(i,s,u);if("undefined"!=typeof SharedArrayBuffer&&(isInstance(i,SharedArrayBuffer)||i&&isInstance(i.buffer,SharedArrayBuffer)))return fromArrayBuffer(i,s,u);if("number"==typeof i)throw new TypeError('The "value" argument must not be of type number. Received type number');const m=i.valueOf&&i.valueOf();if(null!=m&&m!==i)return Buffer.from(m,s,u);const v=function fromObject(i){if(Buffer.isBuffer(i)){const s=0|checked(i.length),u=createBuffer(s);return 0===u.length||i.copy(u,0,0,s),u}if(void 0!==i.length)return"number"!=typeof i.length||numberIsNaN(i.length)?createBuffer(0):fromArrayLike(i);if("Buffer"===i.type&&Array.isArray(i.data))return fromArrayLike(i.data)}(i);if(v)return v;if("undefined"!=typeof Symbol&&null!=Symbol.toPrimitive&&"function"==typeof i[Symbol.toPrimitive])return Buffer.from(i[Symbol.toPrimitive]("string"),s,u);throw new TypeError("The first argument must be one of type string, Buffer, ArrayBuffer, Array, or Array-like Object. Received type "+typeof i)}function assertSize(i){if("number"!=typeof i)throw new TypeError('"size" argument must be of type number');if(i<0)throw new RangeError('The value "'+i+'" is invalid for option "size"')}function allocUnsafe(i){return assertSize(i),createBuffer(i<0?0:0|checked(i))}function fromArrayLike(i){const s=i.length<0?0:0|checked(i.length),u=createBuffer(s);for(let m=0;m=j)throw new RangeError("Attempt to allocate Buffer larger than maximum size: 0x"+j.toString(16)+" bytes");return 0|i}function byteLength(i,s){if(Buffer.isBuffer(i))return i.length;if(ArrayBuffer.isView(i)||isInstance(i,ArrayBuffer))return i.byteLength;if("string"!=typeof i)throw new TypeError('The "string" argument must be one of type string, Buffer, or ArrayBuffer. Received type '+typeof i);const u=i.length,m=arguments.length>2&&!0===arguments[2];if(!m&&0===u)return 0;let v=!1;for(;;)switch(s){case"ascii":case"latin1":case"binary":return u;case"utf8":case"utf-8":return utf8ToBytes(i).length;case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return 2*u;case"hex":return u>>>1;case"base64":return base64ToBytes(i).length;default:if(v)return m?-1:utf8ToBytes(i).length;s=(""+s).toLowerCase(),v=!0}}function slowToString(i,s,u){let m=!1;if((void 0===s||s<0)&&(s=0),s>this.length)return"";if((void 0===u||u>this.length)&&(u=this.length),u<=0)return"";if((u>>>=0)<=(s>>>=0))return"";for(i||(i="utf8");;)switch(i){case"hex":return hexSlice(this,s,u);case"utf8":case"utf-8":return utf8Slice(this,s,u);case"ascii":return asciiSlice(this,s,u);case"latin1":case"binary":return latin1Slice(this,s,u);case"base64":return base64Slice(this,s,u);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return utf16leSlice(this,s,u);default:if(m)throw new TypeError("Unknown encoding: "+i);i=(i+"").toLowerCase(),m=!0}}function swap(i,s,u){const m=i[s];i[s]=i[u],i[u]=m}function bidirectionalIndexOf(i,s,u,m,v){if(0===i.length)return-1;if("string"==typeof u?(m=u,u=0):u>2147483647?u=2147483647:u<-2147483648&&(u=-2147483648),numberIsNaN(u=+u)&&(u=v?0:i.length-1),u<0&&(u=i.length+u),u>=i.length){if(v)return-1;u=i.length-1}else if(u<0){if(!v)return-1;u=0}if("string"==typeof s&&(s=Buffer.from(s,m)),Buffer.isBuffer(s))return 0===s.length?-1:arrayIndexOf(i,s,u,m,v);if("number"==typeof s)return s&=255,"function"==typeof Uint8Array.prototype.indexOf?v?Uint8Array.prototype.indexOf.call(i,s,u):Uint8Array.prototype.lastIndexOf.call(i,s,u):arrayIndexOf(i,[s],u,m,v);throw new TypeError("val must be string, number or Buffer")}function arrayIndexOf(i,s,u,m,v){let _,j=1,M=i.length,$=s.length;if(void 0!==m&&("ucs2"===(m=String(m).toLowerCase())||"ucs-2"===m||"utf16le"===m||"utf-16le"===m)){if(i.length<2||s.length<2)return-1;j=2,M/=2,$/=2,u/=2}function read(i,s){return 1===j?i[s]:i.readUInt16BE(s*j)}if(v){let m=-1;for(_=u;_M&&(u=M-$),_=u;_>=0;_--){let u=!0;for(let m=0;m<$;m++)if(read(i,_+m)!==read(s,m)){u=!1;break}if(u)return _}return-1}function hexWrite(i,s,u,m){u=Number(u)||0;const v=i.length-u;m?(m=Number(m))>v&&(m=v):m=v;const _=s.length;let j;for(m>_/2&&(m=_/2),j=0;j>8,v=u%256,_.push(v),_.push(m);return _}(s,i.length-u),i,u,m)}function base64Slice(i,s,u){return 0===s&&u===i.length?m.fromByteArray(i):m.fromByteArray(i.slice(s,u))}function utf8Slice(i,s,u){u=Math.min(i.length,u);const m=[];let v=s;for(;v239?4:s>223?3:s>191?2:1;if(v+j<=u){let u,m,M,$;switch(j){case 1:s<128&&(_=s);break;case 2:u=i[v+1],128==(192&u)&&($=(31&s)<<6|63&u,$>127&&(_=$));break;case 3:u=i[v+1],m=i[v+2],128==(192&u)&&128==(192&m)&&($=(15&s)<<12|(63&u)<<6|63&m,$>2047&&($<55296||$>57343)&&(_=$));break;case 4:u=i[v+1],m=i[v+2],M=i[v+3],128==(192&u)&&128==(192&m)&&128==(192&M)&&($=(15&s)<<18|(63&u)<<12|(63&m)<<6|63&M,$>65535&&$<1114112&&(_=$))}}null===_?(_=65533,j=1):_>65535&&(_-=65536,m.push(_>>>10&1023|55296),_=56320|1023&_),m.push(_),v+=j}return function decodeCodePointsArray(i){const s=i.length;if(s<=M)return String.fromCharCode.apply(String,i);let u="",m=0;for(;mm.length?(Buffer.isBuffer(s)||(s=Buffer.from(s)),s.copy(m,v)):Uint8Array.prototype.set.call(m,s,v);else{if(!Buffer.isBuffer(s))throw new TypeError('"list" argument must be an Array of Buffers');s.copy(m,v)}v+=s.length}return m},Buffer.byteLength=byteLength,Buffer.prototype._isBuffer=!0,Buffer.prototype.swap16=function swap16(){const i=this.length;if(i%2!=0)throw new RangeError("Buffer size must be a multiple of 16-bits");for(let s=0;su&&(i+=" ... "),""},_&&(Buffer.prototype[_]=Buffer.prototype.inspect),Buffer.prototype.compare=function compare(i,s,u,m,v){if(isInstance(i,Uint8Array)&&(i=Buffer.from(i,i.offset,i.byteLength)),!Buffer.isBuffer(i))throw new TypeError('The "target" argument must be one of type Buffer or Uint8Array. Received type '+typeof i);if(void 0===s&&(s=0),void 0===u&&(u=i?i.length:0),void 0===m&&(m=0),void 0===v&&(v=this.length),s<0||u>i.length||m<0||v>this.length)throw new RangeError("out of range index");if(m>=v&&s>=u)return 0;if(m>=v)return-1;if(s>=u)return 1;if(this===i)return 0;let _=(v>>>=0)-(m>>>=0),j=(u>>>=0)-(s>>>=0);const M=Math.min(_,j),$=this.slice(m,v),W=i.slice(s,u);for(let i=0;i>>=0,isFinite(u)?(u>>>=0,void 0===m&&(m="utf8")):(m=u,u=void 0)}const v=this.length-s;if((void 0===u||u>v)&&(u=v),i.length>0&&(u<0||s<0)||s>this.length)throw new RangeError("Attempt to write outside buffer bounds");m||(m="utf8");let _=!1;for(;;)switch(m){case"hex":return hexWrite(this,i,s,u);case"utf8":case"utf-8":return utf8Write(this,i,s,u);case"ascii":case"latin1":case"binary":return asciiWrite(this,i,s,u);case"base64":return base64Write(this,i,s,u);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return ucs2Write(this,i,s,u);default:if(_)throw new TypeError("Unknown encoding: "+m);m=(""+m).toLowerCase(),_=!0}},Buffer.prototype.toJSON=function toJSON(){return{type:"Buffer",data:Array.prototype.slice.call(this._arr||this,0)}};const M=4096;function asciiSlice(i,s,u){let m="";u=Math.min(i.length,u);for(let v=s;vm)&&(u=m);let v="";for(let m=s;mu)throw new RangeError("Trying to access beyond buffer length")}function checkInt(i,s,u,m,v,_){if(!Buffer.isBuffer(i))throw new TypeError('"buffer" argument must be a Buffer instance');if(s>v||s<_)throw new RangeError('"value" argument is out of bounds');if(u+m>i.length)throw new RangeError("Index out of range")}function wrtBigUInt64LE(i,s,u,m,v){checkIntBI(s,m,v,i,u,7);let _=Number(s&BigInt(4294967295));i[u++]=_,_>>=8,i[u++]=_,_>>=8,i[u++]=_,_>>=8,i[u++]=_;let j=Number(s>>BigInt(32)&BigInt(4294967295));return i[u++]=j,j>>=8,i[u++]=j,j>>=8,i[u++]=j,j>>=8,i[u++]=j,u}function wrtBigUInt64BE(i,s,u,m,v){checkIntBI(s,m,v,i,u,7);let _=Number(s&BigInt(4294967295));i[u+7]=_,_>>=8,i[u+6]=_,_>>=8,i[u+5]=_,_>>=8,i[u+4]=_;let j=Number(s>>BigInt(32)&BigInt(4294967295));return i[u+3]=j,j>>=8,i[u+2]=j,j>>=8,i[u+1]=j,j>>=8,i[u]=j,u+8}function checkIEEE754(i,s,u,m,v,_){if(u+m>i.length)throw new RangeError("Index out of range");if(u<0)throw new RangeError("Index out of range")}function writeFloat(i,s,u,m,_){return s=+s,u>>>=0,_||checkIEEE754(i,0,u,4),v.write(i,s,u,m,23,4),u+4}function writeDouble(i,s,u,m,_){return s=+s,u>>>=0,_||checkIEEE754(i,0,u,8),v.write(i,s,u,m,52,8),u+8}Buffer.prototype.slice=function slice(i,s){const u=this.length;(i=~~i)<0?(i+=u)<0&&(i=0):i>u&&(i=u),(s=void 0===s?u:~~s)<0?(s+=u)<0&&(s=0):s>u&&(s=u),s>>=0,s>>>=0,u||checkOffset(i,s,this.length);let m=this[i],v=1,_=0;for(;++_>>=0,s>>>=0,u||checkOffset(i,s,this.length);let m=this[i+--s],v=1;for(;s>0&&(v*=256);)m+=this[i+--s]*v;return m},Buffer.prototype.readUint8=Buffer.prototype.readUInt8=function readUInt8(i,s){return i>>>=0,s||checkOffset(i,1,this.length),this[i]},Buffer.prototype.readUint16LE=Buffer.prototype.readUInt16LE=function readUInt16LE(i,s){return i>>>=0,s||checkOffset(i,2,this.length),this[i]|this[i+1]<<8},Buffer.prototype.readUint16BE=Buffer.prototype.readUInt16BE=function readUInt16BE(i,s){return i>>>=0,s||checkOffset(i,2,this.length),this[i]<<8|this[i+1]},Buffer.prototype.readUint32LE=Buffer.prototype.readUInt32LE=function readUInt32LE(i,s){return i>>>=0,s||checkOffset(i,4,this.length),(this[i]|this[i+1]<<8|this[i+2]<<16)+16777216*this[i+3]},Buffer.prototype.readUint32BE=Buffer.prototype.readUInt32BE=function readUInt32BE(i,s){return i>>>=0,s||checkOffset(i,4,this.length),16777216*this[i]+(this[i+1]<<16|this[i+2]<<8|this[i+3])},Buffer.prototype.readBigUInt64LE=defineBigIntMethod((function readBigUInt64LE(i){validateNumber(i>>>=0,"offset");const s=this[i],u=this[i+7];void 0!==s&&void 0!==u||boundsError(i,this.length-8);const m=s+256*this[++i]+65536*this[++i]+this[++i]*2**24,v=this[++i]+256*this[++i]+65536*this[++i]+u*2**24;return BigInt(m)+(BigInt(v)<>>=0,"offset");const s=this[i],u=this[i+7];void 0!==s&&void 0!==u||boundsError(i,this.length-8);const m=s*2**24+65536*this[++i]+256*this[++i]+this[++i],v=this[++i]*2**24+65536*this[++i]+256*this[++i]+u;return(BigInt(m)<>>=0,s>>>=0,u||checkOffset(i,s,this.length);let m=this[i],v=1,_=0;for(;++_=v&&(m-=Math.pow(2,8*s)),m},Buffer.prototype.readIntBE=function readIntBE(i,s,u){i>>>=0,s>>>=0,u||checkOffset(i,s,this.length);let m=s,v=1,_=this[i+--m];for(;m>0&&(v*=256);)_+=this[i+--m]*v;return v*=128,_>=v&&(_-=Math.pow(2,8*s)),_},Buffer.prototype.readInt8=function readInt8(i,s){return i>>>=0,s||checkOffset(i,1,this.length),128&this[i]?-1*(255-this[i]+1):this[i]},Buffer.prototype.readInt16LE=function readInt16LE(i,s){i>>>=0,s||checkOffset(i,2,this.length);const u=this[i]|this[i+1]<<8;return 32768&u?4294901760|u:u},Buffer.prototype.readInt16BE=function readInt16BE(i,s){i>>>=0,s||checkOffset(i,2,this.length);const u=this[i+1]|this[i]<<8;return 32768&u?4294901760|u:u},Buffer.prototype.readInt32LE=function readInt32LE(i,s){return i>>>=0,s||checkOffset(i,4,this.length),this[i]|this[i+1]<<8|this[i+2]<<16|this[i+3]<<24},Buffer.prototype.readInt32BE=function readInt32BE(i,s){return i>>>=0,s||checkOffset(i,4,this.length),this[i]<<24|this[i+1]<<16|this[i+2]<<8|this[i+3]},Buffer.prototype.readBigInt64LE=defineBigIntMethod((function readBigInt64LE(i){validateNumber(i>>>=0,"offset");const s=this[i],u=this[i+7];void 0!==s&&void 0!==u||boundsError(i,this.length-8);const m=this[i+4]+256*this[i+5]+65536*this[i+6]+(u<<24);return(BigInt(m)<>>=0,"offset");const s=this[i],u=this[i+7];void 0!==s&&void 0!==u||boundsError(i,this.length-8);const m=(s<<24)+65536*this[++i]+256*this[++i]+this[++i];return(BigInt(m)<>>=0,s||checkOffset(i,4,this.length),v.read(this,i,!0,23,4)},Buffer.prototype.readFloatBE=function readFloatBE(i,s){return i>>>=0,s||checkOffset(i,4,this.length),v.read(this,i,!1,23,4)},Buffer.prototype.readDoubleLE=function readDoubleLE(i,s){return i>>>=0,s||checkOffset(i,8,this.length),v.read(this,i,!0,52,8)},Buffer.prototype.readDoubleBE=function readDoubleBE(i,s){return i>>>=0,s||checkOffset(i,8,this.length),v.read(this,i,!1,52,8)},Buffer.prototype.writeUintLE=Buffer.prototype.writeUIntLE=function writeUIntLE(i,s,u,m){if(i=+i,s>>>=0,u>>>=0,!m){checkInt(this,i,s,u,Math.pow(2,8*u)-1,0)}let v=1,_=0;for(this[s]=255&i;++_>>=0,u>>>=0,!m){checkInt(this,i,s,u,Math.pow(2,8*u)-1,0)}let v=u-1,_=1;for(this[s+v]=255&i;--v>=0&&(_*=256);)this[s+v]=i/_&255;return s+u},Buffer.prototype.writeUint8=Buffer.prototype.writeUInt8=function writeUInt8(i,s,u){return i=+i,s>>>=0,u||checkInt(this,i,s,1,255,0),this[s]=255&i,s+1},Buffer.prototype.writeUint16LE=Buffer.prototype.writeUInt16LE=function writeUInt16LE(i,s,u){return i=+i,s>>>=0,u||checkInt(this,i,s,2,65535,0),this[s]=255&i,this[s+1]=i>>>8,s+2},Buffer.prototype.writeUint16BE=Buffer.prototype.writeUInt16BE=function writeUInt16BE(i,s,u){return i=+i,s>>>=0,u||checkInt(this,i,s,2,65535,0),this[s]=i>>>8,this[s+1]=255&i,s+2},Buffer.prototype.writeUint32LE=Buffer.prototype.writeUInt32LE=function writeUInt32LE(i,s,u){return i=+i,s>>>=0,u||checkInt(this,i,s,4,4294967295,0),this[s+3]=i>>>24,this[s+2]=i>>>16,this[s+1]=i>>>8,this[s]=255&i,s+4},Buffer.prototype.writeUint32BE=Buffer.prototype.writeUInt32BE=function writeUInt32BE(i,s,u){return i=+i,s>>>=0,u||checkInt(this,i,s,4,4294967295,0),this[s]=i>>>24,this[s+1]=i>>>16,this[s+2]=i>>>8,this[s+3]=255&i,s+4},Buffer.prototype.writeBigUInt64LE=defineBigIntMethod((function writeBigUInt64LE(i,s=0){return wrtBigUInt64LE(this,i,s,BigInt(0),BigInt("0xffffffffffffffff"))})),Buffer.prototype.writeBigUInt64BE=defineBigIntMethod((function writeBigUInt64BE(i,s=0){return wrtBigUInt64BE(this,i,s,BigInt(0),BigInt("0xffffffffffffffff"))})),Buffer.prototype.writeIntLE=function writeIntLE(i,s,u,m){if(i=+i,s>>>=0,!m){const m=Math.pow(2,8*u-1);checkInt(this,i,s,u,m-1,-m)}let v=0,_=1,j=0;for(this[s]=255&i;++v>0)-j&255;return s+u},Buffer.prototype.writeIntBE=function writeIntBE(i,s,u,m){if(i=+i,s>>>=0,!m){const m=Math.pow(2,8*u-1);checkInt(this,i,s,u,m-1,-m)}let v=u-1,_=1,j=0;for(this[s+v]=255&i;--v>=0&&(_*=256);)i<0&&0===j&&0!==this[s+v+1]&&(j=1),this[s+v]=(i/_>>0)-j&255;return s+u},Buffer.prototype.writeInt8=function writeInt8(i,s,u){return i=+i,s>>>=0,u||checkInt(this,i,s,1,127,-128),i<0&&(i=255+i+1),this[s]=255&i,s+1},Buffer.prototype.writeInt16LE=function writeInt16LE(i,s,u){return i=+i,s>>>=0,u||checkInt(this,i,s,2,32767,-32768),this[s]=255&i,this[s+1]=i>>>8,s+2},Buffer.prototype.writeInt16BE=function writeInt16BE(i,s,u){return i=+i,s>>>=0,u||checkInt(this,i,s,2,32767,-32768),this[s]=i>>>8,this[s+1]=255&i,s+2},Buffer.prototype.writeInt32LE=function writeInt32LE(i,s,u){return i=+i,s>>>=0,u||checkInt(this,i,s,4,2147483647,-2147483648),this[s]=255&i,this[s+1]=i>>>8,this[s+2]=i>>>16,this[s+3]=i>>>24,s+4},Buffer.prototype.writeInt32BE=function writeInt32BE(i,s,u){return i=+i,s>>>=0,u||checkInt(this,i,s,4,2147483647,-2147483648),i<0&&(i=4294967295+i+1),this[s]=i>>>24,this[s+1]=i>>>16,this[s+2]=i>>>8,this[s+3]=255&i,s+4},Buffer.prototype.writeBigInt64LE=defineBigIntMethod((function writeBigInt64LE(i,s=0){return wrtBigUInt64LE(this,i,s,-BigInt("0x8000000000000000"),BigInt("0x7fffffffffffffff"))})),Buffer.prototype.writeBigInt64BE=defineBigIntMethod((function writeBigInt64BE(i,s=0){return wrtBigUInt64BE(this,i,s,-BigInt("0x8000000000000000"),BigInt("0x7fffffffffffffff"))})),Buffer.prototype.writeFloatLE=function writeFloatLE(i,s,u){return writeFloat(this,i,s,!0,u)},Buffer.prototype.writeFloatBE=function writeFloatBE(i,s,u){return writeFloat(this,i,s,!1,u)},Buffer.prototype.writeDoubleLE=function writeDoubleLE(i,s,u){return writeDouble(this,i,s,!0,u)},Buffer.prototype.writeDoubleBE=function writeDoubleBE(i,s,u){return writeDouble(this,i,s,!1,u)},Buffer.prototype.copy=function copy(i,s,u,m){if(!Buffer.isBuffer(i))throw new TypeError("argument should be a Buffer");if(u||(u=0),m||0===m||(m=this.length),s>=i.length&&(s=i.length),s||(s=0),m>0&&m=this.length)throw new RangeError("Index out of range");if(m<0)throw new RangeError("sourceEnd out of bounds");m>this.length&&(m=this.length),i.length-s>>=0,u=void 0===u?this.length:u>>>0,i||(i=0),"number"==typeof i)for(v=s;v=m+4;u-=3)s=`_${i.slice(u-3,u)}${s}`;return`${i.slice(0,u)}${s}`}function checkIntBI(i,s,u,m,v,_){if(i>u||i3?0===s||s===BigInt(0)?`>= 0${m} and < 2${m} ** ${8*(_+1)}${m}`:`>= -(2${m} ** ${8*(_+1)-1}${m}) and < 2 ** ${8*(_+1)-1}${m}`:`>= ${s}${m} and <= ${u}${m}`,new $.ERR_OUT_OF_RANGE("value",v,i)}!function checkBounds(i,s,u){validateNumber(s,"offset"),void 0!==i[s]&&void 0!==i[s+u]||boundsError(s,i.length-(u+1))}(m,v,_)}function validateNumber(i,s){if("number"!=typeof i)throw new $.ERR_INVALID_ARG_TYPE(s,"number",i)}function boundsError(i,s,u){if(Math.floor(i)!==i)throw validateNumber(i,u),new $.ERR_OUT_OF_RANGE(u||"offset","an integer",i);if(s<0)throw new $.ERR_BUFFER_OUT_OF_BOUNDS;throw new $.ERR_OUT_OF_RANGE(u||"offset",`>= ${u?1:0} and <= ${s}`,i)}E("ERR_BUFFER_OUT_OF_BOUNDS",(function(i){return i?`${i} is outside of buffer bounds`:"Attempt to access memory outside buffer bounds"}),RangeError),E("ERR_INVALID_ARG_TYPE",(function(i,s){return`The "${i}" argument must be of type number. Received type ${typeof s}`}),TypeError),E("ERR_OUT_OF_RANGE",(function(i,s,u){let m=`The value of "${i}" is out of range.`,v=u;return Number.isInteger(u)&&Math.abs(u)>2**32?v=addNumericalSeparator(String(u)):"bigint"==typeof u&&(v=String(u),(u>BigInt(2)**BigInt(32)||u<-(BigInt(2)**BigInt(32)))&&(v=addNumericalSeparator(v)),v+="n"),m+=` It must be ${s}. Received ${v}`,m}),RangeError);const W=/[^+/0-9A-Za-z-_]/g;function utf8ToBytes(i,s){let u;s=s||1/0;const m=i.length;let v=null;const _=[];for(let j=0;j55295&&u<57344){if(!v){if(u>56319){(s-=3)>-1&&_.push(239,191,189);continue}if(j+1===m){(s-=3)>-1&&_.push(239,191,189);continue}v=u;continue}if(u<56320){(s-=3)>-1&&_.push(239,191,189),v=u;continue}u=65536+(v-55296<<10|u-56320)}else v&&(s-=3)>-1&&_.push(239,191,189);if(v=null,u<128){if((s-=1)<0)break;_.push(u)}else if(u<2048){if((s-=2)<0)break;_.push(u>>6|192,63&u|128)}else if(u<65536){if((s-=3)<0)break;_.push(u>>12|224,u>>6&63|128,63&u|128)}else{if(!(u<1114112))throw new Error("Invalid code point");if((s-=4)<0)break;_.push(u>>18|240,u>>12&63|128,u>>6&63|128,63&u|128)}}return _}function base64ToBytes(i){return m.toByteArray(function base64clean(i){if((i=(i=i.split("=")[0]).trim().replace(W,"")).length<2)return"";for(;i.length%4!=0;)i+="=";return i}(i))}function blitBuffer(i,s,u,m){let v;for(v=0;v=s.length||v>=i.length);++v)s[v+u]=i[v];return v}function isInstance(i,s){return i instanceof s||null!=i&&null!=i.constructor&&null!=i.constructor.name&&i.constructor.name===s.name}function numberIsNaN(i){return i!=i}const X=function(){const i="0123456789abcdef",s=new Array(256);for(let u=0;u<16;++u){const m=16*u;for(let v=0;v<16;++v)s[m+v]=i[u]+i[v]}return s}();function defineBigIntMethod(i){return"undefined"==typeof BigInt?BufferBigIntNotDefined:i}function BufferBigIntNotDefined(){throw new Error("BigInt not supported")}},21924:(i,s,u)=>{"use strict";var m=u(40210),v=u(55559),_=v(m("String.prototype.indexOf"));i.exports=function callBoundIntrinsic(i,s){var u=m(i,!!s);return"function"==typeof u&&_(i,".prototype.")>-1?v(u):u}},55559:(i,s,u)=>{"use strict";var m=u(58612),v=u(40210),_=v("%Function.prototype.apply%"),j=v("%Function.prototype.call%"),M=v("%Reflect.apply%",!0)||m.call(j,_),$=v("%Object.getOwnPropertyDescriptor%",!0),W=v("%Object.defineProperty%",!0),X=v("%Math.max%");if(W)try{W({},"a",{value:1})}catch(i){W=null}i.exports=function callBind(i){var s=M(m,j,arguments);$&&W&&($(s,"length").configurable&&W(s,"length",{value:1+X(0,i.length-(arguments.length-1))}));return s};var Y=function applyBind(){return M(m,_,arguments)};W?W(i.exports,"apply",{value:Y}):i.exports.apply=Y},94184:(i,s)=>{var u;!function(){"use strict";var m={}.hasOwnProperty;function classNames(){for(var i=[],s=0;s{"use strict";s.parse=function parse(i,s){if("string"!=typeof i)throw new TypeError("argument str must be a string");var u={},m=(s||{}).decode||decode,v=0;for(;v{"use strict";var m=u(11742),v={"text/plain":"Text","text/html":"Url",default:"Text"};i.exports=function copy(i,s){var u,_,j,M,$,W,X=!1;s||(s={}),u=s.debug||!1;try{if(j=m(),M=document.createRange(),$=document.getSelection(),(W=document.createElement("span")).textContent=i,W.ariaHidden="true",W.style.all="unset",W.style.position="fixed",W.style.top=0,W.style.clip="rect(0, 0, 0, 0)",W.style.whiteSpace="pre",W.style.webkitUserSelect="text",W.style.MozUserSelect="text",W.style.msUserSelect="text",W.style.userSelect="text",W.addEventListener("copy",(function(m){if(m.stopPropagation(),s.format)if(m.preventDefault(),void 0===m.clipboardData){u&&console.warn("unable to use e.clipboardData"),u&&console.warn("trying IE specific stuff"),window.clipboardData.clearData();var _=v[s.format]||v.default;window.clipboardData.setData(_,i)}else m.clipboardData.clearData(),m.clipboardData.setData(s.format,i);s.onCopy&&(m.preventDefault(),s.onCopy(m.clipboardData))})),document.body.appendChild(W),M.selectNodeContents(W),$.addRange(M),!document.execCommand("copy"))throw new Error("copy command was unsuccessful");X=!0}catch(m){u&&console.error("unable to copy using execCommand: ",m),u&&console.warn("trying IE specific stuff");try{window.clipboardData.setData(s.format||"text",i),s.onCopy&&s.onCopy(window.clipboardData),X=!0}catch(m){u&&console.error("unable to copy using clipboardData: ",m),u&&console.error("falling back to prompt"),_=function format(i){var s=(/mac os x/i.test(navigator.userAgent)?"⌘":"Ctrl")+"+C";return i.replace(/#{\s*key\s*}/g,s)}("message"in s?s.message:"Copy to clipboard: #{key}, Enter"),window.prompt(_,i)}}finally{$&&("function"==typeof $.removeRange?$.removeRange(M):$.removeAllRanges()),W&&document.body.removeChild(W),j()}return X}},44101:(i,s,u)=>{var m=u(18957);i.exports=m},90093:(i,s,u)=>{var m=u(28196);i.exports=m},65362:(i,s,u)=>{var m=u(63383);i.exports=m},50415:(i,s,u)=>{u(61181),u(47627),u(24415),u(66274),u(77971);var m=u(54058);i.exports=m.AggregateError},27700:(i,s,u)=>{u(73381);var m=u(35703);i.exports=m("Function").bind},16246:(i,s,u)=>{var m=u(7046),v=u(27700),_=Function.prototype;i.exports=function(i){var s=i.bind;return i===_||m(_,i)&&s===_.bind?v:s}},45999:(i,s,u)=>{u(49221);var m=u(54058);i.exports=m.Object.assign},16121:(i,s,u)=>{i.exports=u(38644)},14122:(i,s,u)=>{i.exports=u(89097)},60269:(i,s,u)=>{i.exports=u(76936)},38644:(i,s,u)=>{u(89731);var m=u(44101);i.exports=m},89097:(i,s,u)=>{var m=u(90093);i.exports=m},76936:(i,s,u)=>{var m=u(65362);i.exports=m},24883:(i,s,u)=>{var m=u(57475),v=u(69826),_=TypeError;i.exports=function(i){if(m(i))return i;throw _(v(i)+" is not a function")}},11851:(i,s,u)=>{var m=u(57475),v=String,_=TypeError;i.exports=function(i){if("object"==typeof i||m(i))return i;throw _("Can't set "+v(i)+" as a prototype")}},18479:i=>{i.exports=function(){}},96059:(i,s,u)=>{var m=u(10941),v=String,_=TypeError;i.exports=function(i){if(m(i))return i;throw _(v(i)+" is not an object")}},31692:(i,s,u)=>{var m=u(74529),v=u(59413),_=u(10623),createMethod=function(i){return function(s,u,j){var M,$=m(s),W=_($),X=v(j,W);if(i&&u!=u){for(;W>X;)if((M=$[X++])!=M)return!0}else for(;W>X;X++)if((i||X in $)&&$[X]===u)return i||X||0;return!i&&-1}};i.exports={includes:createMethod(!0),indexOf:createMethod(!1)}},93765:(i,s,u)=>{var m=u(95329);i.exports=m([].slice)},82532:(i,s,u)=>{var m=u(95329),v=m({}.toString),_=m("".slice);i.exports=function(i){return _(v(i),8,-1)}},9697:(i,s,u)=>{var m=u(22885),v=u(57475),_=u(82532),j=u(99813)("toStringTag"),M=Object,$="Arguments"==_(function(){return arguments}());i.exports=m?_:function(i){var s,u,m;return void 0===i?"Undefined":null===i?"Null":"string"==typeof(u=function(i,s){try{return i[s]}catch(i){}}(s=M(i),j))?u:$?_(s):"Object"==(m=_(s))&&v(s.callee)?"Arguments":m}},23489:(i,s,u)=>{var m=u(90953),v=u(31136),_=u(49677),j=u(65988);i.exports=function(i,s,u){for(var M=v(s),$=j.f,W=_.f,X=0;X{var m=u(95981);i.exports=!m((function(){function F(){}return F.prototype.constructor=null,Object.getPrototypeOf(new F)!==F.prototype}))},23538:i=>{i.exports=function(i,s){return{value:i,done:s}}},32029:(i,s,u)=>{var m=u(55746),v=u(65988),_=u(31887);i.exports=m?function(i,s,u){return v.f(i,s,_(1,u))}:function(i,s,u){return i[s]=u,i}},31887:i=>{i.exports=function(i,s){return{enumerable:!(1&i),configurable:!(2&i),writable:!(4&i),value:s}}},95929:(i,s,u)=>{var m=u(32029);i.exports=function(i,s,u,v){return v&&v.enumerable?i[s]=u:m(i,s,u),i}},75609:(i,s,u)=>{var m=u(21899),v=Object.defineProperty;i.exports=function(i,s){try{v(m,i,{value:s,configurable:!0,writable:!0})}catch(u){m[i]=s}return s}},55746:(i,s,u)=>{var m=u(95981);i.exports=!m((function(){return 7!=Object.defineProperty({},1,{get:function(){return 7}})[1]}))},76616:i=>{var s="object"==typeof document&&document.all,u=void 0===s&&void 0!==s;i.exports={all:s,IS_HTMLDDA:u}},61333:(i,s,u)=>{var m=u(21899),v=u(10941),_=m.document,j=v(_)&&v(_.createElement);i.exports=function(i){return j?_.createElement(i):{}}},63281:i=>{i.exports={CSSRuleList:0,CSSStyleDeclaration:0,CSSValueList:0,ClientRectList:0,DOMRectList:0,DOMStringList:0,DOMTokenList:1,DataTransferItemList:0,FileList:0,HTMLAllCollection:0,HTMLCollection:0,HTMLFormElement:0,HTMLSelectElement:0,MediaList:0,MimeTypeArray:0,NamedNodeMap:0,NodeList:1,PaintRequestList:0,Plugin:0,PluginArray:0,SVGLengthList:0,SVGNumberList:0,SVGPathSegList:0,SVGPointList:0,SVGStringList:0,SVGTransformList:0,SourceBufferList:0,StyleSheetList:0,TextTrackCueList:0,TextTrackList:0,TouchList:0}},2861:i=>{i.exports="undefined"!=typeof navigator&&String(navigator.userAgent)||""},53385:(i,s,u)=>{var m,v,_=u(21899),j=u(2861),M=_.process,$=_.Deno,W=M&&M.versions||$&&$.version,X=W&&W.v8;X&&(v=(m=X.split("."))[0]>0&&m[0]<4?1:+(m[0]+m[1])),!v&&j&&(!(m=j.match(/Edge\/(\d+)/))||m[1]>=74)&&(m=j.match(/Chrome\/(\d+)/))&&(v=+m[1]),i.exports=v},35703:(i,s,u)=>{var m=u(54058);i.exports=function(i){return m[i+"Prototype"]}},56759:i=>{i.exports=["constructor","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","toLocaleString","toString","valueOf"]},53995:(i,s,u)=>{var m=u(95329),v=Error,_=m("".replace),j=String(v("zxcasd").stack),M=/\n\s*at [^:]*:[^\n]*/,$=M.test(j);i.exports=function(i,s){if($&&"string"==typeof i&&!v.prepareStackTrace)for(;s--;)i=_(i,M,"");return i}},79585:(i,s,u)=>{var m=u(32029),v=u(53995),_=u(18780),j=Error.captureStackTrace;i.exports=function(i,s,u,M){_&&(j?j(i,s):m(i,"stack",v(u,M)))}},18780:(i,s,u)=>{var m=u(95981),v=u(31887);i.exports=!m((function(){var i=Error("a");return!("stack"in i)||(Object.defineProperty(i,"stack",v(1,7)),7!==i.stack)}))},76887:(i,s,u)=>{"use strict";var m=u(21899),v=u(79730),_=u(97484),j=u(57475),M=u(49677).f,$=u(37252),W=u(54058),X=u(86843),Y=u(32029),Z=u(90953),wrapConstructor=function(i){var Wrapper=function(s,u,m){if(this instanceof Wrapper){switch(arguments.length){case 0:return new i;case 1:return new i(s);case 2:return new i(s,u)}return new i(s,u,m)}return v(i,this,arguments)};return Wrapper.prototype=i.prototype,Wrapper};i.exports=function(i,s){var u,v,ee,ie,ae,le,ce,pe,de,fe=i.target,ye=i.global,be=i.stat,_e=i.proto,we=ye?m:be?m[fe]:(m[fe]||{}).prototype,Se=ye?W:W[fe]||Y(W,fe,{})[fe],xe=Se.prototype;for(ie in s)v=!(u=$(ye?ie:fe+(be?".":"#")+ie,i.forced))&&we&&Z(we,ie),le=Se[ie],v&&(ce=i.dontCallGetSet?(de=M(we,ie))&&de.value:we[ie]),ae=v&&ce?ce:s[ie],v&&typeof le==typeof ae||(pe=i.bind&&v?X(ae,m):i.wrap&&v?wrapConstructor(ae):_e&&j(ae)?_(ae):ae,(i.sham||ae&&ae.sham||le&&le.sham)&&Y(pe,"sham",!0),Y(Se,ie,pe),_e&&(Z(W,ee=fe+"Prototype")||Y(W,ee,{}),Y(W[ee],ie,ae),i.real&&xe&&(u||!xe[ie])&&Y(xe,ie,ae)))}},95981:i=>{i.exports=function(i){try{return!!i()}catch(i){return!0}}},79730:(i,s,u)=>{var m=u(18285),v=Function.prototype,_=v.apply,j=v.call;i.exports="object"==typeof Reflect&&Reflect.apply||(m?j.bind(_):function(){return j.apply(_,arguments)})},86843:(i,s,u)=>{var m=u(97484),v=u(24883),_=u(18285),j=m(m.bind);i.exports=function(i,s){return v(i),void 0===s?i:_?j(i,s):function(){return i.apply(s,arguments)}}},18285:(i,s,u)=>{var m=u(95981);i.exports=!m((function(){var i=function(){}.bind();return"function"!=typeof i||i.hasOwnProperty("prototype")}))},98308:(i,s,u)=>{"use strict";var m=u(95329),v=u(24883),_=u(10941),j=u(90953),M=u(93765),$=u(18285),W=Function,X=m([].concat),Y=m([].join),Z={};i.exports=$?W.bind:function bind(i){var s=v(this),u=s.prototype,m=M(arguments,1),$=function bound(){var u=X(m,M(arguments));return this instanceof $?function(i,s,u){if(!j(Z,s)){for(var m=[],v=0;v{var m=u(18285),v=Function.prototype.call;i.exports=m?v.bind(v):function(){return v.apply(v,arguments)}},79417:(i,s,u)=>{var m=u(55746),v=u(90953),_=Function.prototype,j=m&&Object.getOwnPropertyDescriptor,M=v(_,"name"),$=M&&"something"===function something(){}.name,W=M&&(!m||m&&j(_,"name").configurable);i.exports={EXISTS:M,PROPER:$,CONFIGURABLE:W}},45526:(i,s,u)=>{var m=u(95329),v=u(24883);i.exports=function(i,s,u){try{return m(v(Object.getOwnPropertyDescriptor(i,s)[u]))}catch(i){}}},97484:(i,s,u)=>{var m=u(82532),v=u(95329);i.exports=function(i){if("Function"===m(i))return v(i)}},95329:(i,s,u)=>{var m=u(18285),v=Function.prototype,_=v.call,j=m&&v.bind.bind(_,_);i.exports=m?j:function(i){return function(){return _.apply(i,arguments)}}},626:(i,s,u)=>{var m=u(54058),v=u(21899),_=u(57475),aFunction=function(i){return _(i)?i:void 0};i.exports=function(i,s){return arguments.length<2?aFunction(m[i])||aFunction(v[i]):m[i]&&m[i][s]||v[i]&&v[i][s]}},22902:(i,s,u)=>{var m=u(9697),v=u(14229),_=u(82119),j=u(12077),M=u(99813)("iterator");i.exports=function(i){if(!_(i))return v(i,M)||v(i,"@@iterator")||j[m(i)]}},53476:(i,s,u)=>{var m=u(78834),v=u(24883),_=u(96059),j=u(69826),M=u(22902),$=TypeError;i.exports=function(i,s){var u=arguments.length<2?M(i):s;if(v(u))return _(m(u,i));throw $(j(i)+" is not iterable")}},14229:(i,s,u)=>{var m=u(24883),v=u(82119);i.exports=function(i,s){var u=i[s];return v(u)?void 0:m(u)}},21899:function(i,s,u){var check=function(i){return i&&i.Math==Math&&i};i.exports=check("object"==typeof globalThis&&globalThis)||check("object"==typeof window&&window)||check("object"==typeof self&&self)||check("object"==typeof u.g&&u.g)||function(){return this}()||this||Function("return this")()},90953:(i,s,u)=>{var m=u(95329),v=u(89678),_=m({}.hasOwnProperty);i.exports=Object.hasOwn||function hasOwn(i,s){return _(v(i),s)}},27748:i=>{i.exports={}},15463:(i,s,u)=>{var m=u(626);i.exports=m("document","documentElement")},2840:(i,s,u)=>{var m=u(55746),v=u(95981),_=u(61333);i.exports=!m&&!v((function(){return 7!=Object.defineProperty(_("div"),"a",{get:function(){return 7}}).a}))},37026:(i,s,u)=>{var m=u(95329),v=u(95981),_=u(82532),j=Object,M=m("".split);i.exports=v((function(){return!j("z").propertyIsEnumerable(0)}))?function(i){return"String"==_(i)?M(i,""):j(i)}:j},70926:(i,s,u)=>{var m=u(57475),v=u(10941),_=u(88929);i.exports=function(i,s,u){var j,M;return _&&m(j=s.constructor)&&j!==u&&v(M=j.prototype)&&M!==u.prototype&&_(i,M),i}},53794:(i,s,u)=>{var m=u(10941),v=u(32029);i.exports=function(i,s){m(s)&&"cause"in s&&v(i,"cause",s.cause)}},45402:(i,s,u)=>{var m,v,_,j=u(47093),M=u(21899),$=u(10941),W=u(32029),X=u(90953),Y=u(63030),Z=u(44262),ee=u(27748),ie="Object already initialized",ae=M.TypeError,le=M.WeakMap;if(j||Y.state){var ce=Y.state||(Y.state=new le);ce.get=ce.get,ce.has=ce.has,ce.set=ce.set,m=function(i,s){if(ce.has(i))throw ae(ie);return s.facade=i,ce.set(i,s),s},v=function(i){return ce.get(i)||{}},_=function(i){return ce.has(i)}}else{var pe=Z("state");ee[pe]=!0,m=function(i,s){if(X(i,pe))throw ae(ie);return s.facade=i,W(i,pe,s),s},v=function(i){return X(i,pe)?i[pe]:{}},_=function(i){return X(i,pe)}}i.exports={set:m,get:v,has:_,enforce:function(i){return _(i)?v(i):m(i,{})},getterFor:function(i){return function(s){var u;if(!$(s)||(u=v(s)).type!==i)throw ae("Incompatible receiver, "+i+" required");return u}}}},6782:(i,s,u)=>{var m=u(99813),v=u(12077),_=m("iterator"),j=Array.prototype;i.exports=function(i){return void 0!==i&&(v.Array===i||j[_]===i)}},57475:(i,s,u)=>{var m=u(76616),v=m.all;i.exports=m.IS_HTMLDDA?function(i){return"function"==typeof i||i===v}:function(i){return"function"==typeof i}},37252:(i,s,u)=>{var m=u(95981),v=u(57475),_=/#|\.prototype\./,isForced=function(i,s){var u=M[j(i)];return u==W||u!=$&&(v(s)?m(s):!!s)},j=isForced.normalize=function(i){return String(i).replace(_,".").toLowerCase()},M=isForced.data={},$=isForced.NATIVE="N",W=isForced.POLYFILL="P";i.exports=isForced},82119:i=>{i.exports=function(i){return null==i}},10941:(i,s,u)=>{var m=u(57475),v=u(76616),_=v.all;i.exports=v.IS_HTMLDDA?function(i){return"object"==typeof i?null!==i:m(i)||i===_}:function(i){return"object"==typeof i?null!==i:m(i)}},82529:i=>{i.exports=!0},56664:(i,s,u)=>{var m=u(626),v=u(57475),_=u(7046),j=u(32302),M=Object;i.exports=j?function(i){return"symbol"==typeof i}:function(i){var s=m("Symbol");return v(s)&&_(s.prototype,M(i))}},93091:(i,s,u)=>{var m=u(86843),v=u(78834),_=u(96059),j=u(69826),M=u(6782),$=u(10623),W=u(7046),X=u(53476),Y=u(22902),Z=u(7609),ee=TypeError,Result=function(i,s){this.stopped=i,this.result=s},ie=Result.prototype;i.exports=function(i,s,u){var ae,le,ce,pe,de,fe,ye,be=u&&u.that,_e=!(!u||!u.AS_ENTRIES),we=!(!u||!u.IS_RECORD),Se=!(!u||!u.IS_ITERATOR),xe=!(!u||!u.INTERRUPTED),Ie=m(s,be),stop=function(i){return ae&&Z(ae,"normal",i),new Result(!0,i)},callFn=function(i){return _e?(_(i),xe?Ie(i[0],i[1],stop):Ie(i[0],i[1])):xe?Ie(i,stop):Ie(i)};if(we)ae=i.iterator;else if(Se)ae=i;else{if(!(le=Y(i)))throw ee(j(i)+" is not iterable");if(M(le)){for(ce=0,pe=$(i);pe>ce;ce++)if((de=callFn(i[ce]))&&W(ie,de))return de;return new Result(!1)}ae=X(i,le)}for(fe=we?i.next:ae.next;!(ye=v(fe,ae)).done;){try{de=callFn(ye.value)}catch(i){Z(ae,"throw",i)}if("object"==typeof de&&de&&W(ie,de))return de}return new Result(!1)}},7609:(i,s,u)=>{var m=u(78834),v=u(96059),_=u(14229);i.exports=function(i,s,u){var j,M;v(i);try{if(!(j=_(i,"return"))){if("throw"===s)throw u;return u}j=m(j,i)}catch(i){M=!0,j=i}if("throw"===s)throw u;if(M)throw j;return v(j),u}},53847:(i,s,u)=>{"use strict";var m=u(35143).IteratorPrototype,v=u(29290),_=u(31887),j=u(90904),M=u(12077),returnThis=function(){return this};i.exports=function(i,s,u,$){var W=s+" Iterator";return i.prototype=v(m,{next:_(+!$,u)}),j(i,W,!1,!0),M[W]=returnThis,i}},75105:(i,s,u)=>{"use strict";var m=u(76887),v=u(78834),_=u(82529),j=u(79417),M=u(57475),$=u(53847),W=u(249),X=u(88929),Y=u(90904),Z=u(32029),ee=u(95929),ie=u(99813),ae=u(12077),le=u(35143),ce=j.PROPER,pe=j.CONFIGURABLE,de=le.IteratorPrototype,fe=le.BUGGY_SAFARI_ITERATORS,ye=ie("iterator"),be="keys",_e="values",we="entries",returnThis=function(){return this};i.exports=function(i,s,u,j,ie,le,Se){$(u,s,j);var xe,Ie,Pe,getIterationMethod=function(i){if(i===ie&&Ve)return Ve;if(!fe&&i in qe)return qe[i];switch(i){case be:return function keys(){return new u(this,i)};case _e:return function values(){return new u(this,i)};case we:return function entries(){return new u(this,i)}}return function(){return new u(this)}},Te=s+" Iterator",Re=!1,qe=i.prototype,ze=qe[ye]||qe["@@iterator"]||ie&&qe[ie],Ve=!fe&&ze||getIterationMethod(ie),We="Array"==s&&qe.entries||ze;if(We&&(xe=W(We.call(new i)))!==Object.prototype&&xe.next&&(_||W(xe)===de||(X?X(xe,de):M(xe[ye])||ee(xe,ye,returnThis)),Y(xe,Te,!0,!0),_&&(ae[Te]=returnThis)),ce&&ie==_e&&ze&&ze.name!==_e&&(!_&&pe?Z(qe,"name",_e):(Re=!0,Ve=function values(){return v(ze,this)})),ie)if(Ie={values:getIterationMethod(_e),keys:le?Ve:getIterationMethod(be),entries:getIterationMethod(we)},Se)for(Pe in Ie)(fe||Re||!(Pe in qe))&&ee(qe,Pe,Ie[Pe]);else m({target:s,proto:!0,forced:fe||Re},Ie);return _&&!Se||qe[ye]===Ve||ee(qe,ye,Ve,{name:ie}),ae[s]=Ve,Ie}},35143:(i,s,u)=>{"use strict";var m,v,_,j=u(95981),M=u(57475),$=u(10941),W=u(29290),X=u(249),Y=u(95929),Z=u(99813),ee=u(82529),ie=Z("iterator"),ae=!1;[].keys&&("next"in(_=[].keys())?(v=X(X(_)))!==Object.prototype&&(m=v):ae=!0),!$(m)||j((function(){var i={};return m[ie].call(i)!==i}))?m={}:ee&&(m=W(m)),M(m[ie])||Y(m,ie,(function(){return this})),i.exports={IteratorPrototype:m,BUGGY_SAFARI_ITERATORS:ae}},12077:i=>{i.exports={}},10623:(i,s,u)=>{var m=u(43057);i.exports=function(i){return m(i.length)}},35331:i=>{var s=Math.ceil,u=Math.floor;i.exports=Math.trunc||function trunc(i){var m=+i;return(m>0?u:s)(m)}},14649:(i,s,u)=>{var m=u(85803);i.exports=function(i,s){return void 0===i?arguments.length<2?"":s:m(i)}},24420:(i,s,u)=>{"use strict";var m=u(55746),v=u(95329),_=u(78834),j=u(95981),M=u(14771),$=u(87857),W=u(36760),X=u(89678),Y=u(37026),Z=Object.assign,ee=Object.defineProperty,ie=v([].concat);i.exports=!Z||j((function(){if(m&&1!==Z({b:1},Z(ee({},"a",{enumerable:!0,get:function(){ee(this,"b",{value:3,enumerable:!1})}}),{b:2})).b)return!0;var i={},s={},u=Symbol(),v="abcdefghijklmnopqrst";return i[u]=7,v.split("").forEach((function(i){s[i]=i})),7!=Z({},i)[u]||M(Z({},s)).join("")!=v}))?function assign(i,s){for(var u=X(i),v=arguments.length,j=1,Z=$.f,ee=W.f;v>j;)for(var ae,le=Y(arguments[j++]),ce=Z?ie(M(le),Z(le)):M(le),pe=ce.length,de=0;pe>de;)ae=ce[de++],m&&!_(ee,le,ae)||(u[ae]=le[ae]);return u}:Z},29290:(i,s,u)=>{var m,v=u(96059),_=u(59938),j=u(56759),M=u(27748),$=u(15463),W=u(61333),X=u(44262),Y="prototype",Z="script",ee=X("IE_PROTO"),EmptyConstructor=function(){},scriptTag=function(i){return"<"+Z+">"+i+""},NullProtoObjectViaActiveX=function(i){i.write(scriptTag("")),i.close();var s=i.parentWindow.Object;return i=null,s},NullProtoObject=function(){try{m=new ActiveXObject("htmlfile")}catch(i){}var i,s,u;NullProtoObject="undefined"!=typeof document?document.domain&&m?NullProtoObjectViaActiveX(m):(s=W("iframe"),u="java"+Z+":",s.style.display="none",$.appendChild(s),s.src=String(u),(i=s.contentWindow.document).open(),i.write(scriptTag("document.F=Object")),i.close(),i.F):NullProtoObjectViaActiveX(m);for(var v=j.length;v--;)delete NullProtoObject[Y][j[v]];return NullProtoObject()};M[ee]=!0,i.exports=Object.create||function create(i,s){var u;return null!==i?(EmptyConstructor[Y]=v(i),u=new EmptyConstructor,EmptyConstructor[Y]=null,u[ee]=i):u=NullProtoObject(),void 0===s?u:_.f(u,s)}},59938:(i,s,u)=>{var m=u(55746),v=u(83937),_=u(65988),j=u(96059),M=u(74529),$=u(14771);s.f=m&&!v?Object.defineProperties:function defineProperties(i,s){j(i);for(var u,m=M(s),v=$(s),W=v.length,X=0;W>X;)_.f(i,u=v[X++],m[u]);return i}},65988:(i,s,u)=>{var m=u(55746),v=u(2840),_=u(83937),j=u(96059),M=u(83894),$=TypeError,W=Object.defineProperty,X=Object.getOwnPropertyDescriptor,Y="enumerable",Z="configurable",ee="writable";s.f=m?_?function defineProperty(i,s,u){if(j(i),s=M(s),j(u),"function"==typeof i&&"prototype"===s&&"value"in u&&ee in u&&!u[ee]){var m=X(i,s);m&&m[ee]&&(i[s]=u.value,u={configurable:Z in u?u[Z]:m[Z],enumerable:Y in u?u[Y]:m[Y],writable:!1})}return W(i,s,u)}:W:function defineProperty(i,s,u){if(j(i),s=M(s),j(u),v)try{return W(i,s,u)}catch(i){}if("get"in u||"set"in u)throw $("Accessors not supported");return"value"in u&&(i[s]=u.value),i}},49677:(i,s,u)=>{var m=u(55746),v=u(78834),_=u(36760),j=u(31887),M=u(74529),$=u(83894),W=u(90953),X=u(2840),Y=Object.getOwnPropertyDescriptor;s.f=m?Y:function getOwnPropertyDescriptor(i,s){if(i=M(i),s=$(s),X)try{return Y(i,s)}catch(i){}if(W(i,s))return j(!v(_.f,i,s),i[s])}},10946:(i,s,u)=>{var m=u(55629),v=u(56759).concat("length","prototype");s.f=Object.getOwnPropertyNames||function getOwnPropertyNames(i){return m(i,v)}},87857:(i,s)=>{s.f=Object.getOwnPropertySymbols},249:(i,s,u)=>{var m=u(90953),v=u(57475),_=u(89678),j=u(44262),M=u(91310),$=j("IE_PROTO"),W=Object,X=W.prototype;i.exports=M?W.getPrototypeOf:function(i){var s=_(i);if(m(s,$))return s[$];var u=s.constructor;return v(u)&&s instanceof u?u.prototype:s instanceof W?X:null}},7046:(i,s,u)=>{var m=u(95329);i.exports=m({}.isPrototypeOf)},55629:(i,s,u)=>{var m=u(95329),v=u(90953),_=u(74529),j=u(31692).indexOf,M=u(27748),$=m([].push);i.exports=function(i,s){var u,m=_(i),W=0,X=[];for(u in m)!v(M,u)&&v(m,u)&&$(X,u);for(;s.length>W;)v(m,u=s[W++])&&(~j(X,u)||$(X,u));return X}},14771:(i,s,u)=>{var m=u(55629),v=u(56759);i.exports=Object.keys||function keys(i){return m(i,v)}},36760:(i,s)=>{"use strict";var u={}.propertyIsEnumerable,m=Object.getOwnPropertyDescriptor,v=m&&!u.call({1:2},1);s.f=v?function propertyIsEnumerable(i){var s=m(this,i);return!!s&&s.enumerable}:u},88929:(i,s,u)=>{var m=u(45526),v=u(96059),_=u(11851);i.exports=Object.setPrototypeOf||("__proto__"in{}?function(){var i,s=!1,u={};try{(i=m(Object.prototype,"__proto__","set"))(u,[]),s=u instanceof Array}catch(i){}return function setPrototypeOf(u,m){return v(u),_(m),s?i(u,m):u.__proto__=m,u}}():void 0)},95623:(i,s,u)=>{"use strict";var m=u(22885),v=u(9697);i.exports=m?{}.toString:function toString(){return"[object "+v(this)+"]"}},39811:(i,s,u)=>{var m=u(78834),v=u(57475),_=u(10941),j=TypeError;i.exports=function(i,s){var u,M;if("string"===s&&v(u=i.toString)&&!_(M=m(u,i)))return M;if(v(u=i.valueOf)&&!_(M=m(u,i)))return M;if("string"!==s&&v(u=i.toString)&&!_(M=m(u,i)))return M;throw j("Can't convert object to primitive value")}},31136:(i,s,u)=>{var m=u(626),v=u(95329),_=u(10946),j=u(87857),M=u(96059),$=v([].concat);i.exports=m("Reflect","ownKeys")||function ownKeys(i){var s=_.f(M(i)),u=j.f;return u?$(s,u(i)):s}},54058:i=>{i.exports={}},9056:(i,s,u)=>{var m=u(65988).f;i.exports=function(i,s,u){u in i||m(i,u,{configurable:!0,get:function(){return s[u]},set:function(i){s[u]=i}})}},48219:(i,s,u)=>{var m=u(82119),v=TypeError;i.exports=function(i){if(m(i))throw v("Can't call method on "+i);return i}},90904:(i,s,u)=>{var m=u(22885),v=u(65988).f,_=u(32029),j=u(90953),M=u(95623),$=u(99813)("toStringTag");i.exports=function(i,s,u,W){if(i){var X=u?i:i.prototype;j(X,$)||v(X,$,{configurable:!0,value:s}),W&&!m&&_(X,"toString",M)}}},44262:(i,s,u)=>{var m=u(68726),v=u(99418),_=m("keys");i.exports=function(i){return _[i]||(_[i]=v(i))}},63030:(i,s,u)=>{var m=u(21899),v=u(75609),_="__core-js_shared__",j=m[_]||v(_,{});i.exports=j},68726:(i,s,u)=>{var m=u(82529),v=u(63030);(i.exports=function(i,s){return v[i]||(v[i]=void 0!==s?s:{})})("versions",[]).push({version:"3.31.1",mode:m?"pure":"global",copyright:"© 2014-2023 Denis Pushkarev (zloirock.ru)",license:"https://github.com/zloirock/core-js/blob/v3.31.1/LICENSE",source:"https://github.com/zloirock/core-js"})},64620:(i,s,u)=>{var m=u(95329),v=u(62435),_=u(85803),j=u(48219),M=m("".charAt),$=m("".charCodeAt),W=m("".slice),createMethod=function(i){return function(s,u){var m,X,Y=_(j(s)),Z=v(u),ee=Y.length;return Z<0||Z>=ee?i?"":void 0:(m=$(Y,Z))<55296||m>56319||Z+1===ee||(X=$(Y,Z+1))<56320||X>57343?i?M(Y,Z):m:i?W(Y,Z,Z+2):X-56320+(m-55296<<10)+65536}};i.exports={codeAt:createMethod(!1),charAt:createMethod(!0)}},63405:(i,s,u)=>{var m=u(53385),v=u(95981),_=u(21899).String;i.exports=!!Object.getOwnPropertySymbols&&!v((function(){var i=Symbol();return!_(i)||!(Object(i)instanceof Symbol)||!Symbol.sham&&m&&m<41}))},59413:(i,s,u)=>{var m=u(62435),v=Math.max,_=Math.min;i.exports=function(i,s){var u=m(i);return u<0?v(u+s,0):_(u,s)}},74529:(i,s,u)=>{var m=u(37026),v=u(48219);i.exports=function(i){return m(v(i))}},62435:(i,s,u)=>{var m=u(35331);i.exports=function(i){var s=+i;return s!=s||0===s?0:m(s)}},43057:(i,s,u)=>{var m=u(62435),v=Math.min;i.exports=function(i){return i>0?v(m(i),9007199254740991):0}},89678:(i,s,u)=>{var m=u(48219),v=Object;i.exports=function(i){return v(m(i))}},46935:(i,s,u)=>{var m=u(78834),v=u(10941),_=u(56664),j=u(14229),M=u(39811),$=u(99813),W=TypeError,X=$("toPrimitive");i.exports=function(i,s){if(!v(i)||_(i))return i;var u,$=j(i,X);if($){if(void 0===s&&(s="default"),u=m($,i,s),!v(u)||_(u))return u;throw W("Can't convert object to primitive value")}return void 0===s&&(s="number"),M(i,s)}},83894:(i,s,u)=>{var m=u(46935),v=u(56664);i.exports=function(i){var s=m(i,"string");return v(s)?s:s+""}},22885:(i,s,u)=>{var m={};m[u(99813)("toStringTag")]="z",i.exports="[object z]"===String(m)},85803:(i,s,u)=>{var m=u(9697),v=String;i.exports=function(i){if("Symbol"===m(i))throw TypeError("Cannot convert a Symbol value to a string");return v(i)}},69826:i=>{var s=String;i.exports=function(i){try{return s(i)}catch(i){return"Object"}}},99418:(i,s,u)=>{var m=u(95329),v=0,_=Math.random(),j=m(1..toString);i.exports=function(i){return"Symbol("+(void 0===i?"":i)+")_"+j(++v+_,36)}},32302:(i,s,u)=>{var m=u(63405);i.exports=m&&!Symbol.sham&&"symbol"==typeof Symbol.iterator},83937:(i,s,u)=>{var m=u(55746),v=u(95981);i.exports=m&&v((function(){return 42!=Object.defineProperty((function(){}),"prototype",{value:42,writable:!1}).prototype}))},47093:(i,s,u)=>{var m=u(21899),v=u(57475),_=m.WeakMap;i.exports=v(_)&&/native code/.test(String(_))},99813:(i,s,u)=>{var m=u(21899),v=u(68726),_=u(90953),j=u(99418),M=u(63405),$=u(32302),W=m.Symbol,X=v("wks"),Y=$?W.for||W:W&&W.withoutSetter||j;i.exports=function(i){return _(X,i)||(X[i]=M&&_(W,i)?W[i]:Y("Symbol."+i)),X[i]}},62864:(i,s,u)=>{"use strict";var m=u(626),v=u(90953),_=u(32029),j=u(7046),M=u(88929),$=u(23489),W=u(9056),X=u(70926),Y=u(14649),Z=u(53794),ee=u(79585),ie=u(55746),ae=u(82529);i.exports=function(i,s,u,le){var ce="stackTraceLimit",pe=le?2:1,de=i.split("."),fe=de[de.length-1],ye=m.apply(null,de);if(ye){var be=ye.prototype;if(!ae&&v(be,"cause")&&delete be.cause,!u)return ye;var _e=m("Error"),we=s((function(i,s){var u=Y(le?s:i,void 0),m=le?new ye(i):new ye;return void 0!==u&&_(m,"message",u),ee(m,we,m.stack,2),this&&j(be,this)&&X(m,this,we),arguments.length>pe&&Z(m,arguments[pe]),m}));if(we.prototype=be,"Error"!==fe?M?M(we,_e):$(we,_e,{name:!0}):ie&&ce in ye&&(W(we,ye,ce),W(we,ye,"prepareStackTrace")),$(we,ye),!ae)try{be.name!==fe&&_(be,"name",fe),be.constructor=we}catch(i){}return we}}},24415:(i,s,u)=>{var m=u(76887),v=u(626),_=u(79730),j=u(95981),M=u(62864),$="AggregateError",W=v($),X=!j((function(){return 1!==W([1]).errors[0]}))&&j((function(){return 7!==W([1],$,{cause:7}).cause}));m({global:!0,constructor:!0,arity:2,forced:X},{AggregateError:M($,(function(i){return function AggregateError(s,u){return _(i,this,arguments)}}),X,!0)})},49812:(i,s,u)=>{"use strict";var m=u(76887),v=u(7046),_=u(249),j=u(88929),M=u(23489),$=u(29290),W=u(32029),X=u(31887),Y=u(53794),Z=u(79585),ee=u(93091),ie=u(14649),ae=u(99813)("toStringTag"),le=Error,ce=[].push,pe=function AggregateError(i,s){var u,m=v(de,this);j?u=j(le(),m?_(this):de):(u=m?this:$(de),W(u,ae,"Error")),void 0!==s&&W(u,"message",ie(s)),Z(u,pe,u.stack,1),arguments.length>2&&Y(u,arguments[2]);var M=[];return ee(i,ce,{that:M}),W(u,"errors",M),u};j?j(pe,le):M(pe,le,{name:!0});var de=pe.prototype=$(le.prototype,{constructor:X(1,pe),message:X(1,""),name:X(1,"AggregateError")});m({global:!0,constructor:!0,arity:2},{AggregateError:pe})},47627:(i,s,u)=>{u(49812)},66274:(i,s,u)=>{"use strict";var m=u(74529),v=u(18479),_=u(12077),j=u(45402),M=u(65988).f,$=u(75105),W=u(23538),X=u(82529),Y=u(55746),Z="Array Iterator",ee=j.set,ie=j.getterFor(Z);i.exports=$(Array,"Array",(function(i,s){ee(this,{type:Z,target:m(i),index:0,kind:s})}),(function(){var i=ie(this),s=i.target,u=i.kind,m=i.index++;return!s||m>=s.length?(i.target=void 0,W(void 0,!0)):W("keys"==u?m:"values"==u?s[m]:[m,s[m]],!1)}),"values");var ae=_.Arguments=_.Array;if(v("keys"),v("values"),v("entries"),!X&&Y&&"values"!==ae.name)try{M(ae,"name",{value:"values"})}catch(i){}},61181:(i,s,u)=>{var m=u(76887),v=u(21899),_=u(79730),j=u(62864),M="WebAssembly",$=v[M],W=7!==Error("e",{cause:7}).cause,exportGlobalErrorCauseWrapper=function(i,s){var u={};u[i]=j(i,s,W),m({global:!0,constructor:!0,arity:1,forced:W},u)},exportWebAssemblyErrorCauseWrapper=function(i,s){if($&&$[i]){var u={};u[i]=j(M+"."+i,s,W),m({target:M,stat:!0,constructor:!0,arity:1,forced:W},u)}};exportGlobalErrorCauseWrapper("Error",(function(i){return function Error(s){return _(i,this,arguments)}})),exportGlobalErrorCauseWrapper("EvalError",(function(i){return function EvalError(s){return _(i,this,arguments)}})),exportGlobalErrorCauseWrapper("RangeError",(function(i){return function RangeError(s){return _(i,this,arguments)}})),exportGlobalErrorCauseWrapper("ReferenceError",(function(i){return function ReferenceError(s){return _(i,this,arguments)}})),exportGlobalErrorCauseWrapper("SyntaxError",(function(i){return function SyntaxError(s){return _(i,this,arguments)}})),exportGlobalErrorCauseWrapper("TypeError",(function(i){return function TypeError(s){return _(i,this,arguments)}})),exportGlobalErrorCauseWrapper("URIError",(function(i){return function URIError(s){return _(i,this,arguments)}})),exportWebAssemblyErrorCauseWrapper("CompileError",(function(i){return function CompileError(s){return _(i,this,arguments)}})),exportWebAssemblyErrorCauseWrapper("LinkError",(function(i){return function LinkError(s){return _(i,this,arguments)}})),exportWebAssemblyErrorCauseWrapper("RuntimeError",(function(i){return function RuntimeError(s){return _(i,this,arguments)}}))},73381:(i,s,u)=>{var m=u(76887),v=u(98308);m({target:"Function",proto:!0,forced:Function.bind!==v},{bind:v})},49221:(i,s,u)=>{var m=u(76887),v=u(24420);m({target:"Object",stat:!0,arity:2,forced:Object.assign!==v},{assign:v})},77971:(i,s,u)=>{"use strict";var m=u(64620).charAt,v=u(85803),_=u(45402),j=u(75105),M=u(23538),$="String Iterator",W=_.set,X=_.getterFor($);j(String,"String",(function(i){W(this,{type:$,string:v(i),index:0})}),(function next(){var i,s=X(this),u=s.string,v=s.index;return v>=u.length?M(void 0,!0):(i=m(u,v),s.index+=i.length,M(i,!1))}))},89731:(i,s,u)=>{u(47627)},7634:(i,s,u)=>{u(66274);var m=u(63281),v=u(21899),_=u(9697),j=u(32029),M=u(12077),$=u(99813)("toStringTag");for(var W in m){var X=v[W],Y=X&&X.prototype;Y&&_(Y)!==$&&j(Y,$,W),M[W]=M.Array}},18957:(i,s,u)=>{u(89731);var m=u(50415);u(7634),i.exports=m},28196:(i,s,u)=>{var m=u(16246);i.exports=m},63383:(i,s,u)=>{var m=u(45999);i.exports=m},8269:function(i,s,u){var m;m=void 0!==u.g?u.g:this,i.exports=function(i){if(i.CSS&&i.CSS.escape)return i.CSS.escape;var cssEscape=function(i){if(0==arguments.length)throw new TypeError("`CSS.escape` requires an argument.");for(var s,u=String(i),m=u.length,v=-1,_="",j=u.charCodeAt(0);++v=1&&s<=31||127==s||0==v&&s>=48&&s<=57||1==v&&s>=48&&s<=57&&45==j?"\\"+s.toString(16)+" ":0==v&&1==m&&45==s||!(s>=128||45==s||95==s||s>=48&&s<=57||s>=65&&s<=90||s>=97&&s<=122)?"\\"+u.charAt(v):u.charAt(v):_+="�";return _};return i.CSS||(i.CSS={}),i.CSS.escape=cssEscape,cssEscape}(m)},27698:(i,s,u)=>{"use strict";var m=u(48764).Buffer;function isSpecificValue(i){return i instanceof m||i instanceof Date||i instanceof RegExp}function cloneSpecificValue(i){if(i instanceof m){var s=m.alloc?m.alloc(i.length):new m(i.length);return i.copy(s),s}if(i instanceof Date)return new Date(i.getTime());if(i instanceof RegExp)return new RegExp(i);throw new Error("Unexpected situation")}function deepCloneArray(i){var s=[];return i.forEach((function(i,u){"object"==typeof i&&null!==i?Array.isArray(i)?s[u]=deepCloneArray(i):isSpecificValue(i)?s[u]=cloneSpecificValue(i):s[u]=v({},i):s[u]=i})),s}function safeGetProperty(i,s){return"__proto__"===s?void 0:i[s]}var v=i.exports=function(){if(arguments.length<1||"object"!=typeof arguments[0])return!1;if(arguments.length<2)return arguments[0];var i,s,u=arguments[0];return Array.prototype.slice.call(arguments,1).forEach((function(m){"object"!=typeof m||null===m||Array.isArray(m)||Object.keys(m).forEach((function(_){return s=safeGetProperty(u,_),(i=safeGetProperty(m,_))===u?void 0:"object"!=typeof i||null===i?void(u[_]=i):Array.isArray(i)?void(u[_]=deepCloneArray(i)):isSpecificValue(i)?void(u[_]=cloneSpecificValue(i)):"object"!=typeof s||null===s||Array.isArray(s)?void(u[_]=v({},i)):void(u[_]=v(s,i))}))})),u}},9996:i=>{"use strict";var s=function isMergeableObject(i){return function isNonNullObject(i){return!!i&&"object"==typeof i}(i)&&!function isSpecial(i){var s=Object.prototype.toString.call(i);return"[object RegExp]"===s||"[object Date]"===s||function isReactElement(i){return i.$$typeof===u}(i)}(i)};var u="function"==typeof Symbol&&Symbol.for?Symbol.for("react.element"):60103;function cloneUnlessOtherwiseSpecified(i,s){return!1!==s.clone&&s.isMergeableObject(i)?deepmerge(function emptyTarget(i){return Array.isArray(i)?[]:{}}(i),i,s):i}function defaultArrayMerge(i,s,u){return i.concat(s).map((function(i){return cloneUnlessOtherwiseSpecified(i,u)}))}function getKeys(i){return Object.keys(i).concat(function getEnumerableOwnPropertySymbols(i){return Object.getOwnPropertySymbols?Object.getOwnPropertySymbols(i).filter((function(s){return Object.propertyIsEnumerable.call(i,s)})):[]}(i))}function propertyIsOnObject(i,s){try{return s in i}catch(i){return!1}}function mergeObject(i,s,u){var m={};return u.isMergeableObject(i)&&getKeys(i).forEach((function(s){m[s]=cloneUnlessOtherwiseSpecified(i[s],u)})),getKeys(s).forEach((function(v){(function propertyIsUnsafe(i,s){return propertyIsOnObject(i,s)&&!(Object.hasOwnProperty.call(i,s)&&Object.propertyIsEnumerable.call(i,s))})(i,v)||(propertyIsOnObject(i,v)&&u.isMergeableObject(s[v])?m[v]=function getMergeFunction(i,s){if(!s.customMerge)return deepmerge;var u=s.customMerge(i);return"function"==typeof u?u:deepmerge}(v,u)(i[v],s[v],u):m[v]=cloneUnlessOtherwiseSpecified(s[v],u))})),m}function deepmerge(i,u,m){(m=m||{}).arrayMerge=m.arrayMerge||defaultArrayMerge,m.isMergeableObject=m.isMergeableObject||s,m.cloneUnlessOtherwiseSpecified=cloneUnlessOtherwiseSpecified;var v=Array.isArray(u);return v===Array.isArray(i)?v?m.arrayMerge(i,u,m):mergeObject(i,u,m):cloneUnlessOtherwiseSpecified(u,m)}deepmerge.all=function deepmergeAll(i,s){if(!Array.isArray(i))throw new Error("first argument should be an array");return i.reduce((function(i,u){return deepmerge(i,u,s)}),{})};var m=deepmerge;i.exports=m},27856:function(i){i.exports=function(){"use strict";const{entries:i,setPrototypeOf:s,isFrozen:u,getPrototypeOf:m,getOwnPropertyDescriptor:v}=Object;let{freeze:_,seal:j,create:M}=Object,{apply:$,construct:W}="undefined"!=typeof Reflect&&Reflect;_||(_=function freeze(i){return i}),j||(j=function seal(i){return i}),$||($=function apply(i,s,u){return i.apply(s,u)}),W||(W=function construct(i,s){return new i(...s)});const X=unapply(Array.prototype.forEach),Y=unapply(Array.prototype.pop),Z=unapply(Array.prototype.push),ee=unapply(String.prototype.toLowerCase),ie=unapply(String.prototype.toString),ae=unapply(String.prototype.match),le=unapply(String.prototype.replace),ce=unapply(String.prototype.indexOf),pe=unapply(String.prototype.trim),de=unapply(RegExp.prototype.test),fe=unconstruct(TypeError);function unapply(i){return function(s){for(var u=arguments.length,m=new Array(u>1?u-1:0),v=1;v2&&void 0!==arguments[2]?arguments[2]:ee;s&&s(i,null);let _=m.length;for(;_--;){let s=m[_];if("string"==typeof s){const i=v(s);i!==s&&(u(m)||(m[_]=i),s=i)}i[s]=!0}return i}function clone(s){const u=M(null);for(const[m,_]of i(s))void 0!==v(s,m)&&(u[m]=_);return u}function lookupGetter(i,s){for(;null!==i;){const u=v(i,s);if(u){if(u.get)return unapply(u.get);if("function"==typeof u.value)return unapply(u.value)}i=m(i)}function fallbackValue(i){return console.warn("fallback value for",i),null}return fallbackValue}const ye=_(["a","abbr","acronym","address","area","article","aside","audio","b","bdi","bdo","big","blink","blockquote","body","br","button","canvas","caption","center","cite","code","col","colgroup","content","data","datalist","dd","decorator","del","details","dfn","dialog","dir","div","dl","dt","element","em","fieldset","figcaption","figure","font","footer","form","h1","h2","h3","h4","h5","h6","head","header","hgroup","hr","html","i","img","input","ins","kbd","label","legend","li","main","map","mark","marquee","menu","menuitem","meter","nav","nobr","ol","optgroup","option","output","p","picture","pre","progress","q","rp","rt","ruby","s","samp","section","select","shadow","small","source","spacer","span","strike","strong","style","sub","summary","sup","table","tbody","td","template","textarea","tfoot","th","thead","time","tr","track","tt","u","ul","var","video","wbr"]),be=_(["svg","a","altglyph","altglyphdef","altglyphitem","animatecolor","animatemotion","animatetransform","circle","clippath","defs","desc","ellipse","filter","font","g","glyph","glyphref","hkern","image","line","lineargradient","marker","mask","metadata","mpath","path","pattern","polygon","polyline","radialgradient","rect","stop","style","switch","symbol","text","textpath","title","tref","tspan","view","vkern"]),_e=_(["feBlend","feColorMatrix","feComponentTransfer","feComposite","feConvolveMatrix","feDiffuseLighting","feDisplacementMap","feDistantLight","feDropShadow","feFlood","feFuncA","feFuncB","feFuncG","feFuncR","feGaussianBlur","feImage","feMerge","feMergeNode","feMorphology","feOffset","fePointLight","feSpecularLighting","feSpotLight","feTile","feTurbulence"]),we=_(["animate","color-profile","cursor","discard","font-face","font-face-format","font-face-name","font-face-src","font-face-uri","foreignobject","hatch","hatchpath","mesh","meshgradient","meshpatch","meshrow","missing-glyph","script","set","solidcolor","unknown","use"]),Se=_(["math","menclose","merror","mfenced","mfrac","mglyph","mi","mlabeledtr","mmultiscripts","mn","mo","mover","mpadded","mphantom","mroot","mrow","ms","mspace","msqrt","mstyle","msub","msup","msubsup","mtable","mtd","mtext","mtr","munder","munderover","mprescripts"]),xe=_(["maction","maligngroup","malignmark","mlongdiv","mscarries","mscarry","msgroup","mstack","msline","msrow","semantics","annotation","annotation-xml","mprescripts","none"]),Ie=_(["#text"]),Pe=_(["accept","action","align","alt","autocapitalize","autocomplete","autopictureinpicture","autoplay","background","bgcolor","border","capture","cellpadding","cellspacing","checked","cite","class","clear","color","cols","colspan","controls","controlslist","coords","crossorigin","datetime","decoding","default","dir","disabled","disablepictureinpicture","disableremoteplayback","download","draggable","enctype","enterkeyhint","face","for","headers","height","hidden","high","href","hreflang","id","inputmode","integrity","ismap","kind","label","lang","list","loading","loop","low","max","maxlength","media","method","min","minlength","multiple","muted","name","nonce","noshade","novalidate","nowrap","open","optimum","pattern","placeholder","playsinline","poster","preload","pubdate","radiogroup","readonly","rel","required","rev","reversed","role","rows","rowspan","spellcheck","scope","selected","shape","size","sizes","span","srclang","start","src","srcset","step","style","summary","tabindex","title","translate","type","usemap","valign","value","width","xmlns","slot"]),Te=_(["accent-height","accumulate","additive","alignment-baseline","ascent","attributename","attributetype","azimuth","basefrequency","baseline-shift","begin","bias","by","class","clip","clippathunits","clip-path","clip-rule","color","color-interpolation","color-interpolation-filters","color-profile","color-rendering","cx","cy","d","dx","dy","diffuseconstant","direction","display","divisor","dur","edgemode","elevation","end","fill","fill-opacity","fill-rule","filter","filterunits","flood-color","flood-opacity","font-family","font-size","font-size-adjust","font-stretch","font-style","font-variant","font-weight","fx","fy","g1","g2","glyph-name","glyphref","gradientunits","gradienttransform","height","href","id","image-rendering","in","in2","k","k1","k2","k3","k4","kerning","keypoints","keysplines","keytimes","lang","lengthadjust","letter-spacing","kernelmatrix","kernelunitlength","lighting-color","local","marker-end","marker-mid","marker-start","markerheight","markerunits","markerwidth","maskcontentunits","maskunits","max","mask","media","method","mode","min","name","numoctaves","offset","operator","opacity","order","orient","orientation","origin","overflow","paint-order","path","pathlength","patterncontentunits","patterntransform","patternunits","points","preservealpha","preserveaspectratio","primitiveunits","r","rx","ry","radius","refx","refy","repeatcount","repeatdur","restart","result","rotate","scale","seed","shape-rendering","specularconstant","specularexponent","spreadmethod","startoffset","stddeviation","stitchtiles","stop-color","stop-opacity","stroke-dasharray","stroke-dashoffset","stroke-linecap","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke","stroke-width","style","surfacescale","systemlanguage","tabindex","targetx","targety","transform","transform-origin","text-anchor","text-decoration","text-rendering","textlength","type","u1","u2","unicode","values","viewbox","visibility","version","vert-adv-y","vert-origin-x","vert-origin-y","width","word-spacing","wrap","writing-mode","xchannelselector","ychannelselector","x","x1","x2","xmlns","y","y1","y2","z","zoomandpan"]),Re=_(["accent","accentunder","align","bevelled","close","columnsalign","columnlines","columnspan","denomalign","depth","dir","display","displaystyle","encoding","fence","frame","height","href","id","largeop","length","linethickness","lspace","lquote","mathbackground","mathcolor","mathsize","mathvariant","maxsize","minsize","movablelimits","notation","numalign","open","rowalign","rowlines","rowspacing","rowspan","rspace","rquote","scriptlevel","scriptminsize","scriptsizemultiplier","selection","separator","separators","stretchy","subscriptshift","supscriptshift","symmetric","voffset","width","xmlns"]),qe=_(["xlink:href","xml:id","xlink:title","xml:space","xmlns:xlink"]),ze=j(/\{\{[\w\W]*|[\w\W]*\}\}/gm),Ve=j(/<%[\w\W]*|[\w\W]*%>/gm),We=j(/\${[\w\W]*}/gm),He=j(/^data-[\-\w.\u00B7-\uFFFF]/),Xe=j(/^aria-[\-\w]+$/),Ye=j(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),Qe=j(/^(?:\w+script|data):/i),et=j(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),tt=j(/^html$/i);var rt=Object.freeze({__proto__:null,MUSTACHE_EXPR:ze,ERB_EXPR:Ve,TMPLIT_EXPR:We,DATA_ATTR:He,ARIA_ATTR:Xe,IS_ALLOWED_URI:Ye,IS_SCRIPT_OR_DATA:Qe,ATTR_WHITESPACE:et,DOCTYPE_NAME:tt});const nt=function getGlobal(){return"undefined"==typeof window?null:window},ot=function _createTrustedTypesPolicy(i,s){if("object"!=typeof i||"function"!=typeof i.createPolicy)return null;let u=null;const m="data-tt-policy-suffix";s&&s.hasAttribute(m)&&(u=s.getAttribute(m));const v="dompurify"+(u?"#"+u:"");try{return i.createPolicy(v,{createHTML:i=>i,createScriptURL:i=>i})}catch(i){return console.warn("TrustedTypes policy "+v+" could not be created."),null}};function createDOMPurify(){let s=arguments.length>0&&void 0!==arguments[0]?arguments[0]:nt();const DOMPurify=i=>createDOMPurify(i);if(DOMPurify.version="3.0.6",DOMPurify.removed=[],!s||!s.document||9!==s.document.nodeType)return DOMPurify.isSupported=!1,DOMPurify;let{document:u}=s;const m=u,v=m.currentScript,{DocumentFragment:j,HTMLTemplateElement:$,Node:W,Element:ze,NodeFilter:Ve,NamedNodeMap:We=s.NamedNodeMap||s.MozNamedAttrMap,HTMLFormElement:He,DOMParser:Xe,trustedTypes:Qe}=s,et=ze.prototype,it=lookupGetter(et,"cloneNode"),at=lookupGetter(et,"nextSibling"),st=lookupGetter(et,"childNodes"),lt=lookupGetter(et,"parentNode");if("function"==typeof $){const i=u.createElement("template");i.content&&i.content.ownerDocument&&(u=i.content.ownerDocument)}let ct,ut="";const{implementation:pt,createNodeIterator:ht,createDocumentFragment:dt,getElementsByTagName:mt}=u,{importNode:gt}=m;let yt={};DOMPurify.isSupported="function"==typeof i&&"function"==typeof lt&&pt&&void 0!==pt.createHTMLDocument;const{MUSTACHE_EXPR:vt,ERB_EXPR:bt,TMPLIT_EXPR:_t,DATA_ATTR:wt,ARIA_ATTR:Et,IS_SCRIPT_OR_DATA:St,ATTR_WHITESPACE:xt}=rt;let{IS_ALLOWED_URI:kt}=rt,Ot=null;const At=addToSet({},[...ye,...be,..._e,...Se,...Ie]);let Ct=null;const jt=addToSet({},[...Pe,...Te,...Re,...qe]);let It=Object.seal(M(null,{tagNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},allowCustomizedBuiltInElements:{writable:!0,configurable:!1,enumerable:!0,value:!1}})),Pt=null,Nt=null,Tt=!0,Mt=!0,Rt=!1,Dt=!0,Bt=!1,Lt=!1,Ft=!1,qt=!1,$t=!1,Ut=!1,zt=!1,Vt=!0,Wt=!1;const Kt="user-content-";let Ht=!0,Jt=!1,Gt={},Xt=null;const Yt=addToSet({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","style","svg","template","thead","title","video","xmp"]);let Qt=null;const Zt=addToSet({},["audio","video","img","source","image","track"]);let er=null;const tr=addToSet({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),rr="http://www.w3.org/1998/Math/MathML",nr="http://www.w3.org/2000/svg",ir="http://www.w3.org/1999/xhtml";let ar=ir,sr=!1,lr=null;const cr=addToSet({},[rr,nr,ir],ie);let ur=null;const pr=["application/xhtml+xml","text/html"],dr="text/html";let fr=null,mr=null;const gr=u.createElement("form"),yr=function isRegexOrFunction(i){return i instanceof RegExp||i instanceof Function},vr=function _parseConfig(){let i=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};if(!mr||mr!==i){if(i&&"object"==typeof i||(i={}),i=clone(i),ur=ur=-1===pr.indexOf(i.PARSER_MEDIA_TYPE)?dr:i.PARSER_MEDIA_TYPE,fr="application/xhtml+xml"===ur?ie:ee,Ot="ALLOWED_TAGS"in i?addToSet({},i.ALLOWED_TAGS,fr):At,Ct="ALLOWED_ATTR"in i?addToSet({},i.ALLOWED_ATTR,fr):jt,lr="ALLOWED_NAMESPACES"in i?addToSet({},i.ALLOWED_NAMESPACES,ie):cr,er="ADD_URI_SAFE_ATTR"in i?addToSet(clone(tr),i.ADD_URI_SAFE_ATTR,fr):tr,Qt="ADD_DATA_URI_TAGS"in i?addToSet(clone(Zt),i.ADD_DATA_URI_TAGS,fr):Zt,Xt="FORBID_CONTENTS"in i?addToSet({},i.FORBID_CONTENTS,fr):Yt,Pt="FORBID_TAGS"in i?addToSet({},i.FORBID_TAGS,fr):{},Nt="FORBID_ATTR"in i?addToSet({},i.FORBID_ATTR,fr):{},Gt="USE_PROFILES"in i&&i.USE_PROFILES,Tt=!1!==i.ALLOW_ARIA_ATTR,Mt=!1!==i.ALLOW_DATA_ATTR,Rt=i.ALLOW_UNKNOWN_PROTOCOLS||!1,Dt=!1!==i.ALLOW_SELF_CLOSE_IN_ATTR,Bt=i.SAFE_FOR_TEMPLATES||!1,Lt=i.WHOLE_DOCUMENT||!1,$t=i.RETURN_DOM||!1,Ut=i.RETURN_DOM_FRAGMENT||!1,zt=i.RETURN_TRUSTED_TYPE||!1,qt=i.FORCE_BODY||!1,Vt=!1!==i.SANITIZE_DOM,Wt=i.SANITIZE_NAMED_PROPS||!1,Ht=!1!==i.KEEP_CONTENT,Jt=i.IN_PLACE||!1,kt=i.ALLOWED_URI_REGEXP||Ye,ar=i.NAMESPACE||ir,It=i.CUSTOM_ELEMENT_HANDLING||{},i.CUSTOM_ELEMENT_HANDLING&&yr(i.CUSTOM_ELEMENT_HANDLING.tagNameCheck)&&(It.tagNameCheck=i.CUSTOM_ELEMENT_HANDLING.tagNameCheck),i.CUSTOM_ELEMENT_HANDLING&&yr(i.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)&&(It.attributeNameCheck=i.CUSTOM_ELEMENT_HANDLING.attributeNameCheck),i.CUSTOM_ELEMENT_HANDLING&&"boolean"==typeof i.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements&&(It.allowCustomizedBuiltInElements=i.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements),Bt&&(Mt=!1),Ut&&($t=!0),Gt&&(Ot=addToSet({},[...Ie]),Ct=[],!0===Gt.html&&(addToSet(Ot,ye),addToSet(Ct,Pe)),!0===Gt.svg&&(addToSet(Ot,be),addToSet(Ct,Te),addToSet(Ct,qe)),!0===Gt.svgFilters&&(addToSet(Ot,_e),addToSet(Ct,Te),addToSet(Ct,qe)),!0===Gt.mathMl&&(addToSet(Ot,Se),addToSet(Ct,Re),addToSet(Ct,qe))),i.ADD_TAGS&&(Ot===At&&(Ot=clone(Ot)),addToSet(Ot,i.ADD_TAGS,fr)),i.ADD_ATTR&&(Ct===jt&&(Ct=clone(Ct)),addToSet(Ct,i.ADD_ATTR,fr)),i.ADD_URI_SAFE_ATTR&&addToSet(er,i.ADD_URI_SAFE_ATTR,fr),i.FORBID_CONTENTS&&(Xt===Yt&&(Xt=clone(Xt)),addToSet(Xt,i.FORBID_CONTENTS,fr)),Ht&&(Ot["#text"]=!0),Lt&&addToSet(Ot,["html","head","body"]),Ot.table&&(addToSet(Ot,["tbody"]),delete Pt.tbody),i.TRUSTED_TYPES_POLICY){if("function"!=typeof i.TRUSTED_TYPES_POLICY.createHTML)throw fe('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.');if("function"!=typeof i.TRUSTED_TYPES_POLICY.createScriptURL)throw fe('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.');ct=i.TRUSTED_TYPES_POLICY,ut=ct.createHTML("")}else void 0===ct&&(ct=ot(Qe,v)),null!==ct&&"string"==typeof ut&&(ut=ct.createHTML(""));_&&_(i),mr=i}},br=addToSet({},["mi","mo","mn","ms","mtext"]),_r=addToSet({},["foreignobject","desc","title","annotation-xml"]),wr=addToSet({},["title","style","font","a","script"]),Er=addToSet({},be);addToSet(Er,_e),addToSet(Er,we);const Sr=addToSet({},Se);addToSet(Sr,xe);const xr=function _checkValidNamespace(i){let s=lt(i);s&&s.tagName||(s={namespaceURI:ar,tagName:"template"});const u=ee(i.tagName),m=ee(s.tagName);return!!lr[i.namespaceURI]&&(i.namespaceURI===nr?s.namespaceURI===ir?"svg"===u:s.namespaceURI===rr?"svg"===u&&("annotation-xml"===m||br[m]):Boolean(Er[u]):i.namespaceURI===rr?s.namespaceURI===ir?"math"===u:s.namespaceURI===nr?"math"===u&&_r[m]:Boolean(Sr[u]):i.namespaceURI===ir?!(s.namespaceURI===nr&&!_r[m])&&!(s.namespaceURI===rr&&!br[m])&&!Sr[u]&&(wr[u]||!Er[u]):!("application/xhtml+xml"!==ur||!lr[i.namespaceURI]))},kr=function _forceRemove(i){Z(DOMPurify.removed,{element:i});try{i.parentNode.removeChild(i)}catch(s){i.remove()}},Or=function _removeAttribute(i,s){try{Z(DOMPurify.removed,{attribute:s.getAttributeNode(i),from:s})}catch(i){Z(DOMPurify.removed,{attribute:null,from:s})}if(s.removeAttribute(i),"is"===i&&!Ct[i])if($t||Ut)try{kr(s)}catch(i){}else try{s.setAttribute(i,"")}catch(i){}},Ar=function _initDocument(i){let s=null,m=null;if(qt)i=""+i;else{const s=ae(i,/^[\r\n\t ]+/);m=s&&s[0]}"application/xhtml+xml"===ur&&ar===ir&&(i=''+i+"");const v=ct?ct.createHTML(i):i;if(ar===ir)try{s=(new Xe).parseFromString(v,ur)}catch(i){}if(!s||!s.documentElement){s=pt.createDocument(ar,"template",null);try{s.documentElement.innerHTML=sr?ut:v}catch(i){}}const _=s.body||s.documentElement;return i&&m&&_.insertBefore(u.createTextNode(m),_.childNodes[0]||null),ar===ir?mt.call(s,Lt?"html":"body")[0]:Lt?s.documentElement:_},Cr=function _createNodeIterator(i){return ht.call(i.ownerDocument||i,i,Ve.SHOW_ELEMENT|Ve.SHOW_COMMENT|Ve.SHOW_TEXT,null)},jr=function _isClobbered(i){return i instanceof He&&("string"!=typeof i.nodeName||"string"!=typeof i.textContent||"function"!=typeof i.removeChild||!(i.attributes instanceof We)||"function"!=typeof i.removeAttribute||"function"!=typeof i.setAttribute||"string"!=typeof i.namespaceURI||"function"!=typeof i.insertBefore||"function"!=typeof i.hasChildNodes)},Ir=function _isNode(i){return"function"==typeof W&&i instanceof W},Pr=function _executeHook(i,s,u){yt[i]&&X(yt[i],(i=>{i.call(DOMPurify,s,u,mr)}))},Nr=function _sanitizeElements(i){let s=null;if(Pr("beforeSanitizeElements",i,null),jr(i))return kr(i),!0;const u=fr(i.nodeName);if(Pr("uponSanitizeElement",i,{tagName:u,allowedTags:Ot}),i.hasChildNodes()&&!Ir(i.firstElementChild)&&de(/<[/\w]/g,i.innerHTML)&&de(/<[/\w]/g,i.textContent))return kr(i),!0;if(!Ot[u]||Pt[u]){if(!Pt[u]&&Mr(u)){if(It.tagNameCheck instanceof RegExp&&de(It.tagNameCheck,u))return!1;if(It.tagNameCheck instanceof Function&&It.tagNameCheck(u))return!1}if(Ht&&!Xt[u]){const s=lt(i)||i.parentNode,u=st(i)||i.childNodes;if(u&&s)for(let m=u.length-1;m>=0;--m)s.insertBefore(it(u[m],!0),at(i))}return kr(i),!0}return i instanceof ze&&!xr(i)?(kr(i),!0):"noscript"!==u&&"noembed"!==u&&"noframes"!==u||!de(/<\/no(script|embed|frames)/i,i.innerHTML)?(Bt&&3===i.nodeType&&(s=i.textContent,X([vt,bt,_t],(i=>{s=le(s,i," ")})),i.textContent!==s&&(Z(DOMPurify.removed,{element:i.cloneNode()}),i.textContent=s)),Pr("afterSanitizeElements",i,null),!1):(kr(i),!0)},Tr=function _isValidAttribute(i,s,m){if(Vt&&("id"===s||"name"===s)&&(m in u||m in gr))return!1;if(Mt&&!Nt[s]&&de(wt,s));else if(Tt&&de(Et,s));else if(!Ct[s]||Nt[s]){if(!(Mr(i)&&(It.tagNameCheck instanceof RegExp&&de(It.tagNameCheck,i)||It.tagNameCheck instanceof Function&&It.tagNameCheck(i))&&(It.attributeNameCheck instanceof RegExp&&de(It.attributeNameCheck,s)||It.attributeNameCheck instanceof Function&&It.attributeNameCheck(s))||"is"===s&&It.allowCustomizedBuiltInElements&&(It.tagNameCheck instanceof RegExp&&de(It.tagNameCheck,m)||It.tagNameCheck instanceof Function&&It.tagNameCheck(m))))return!1}else if(er[s]);else if(de(kt,le(m,xt,"")));else if("src"!==s&&"xlink:href"!==s&&"href"!==s||"script"===i||0!==ce(m,"data:")||!Qt[i])if(Rt&&!de(St,le(m,xt,"")));else if(m)return!1;return!0},Mr=function _isBasicCustomElement(i){return i.indexOf("-")>0},Rr=function _sanitizeAttributes(i){Pr("beforeSanitizeAttributes",i,null);const{attributes:s}=i;if(!s)return;const u={attrName:"",attrValue:"",keepAttr:!0,allowedAttributes:Ct};let m=s.length;for(;m--;){const v=s[m],{name:_,namespaceURI:j,value:M}=v,$=fr(_);let W="value"===_?M:pe(M);if(u.attrName=$,u.attrValue=W,u.keepAttr=!0,u.forceKeepAttr=void 0,Pr("uponSanitizeAttribute",i,u),W=u.attrValue,u.forceKeepAttr)continue;if(Or(_,i),!u.keepAttr)continue;if(!Dt&&de(/\/>/i,W)){Or(_,i);continue}Bt&&X([vt,bt,_t],(i=>{W=le(W,i," ")}));const Z=fr(i.nodeName);if(Tr(Z,$,W)){if(!Wt||"id"!==$&&"name"!==$||(Or(_,i),W=Kt+W),ct&&"object"==typeof Qe&&"function"==typeof Qe.getAttributeType)if(j);else switch(Qe.getAttributeType(Z,$)){case"TrustedHTML":W=ct.createHTML(W);break;case"TrustedScriptURL":W=ct.createScriptURL(W)}try{j?i.setAttributeNS(j,_,W):i.setAttribute(_,W),Y(DOMPurify.removed)}catch(i){}}}Pr("afterSanitizeAttributes",i,null)},Dr=function _sanitizeShadowDOM(i){let s=null;const u=Cr(i);for(Pr("beforeSanitizeShadowDOM",i,null);s=u.nextNode();)Pr("uponSanitizeShadowNode",s,null),Nr(s)||(s.content instanceof j&&_sanitizeShadowDOM(s.content),Rr(s));Pr("afterSanitizeShadowDOM",i,null)};return DOMPurify.sanitize=function(i){let s=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},u=null,v=null,_=null,M=null;if(sr=!i,sr&&(i="\x3c!--\x3e"),"string"!=typeof i&&!Ir(i)){if("function"!=typeof i.toString)throw fe("toString is not a function");if("string"!=typeof(i=i.toString()))throw fe("dirty is not a string, aborting")}if(!DOMPurify.isSupported)return i;if(Ft||vr(s),DOMPurify.removed=[],"string"==typeof i&&(Jt=!1),Jt){if(i.nodeName){const s=fr(i.nodeName);if(!Ot[s]||Pt[s])throw fe("root node is forbidden and cannot be sanitized in-place")}}else if(i instanceof W)u=Ar("\x3c!----\x3e"),v=u.ownerDocument.importNode(i,!0),1===v.nodeType&&"BODY"===v.nodeName||"HTML"===v.nodeName?u=v:u.appendChild(v);else{if(!$t&&!Bt&&!Lt&&-1===i.indexOf("<"))return ct&&zt?ct.createHTML(i):i;if(u=Ar(i),!u)return $t?null:zt?ut:""}u&&qt&&kr(u.firstChild);const $=Cr(Jt?i:u);for(;_=$.nextNode();)Nr(_)||(_.content instanceof j&&Dr(_.content),Rr(_));if(Jt)return i;if($t){if(Ut)for(M=dt.call(u.ownerDocument);u.firstChild;)M.appendChild(u.firstChild);else M=u;return(Ct.shadowroot||Ct.shadowrootmode)&&(M=gt.call(m,M,!0)),M}let Y=Lt?u.outerHTML:u.innerHTML;return Lt&&Ot["!doctype"]&&u.ownerDocument&&u.ownerDocument.doctype&&u.ownerDocument.doctype.name&&de(tt,u.ownerDocument.doctype.name)&&(Y="\n"+Y),Bt&&X([vt,bt,_t],(i=>{Y=le(Y,i," ")})),ct&&zt?ct.createHTML(Y):Y},DOMPurify.setConfig=function(){vr(arguments.length>0&&void 0!==arguments[0]?arguments[0]:{}),Ft=!0},DOMPurify.clearConfig=function(){mr=null,Ft=!1},DOMPurify.isValidAttribute=function(i,s,u){mr||vr({});const m=fr(i),v=fr(s);return Tr(m,v,u)},DOMPurify.addHook=function(i,s){"function"==typeof s&&(yt[i]=yt[i]||[],Z(yt[i],s))},DOMPurify.removeHook=function(i){if(yt[i])return Y(yt[i])},DOMPurify.removeHooks=function(i){yt[i]&&(yt[i]=[])},DOMPurify.removeAllHooks=function(){yt={}},DOMPurify}return createDOMPurify()}()},69450:i=>{"use strict";class SubRange{constructor(i,s){this.low=i,this.high=s,this.length=1+s-i}overlaps(i){return!(this.highi.high)}touches(i){return!(this.high+1i.high)}add(i){return new SubRange(Math.min(this.low,i.low),Math.max(this.high,i.high))}subtract(i){return i.low<=this.low&&i.high>=this.high?[]:i.low>this.low&&i.highi+s.length),0)}add(i,s){var _add=i=>{for(var s=0;s{for(var s=0;s{for(var s=0;s{for(var u=s.low;u<=s.high;)i.push(u),u++;return i}),[])}subranges(){return this.ranges.map((i=>({low:i.low,high:i.high,length:1+i.high-i.low})))}}i.exports=DRange},17187:i=>{"use strict";var s,u="object"==typeof Reflect?Reflect:null,m=u&&"function"==typeof u.apply?u.apply:function ReflectApply(i,s,u){return Function.prototype.apply.call(i,s,u)};s=u&&"function"==typeof u.ownKeys?u.ownKeys:Object.getOwnPropertySymbols?function ReflectOwnKeys(i){return Object.getOwnPropertyNames(i).concat(Object.getOwnPropertySymbols(i))}:function ReflectOwnKeys(i){return Object.getOwnPropertyNames(i)};var v=Number.isNaN||function NumberIsNaN(i){return i!=i};function EventEmitter(){EventEmitter.init.call(this)}i.exports=EventEmitter,i.exports.once=function once(i,s){return new Promise((function(u,m){function errorListener(u){i.removeListener(s,resolver),m(u)}function resolver(){"function"==typeof i.removeListener&&i.removeListener("error",errorListener),u([].slice.call(arguments))}eventTargetAgnosticAddListener(i,s,resolver,{once:!0}),"error"!==s&&function addErrorHandlerIfEventEmitter(i,s,u){"function"==typeof i.on&&eventTargetAgnosticAddListener(i,"error",s,u)}(i,errorListener,{once:!0})}))},EventEmitter.EventEmitter=EventEmitter,EventEmitter.prototype._events=void 0,EventEmitter.prototype._eventsCount=0,EventEmitter.prototype._maxListeners=void 0;var _=10;function checkListener(i){if("function"!=typeof i)throw new TypeError('The "listener" argument must be of type Function. Received type '+typeof i)}function _getMaxListeners(i){return void 0===i._maxListeners?EventEmitter.defaultMaxListeners:i._maxListeners}function _addListener(i,s,u,m){var v,_,j;if(checkListener(u),void 0===(_=i._events)?(_=i._events=Object.create(null),i._eventsCount=0):(void 0!==_.newListener&&(i.emit("newListener",s,u.listener?u.listener:u),_=i._events),j=_[s]),void 0===j)j=_[s]=u,++i._eventsCount;else if("function"==typeof j?j=_[s]=m?[u,j]:[j,u]:m?j.unshift(u):j.push(u),(v=_getMaxListeners(i))>0&&j.length>v&&!j.warned){j.warned=!0;var M=new Error("Possible EventEmitter memory leak detected. "+j.length+" "+String(s)+" listeners added. Use emitter.setMaxListeners() to increase limit");M.name="MaxListenersExceededWarning",M.emitter=i,M.type=s,M.count=j.length,function ProcessEmitWarning(i){console&&console.warn&&console.warn(i)}(M)}return i}function onceWrapper(){if(!this.fired)return this.target.removeListener(this.type,this.wrapFn),this.fired=!0,0===arguments.length?this.listener.call(this.target):this.listener.apply(this.target,arguments)}function _onceWrap(i,s,u){var m={fired:!1,wrapFn:void 0,target:i,type:s,listener:u},v=onceWrapper.bind(m);return v.listener=u,m.wrapFn=v,v}function _listeners(i,s,u){var m=i._events;if(void 0===m)return[];var v=m[s];return void 0===v?[]:"function"==typeof v?u?[v.listener||v]:[v]:u?function unwrapListeners(i){for(var s=new Array(i.length),u=0;u0&&(j=s[0]),j instanceof Error)throw j;var M=new Error("Unhandled error."+(j?" ("+j.message+")":""));throw M.context=j,M}var $=_[i];if(void 0===$)return!1;if("function"==typeof $)m($,this,s);else{var W=$.length,X=arrayClone($,W);for(u=0;u=0;_--)if(u[_]===s||u[_].listener===s){j=u[_].listener,v=_;break}if(v<0)return this;0===v?u.shift():function spliceOne(i,s){for(;s+1=0;m--)this.removeListener(i,s[m]);return this},EventEmitter.prototype.listeners=function listeners(i){return _listeners(this,i,!0)},EventEmitter.prototype.rawListeners=function rawListeners(i){return _listeners(this,i,!1)},EventEmitter.listenerCount=function(i,s){return"function"==typeof i.listenerCount?i.listenerCount(s):listenerCount.call(i,s)},EventEmitter.prototype.listenerCount=listenerCount,EventEmitter.prototype.eventNames=function eventNames(){return this._eventsCount>0?s(this._events):[]}},21102:(i,s,u)=>{"use strict";var m=u(46291),v=create(Error);function create(i){return FormattedError.displayName=i.displayName||i.name,FormattedError;function FormattedError(s){return s&&(s=m.apply(null,arguments)),new i(s)}}i.exports=v,v.eval=create(EvalError),v.range=create(RangeError),v.reference=create(ReferenceError),v.syntax=create(SyntaxError),v.type=create(TypeError),v.uri=create(URIError),v.create=create},46291:i=>{!function(){var s;function format(i){for(var s,u,m,v,_=1,j=[].slice.call(arguments),M=0,$=i.length,W="",X=!1,Y=!1,nextArg=function(){return j[_++]},slurpNumber=function(){for(var u="";/\d/.test(i[M]);)u+=i[M++],s=i[M];return u.length>0?parseInt(u):null};M<$;++M)if(s=i[M],X)switch(X=!1,"."==s?(Y=!1,s=i[++M]):"0"==s&&"."==i[M+1]?(Y=!0,s=i[M+=2]):Y=!0,v=slurpNumber(),s){case"b":W+=parseInt(nextArg(),10).toString(2);break;case"c":W+="string"==typeof(u=nextArg())||u instanceof String?u:String.fromCharCode(parseInt(u,10));break;case"d":W+=parseInt(nextArg(),10);break;case"f":m=String(parseFloat(nextArg()).toFixed(v||6)),W+=Y?m:m.replace(/^0/,"");break;case"j":W+=JSON.stringify(nextArg());break;case"o":W+="0"+parseInt(nextArg(),10).toString(8);break;case"s":W+=nextArg();break;case"x":W+="0x"+parseInt(nextArg(),10).toString(16);break;case"X":W+="0x"+parseInt(nextArg(),10).toString(16).toUpperCase();break;default:W+=s}else"%"===s?X=!0:W+=s;return W}(s=i.exports=format).format=format,s.vsprintf=function vsprintf(i,s){return format.apply(null,[i].concat(s))},"undefined"!=typeof console&&"function"==typeof console.log&&(s.printf=function printf(){console.log(format.apply(null,arguments))})}()},17648:i=>{"use strict";var s=Object.prototype.toString,u=Math.max,m=function concatty(i,s){for(var u=[],m=0;m{"use strict";var m=u(17648);i.exports=Function.prototype.bind||m},40210:(i,s,u)=>{"use strict";var m,v=SyntaxError,_=Function,j=TypeError,getEvalledConstructor=function(i){try{return _('"use strict"; return ('+i+").constructor;")()}catch(i){}},M=Object.getOwnPropertyDescriptor;if(M)try{M({},"")}catch(i){M=null}var throwTypeError=function(){throw new j},$=M?function(){try{return throwTypeError}catch(i){try{return M(arguments,"callee").get}catch(i){return throwTypeError}}}():throwTypeError,W=u(41405)(),X=u(28185)(),Y=Object.getPrototypeOf||(X?function(i){return i.__proto__}:null),Z={},ee="undefined"!=typeof Uint8Array&&Y?Y(Uint8Array):m,ie={"%AggregateError%":"undefined"==typeof AggregateError?m:AggregateError,"%Array%":Array,"%ArrayBuffer%":"undefined"==typeof ArrayBuffer?m:ArrayBuffer,"%ArrayIteratorPrototype%":W&&Y?Y([][Symbol.iterator]()):m,"%AsyncFromSyncIteratorPrototype%":m,"%AsyncFunction%":Z,"%AsyncGenerator%":Z,"%AsyncGeneratorFunction%":Z,"%AsyncIteratorPrototype%":Z,"%Atomics%":"undefined"==typeof Atomics?m:Atomics,"%BigInt%":"undefined"==typeof BigInt?m:BigInt,"%BigInt64Array%":"undefined"==typeof BigInt64Array?m:BigInt64Array,"%BigUint64Array%":"undefined"==typeof BigUint64Array?m:BigUint64Array,"%Boolean%":Boolean,"%DataView%":"undefined"==typeof DataView?m:DataView,"%Date%":Date,"%decodeURI%":decodeURI,"%decodeURIComponent%":decodeURIComponent,"%encodeURI%":encodeURI,"%encodeURIComponent%":encodeURIComponent,"%Error%":Error,"%eval%":eval,"%EvalError%":EvalError,"%Float32Array%":"undefined"==typeof Float32Array?m:Float32Array,"%Float64Array%":"undefined"==typeof Float64Array?m:Float64Array,"%FinalizationRegistry%":"undefined"==typeof FinalizationRegistry?m:FinalizationRegistry,"%Function%":_,"%GeneratorFunction%":Z,"%Int8Array%":"undefined"==typeof Int8Array?m:Int8Array,"%Int16Array%":"undefined"==typeof Int16Array?m:Int16Array,"%Int32Array%":"undefined"==typeof Int32Array?m:Int32Array,"%isFinite%":isFinite,"%isNaN%":isNaN,"%IteratorPrototype%":W&&Y?Y(Y([][Symbol.iterator]())):m,"%JSON%":"object"==typeof JSON?JSON:m,"%Map%":"undefined"==typeof Map?m:Map,"%MapIteratorPrototype%":"undefined"!=typeof Map&&W&&Y?Y((new Map)[Symbol.iterator]()):m,"%Math%":Math,"%Number%":Number,"%Object%":Object,"%parseFloat%":parseFloat,"%parseInt%":parseInt,"%Promise%":"undefined"==typeof Promise?m:Promise,"%Proxy%":"undefined"==typeof Proxy?m:Proxy,"%RangeError%":RangeError,"%ReferenceError%":ReferenceError,"%Reflect%":"undefined"==typeof Reflect?m:Reflect,"%RegExp%":RegExp,"%Set%":"undefined"==typeof Set?m:Set,"%SetIteratorPrototype%":"undefined"!=typeof Set&&W&&Y?Y((new Set)[Symbol.iterator]()):m,"%SharedArrayBuffer%":"undefined"==typeof SharedArrayBuffer?m:SharedArrayBuffer,"%String%":String,"%StringIteratorPrototype%":W&&Y?Y(""[Symbol.iterator]()):m,"%Symbol%":W?Symbol:m,"%SyntaxError%":v,"%ThrowTypeError%":$,"%TypedArray%":ee,"%TypeError%":j,"%Uint8Array%":"undefined"==typeof Uint8Array?m:Uint8Array,"%Uint8ClampedArray%":"undefined"==typeof Uint8ClampedArray?m:Uint8ClampedArray,"%Uint16Array%":"undefined"==typeof Uint16Array?m:Uint16Array,"%Uint32Array%":"undefined"==typeof Uint32Array?m:Uint32Array,"%URIError%":URIError,"%WeakMap%":"undefined"==typeof WeakMap?m:WeakMap,"%WeakRef%":"undefined"==typeof WeakRef?m:WeakRef,"%WeakSet%":"undefined"==typeof WeakSet?m:WeakSet};if(Y)try{null.error}catch(i){var ae=Y(Y(i));ie["%Error.prototype%"]=ae}var le=function doEval(i){var s;if("%AsyncFunction%"===i)s=getEvalledConstructor("async function () {}");else if("%GeneratorFunction%"===i)s=getEvalledConstructor("function* () {}");else if("%AsyncGeneratorFunction%"===i)s=getEvalledConstructor("async function* () {}");else if("%AsyncGenerator%"===i){var u=doEval("%AsyncGeneratorFunction%");u&&(s=u.prototype)}else if("%AsyncIteratorPrototype%"===i){var m=doEval("%AsyncGenerator%");m&&Y&&(s=Y(m.prototype))}return ie[i]=s,s},ce={"%ArrayBufferPrototype%":["ArrayBuffer","prototype"],"%ArrayPrototype%":["Array","prototype"],"%ArrayProto_entries%":["Array","prototype","entries"],"%ArrayProto_forEach%":["Array","prototype","forEach"],"%ArrayProto_keys%":["Array","prototype","keys"],"%ArrayProto_values%":["Array","prototype","values"],"%AsyncFunctionPrototype%":["AsyncFunction","prototype"],"%AsyncGenerator%":["AsyncGeneratorFunction","prototype"],"%AsyncGeneratorPrototype%":["AsyncGeneratorFunction","prototype","prototype"],"%BooleanPrototype%":["Boolean","prototype"],"%DataViewPrototype%":["DataView","prototype"],"%DatePrototype%":["Date","prototype"],"%ErrorPrototype%":["Error","prototype"],"%EvalErrorPrototype%":["EvalError","prototype"],"%Float32ArrayPrototype%":["Float32Array","prototype"],"%Float64ArrayPrototype%":["Float64Array","prototype"],"%FunctionPrototype%":["Function","prototype"],"%Generator%":["GeneratorFunction","prototype"],"%GeneratorPrototype%":["GeneratorFunction","prototype","prototype"],"%Int8ArrayPrototype%":["Int8Array","prototype"],"%Int16ArrayPrototype%":["Int16Array","prototype"],"%Int32ArrayPrototype%":["Int32Array","prototype"],"%JSONParse%":["JSON","parse"],"%JSONStringify%":["JSON","stringify"],"%MapPrototype%":["Map","prototype"],"%NumberPrototype%":["Number","prototype"],"%ObjectPrototype%":["Object","prototype"],"%ObjProto_toString%":["Object","prototype","toString"],"%ObjProto_valueOf%":["Object","prototype","valueOf"],"%PromisePrototype%":["Promise","prototype"],"%PromiseProto_then%":["Promise","prototype","then"],"%Promise_all%":["Promise","all"],"%Promise_reject%":["Promise","reject"],"%Promise_resolve%":["Promise","resolve"],"%RangeErrorPrototype%":["RangeError","prototype"],"%ReferenceErrorPrototype%":["ReferenceError","prototype"],"%RegExpPrototype%":["RegExp","prototype"],"%SetPrototype%":["Set","prototype"],"%SharedArrayBufferPrototype%":["SharedArrayBuffer","prototype"],"%StringPrototype%":["String","prototype"],"%SymbolPrototype%":["Symbol","prototype"],"%SyntaxErrorPrototype%":["SyntaxError","prototype"],"%TypedArrayPrototype%":["TypedArray","prototype"],"%TypeErrorPrototype%":["TypeError","prototype"],"%Uint8ArrayPrototype%":["Uint8Array","prototype"],"%Uint8ClampedArrayPrototype%":["Uint8ClampedArray","prototype"],"%Uint16ArrayPrototype%":["Uint16Array","prototype"],"%Uint32ArrayPrototype%":["Uint32Array","prototype"],"%URIErrorPrototype%":["URIError","prototype"],"%WeakMapPrototype%":["WeakMap","prototype"],"%WeakSetPrototype%":["WeakSet","prototype"]},pe=u(58612),de=u(17642),fe=pe.call(Function.call,Array.prototype.concat),ye=pe.call(Function.apply,Array.prototype.splice),be=pe.call(Function.call,String.prototype.replace),_e=pe.call(Function.call,String.prototype.slice),we=pe.call(Function.call,RegExp.prototype.exec),Se=/[^%.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|%$))/g,xe=/\\(\\)?/g,Ie=function getBaseIntrinsic(i,s){var u,m=i;if(de(ce,m)&&(m="%"+(u=ce[m])[0]+"%"),de(ie,m)){var _=ie[m];if(_===Z&&(_=le(m)),void 0===_&&!s)throw new j("intrinsic "+i+" exists, but is not available. Please file an issue!");return{alias:u,name:m,value:_}}throw new v("intrinsic "+i+" does not exist!")};i.exports=function GetIntrinsic(i,s){if("string"!=typeof i||0===i.length)throw new j("intrinsic name must be a non-empty string");if(arguments.length>1&&"boolean"!=typeof s)throw new j('"allowMissing" argument must be a boolean');if(null===we(/^%?[^%]*%?$/,i))throw new v("`%` may not be present anywhere but at the beginning and end of the intrinsic name");var u=function stringToPath(i){var s=_e(i,0,1),u=_e(i,-1);if("%"===s&&"%"!==u)throw new v("invalid intrinsic syntax, expected closing `%`");if("%"===u&&"%"!==s)throw new v("invalid intrinsic syntax, expected opening `%`");var m=[];return be(i,Se,(function(i,s,u,v){m[m.length]=u?be(v,xe,"$1"):s||i})),m}(i),m=u.length>0?u[0]:"",_=Ie("%"+m+"%",s),$=_.name,W=_.value,X=!1,Y=_.alias;Y&&(m=Y[0],ye(u,fe([0,1],Y)));for(var Z=1,ee=!0;Z=u.length){var pe=M(W,ae);W=(ee=!!pe)&&"get"in pe&&!("originalValue"in pe.get)?pe.get:W[ae]}else ee=de(W,ae),W=W[ae];ee&&!X&&(ie[$]=W)}}return W}},28185:i=>{"use strict";var s={foo:{}},u=Object;i.exports=function hasProto(){return{__proto__:s}.foo===s.foo&&!({__proto__:null}instanceof u)}},41405:(i,s,u)=>{"use strict";var m="undefined"!=typeof Symbol&&Symbol,v=u(55419);i.exports=function hasNativeSymbols(){return"function"==typeof m&&("function"==typeof Symbol&&("symbol"==typeof m("foo")&&("symbol"==typeof Symbol("bar")&&v())))}},55419:i=>{"use strict";i.exports=function hasSymbols(){if("function"!=typeof Symbol||"function"!=typeof Object.getOwnPropertySymbols)return!1;if("symbol"==typeof Symbol.iterator)return!0;var i={},s=Symbol("test"),u=Object(s);if("string"==typeof s)return!1;if("[object Symbol]"!==Object.prototype.toString.call(s))return!1;if("[object Symbol]"!==Object.prototype.toString.call(u))return!1;for(s in i[s]=42,i)return!1;if("function"==typeof Object.keys&&0!==Object.keys(i).length)return!1;if("function"==typeof Object.getOwnPropertyNames&&0!==Object.getOwnPropertyNames(i).length)return!1;var m=Object.getOwnPropertySymbols(i);if(1!==m.length||m[0]!==s)return!1;if(!Object.prototype.propertyIsEnumerable.call(i,s))return!1;if("function"==typeof Object.getOwnPropertyDescriptor){var v=Object.getOwnPropertyDescriptor(i,s);if(42!==v.value||!0!==v.enumerable)return!1}return!0}},17642:(i,s,u)=>{"use strict";var m=u(58612);i.exports=m.call(Function.call,Object.prototype.hasOwnProperty)},47802:i=>{function deepFreeze(i){return i instanceof Map?i.clear=i.delete=i.set=function(){throw new Error("map is read-only")}:i instanceof Set&&(i.add=i.clear=i.delete=function(){throw new Error("set is read-only")}),Object.freeze(i),Object.getOwnPropertyNames(i).forEach((function(s){var u=i[s];"object"!=typeof u||Object.isFrozen(u)||deepFreeze(u)})),i}var s=deepFreeze,u=deepFreeze;s.default=u;class Response{constructor(i){void 0===i.data&&(i.data={}),this.data=i.data,this.isMatchIgnored=!1}ignoreMatch(){this.isMatchIgnored=!0}}function escapeHTML(i){return i.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}function inherit(i,...s){const u=Object.create(null);for(const s in i)u[s]=i[s];return s.forEach((function(i){for(const s in i)u[s]=i[s]})),u}const emitsWrappingTags=i=>!!i.kind;class HTMLRenderer{constructor(i,s){this.buffer="",this.classPrefix=s.classPrefix,i.walk(this)}addText(i){this.buffer+=escapeHTML(i)}openNode(i){if(!emitsWrappingTags(i))return;let s=i.kind;i.sublanguage||(s=`${this.classPrefix}${s}`),this.span(s)}closeNode(i){emitsWrappingTags(i)&&(this.buffer+="")}value(){return this.buffer}span(i){this.buffer+=``}}class TokenTree{constructor(){this.rootNode={children:[]},this.stack=[this.rootNode]}get top(){return this.stack[this.stack.length-1]}get root(){return this.rootNode}add(i){this.top.children.push(i)}openNode(i){const s={kind:i,children:[]};this.add(s),this.stack.push(s)}closeNode(){if(this.stack.length>1)return this.stack.pop()}closeAllNodes(){for(;this.closeNode(););}toJSON(){return JSON.stringify(this.rootNode,null,4)}walk(i){return this.constructor._walk(i,this.rootNode)}static _walk(i,s){return"string"==typeof s?i.addText(s):s.children&&(i.openNode(s),s.children.forEach((s=>this._walk(i,s))),i.closeNode(s)),i}static _collapse(i){"string"!=typeof i&&i.children&&(i.children.every((i=>"string"==typeof i))?i.children=[i.children.join("")]:i.children.forEach((i=>{TokenTree._collapse(i)})))}}class TokenTreeEmitter extends TokenTree{constructor(i){super(),this.options=i}addKeyword(i,s){""!==i&&(this.openNode(s),this.addText(i),this.closeNode())}addText(i){""!==i&&this.add(i)}addSublanguage(i,s){const u=i.root;u.kind=s,u.sublanguage=!0,this.add(u)}toHTML(){return new HTMLRenderer(this,this.options).value()}finalize(){return!0}}function source(i){return i?"string"==typeof i?i:i.source:null}const m=/\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./;const v="[a-zA-Z]\\w*",_="[a-zA-Z_]\\w*",j="\\b\\d+(\\.\\d+)?",M="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",$="\\b(0b[01]+)",W={begin:"\\\\[\\s\\S]",relevance:0},X={className:"string",begin:"'",end:"'",illegal:"\\n",contains:[W]},Y={className:"string",begin:'"',end:'"',illegal:"\\n",contains:[W]},Z={begin:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/},COMMENT=function(i,s,u={}){const m=inherit({className:"comment",begin:i,end:s,contains:[]},u);return m.contains.push(Z),m.contains.push({className:"doctag",begin:"(?:TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):",relevance:0}),m},ee=COMMENT("//","$"),ie=COMMENT("/\\*","\\*/"),ae=COMMENT("#","$"),le={className:"number",begin:j,relevance:0},ce={className:"number",begin:M,relevance:0},pe={className:"number",begin:$,relevance:0},de={className:"number",begin:j+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",relevance:0},fe={begin:/(?=\/[^/\n]*\/)/,contains:[{className:"regexp",begin:/\//,end:/\/[gimuy]*/,illegal:/\n/,contains:[W,{begin:/\[/,end:/\]/,relevance:0,contains:[W]}]}]},ye={className:"title",begin:v,relevance:0},be={className:"title",begin:_,relevance:0},_e={begin:"\\.\\s*"+_,relevance:0};var we=Object.freeze({__proto__:null,MATCH_NOTHING_RE:/\b\B/,IDENT_RE:v,UNDERSCORE_IDENT_RE:_,NUMBER_RE:j,C_NUMBER_RE:M,BINARY_NUMBER_RE:$,RE_STARTERS_RE:"!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",SHEBANG:(i={})=>{const s=/^#![ ]*\//;return i.binary&&(i.begin=function concat(...i){return i.map((i=>source(i))).join("")}(s,/.*\b/,i.binary,/\b.*/)),inherit({className:"meta",begin:s,end:/$/,relevance:0,"on:begin":(i,s)=>{0!==i.index&&s.ignoreMatch()}},i)},BACKSLASH_ESCAPE:W,APOS_STRING_MODE:X,QUOTE_STRING_MODE:Y,PHRASAL_WORDS_MODE:Z,COMMENT,C_LINE_COMMENT_MODE:ee,C_BLOCK_COMMENT_MODE:ie,HASH_COMMENT_MODE:ae,NUMBER_MODE:le,C_NUMBER_MODE:ce,BINARY_NUMBER_MODE:pe,CSS_NUMBER_MODE:de,REGEXP_MODE:fe,TITLE_MODE:ye,UNDERSCORE_TITLE_MODE:be,METHOD_GUARD:_e,END_SAME_AS_BEGIN:function(i){return Object.assign(i,{"on:begin":(i,s)=>{s.data._beginMatch=i[1]},"on:end":(i,s)=>{s.data._beginMatch!==i[1]&&s.ignoreMatch()}})}});function skipIfhasPrecedingDot(i,s){"."===i.input[i.index-1]&&s.ignoreMatch()}function beginKeywords(i,s){s&&i.beginKeywords&&(i.begin="\\b("+i.beginKeywords.split(" ").join("|")+")(?!\\.)(?=\\b|\\s)",i.__beforeBegin=skipIfhasPrecedingDot,i.keywords=i.keywords||i.beginKeywords,delete i.beginKeywords,void 0===i.relevance&&(i.relevance=0))}function compileIllegal(i,s){Array.isArray(i.illegal)&&(i.illegal=function either(...i){return"("+i.map((i=>source(i))).join("|")+")"}(...i.illegal))}function compileMatch(i,s){if(i.match){if(i.begin||i.end)throw new Error("begin & end are not supported with match");i.begin=i.match,delete i.match}}function compileRelevance(i,s){void 0===i.relevance&&(i.relevance=1)}const Se=["of","and","for","in","not","or","if","then","parent","list","value"],xe="keyword";function compileKeywords(i,s,u=xe){const m={};return"string"==typeof i?compileList(u,i.split(" ")):Array.isArray(i)?compileList(u,i):Object.keys(i).forEach((function(u){Object.assign(m,compileKeywords(i[u],s,u))})),m;function compileList(i,u){s&&(u=u.map((i=>i.toLowerCase()))),u.forEach((function(s){const u=s.split("|");m[u[0]]=[i,scoreForKeyword(u[0],u[1])]}))}}function scoreForKeyword(i,s){return s?Number(s):function commonKeyword(i){return Se.includes(i.toLowerCase())}(i)?0:1}function compileLanguage(i,{plugins:s}){function langRe(s,u){return new RegExp(source(s),"m"+(i.case_insensitive?"i":"")+(u?"g":""))}class MultiRegex{constructor(){this.matchIndexes={},this.regexes=[],this.matchAt=1,this.position=0}addRule(i,s){s.position=this.position++,this.matchIndexes[this.matchAt]=s,this.regexes.push([s,i]),this.matchAt+=function countMatchGroups(i){return new RegExp(i.toString()+"|").exec("").length-1}(i)+1}compile(){0===this.regexes.length&&(this.exec=()=>null);const i=this.regexes.map((i=>i[1]));this.matcherRe=langRe(function join(i,s="|"){let u=0;return i.map((i=>{u+=1;const s=u;let v=source(i),_="";for(;v.length>0;){const i=m.exec(v);if(!i){_+=v;break}_+=v.substring(0,i.index),v=v.substring(i.index+i[0].length),"\\"===i[0][0]&&i[1]?_+="\\"+String(Number(i[1])+s):(_+=i[0],"("===i[0]&&u++)}return _})).map((i=>`(${i})`)).join(s)}(i),!0),this.lastIndex=0}exec(i){this.matcherRe.lastIndex=this.lastIndex;const s=this.matcherRe.exec(i);if(!s)return null;const u=s.findIndex(((i,s)=>s>0&&void 0!==i)),m=this.matchIndexes[u];return s.splice(0,u),Object.assign(s,m)}}class ResumableMultiRegex{constructor(){this.rules=[],this.multiRegexes=[],this.count=0,this.lastIndex=0,this.regexIndex=0}getMatcher(i){if(this.multiRegexes[i])return this.multiRegexes[i];const s=new MultiRegex;return this.rules.slice(i).forEach((([i,u])=>s.addRule(i,u))),s.compile(),this.multiRegexes[i]=s,s}resumingScanAtSamePosition(){return 0!==this.regexIndex}considerAll(){this.regexIndex=0}addRule(i,s){this.rules.push([i,s]),"begin"===s.type&&this.count++}exec(i){const s=this.getMatcher(this.regexIndex);s.lastIndex=this.lastIndex;let u=s.exec(i);if(this.resumingScanAtSamePosition())if(u&&u.index===this.lastIndex);else{const s=this.getMatcher(0);s.lastIndex=this.lastIndex+1,u=s.exec(i)}return u&&(this.regexIndex+=u.position+1,this.regexIndex===this.count&&this.considerAll()),u}}if(i.compilerExtensions||(i.compilerExtensions=[]),i.contains&&i.contains.includes("self"))throw new Error("ERR: contains `self` is not supported at the top-level of a language. See documentation.");return i.classNameAliases=inherit(i.classNameAliases||{}),function compileMode(s,u){const m=s;if(s.isCompiled)return m;[compileMatch].forEach((i=>i(s,u))),i.compilerExtensions.forEach((i=>i(s,u))),s.__beforeBegin=null,[beginKeywords,compileIllegal,compileRelevance].forEach((i=>i(s,u))),s.isCompiled=!0;let v=null;if("object"==typeof s.keywords&&(v=s.keywords.$pattern,delete s.keywords.$pattern),s.keywords&&(s.keywords=compileKeywords(s.keywords,i.case_insensitive)),s.lexemes&&v)throw new Error("ERR: Prefer `keywords.$pattern` to `mode.lexemes`, BOTH are not allowed. (see mode reference) ");return v=v||s.lexemes||/\w+/,m.keywordPatternRe=langRe(v,!0),u&&(s.begin||(s.begin=/\B|\b/),m.beginRe=langRe(s.begin),s.endSameAsBegin&&(s.end=s.begin),s.end||s.endsWithParent||(s.end=/\B|\b/),s.end&&(m.endRe=langRe(s.end)),m.terminatorEnd=source(s.end)||"",s.endsWithParent&&u.terminatorEnd&&(m.terminatorEnd+=(s.end?"|":"")+u.terminatorEnd)),s.illegal&&(m.illegalRe=langRe(s.illegal)),s.contains||(s.contains=[]),s.contains=[].concat(...s.contains.map((function(i){return function expandOrCloneMode(i){i.variants&&!i.cachedVariants&&(i.cachedVariants=i.variants.map((function(s){return inherit(i,{variants:null},s)})));if(i.cachedVariants)return i.cachedVariants;if(dependencyOnParent(i))return inherit(i,{starts:i.starts?inherit(i.starts):null});if(Object.isFrozen(i))return inherit(i);return i}("self"===i?s:i)}))),s.contains.forEach((function(i){compileMode(i,m)})),s.starts&&compileMode(s.starts,u),m.matcher=function buildModeRegex(i){const s=new ResumableMultiRegex;return i.contains.forEach((i=>s.addRule(i.begin,{rule:i,type:"begin"}))),i.terminatorEnd&&s.addRule(i.terminatorEnd,{type:"end"}),i.illegal&&s.addRule(i.illegal,{type:"illegal"}),s}(m),m}(i)}function dependencyOnParent(i){return!!i&&(i.endsWithParent||dependencyOnParent(i.starts))}function BuildVuePlugin(i){const s={props:["language","code","autodetect"],data:function(){return{detectedLanguage:"",unknownLanguage:!1}},computed:{className(){return this.unknownLanguage?"":"hljs "+this.detectedLanguage},highlighted(){if(!this.autoDetect&&!i.getLanguage(this.language))return console.warn(`The language "${this.language}" you specified could not be found.`),this.unknownLanguage=!0,escapeHTML(this.code);let s={};return this.autoDetect?(s=i.highlightAuto(this.code),this.detectedLanguage=s.language):(s=i.highlight(this.language,this.code,this.ignoreIllegals),this.detectedLanguage=this.language),s.value},autoDetect(){return!this.language||function hasValueOrEmptyAttribute(i){return Boolean(i||""===i)}(this.autodetect)},ignoreIllegals:()=>!0},render(i){return i("pre",{},[i("code",{class:this.className,domProps:{innerHTML:this.highlighted}})])}};return{Component:s,VuePlugin:{install(i){i.component("highlightjs",s)}}}}const Ie={"after:highlightElement":({el:i,result:s,text:u})=>{const m=nodeStream(i);if(!m.length)return;const v=document.createElement("div");v.innerHTML=s.value,s.value=function mergeStreams(i,s,u){let m=0,v="";const _=[];function selectStream(){return i.length&&s.length?i[0].offset!==s[0].offset?i[0].offset"}function close(i){v+=""}function render(i){("start"===i.event?open:close)(i.node)}for(;i.length||s.length;){let s=selectStream();if(v+=escapeHTML(u.substring(m,s[0].offset)),m=s[0].offset,s===i){_.reverse().forEach(close);do{render(s.splice(0,1)[0]),s=selectStream()}while(s===i&&s.length&&s[0].offset===m);_.reverse().forEach(open)}else"start"===s[0].event?_.push(s[0].node):_.pop(),render(s.splice(0,1)[0])}return v+escapeHTML(u.substr(m))}(m,nodeStream(v),u)}};function tag(i){return i.nodeName.toLowerCase()}function nodeStream(i){const s=[];return function _nodeStream(i,u){for(let m=i.firstChild;m;m=m.nextSibling)3===m.nodeType?u+=m.nodeValue.length:1===m.nodeType&&(s.push({event:"start",offset:u,node:m}),u=_nodeStream(m,u),tag(m).match(/br|hr|img|input/)||s.push({event:"stop",offset:u,node:m}));return u}(i,0),s}const Pe={},error=i=>{console.error(i)},warn=(i,...s)=>{console.log(`WARN: ${i}`,...s)},deprecated=(i,s)=>{Pe[`${i}/${s}`]||(console.log(`Deprecated as of ${i}. ${s}`),Pe[`${i}/${s}`]=!0)},Te=escapeHTML,Re=inherit,qe=Symbol("nomatch");var ze=function(i){const u=Object.create(null),m=Object.create(null),v=[];let _=!0;const j=/(^(<[^>]+>|\t|)+|\n)/gm,M="Could not find the language '{}', did you forget to load/include a language module?",$={disableAutodetect:!0,name:"Plain text",contains:[]};let W={noHighlightRe:/^(no-?highlight)$/i,languageDetectRe:/\blang(?:uage)?-([\w-]+)\b/i,classPrefix:"hljs-",tabReplace:null,useBR:!1,languages:null,__emitter:TokenTreeEmitter};function shouldNotHighlight(i){return W.noHighlightRe.test(i)}function highlight(i,s,u,m){let v="",_="";"object"==typeof s?(v=i,u=s.ignoreIllegals,_=s.language,m=void 0):(deprecated("10.7.0","highlight(lang, code, ...args) has been deprecated."),deprecated("10.7.0","Please use highlight(code, options) instead.\nhttps://github.com/highlightjs/highlight.js/issues/2277"),_=i,v=s);const j={code:v,language:_};fire("before:highlight",j);const M=j.result?j.result:_highlight(j.language,j.code,u,m);return M.code=j.code,fire("after:highlight",M),M}function _highlight(i,s,m,j){function keywordData(i,s){const u=X.case_insensitive?s[0].toLowerCase():s[0];return Object.prototype.hasOwnProperty.call(i.keywords,u)&&i.keywords[u]}function processBuffer(){null!=ee.subLanguage?function processSubLanguage(){if(""===le)return;let i=null;if("string"==typeof ee.subLanguage){if(!u[ee.subLanguage])return void ae.addText(le);i=_highlight(ee.subLanguage,le,!0,ie[ee.subLanguage]),ie[ee.subLanguage]=i.top}else i=highlightAuto(le,ee.subLanguage.length?ee.subLanguage:null);ee.relevance>0&&(ce+=i.relevance),ae.addSublanguage(i.emitter,i.language)}():function processKeywords(){if(!ee.keywords)return void ae.addText(le);let i=0;ee.keywordPatternRe.lastIndex=0;let s=ee.keywordPatternRe.exec(le),u="";for(;s;){u+=le.substring(i,s.index);const m=keywordData(ee,s);if(m){const[i,v]=m;if(ae.addText(u),u="",ce+=v,i.startsWith("_"))u+=s[0];else{const u=X.classNameAliases[i]||i;ae.addKeyword(s[0],u)}}else u+=s[0];i=ee.keywordPatternRe.lastIndex,s=ee.keywordPatternRe.exec(le)}u+=le.substr(i),ae.addText(u)}(),le=""}function startNewMode(i){return i.className&&ae.openNode(X.classNameAliases[i.className]||i.className),ee=Object.create(i,{parent:{value:ee}}),ee}function endOfMode(i,s,u){let m=function startsWith(i,s){const u=i&&i.exec(s);return u&&0===u.index}(i.endRe,u);if(m){if(i["on:end"]){const u=new Response(i);i["on:end"](s,u),u.isMatchIgnored&&(m=!1)}if(m){for(;i.endsParent&&i.parent;)i=i.parent;return i}}if(i.endsWithParent)return endOfMode(i.parent,s,u)}function doIgnore(i){return 0===ee.matcher.regexIndex?(le+=i[0],1):(fe=!0,0)}function doBeginMatch(i){const s=i[0],u=i.rule,m=new Response(u),v=[u.__beforeBegin,u["on:begin"]];for(const u of v)if(u&&(u(i,m),m.isMatchIgnored))return doIgnore(s);return u&&u.endSameAsBegin&&(u.endRe=function escape(i){return new RegExp(i.replace(/[-/\\^$*+?.()|[\]{}]/g,"\\$&"),"m")}(s)),u.skip?le+=s:(u.excludeBegin&&(le+=s),processBuffer(),u.returnBegin||u.excludeBegin||(le=s)),startNewMode(u),u.returnBegin?0:s.length}function doEndMatch(i){const u=i[0],m=s.substr(i.index),v=endOfMode(ee,i,m);if(!v)return qe;const _=ee;_.skip?le+=u:(_.returnEnd||_.excludeEnd||(le+=u),processBuffer(),_.excludeEnd&&(le=u));do{ee.className&&ae.closeNode(),ee.skip||ee.subLanguage||(ce+=ee.relevance),ee=ee.parent}while(ee!==v.parent);return v.starts&&(v.endSameAsBegin&&(v.starts.endRe=v.endRe),startNewMode(v.starts)),_.returnEnd?0:u.length}let $={};function processLexeme(u,v){const j=v&&v[0];if(le+=u,null==j)return processBuffer(),0;if("begin"===$.type&&"end"===v.type&&$.index===v.index&&""===j){if(le+=s.slice(v.index,v.index+1),!_){const s=new Error("0 width match regex");throw s.languageName=i,s.badRule=$.rule,s}return 1}if($=v,"begin"===v.type)return doBeginMatch(v);if("illegal"===v.type&&!m){const i=new Error('Illegal lexeme "'+j+'" for mode "'+(ee.className||"")+'"');throw i.mode=ee,i}if("end"===v.type){const i=doEndMatch(v);if(i!==qe)return i}if("illegal"===v.type&&""===j)return 1;if(de>1e5&&de>3*v.index){throw new Error("potential infinite loop, way more iterations than matches")}return le+=j,j.length}const X=getLanguage(i);if(!X)throw error(M.replace("{}",i)),new Error('Unknown language: "'+i+'"');const Y=compileLanguage(X,{plugins:v});let Z="",ee=j||Y;const ie={},ae=new W.__emitter(W);!function processContinuations(){const i=[];for(let s=ee;s!==X;s=s.parent)s.className&&i.unshift(s.className);i.forEach((i=>ae.openNode(i)))}();let le="",ce=0,pe=0,de=0,fe=!1;try{for(ee.matcher.considerAll();;){de++,fe?fe=!1:ee.matcher.considerAll(),ee.matcher.lastIndex=pe;const i=ee.matcher.exec(s);if(!i)break;const u=processLexeme(s.substring(pe,i.index),i);pe=i.index+u}return processLexeme(s.substr(pe)),ae.closeAllNodes(),ae.finalize(),Z=ae.toHTML(),{relevance:Math.floor(ce),value:Z,language:i,illegal:!1,emitter:ae,top:ee}}catch(u){if(u.message&&u.message.includes("Illegal"))return{illegal:!0,illegalBy:{msg:u.message,context:s.slice(pe-100,pe+100),mode:u.mode},sofar:Z,relevance:0,value:Te(s),emitter:ae};if(_)return{illegal:!1,relevance:0,value:Te(s),emitter:ae,language:i,top:ee,errorRaised:u};throw u}}function highlightAuto(i,s){s=s||W.languages||Object.keys(u);const m=function justTextHighlightResult(i){const s={relevance:0,emitter:new W.__emitter(W),value:Te(i),illegal:!1,top:$};return s.emitter.addText(i),s}(i),v=s.filter(getLanguage).filter(autoDetection).map((s=>_highlight(s,i,!1)));v.unshift(m);const _=v.sort(((i,s)=>{if(i.relevance!==s.relevance)return s.relevance-i.relevance;if(i.language&&s.language){if(getLanguage(i.language).supersetOf===s.language)return 1;if(getLanguage(s.language).supersetOf===i.language)return-1}return 0})),[j,M]=_,X=j;return X.second_best=M,X}const X={"before:highlightElement":({el:i})=>{W.useBR&&(i.innerHTML=i.innerHTML.replace(/\n/g,"").replace(//g,"\n"))},"after:highlightElement":({result:i})=>{W.useBR&&(i.value=i.value.replace(/\n/g,"
"))}},Y=/^(<[^>]+>|\t)+/gm,Z={"after:highlightElement":({result:i})=>{W.tabReplace&&(i.value=i.value.replace(Y,(i=>i.replace(/\t/g,W.tabReplace))))}};function highlightElement(i){let s=null;const u=function blockLanguage(i){let s=i.className+" ";s+=i.parentNode?i.parentNode.className:"";const u=W.languageDetectRe.exec(s);if(u){const s=getLanguage(u[1]);return s||(warn(M.replace("{}",u[1])),warn("Falling back to no-highlight mode for this block.",i)),s?u[1]:"no-highlight"}return s.split(/\s+/).find((i=>shouldNotHighlight(i)||getLanguage(i)))}(i);if(shouldNotHighlight(u))return;fire("before:highlightElement",{el:i,language:u}),s=i;const v=s.textContent,_=u?highlight(v,{language:u,ignoreIllegals:!0}):highlightAuto(v);fire("after:highlightElement",{el:i,result:_,text:v}),i.innerHTML=_.value,function updateClassName(i,s,u){const v=s?m[s]:u;i.classList.add("hljs"),v&&i.classList.add(v)}(i,u,_.language),i.result={language:_.language,re:_.relevance,relavance:_.relevance},_.second_best&&(i.second_best={language:_.second_best.language,re:_.second_best.relevance,relavance:_.second_best.relevance})}const initHighlighting=()=>{if(initHighlighting.called)return;initHighlighting.called=!0,deprecated("10.6.0","initHighlighting() is deprecated. Use highlightAll() instead.");document.querySelectorAll("pre code").forEach(highlightElement)};let ee=!1;function highlightAll(){if("loading"===document.readyState)return void(ee=!0);document.querySelectorAll("pre code").forEach(highlightElement)}function getLanguage(i){return i=(i||"").toLowerCase(),u[i]||u[m[i]]}function registerAliases(i,{languageName:s}){"string"==typeof i&&(i=[i]),i.forEach((i=>{m[i.toLowerCase()]=s}))}function autoDetection(i){const s=getLanguage(i);return s&&!s.disableAutodetect}function fire(i,s){const u=i;v.forEach((function(i){i[u]&&i[u](s)}))}"undefined"!=typeof window&&window.addEventListener&&window.addEventListener("DOMContentLoaded",(function boot(){ee&&highlightAll()}),!1),Object.assign(i,{highlight,highlightAuto,highlightAll,fixMarkup:function deprecateFixMarkup(i){return deprecated("10.2.0","fixMarkup will be removed entirely in v11.0"),deprecated("10.2.0","Please see https://github.com/highlightjs/highlight.js/issues/2534"),function fixMarkup(i){return W.tabReplace||W.useBR?i.replace(j,(i=>"\n"===i?W.useBR?"
":i:W.tabReplace?i.replace(/\t/g,W.tabReplace):i)):i}(i)},highlightElement,highlightBlock:function deprecateHighlightBlock(i){return deprecated("10.7.0","highlightBlock will be removed entirely in v12.0"),deprecated("10.7.0","Please use highlightElement now."),highlightElement(i)},configure:function configure(i){i.useBR&&(deprecated("10.3.0","'useBR' will be removed entirely in v11.0"),deprecated("10.3.0","Please see https://github.com/highlightjs/highlight.js/issues/2559")),W=Re(W,i)},initHighlighting,initHighlightingOnLoad:function initHighlightingOnLoad(){deprecated("10.6.0","initHighlightingOnLoad() is deprecated. Use highlightAll() instead."),ee=!0},registerLanguage:function registerLanguage(s,m){let v=null;try{v=m(i)}catch(i){if(error("Language definition for '{}' could not be registered.".replace("{}",s)),!_)throw i;error(i),v=$}v.name||(v.name=s),u[s]=v,v.rawDefinition=m.bind(null,i),v.aliases&®isterAliases(v.aliases,{languageName:s})},unregisterLanguage:function unregisterLanguage(i){delete u[i];for(const s of Object.keys(m))m[s]===i&&delete m[s]},listLanguages:function listLanguages(){return Object.keys(u)},getLanguage,registerAliases,requireLanguage:function requireLanguage(i){deprecated("10.4.0","requireLanguage will be removed entirely in v11."),deprecated("10.4.0","Please see https://github.com/highlightjs/highlight.js/pull/2844");const s=getLanguage(i);if(s)return s;throw new Error("The '{}' language is required, but not loaded.".replace("{}",i))},autoDetection,inherit:Re,addPlugin:function addPlugin(i){!function upgradePluginAPI(i){i["before:highlightBlock"]&&!i["before:highlightElement"]&&(i["before:highlightElement"]=s=>{i["before:highlightBlock"](Object.assign({block:s.el},s))}),i["after:highlightBlock"]&&!i["after:highlightElement"]&&(i["after:highlightElement"]=s=>{i["after:highlightBlock"](Object.assign({block:s.el},s))})}(i),v.push(i)},vuePlugin:BuildVuePlugin(i).VuePlugin}),i.debugMode=function(){_=!1},i.safeMode=function(){_=!0},i.versionString="10.7.3";for(const i in we)"object"==typeof we[i]&&s(we[i]);return Object.assign(i,we),i.addPlugin(X),i.addPlugin(Ie),i.addPlugin(Z),i}({});i.exports=ze},61519:i=>{function concat(...i){return i.map((i=>function source(i){return i?"string"==typeof i?i:i.source:null}(i))).join("")}i.exports=function bash(i){const s={},u={begin:/\$\{/,end:/\}/,contains:["self",{begin:/:-/,contains:[s]}]};Object.assign(s,{className:"variable",variants:[{begin:concat(/\$[\w\d#@][\w\d_]*/,"(?![\\w\\d])(?![$])")},u]});const m={className:"subst",begin:/\$\(/,end:/\)/,contains:[i.BACKSLASH_ESCAPE]},v={begin:/<<-?\s*(?=\w+)/,starts:{contains:[i.END_SAME_AS_BEGIN({begin:/(\w+)/,end:/(\w+)/,className:"string"})]}},_={className:"string",begin:/"/,end:/"/,contains:[i.BACKSLASH_ESCAPE,s,m]};m.contains.push(_);const j={begin:/\$\(\(/,end:/\)\)/,contains:[{begin:/\d+#[0-9a-f]+/,className:"number"},i.NUMBER_MODE,s]},M=i.SHEBANG({binary:`(${["fish","bash","zsh","sh","csh","ksh","tcsh","dash","scsh"].join("|")})`,relevance:10}),$={className:"function",begin:/\w[\w\d_]*\s*\(\s*\)\s*\{/,returnBegin:!0,contains:[i.inherit(i.TITLE_MODE,{begin:/\w[\w\d_]*/})],relevance:0};return{name:"Bash",aliases:["sh","zsh"],keywords:{$pattern:/\b[a-z._-]+\b/,keyword:"if then else elif fi for while in do done case esac function",literal:"true false",built_in:"break cd continue eval exec exit export getopts hash pwd readonly return shift test times trap umask unset alias bind builtin caller command declare echo enable help let local logout mapfile printf read readarray source type typeset ulimit unalias set shopt autoload bg bindkey bye cap chdir clone comparguments compcall compctl compdescribe compfiles compgroups compquote comptags comptry compvalues dirs disable disown echotc echoti emulate fc fg float functions getcap getln history integer jobs kill limit log noglob popd print pushd pushln rehash sched setcap setopt stat suspend ttyctl unfunction unhash unlimit unsetopt vared wait whence where which zcompile zformat zftp zle zmodload zparseopts zprof zpty zregexparse zsocket zstyle ztcp"},contains:[M,i.SHEBANG(),$,j,i.HASH_COMMENT_MODE,v,_,{className:"",begin:/\\"/},{className:"string",begin:/'/,end:/'/},s]}}},30786:i=>{function concat(...i){return i.map((i=>function source(i){return i?"string"==typeof i?i:i.source:null}(i))).join("")}i.exports=function http(i){const s="HTTP/(2|1\\.[01])",u={className:"attribute",begin:concat("^",/[A-Za-z][A-Za-z0-9-]*/,"(?=\\:\\s)"),starts:{contains:[{className:"punctuation",begin:/: /,relevance:0,starts:{end:"$",relevance:0}}]}},m=[u,{begin:"\\n\\n",starts:{subLanguage:[],endsWithParent:!0}}];return{name:"HTTP",aliases:["https"],illegal:/\S/,contains:[{begin:"^(?="+s+" \\d{3})",end:/$/,contains:[{className:"meta",begin:s},{className:"number",begin:"\\b\\d{3}\\b"}],starts:{end:/\b\B/,illegal:/\S/,contains:m}},{begin:"(?=^[A-Z]+ (.*?) "+s+"$)",end:/$/,contains:[{className:"string",begin:" ",end:" ",excludeBegin:!0,excludeEnd:!0},{className:"meta",begin:s},{className:"keyword",begin:"[A-Z]+"}],starts:{end:/\b\B/,illegal:/\S/,contains:m}},i.inherit(u,{relevance:0})]}}},96344:i=>{const s="[A-Za-z$_][0-9A-Za-z$_]*",u=["as","in","of","if","for","while","finally","var","new","function","do","return","void","else","break","catch","instanceof","with","throw","case","default","try","switch","continue","typeof","delete","let","yield","const","class","debugger","async","await","static","import","from","export","extends"],m=["true","false","null","undefined","NaN","Infinity"],v=[].concat(["setInterval","setTimeout","clearInterval","clearTimeout","require","exports","eval","isFinite","isNaN","parseFloat","parseInt","decodeURI","decodeURIComponent","encodeURI","encodeURIComponent","escape","unescape"],["arguments","this","super","console","window","document","localStorage","module","global"],["Intl","DataView","Number","Math","Date","String","RegExp","Object","Function","Boolean","Error","Symbol","Set","Map","WeakSet","WeakMap","Proxy","Reflect","JSON","Promise","Float64Array","Int16Array","Int32Array","Int8Array","Uint16Array","Uint32Array","Float32Array","Array","Uint8Array","Uint8ClampedArray","ArrayBuffer","BigInt64Array","BigUint64Array","BigInt"],["EvalError","InternalError","RangeError","ReferenceError","SyntaxError","TypeError","URIError"]);function lookahead(i){return concat("(?=",i,")")}function concat(...i){return i.map((i=>function source(i){return i?"string"==typeof i?i:i.source:null}(i))).join("")}i.exports=function javascript(i){const _=s,j="<>",M="",$={begin:/<[A-Za-z0-9\\._:-]+/,end:/\/[A-Za-z0-9\\._:-]+>|\/>/,isTrulyOpeningTag:(i,s)=>{const u=i[0].length+i.index,m=i.input[u];"<"!==m?">"===m&&(((i,{after:s})=>{const u="",returnBegin:!0,end:"\\s*=>",contains:[{className:"params",variants:[{begin:i.UNDERSCORE_IDENT_RE,relevance:0},{className:null,begin:/\(\s*\)/,skip:!0},{begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:W,contains:ye}]}]},{begin:/,/,relevance:0},{className:"",begin:/\s/,end:/\s*/,skip:!0},{variants:[{begin:j,end:M},{begin:$.begin,"on:begin":$.isTrulyOpeningTag,end:$.end}],subLanguage:"xml",contains:[{begin:$.begin,end:$.end,skip:!0,contains:["self"]}]}],relevance:0},{className:"function",beginKeywords:"function",end:/[{;]/,excludeEnd:!0,keywords:W,contains:["self",i.inherit(i.TITLE_MODE,{begin:_}),be],illegal:/%/},{beginKeywords:"while if switch catch for"},{className:"function",begin:i.UNDERSCORE_IDENT_RE+"\\([^()]*(\\([^()]*(\\([^()]*\\)[^()]*)*\\)[^()]*)*\\)\\s*\\{",returnBegin:!0,contains:[be,i.inherit(i.TITLE_MODE,{begin:_})]},{variants:[{begin:"\\."+_},{begin:"\\$"+_}],relevance:0},{className:"class",beginKeywords:"class",end:/[{;=]/,excludeEnd:!0,illegal:/[:"[\]]/,contains:[{beginKeywords:"extends"},i.UNDERSCORE_TITLE_MODE]},{begin:/\b(?=constructor)/,end:/[{;]/,excludeEnd:!0,contains:[i.inherit(i.TITLE_MODE,{begin:_}),"self",be]},{begin:"(get|set)\\s+(?="+_+"\\()",end:/\{/,keywords:"get set",contains:[i.inherit(i.TITLE_MODE,{begin:_}),{begin:/\(\)/},be]},{begin:/\$[(.]/}]}}},82026:i=>{i.exports=function json(i){const s={literal:"true false null"},u=[i.C_LINE_COMMENT_MODE,i.C_BLOCK_COMMENT_MODE],m=[i.QUOTE_STRING_MODE,i.C_NUMBER_MODE],v={end:",",endsWithParent:!0,excludeEnd:!0,contains:m,keywords:s},_={begin:/\{/,end:/\}/,contains:[{className:"attr",begin:/"/,end:/"/,contains:[i.BACKSLASH_ESCAPE],illegal:"\\n"},i.inherit(v,{begin:/:/})].concat(u),illegal:"\\S"},j={begin:"\\[",end:"\\]",contains:[i.inherit(v)],illegal:"\\S"};return m.push(_,j),u.forEach((function(i){m.push(i)})),{name:"JSON",contains:m,keywords:s,illegal:"\\S"}}},66336:i=>{i.exports=function powershell(i){const s={$pattern:/-?[A-z\.\-]+\b/,keyword:"if else foreach return do while until elseif begin for trap data dynamicparam end break throw param continue finally in switch exit filter try process catch hidden static parameter",built_in:"ac asnp cat cd CFS chdir clc clear clhy cli clp cls clv cnsn compare copy cp cpi cpp curl cvpa dbp del diff dir dnsn ebp echo|0 epal epcsv epsn erase etsn exsn fc fhx fl ft fw gal gbp gc gcb gci gcm gcs gdr gerr ghy gi gin gjb gl gm gmo gp gps gpv group gsn gsnp gsv gtz gu gv gwmi h history icm iex ihy ii ipal ipcsv ipmo ipsn irm ise iwmi iwr kill lp ls man md measure mi mount move mp mv nal ndr ni nmo npssc nsn nv ogv oh popd ps pushd pwd r rbp rcjb rcsn rd rdr ren ri rjb rm rmdir rmo rni rnp rp rsn rsnp rujb rv rvpa rwmi sajb sal saps sasv sbp sc scb select set shcm si sl sleep sls sort sp spjb spps spsv start stz sujb sv swmi tee trcm type wget where wjb write"},u={begin:"`[\\s\\S]",relevance:0},m={className:"variable",variants:[{begin:/\$\B/},{className:"keyword",begin:/\$this/},{begin:/\$[\w\d][\w\d_:]*/}]},v={className:"string",variants:[{begin:/"/,end:/"/},{begin:/@"/,end:/^"@/}],contains:[u,m,{className:"variable",begin:/\$[A-z]/,end:/[^A-z]/}]},_={className:"string",variants:[{begin:/'/,end:/'/},{begin:/@'/,end:/^'@/}]},j=i.inherit(i.COMMENT(null,null),{variants:[{begin:/#/,end:/$/},{begin:/<#/,end:/#>/}],contains:[{className:"doctag",variants:[{begin:/\.(synopsis|description|example|inputs|outputs|notes|link|component|role|functionality)/},{begin:/\.(parameter|forwardhelptargetname|forwardhelpcategory|remotehelprunspace|externalhelp)\s+\S+/}]}]}),M={className:"built_in",variants:[{begin:"(".concat("Add|Clear|Close|Copy|Enter|Exit|Find|Format|Get|Hide|Join|Lock|Move|New|Open|Optimize|Pop|Push|Redo|Remove|Rename|Reset|Resize|Search|Select|Set|Show|Skip|Split|Step|Switch|Undo|Unlock|Watch|Backup|Checkpoint|Compare|Compress|Convert|ConvertFrom|ConvertTo|Dismount|Edit|Expand|Export|Group|Import|Initialize|Limit|Merge|Mount|Out|Publish|Restore|Save|Sync|Unpublish|Update|Approve|Assert|Build|Complete|Confirm|Deny|Deploy|Disable|Enable|Install|Invoke|Register|Request|Restart|Resume|Start|Stop|Submit|Suspend|Uninstall|Unregister|Wait|Debug|Measure|Ping|Repair|Resolve|Test|Trace|Connect|Disconnect|Read|Receive|Send|Write|Block|Grant|Protect|Revoke|Unblock|Unprotect|Use|ForEach|Sort|Tee|Where",")+(-)[\\w\\d]+")}]},$={className:"class",beginKeywords:"class enum",end:/\s*[{]/,excludeEnd:!0,relevance:0,contains:[i.TITLE_MODE]},W={className:"function",begin:/function\s+/,end:/\s*\{|$/,excludeEnd:!0,returnBegin:!0,relevance:0,contains:[{begin:"function",relevance:0,className:"keyword"},{className:"title",begin:/\w[\w\d]*((-)[\w\d]+)*/,relevance:0},{begin:/\(/,end:/\)/,className:"params",relevance:0,contains:[m]}]},X={begin:/using\s/,end:/$/,returnBegin:!0,contains:[v,_,{className:"keyword",begin:/(using|assembly|command|module|namespace|type)/}]},Y={variants:[{className:"operator",begin:"(".concat("-and|-as|-band|-bnot|-bor|-bxor|-casesensitive|-ccontains|-ceq|-cge|-cgt|-cle|-clike|-clt|-cmatch|-cne|-cnotcontains|-cnotlike|-cnotmatch|-contains|-creplace|-csplit|-eq|-exact|-f|-file|-ge|-gt|-icontains|-ieq|-ige|-igt|-ile|-ilike|-ilt|-imatch|-in|-ine|-inotcontains|-inotlike|-inotmatch|-ireplace|-is|-isnot|-isplit|-join|-le|-like|-lt|-match|-ne|-not|-notcontains|-notin|-notlike|-notmatch|-or|-regex|-replace|-shl|-shr|-split|-wildcard|-xor",")\\b")},{className:"literal",begin:/(-)[\w\d]+/,relevance:0}]},Z={className:"function",begin:/\[.*\]\s*[\w]+[ ]??\(/,end:/$/,returnBegin:!0,relevance:0,contains:[{className:"keyword",begin:"(".concat(s.keyword.toString().replace(/\s/g,"|"),")\\b"),endsParent:!0,relevance:0},i.inherit(i.TITLE_MODE,{endsParent:!0})]},ee=[Z,j,u,i.NUMBER_MODE,v,_,M,m,{className:"literal",begin:/\$(null|true|false)\b/},{className:"selector-tag",begin:/@\B/,relevance:0}],ie={begin:/\[/,end:/\]/,excludeBegin:!0,excludeEnd:!0,relevance:0,contains:[].concat("self",ee,{begin:"("+["string","char","byte","int","long","bool","decimal","single","double","DateTime","xml","array","hashtable","void"].join("|")+")",className:"built_in",relevance:0},{className:"type",begin:/[\.\w\d]+/,relevance:0})};return Z.contains.unshift(ie),{name:"PowerShell",aliases:["ps","ps1"],case_insensitive:!0,keywords:s,contains:ee.concat($,W,X,Y,ie)}}},42157:i=>{function source(i){return i?"string"==typeof i?i:i.source:null}function lookahead(i){return concat("(?=",i,")")}function concat(...i){return i.map((i=>source(i))).join("")}function either(...i){return"("+i.map((i=>source(i))).join("|")+")"}i.exports=function xml(i){const s=concat(/[A-Z_]/,function optional(i){return concat("(",i,")?")}(/[A-Z0-9_.-]*:/),/[A-Z0-9_.-]*/),u={className:"symbol",begin:/&[a-z]+;|&#[0-9]+;|&#x[a-f0-9]+;/},m={begin:/\s/,contains:[{className:"meta-keyword",begin:/#?[a-z_][a-z1-9_-]+/,illegal:/\n/}]},v=i.inherit(m,{begin:/\(/,end:/\)/}),_=i.inherit(i.APOS_STRING_MODE,{className:"meta-string"}),j=i.inherit(i.QUOTE_STRING_MODE,{className:"meta-string"}),M={endsWithParent:!0,illegal:/`]+/}]}]}]};return{name:"HTML, XML",aliases:["html","xhtml","rss","atom","xjb","xsd","xsl","plist","wsf","svg"],case_insensitive:!0,contains:[{className:"meta",begin://,relevance:10,contains:[m,j,_,v,{begin:/\[/,end:/\]/,contains:[{className:"meta",begin://,contains:[m,v,j,_]}]}]},i.COMMENT(//,{relevance:10}),{begin://,relevance:10},u,{className:"meta",begin:/<\?xml/,end:/\?>/,relevance:10},{className:"tag",begin:/)/,end:/>/,keywords:{name:"style"},contains:[M],starts:{end:/<\/style>/,returnEnd:!0,subLanguage:["css","xml"]}},{className:"tag",begin:/)/,end:/>/,keywords:{name:"script"},contains:[M],starts:{end:/<\/script>/,returnEnd:!0,subLanguage:["javascript","handlebars","xml"]}},{className:"tag",begin:/<>|<\/>/},{className:"tag",begin:concat(//,/>/,/\s/)))),end:/\/?>/,contains:[{className:"name",begin:s,relevance:0,starts:M}]},{className:"tag",begin:concat(/<\//,lookahead(concat(s,/>/))),contains:[{className:"name",begin:s,relevance:0},{begin:/>/,relevance:0,endsParent:!0}]}]}}},54587:i=>{i.exports=function yaml(i){var s="true false yes no null",u="[\\w#;/?:@&=+$,.~*'()[\\]]+",m={className:"string",relevance:0,variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/\S+/}],contains:[i.BACKSLASH_ESCAPE,{className:"template-variable",variants:[{begin:/\{\{/,end:/\}\}/},{begin:/%\{/,end:/\}/}]}]},v=i.inherit(m,{variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/[^\s,{}[\]]+/}]}),_={className:"number",begin:"\\b[0-9]{4}(-[0-9][0-9]){0,2}([Tt \\t][0-9][0-9]?(:[0-9][0-9]){2})?(\\.[0-9]*)?([ \\t])*(Z|[-+][0-9][0-9]?(:[0-9][0-9])?)?\\b"},j={end:",",endsWithParent:!0,excludeEnd:!0,keywords:s,relevance:0},M={begin:/\{/,end:/\}/,contains:[j],illegal:"\\n",relevance:0},$={begin:"\\[",end:"\\]",contains:[j],illegal:"\\n",relevance:0},W=[{className:"attr",variants:[{begin:"\\w[\\w :\\/.-]*:(?=[ \t]|$)"},{begin:'"\\w[\\w :\\/.-]*":(?=[ \t]|$)'},{begin:"'\\w[\\w :\\/.-]*':(?=[ \t]|$)"}]},{className:"meta",begin:"^---\\s*$",relevance:10},{className:"string",begin:"[\\|>]([1-9]?[+-])?[ ]*\\n( +)[^ ][^\\n]*\\n(\\2[^\\n]+\\n?)*"},{begin:"<%[%=-]?",end:"[%-]?%>",subLanguage:"ruby",excludeBegin:!0,excludeEnd:!0,relevance:0},{className:"type",begin:"!\\w+!"+u},{className:"type",begin:"!<"+u+">"},{className:"type",begin:"!"+u},{className:"type",begin:"!!"+u},{className:"meta",begin:"&"+i.UNDERSCORE_IDENT_RE+"$"},{className:"meta",begin:"\\*"+i.UNDERSCORE_IDENT_RE+"$"},{className:"bullet",begin:"-(?=[ ]|$)",relevance:0},i.HASH_COMMENT_MODE,{beginKeywords:s,keywords:{literal:s}},_,{className:"number",begin:i.C_NUMBER_RE+"\\b",relevance:0},M,$,m],X=[...W];return X.pop(),X.push(v),j.contains=X,{name:"YAML",case_insensitive:!0,aliases:["yml"],contains:W}}},8679:(i,s,u)=>{"use strict";var m=u(59864),v={childContextTypes:!0,contextType:!0,contextTypes:!0,defaultProps:!0,displayName:!0,getDefaultProps:!0,getDerivedStateFromError:!0,getDerivedStateFromProps:!0,mixins:!0,propTypes:!0,type:!0},_={name:!0,length:!0,prototype:!0,caller:!0,callee:!0,arguments:!0,arity:!0},j={$$typeof:!0,compare:!0,defaultProps:!0,displayName:!0,propTypes:!0,type:!0},M={};function getStatics(i){return m.isMemo(i)?j:M[i.$$typeof]||v}M[m.ForwardRef]={$$typeof:!0,render:!0,defaultProps:!0,displayName:!0,propTypes:!0},M[m.Memo]=j;var $=Object.defineProperty,W=Object.getOwnPropertyNames,X=Object.getOwnPropertySymbols,Y=Object.getOwnPropertyDescriptor,Z=Object.getPrototypeOf,ee=Object.prototype;i.exports=function hoistNonReactStatics(i,s,u){if("string"!=typeof s){if(ee){var m=Z(s);m&&m!==ee&&hoistNonReactStatics(i,m,u)}var v=W(s);X&&(v=v.concat(X(s)));for(var j=getStatics(i),M=getStatics(s),ie=0;ie{s.read=function(i,s,u,m,v){var _,j,M=8*v-m-1,$=(1<>1,X=-7,Y=u?v-1:0,Z=u?-1:1,ee=i[s+Y];for(Y+=Z,_=ee&(1<<-X)-1,ee>>=-X,X+=M;X>0;_=256*_+i[s+Y],Y+=Z,X-=8);for(j=_&(1<<-X)-1,_>>=-X,X+=m;X>0;j=256*j+i[s+Y],Y+=Z,X-=8);if(0===_)_=1-W;else{if(_===$)return j?NaN:1/0*(ee?-1:1);j+=Math.pow(2,m),_-=W}return(ee?-1:1)*j*Math.pow(2,_-m)},s.write=function(i,s,u,m,v,_){var j,M,$,W=8*_-v-1,X=(1<>1,Z=23===v?Math.pow(2,-24)-Math.pow(2,-77):0,ee=m?0:_-1,ie=m?1:-1,ae=s<0||0===s&&1/s<0?1:0;for(s=Math.abs(s),isNaN(s)||s===1/0?(M=isNaN(s)?1:0,j=X):(j=Math.floor(Math.log(s)/Math.LN2),s*($=Math.pow(2,-j))<1&&(j--,$*=2),(s+=j+Y>=1?Z/$:Z*Math.pow(2,1-Y))*$>=2&&(j++,$/=2),j+Y>=X?(M=0,j=X):j+Y>=1?(M=(s*$-1)*Math.pow(2,v),j+=Y):(M=s*Math.pow(2,Y-1)*Math.pow(2,v),j=0));v>=8;i[u+ee]=255&M,ee+=ie,M/=256,v-=8);for(j=j<0;i[u+ee]=255&j,ee+=ie,j/=256,W-=8);i[u+ee-ie]|=128*ae}},43393:function(i){i.exports=function(){"use strict";var i=Array.prototype.slice;function createClass(i,s){s&&(i.prototype=Object.create(s.prototype)),i.prototype.constructor=i}function Iterable(i){return isIterable(i)?i:Seq(i)}function KeyedIterable(i){return isKeyed(i)?i:KeyedSeq(i)}function IndexedIterable(i){return isIndexed(i)?i:IndexedSeq(i)}function SetIterable(i){return isIterable(i)&&!isAssociative(i)?i:SetSeq(i)}function isIterable(i){return!(!i||!i[s])}function isKeyed(i){return!(!i||!i[u])}function isIndexed(i){return!(!i||!i[m])}function isAssociative(i){return isKeyed(i)||isIndexed(i)}function isOrdered(i){return!(!i||!i[v])}createClass(KeyedIterable,Iterable),createClass(IndexedIterable,Iterable),createClass(SetIterable,Iterable),Iterable.isIterable=isIterable,Iterable.isKeyed=isKeyed,Iterable.isIndexed=isIndexed,Iterable.isAssociative=isAssociative,Iterable.isOrdered=isOrdered,Iterable.Keyed=KeyedIterable,Iterable.Indexed=IndexedIterable,Iterable.Set=SetIterable;var s="@@__IMMUTABLE_ITERABLE__@@",u="@@__IMMUTABLE_KEYED__@@",m="@@__IMMUTABLE_INDEXED__@@",v="@@__IMMUTABLE_ORDERED__@@",_="delete",j=5,M=1<>>0;if(""+u!==s||4294967295===u)return NaN;s=u}return s<0?ensureSize(i)+s:s}function returnTrue(){return!0}function wholeSlice(i,s,u){return(0===i||void 0!==u&&i<=-u)&&(void 0===s||void 0!==u&&s>=u)}function resolveBegin(i,s){return resolveIndex(i,s,0)}function resolveEnd(i,s){return resolveIndex(i,s,s)}function resolveIndex(i,s,u){return void 0===i?u:i<0?Math.max(0,s+i):void 0===s?i:Math.min(s,i)}var Z=0,ee=1,ie=2,ae="function"==typeof Symbol&&Symbol.iterator,le="@@iterator",ce=ae||le;function Iterator(i){this.next=i}function iteratorValue(i,s,u,m){var v=0===i?s:1===i?u:[s,u];return m?m.value=v:m={value:v,done:!1},m}function iteratorDone(){return{value:void 0,done:!0}}function hasIterator(i){return!!getIteratorFn(i)}function isIterator(i){return i&&"function"==typeof i.next}function getIterator(i){var s=getIteratorFn(i);return s&&s.call(i)}function getIteratorFn(i){var s=i&&(ae&&i[ae]||i[le]);if("function"==typeof s)return s}function isArrayLike(i){return i&&"number"==typeof i.length}function Seq(i){return null==i?emptySequence():isIterable(i)?i.toSeq():seqFromValue(i)}function KeyedSeq(i){return null==i?emptySequence().toKeyedSeq():isIterable(i)?isKeyed(i)?i.toSeq():i.fromEntrySeq():keyedSeqFromValue(i)}function IndexedSeq(i){return null==i?emptySequence():isIterable(i)?isKeyed(i)?i.entrySeq():i.toIndexedSeq():indexedSeqFromValue(i)}function SetSeq(i){return(null==i?emptySequence():isIterable(i)?isKeyed(i)?i.entrySeq():i:indexedSeqFromValue(i)).toSetSeq()}Iterator.prototype.toString=function(){return"[Iterator]"},Iterator.KEYS=Z,Iterator.VALUES=ee,Iterator.ENTRIES=ie,Iterator.prototype.inspect=Iterator.prototype.toSource=function(){return this.toString()},Iterator.prototype[ce]=function(){return this},createClass(Seq,Iterable),Seq.of=function(){return Seq(arguments)},Seq.prototype.toSeq=function(){return this},Seq.prototype.toString=function(){return this.__toString("Seq {","}")},Seq.prototype.cacheResult=function(){return!this._cache&&this.__iterateUncached&&(this._cache=this.entrySeq().toArray(),this.size=this._cache.length),this},Seq.prototype.__iterate=function(i,s){return seqIterate(this,i,s,!0)},Seq.prototype.__iterator=function(i,s){return seqIterator(this,i,s,!0)},createClass(KeyedSeq,Seq),KeyedSeq.prototype.toKeyedSeq=function(){return this},createClass(IndexedSeq,Seq),IndexedSeq.of=function(){return IndexedSeq(arguments)},IndexedSeq.prototype.toIndexedSeq=function(){return this},IndexedSeq.prototype.toString=function(){return this.__toString("Seq [","]")},IndexedSeq.prototype.__iterate=function(i,s){return seqIterate(this,i,s,!1)},IndexedSeq.prototype.__iterator=function(i,s){return seqIterator(this,i,s,!1)},createClass(SetSeq,Seq),SetSeq.of=function(){return SetSeq(arguments)},SetSeq.prototype.toSetSeq=function(){return this},Seq.isSeq=isSeq,Seq.Keyed=KeyedSeq,Seq.Set=SetSeq,Seq.Indexed=IndexedSeq;var pe,de,fe,ye="@@__IMMUTABLE_SEQ__@@";function ArraySeq(i){this._array=i,this.size=i.length}function ObjectSeq(i){var s=Object.keys(i);this._object=i,this._keys=s,this.size=s.length}function IterableSeq(i){this._iterable=i,this.size=i.length||i.size}function IteratorSeq(i){this._iterator=i,this._iteratorCache=[]}function isSeq(i){return!(!i||!i[ye])}function emptySequence(){return pe||(pe=new ArraySeq([]))}function keyedSeqFromValue(i){var s=Array.isArray(i)?new ArraySeq(i).fromEntrySeq():isIterator(i)?new IteratorSeq(i).fromEntrySeq():hasIterator(i)?new IterableSeq(i).fromEntrySeq():"object"==typeof i?new ObjectSeq(i):void 0;if(!s)throw new TypeError("Expected Array or iterable object of [k, v] entries, or keyed object: "+i);return s}function indexedSeqFromValue(i){var s=maybeIndexedSeqFromValue(i);if(!s)throw new TypeError("Expected Array or iterable object of values: "+i);return s}function seqFromValue(i){var s=maybeIndexedSeqFromValue(i)||"object"==typeof i&&new ObjectSeq(i);if(!s)throw new TypeError("Expected Array or iterable object of values, or keyed object: "+i);return s}function maybeIndexedSeqFromValue(i){return isArrayLike(i)?new ArraySeq(i):isIterator(i)?new IteratorSeq(i):hasIterator(i)?new IterableSeq(i):void 0}function seqIterate(i,s,u,m){var v=i._cache;if(v){for(var _=v.length-1,j=0;j<=_;j++){var M=v[u?_-j:j];if(!1===s(M[1],m?M[0]:j,i))return j+1}return j}return i.__iterateUncached(s,u)}function seqIterator(i,s,u,m){var v=i._cache;if(v){var _=v.length-1,j=0;return new Iterator((function(){var i=v[u?_-j:j];return j++>_?iteratorDone():iteratorValue(s,m?i[0]:j-1,i[1])}))}return i.__iteratorUncached(s,u)}function fromJS(i,s){return s?fromJSWith(s,i,"",{"":i}):fromJSDefault(i)}function fromJSWith(i,s,u,m){return Array.isArray(s)?i.call(m,u,IndexedSeq(s).map((function(u,m){return fromJSWith(i,u,m,s)}))):isPlainObj(s)?i.call(m,u,KeyedSeq(s).map((function(u,m){return fromJSWith(i,u,m,s)}))):s}function fromJSDefault(i){return Array.isArray(i)?IndexedSeq(i).map(fromJSDefault).toList():isPlainObj(i)?KeyedSeq(i).map(fromJSDefault).toMap():i}function isPlainObj(i){return i&&(i.constructor===Object||void 0===i.constructor)}function is(i,s){if(i===s||i!=i&&s!=s)return!0;if(!i||!s)return!1;if("function"==typeof i.valueOf&&"function"==typeof s.valueOf){if((i=i.valueOf())===(s=s.valueOf())||i!=i&&s!=s)return!0;if(!i||!s)return!1}return!("function"!=typeof i.equals||"function"!=typeof s.equals||!i.equals(s))}function deepEqual(i,s){if(i===s)return!0;if(!isIterable(s)||void 0!==i.size&&void 0!==s.size&&i.size!==s.size||void 0!==i.__hash&&void 0!==s.__hash&&i.__hash!==s.__hash||isKeyed(i)!==isKeyed(s)||isIndexed(i)!==isIndexed(s)||isOrdered(i)!==isOrdered(s))return!1;if(0===i.size&&0===s.size)return!0;var u=!isAssociative(i);if(isOrdered(i)){var m=i.entries();return s.every((function(i,s){var v=m.next().value;return v&&is(v[1],i)&&(u||is(v[0],s))}))&&m.next().done}var v=!1;if(void 0===i.size)if(void 0===s.size)"function"==typeof i.cacheResult&&i.cacheResult();else{v=!0;var _=i;i=s,s=_}var j=!0,M=s.__iterate((function(s,m){if(u?!i.has(s):v?!is(s,i.get(m,W)):!is(i.get(m,W),s))return j=!1,!1}));return j&&i.size===M}function Repeat(i,s){if(!(this instanceof Repeat))return new Repeat(i,s);if(this._value=i,this.size=void 0===s?1/0:Math.max(0,s),0===this.size){if(de)return de;de=this}}function invariant(i,s){if(!i)throw new Error(s)}function Range(i,s,u){if(!(this instanceof Range))return new Range(i,s,u);if(invariant(0!==u,"Cannot step a Range by 0"),i=i||0,void 0===s&&(s=1/0),u=void 0===u?1:Math.abs(u),sm?iteratorDone():iteratorValue(i,v,u[s?m-v++:v++])}))},createClass(ObjectSeq,KeyedSeq),ObjectSeq.prototype.get=function(i,s){return void 0===s||this.has(i)?this._object[i]:s},ObjectSeq.prototype.has=function(i){return this._object.hasOwnProperty(i)},ObjectSeq.prototype.__iterate=function(i,s){for(var u=this._object,m=this._keys,v=m.length-1,_=0;_<=v;_++){var j=m[s?v-_:_];if(!1===i(u[j],j,this))return _+1}return _},ObjectSeq.prototype.__iterator=function(i,s){var u=this._object,m=this._keys,v=m.length-1,_=0;return new Iterator((function(){var j=m[s?v-_:_];return _++>v?iteratorDone():iteratorValue(i,j,u[j])}))},ObjectSeq.prototype[v]=!0,createClass(IterableSeq,IndexedSeq),IterableSeq.prototype.__iterateUncached=function(i,s){if(s)return this.cacheResult().__iterate(i,s);var u=getIterator(this._iterable),m=0;if(isIterator(u))for(var v;!(v=u.next()).done&&!1!==i(v.value,m++,this););return m},IterableSeq.prototype.__iteratorUncached=function(i,s){if(s)return this.cacheResult().__iterator(i,s);var u=getIterator(this._iterable);if(!isIterator(u))return new Iterator(iteratorDone);var m=0;return new Iterator((function(){var s=u.next();return s.done?s:iteratorValue(i,m++,s.value)}))},createClass(IteratorSeq,IndexedSeq),IteratorSeq.prototype.__iterateUncached=function(i,s){if(s)return this.cacheResult().__iterate(i,s);for(var u,m=this._iterator,v=this._iteratorCache,_=0;_=m.length){var s=u.next();if(s.done)return s;m[v]=s.value}return iteratorValue(i,v,m[v++])}))},createClass(Repeat,IndexedSeq),Repeat.prototype.toString=function(){return 0===this.size?"Repeat []":"Repeat [ "+this._value+" "+this.size+" times ]"},Repeat.prototype.get=function(i,s){return this.has(i)?this._value:s},Repeat.prototype.includes=function(i){return is(this._value,i)},Repeat.prototype.slice=function(i,s){var u=this.size;return wholeSlice(i,s,u)?this:new Repeat(this._value,resolveEnd(s,u)-resolveBegin(i,u))},Repeat.prototype.reverse=function(){return this},Repeat.prototype.indexOf=function(i){return is(this._value,i)?0:-1},Repeat.prototype.lastIndexOf=function(i){return is(this._value,i)?this.size:-1},Repeat.prototype.__iterate=function(i,s){for(var u=0;u=0&&s=0&&uu?iteratorDone():iteratorValue(i,_++,j)}))},Range.prototype.equals=function(i){return i instanceof Range?this._start===i._start&&this._end===i._end&&this._step===i._step:deepEqual(this,i)},createClass(Collection,Iterable),createClass(KeyedCollection,Collection),createClass(IndexedCollection,Collection),createClass(SetCollection,Collection),Collection.Keyed=KeyedCollection,Collection.Indexed=IndexedCollection,Collection.Set=SetCollection;var be="function"==typeof Math.imul&&-2===Math.imul(4294967295,2)?Math.imul:function imul(i,s){var u=65535&(i|=0),m=65535&(s|=0);return u*m+((i>>>16)*m+u*(s>>>16)<<16>>>0)|0};function smi(i){return i>>>1&1073741824|3221225471&i}function hash(i){if(!1===i||null==i)return 0;if("function"==typeof i.valueOf&&(!1===(i=i.valueOf())||null==i))return 0;if(!0===i)return 1;var s=typeof i;if("number"===s){if(i!=i||i===1/0)return 0;var u=0|i;for(u!==i&&(u^=4294967295*i);i>4294967295;)u^=i/=4294967295;return smi(u)}if("string"===s)return i.length>Te?cachedHashString(i):hashString(i);if("function"==typeof i.hashCode)return i.hashCode();if("object"===s)return hashJSObj(i);if("function"==typeof i.toString)return hashString(i.toString());throw new Error("Value type "+s+" cannot be hashed.")}function cachedHashString(i){var s=ze[i];return void 0===s&&(s=hashString(i),qe===Re&&(qe=0,ze={}),qe++,ze[i]=s),s}function hashString(i){for(var s=0,u=0;u0)switch(i.nodeType){case 1:return i.uniqueID;case 9:return i.documentElement&&i.documentElement.uniqueID}}var Se,xe="function"==typeof WeakMap;xe&&(Se=new WeakMap);var Ie=0,Pe="__immutablehash__";"function"==typeof Symbol&&(Pe=Symbol(Pe));var Te=16,Re=255,qe=0,ze={};function assertNotInfinite(i){invariant(i!==1/0,"Cannot perform this action with an infinite size.")}function Map(i){return null==i?emptyMap():isMap(i)&&!isOrdered(i)?i:emptyMap().withMutations((function(s){var u=KeyedIterable(i);assertNotInfinite(u.size),u.forEach((function(i,u){return s.set(u,i)}))}))}function isMap(i){return!(!i||!i[We])}createClass(Map,KeyedCollection),Map.of=function(){var s=i.call(arguments,0);return emptyMap().withMutations((function(i){for(var u=0;u=s.length)throw new Error("Missing value for key: "+s[u]);i.set(s[u],s[u+1])}}))},Map.prototype.toString=function(){return this.__toString("Map {","}")},Map.prototype.get=function(i,s){return this._root?this._root.get(0,void 0,i,s):s},Map.prototype.set=function(i,s){return updateMap(this,i,s)},Map.prototype.setIn=function(i,s){return this.updateIn(i,W,(function(){return s}))},Map.prototype.remove=function(i){return updateMap(this,i,W)},Map.prototype.deleteIn=function(i){return this.updateIn(i,(function(){return W}))},Map.prototype.update=function(i,s,u){return 1===arguments.length?i(this):this.updateIn([i],s,u)},Map.prototype.updateIn=function(i,s,u){u||(u=s,s=void 0);var m=updateInDeepMap(this,forceIterator(i),s,u);return m===W?void 0:m},Map.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._root=null,this.__hash=void 0,this.__altered=!0,this):emptyMap()},Map.prototype.merge=function(){return mergeIntoMapWith(this,void 0,arguments)},Map.prototype.mergeWith=function(s){return mergeIntoMapWith(this,s,i.call(arguments,1))},Map.prototype.mergeIn=function(s){var u=i.call(arguments,1);return this.updateIn(s,emptyMap(),(function(i){return"function"==typeof i.merge?i.merge.apply(i,u):u[u.length-1]}))},Map.prototype.mergeDeep=function(){return mergeIntoMapWith(this,deepMerger,arguments)},Map.prototype.mergeDeepWith=function(s){var u=i.call(arguments,1);return mergeIntoMapWith(this,deepMergerWith(s),u)},Map.prototype.mergeDeepIn=function(s){var u=i.call(arguments,1);return this.updateIn(s,emptyMap(),(function(i){return"function"==typeof i.mergeDeep?i.mergeDeep.apply(i,u):u[u.length-1]}))},Map.prototype.sort=function(i){return OrderedMap(sortFactory(this,i))},Map.prototype.sortBy=function(i,s){return OrderedMap(sortFactory(this,s,i))},Map.prototype.withMutations=function(i){var s=this.asMutable();return i(s),s.wasAltered()?s.__ensureOwner(this.__ownerID):this},Map.prototype.asMutable=function(){return this.__ownerID?this:this.__ensureOwner(new OwnerID)},Map.prototype.asImmutable=function(){return this.__ensureOwner()},Map.prototype.wasAltered=function(){return this.__altered},Map.prototype.__iterator=function(i,s){return new MapIterator(this,i,s)},Map.prototype.__iterate=function(i,s){var u=this,m=0;return this._root&&this._root.iterate((function(s){return m++,i(s[1],s[0],u)}),s),m},Map.prototype.__ensureOwner=function(i){return i===this.__ownerID?this:i?makeMap(this.size,this._root,i,this.__hash):(this.__ownerID=i,this.__altered=!1,this)},Map.isMap=isMap;var Ve,We="@@__IMMUTABLE_MAP__@@",He=Map.prototype;function ArrayMapNode(i,s){this.ownerID=i,this.entries=s}function BitmapIndexedNode(i,s,u){this.ownerID=i,this.bitmap=s,this.nodes=u}function HashArrayMapNode(i,s,u){this.ownerID=i,this.count=s,this.nodes=u}function HashCollisionNode(i,s,u){this.ownerID=i,this.keyHash=s,this.entries=u}function ValueNode(i,s,u){this.ownerID=i,this.keyHash=s,this.entry=u}function MapIterator(i,s,u){this._type=s,this._reverse=u,this._stack=i._root&&mapIteratorFrame(i._root)}function mapIteratorValue(i,s){return iteratorValue(i,s[0],s[1])}function mapIteratorFrame(i,s){return{node:i,index:0,__prev:s}}function makeMap(i,s,u,m){var v=Object.create(He);return v.size=i,v._root=s,v.__ownerID=u,v.__hash=m,v.__altered=!1,v}function emptyMap(){return Ve||(Ve=makeMap(0))}function updateMap(i,s,u){var m,v;if(i._root){var _=MakeRef(X),j=MakeRef(Y);if(m=updateNode(i._root,i.__ownerID,0,void 0,s,u,_,j),!j.value)return i;v=i.size+(_.value?u===W?-1:1:0)}else{if(u===W)return i;v=1,m=new ArrayMapNode(i.__ownerID,[[s,u]])}return i.__ownerID?(i.size=v,i._root=m,i.__hash=void 0,i.__altered=!0,i):m?makeMap(v,m):emptyMap()}function updateNode(i,s,u,m,v,_,j,M){return i?i.update(s,u,m,v,_,j,M):_===W?i:(SetRef(M),SetRef(j),new ValueNode(s,m,[v,_]))}function isLeafNode(i){return i.constructor===ValueNode||i.constructor===HashCollisionNode}function mergeIntoNode(i,s,u,m,v){if(i.keyHash===m)return new HashCollisionNode(s,m,[i.entry,v]);var _,M=(0===u?i.keyHash:i.keyHash>>>u)&$,W=(0===u?m:m>>>u)&$;return new BitmapIndexedNode(s,1<>>=1)j[$]=1&u?s[_++]:void 0;return j[m]=v,new HashArrayMapNode(i,_+1,j)}function mergeIntoMapWith(i,s,u){for(var m=[],v=0;v>1&1431655765))+(i>>2&858993459))+(i>>4)&252645135,i+=i>>8,127&(i+=i>>16)}function setIn(i,s,u,m){var v=m?i:arrCopy(i);return v[s]=u,v}function spliceIn(i,s,u,m){var v=i.length+1;if(m&&s+1===v)return i[s]=u,i;for(var _=new Array(v),j=0,M=0;M=Xe)return createNodes(i,$,m,v);var ee=i&&i===this.ownerID,ie=ee?$:arrCopy($);return Z?M?X===Y-1?ie.pop():ie[X]=ie.pop():ie[X]=[m,v]:ie.push([m,v]),ee?(this.entries=ie,this):new ArrayMapNode(i,ie)}},BitmapIndexedNode.prototype.get=function(i,s,u,m){void 0===s&&(s=hash(u));var v=1<<((0===i?s:s>>>i)&$),_=this.bitmap;return 0==(_&v)?m:this.nodes[popCount(_&v-1)].get(i+j,s,u,m)},BitmapIndexedNode.prototype.update=function(i,s,u,m,v,_,M){void 0===u&&(u=hash(m));var X=(0===s?u:u>>>s)&$,Y=1<=Ye)return expandNodes(i,ae,Z,X,ce);if(ee&&!ce&&2===ae.length&&isLeafNode(ae[1^ie]))return ae[1^ie];if(ee&&ce&&1===ae.length&&isLeafNode(ce))return ce;var pe=i&&i===this.ownerID,de=ee?ce?Z:Z^Y:Z|Y,fe=ee?ce?setIn(ae,ie,ce,pe):spliceOut(ae,ie,pe):spliceIn(ae,ie,ce,pe);return pe?(this.bitmap=de,this.nodes=fe,this):new BitmapIndexedNode(i,de,fe)},HashArrayMapNode.prototype.get=function(i,s,u,m){void 0===s&&(s=hash(u));var v=(0===i?s:s>>>i)&$,_=this.nodes[v];return _?_.get(i+j,s,u,m):m},HashArrayMapNode.prototype.update=function(i,s,u,m,v,_,M){void 0===u&&(u=hash(m));var X=(0===s?u:u>>>s)&$,Y=v===W,Z=this.nodes,ee=Z[X];if(Y&&!ee)return this;var ie=updateNode(ee,i,s+j,u,m,v,_,M);if(ie===ee)return this;var ae=this.count;if(ee){if(!ie&&--ae0&&m=0&&i>>s&$;if(m>=this.array.length)return new VNode([],i);var v,_=0===m;if(s>0){var M=this.array[m];if((v=M&&M.removeBefore(i,s-j,u))===M&&_)return this}if(_&&!v)return this;var W=editableVNode(this,i);if(!_)for(var X=0;X>>s&$;if(v>=this.array.length)return this;if(s>0){var _=this.array[v];if((m=_&&_.removeAfter(i,s-j,u))===_&&v===this.array.length-1)return this}var M=editableVNode(this,i);return M.array.splice(v+1),m&&(M.array[v]=m),M};var rt,nt,ot={};function iterateList(i,s){var u=i._origin,m=i._capacity,v=getTailOffset(m),_=i._tail;return iterateNodeOrLeaf(i._root,i._level,0);function iterateNodeOrLeaf(i,s,u){return 0===s?iterateLeaf(i,u):iterateNode(i,s,u)}function iterateLeaf(i,j){var $=j===v?_&&_.array:i&&i.array,W=j>u?0:u-j,X=m-j;return X>M&&(X=M),function(){if(W===X)return ot;var i=s?--X:W++;return $&&$[i]}}function iterateNode(i,v,_){var $,W=i&&i.array,X=_>u?0:u-_>>v,Y=1+(m-_>>v);return Y>M&&(Y=M),function(){for(;;){if($){var i=$();if(i!==ot)return i;$=null}if(X===Y)return ot;var u=s?--Y:X++;$=iterateNodeOrLeaf(W&&W[u],v-j,_+(u<=i.size||s<0)return i.withMutations((function(i){s<0?setListBounds(i,s).set(0,u):setListBounds(i,0,s+1).set(s,u)}));s+=i._origin;var m=i._tail,v=i._root,_=MakeRef(Y);return s>=getTailOffset(i._capacity)?m=updateVNode(m,i.__ownerID,0,s,u,_):v=updateVNode(v,i.__ownerID,i._level,s,u,_),_.value?i.__ownerID?(i._root=v,i._tail=m,i.__hash=void 0,i.__altered=!0,i):makeList(i._origin,i._capacity,i._level,v,m):i}function updateVNode(i,s,u,m,v,_){var M,W=m>>>u&$,X=i&&W0){var Y=i&&i.array[W],Z=updateVNode(Y,s,u-j,m,v,_);return Z===Y?i:((M=editableVNode(i,s)).array[W]=Z,M)}return X&&i.array[W]===v?i:(SetRef(_),M=editableVNode(i,s),void 0===v&&W===M.array.length-1?M.array.pop():M.array[W]=v,M)}function editableVNode(i,s){return s&&i&&s===i.ownerID?i:new VNode(i?i.array.slice():[],s)}function listNodeFor(i,s){if(s>=getTailOffset(i._capacity))return i._tail;if(s<1<0;)u=u.array[s>>>m&$],m-=j;return u}}function setListBounds(i,s,u){void 0!==s&&(s|=0),void 0!==u&&(u|=0);var m=i.__ownerID||new OwnerID,v=i._origin,_=i._capacity,M=v+s,W=void 0===u?_:u<0?_+u:v+u;if(M===v&&W===_)return i;if(M>=W)return i.clear();for(var X=i._level,Y=i._root,Z=0;M+Z<0;)Y=new VNode(Y&&Y.array.length?[void 0,Y]:[],m),Z+=1<<(X+=j);Z&&(M+=Z,v+=Z,W+=Z,_+=Z);for(var ee=getTailOffset(_),ie=getTailOffset(W);ie>=1<ee?new VNode([],m):ae;if(ae&&ie>ee&&M<_&&ae.array.length){for(var ce=Y=editableVNode(Y,m),pe=X;pe>j;pe-=j){var de=ee>>>pe&$;ce=ce.array[de]=editableVNode(ce.array[de],m)}ce.array[ee>>>j&$]=ae}if(W<_&&(le=le&&le.removeAfter(m,0,W)),M>=ie)M-=ie,W-=ie,X=j,Y=null,le=le&&le.removeBefore(m,0,M);else if(M>v||ie>>X&$;if(fe!==ie>>>X&$)break;fe&&(Z+=(1<v&&(Y=Y.removeBefore(m,X,M-Z)),Y&&iev&&(v=M.size),isIterable(j)||(M=M.map((function(i){return fromJS(i)}))),m.push(M)}return v>i.size&&(i=i.setSize(v)),mergeIntoCollectionWith(i,s,m)}function getTailOffset(i){return i>>j<=M&&j.size>=2*_.size?(m=(v=j.filter((function(i,s){return void 0!==i&&$!==s}))).toKeyedSeq().map((function(i){return i[0]})).flip().toMap(),i.__ownerID&&(m.__ownerID=v.__ownerID=i.__ownerID)):(m=_.remove(s),v=$===j.size-1?j.pop():j.set($,void 0))}else if(X){if(u===j.get($)[1])return i;m=_,v=j.set($,[s,u])}else m=_.set(s,j.size),v=j.set(j.size,[s,u]);return i.__ownerID?(i.size=m.size,i._map=m,i._list=v,i.__hash=void 0,i):makeOrderedMap(m,v)}function ToKeyedSequence(i,s){this._iter=i,this._useKeys=s,this.size=i.size}function ToIndexedSequence(i){this._iter=i,this.size=i.size}function ToSetSequence(i){this._iter=i,this.size=i.size}function FromEntriesSequence(i){this._iter=i,this.size=i.size}function flipFactory(i){var s=makeSequence(i);return s._iter=i,s.size=i.size,s.flip=function(){return i},s.reverse=function(){var s=i.reverse.apply(this);return s.flip=function(){return i.reverse()},s},s.has=function(s){return i.includes(s)},s.includes=function(s){return i.has(s)},s.cacheResult=cacheResultThrough,s.__iterateUncached=function(s,u){var m=this;return i.__iterate((function(i,u){return!1!==s(u,i,m)}),u)},s.__iteratorUncached=function(s,u){if(s===ie){var m=i.__iterator(s,u);return new Iterator((function(){var i=m.next();if(!i.done){var s=i.value[0];i.value[0]=i.value[1],i.value[1]=s}return i}))}return i.__iterator(s===ee?Z:ee,u)},s}function mapFactory(i,s,u){var m=makeSequence(i);return m.size=i.size,m.has=function(s){return i.has(s)},m.get=function(m,v){var _=i.get(m,W);return _===W?v:s.call(u,_,m,i)},m.__iterateUncached=function(m,v){var _=this;return i.__iterate((function(i,v,j){return!1!==m(s.call(u,i,v,j),v,_)}),v)},m.__iteratorUncached=function(m,v){var _=i.__iterator(ie,v);return new Iterator((function(){var v=_.next();if(v.done)return v;var j=v.value,M=j[0];return iteratorValue(m,M,s.call(u,j[1],M,i),v)}))},m}function reverseFactory(i,s){var u=makeSequence(i);return u._iter=i,u.size=i.size,u.reverse=function(){return i},i.flip&&(u.flip=function(){var s=flipFactory(i);return s.reverse=function(){return i.flip()},s}),u.get=function(u,m){return i.get(s?u:-1-u,m)},u.has=function(u){return i.has(s?u:-1-u)},u.includes=function(s){return i.includes(s)},u.cacheResult=cacheResultThrough,u.__iterate=function(s,u){var m=this;return i.__iterate((function(i,u){return s(i,u,m)}),!u)},u.__iterator=function(s,u){return i.__iterator(s,!u)},u}function filterFactory(i,s,u,m){var v=makeSequence(i);return m&&(v.has=function(m){var v=i.get(m,W);return v!==W&&!!s.call(u,v,m,i)},v.get=function(m,v){var _=i.get(m,W);return _!==W&&s.call(u,_,m,i)?_:v}),v.__iterateUncached=function(v,_){var j=this,M=0;return i.__iterate((function(i,_,$){if(s.call(u,i,_,$))return M++,v(i,m?_:M-1,j)}),_),M},v.__iteratorUncached=function(v,_){var j=i.__iterator(ie,_),M=0;return new Iterator((function(){for(;;){var _=j.next();if(_.done)return _;var $=_.value,W=$[0],X=$[1];if(s.call(u,X,W,i))return iteratorValue(v,m?W:M++,X,_)}}))},v}function countByFactory(i,s,u){var m=Map().asMutable();return i.__iterate((function(v,_){m.update(s.call(u,v,_,i),0,(function(i){return i+1}))})),m.asImmutable()}function groupByFactory(i,s,u){var m=isKeyed(i),v=(isOrdered(i)?OrderedMap():Map()).asMutable();i.__iterate((function(_,j){v.update(s.call(u,_,j,i),(function(i){return(i=i||[]).push(m?[j,_]:_),i}))}));var _=iterableClass(i);return v.map((function(s){return reify(i,_(s))}))}function sliceFactory(i,s,u,m){var v=i.size;if(void 0!==s&&(s|=0),void 0!==u&&(u===1/0?u=v:u|=0),wholeSlice(s,u,v))return i;var _=resolveBegin(s,v),j=resolveEnd(u,v);if(_!=_||j!=j)return sliceFactory(i.toSeq().cacheResult(),s,u,m);var M,$=j-_;$==$&&(M=$<0?0:$);var W=makeSequence(i);return W.size=0===M?M:i.size&&M||void 0,!m&&isSeq(i)&&M>=0&&(W.get=function(s,u){return(s=wrapIndex(this,s))>=0&&sM)return iteratorDone();var i=v.next();return m||s===ee?i:iteratorValue(s,$-1,s===Z?void 0:i.value[1],i)}))},W}function takeWhileFactory(i,s,u){var m=makeSequence(i);return m.__iterateUncached=function(m,v){var _=this;if(v)return this.cacheResult().__iterate(m,v);var j=0;return i.__iterate((function(i,v,M){return s.call(u,i,v,M)&&++j&&m(i,v,_)})),j},m.__iteratorUncached=function(m,v){var _=this;if(v)return this.cacheResult().__iterator(m,v);var j=i.__iterator(ie,v),M=!0;return new Iterator((function(){if(!M)return iteratorDone();var i=j.next();if(i.done)return i;var v=i.value,$=v[0],W=v[1];return s.call(u,W,$,_)?m===ie?i:iteratorValue(m,$,W,i):(M=!1,iteratorDone())}))},m}function skipWhileFactory(i,s,u,m){var v=makeSequence(i);return v.__iterateUncached=function(v,_){var j=this;if(_)return this.cacheResult().__iterate(v,_);var M=!0,$=0;return i.__iterate((function(i,_,W){if(!M||!(M=s.call(u,i,_,W)))return $++,v(i,m?_:$-1,j)})),$},v.__iteratorUncached=function(v,_){var j=this;if(_)return this.cacheResult().__iterator(v,_);var M=i.__iterator(ie,_),$=!0,W=0;return new Iterator((function(){var i,_,X;do{if((i=M.next()).done)return m||v===ee?i:iteratorValue(v,W++,v===Z?void 0:i.value[1],i);var Y=i.value;_=Y[0],X=Y[1],$&&($=s.call(u,X,_,j))}while($);return v===ie?i:iteratorValue(v,_,X,i)}))},v}function concatFactory(i,s){var u=isKeyed(i),m=[i].concat(s).map((function(i){return isIterable(i)?u&&(i=KeyedIterable(i)):i=u?keyedSeqFromValue(i):indexedSeqFromValue(Array.isArray(i)?i:[i]),i})).filter((function(i){return 0!==i.size}));if(0===m.length)return i;if(1===m.length){var v=m[0];if(v===i||u&&isKeyed(v)||isIndexed(i)&&isIndexed(v))return v}var _=new ArraySeq(m);return u?_=_.toKeyedSeq():isIndexed(i)||(_=_.toSetSeq()),(_=_.flatten(!0)).size=m.reduce((function(i,s){if(void 0!==i){var u=s.size;if(void 0!==u)return i+u}}),0),_}function flattenFactory(i,s,u){var m=makeSequence(i);return m.__iterateUncached=function(m,v){var _=0,j=!1;function flatDeep(i,M){var $=this;i.__iterate((function(i,v){return(!s||M0}function zipWithFactory(i,s,u){var m=makeSequence(i);return m.size=new ArraySeq(u).map((function(i){return i.size})).min(),m.__iterate=function(i,s){for(var u,m=this.__iterator(ee,s),v=0;!(u=m.next()).done&&!1!==i(u.value,v++,this););return v},m.__iteratorUncached=function(i,m){var v=u.map((function(i){return i=Iterable(i),getIterator(m?i.reverse():i)})),_=0,j=!1;return new Iterator((function(){var u;return j||(u=v.map((function(i){return i.next()})),j=u.some((function(i){return i.done}))),j?iteratorDone():iteratorValue(i,_++,s.apply(null,u.map((function(i){return i.value}))))}))},m}function reify(i,s){return isSeq(i)?s:i.constructor(s)}function validateEntry(i){if(i!==Object(i))throw new TypeError("Expected [K, V] tuple: "+i)}function resolveSize(i){return assertNotInfinite(i.size),ensureSize(i)}function iterableClass(i){return isKeyed(i)?KeyedIterable:isIndexed(i)?IndexedIterable:SetIterable}function makeSequence(i){return Object.create((isKeyed(i)?KeyedSeq:isIndexed(i)?IndexedSeq:SetSeq).prototype)}function cacheResultThrough(){return this._iter.cacheResult?(this._iter.cacheResult(),this.size=this._iter.size,this):Seq.prototype.cacheResult.call(this)}function defaultComparator(i,s){return i>s?1:i=0;u--)s={value:arguments[u],next:s};return this.__ownerID?(this.size=i,this._head=s,this.__hash=void 0,this.__altered=!0,this):makeStack(i,s)},Stack.prototype.pushAll=function(i){if(0===(i=IndexedIterable(i)).size)return this;assertNotInfinite(i.size);var s=this.size,u=this._head;return i.reverse().forEach((function(i){s++,u={value:i,next:u}})),this.__ownerID?(this.size=s,this._head=u,this.__hash=void 0,this.__altered=!0,this):makeStack(s,u)},Stack.prototype.pop=function(){return this.slice(1)},Stack.prototype.unshift=function(){return this.push.apply(this,arguments)},Stack.prototype.unshiftAll=function(i){return this.pushAll(i)},Stack.prototype.shift=function(){return this.pop.apply(this,arguments)},Stack.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._head=void 0,this.__hash=void 0,this.__altered=!0,this):emptyStack()},Stack.prototype.slice=function(i,s){if(wholeSlice(i,s,this.size))return this;var u=resolveBegin(i,this.size);if(resolveEnd(s,this.size)!==this.size)return IndexedCollection.prototype.slice.call(this,i,s);for(var m=this.size-u,v=this._head;u--;)v=v.next;return this.__ownerID?(this.size=m,this._head=v,this.__hash=void 0,this.__altered=!0,this):makeStack(m,v)},Stack.prototype.__ensureOwner=function(i){return i===this.__ownerID?this:i?makeStack(this.size,this._head,i,this.__hash):(this.__ownerID=i,this.__altered=!1,this)},Stack.prototype.__iterate=function(i,s){if(s)return this.reverse().__iterate(i);for(var u=0,m=this._head;m&&!1!==i(m.value,u++,this);)m=m.next;return u},Stack.prototype.__iterator=function(i,s){if(s)return this.reverse().__iterator(i);var u=0,m=this._head;return new Iterator((function(){if(m){var s=m.value;return m=m.next,iteratorValue(i,u++,s)}return iteratorDone()}))},Stack.isStack=isStack;var pt,ht="@@__IMMUTABLE_STACK__@@",dt=Stack.prototype;function makeStack(i,s,u,m){var v=Object.create(dt);return v.size=i,v._head=s,v.__ownerID=u,v.__hash=m,v.__altered=!1,v}function emptyStack(){return pt||(pt=makeStack(0))}function mixin(i,s){var keyCopier=function(u){i.prototype[u]=s[u]};return Object.keys(s).forEach(keyCopier),Object.getOwnPropertySymbols&&Object.getOwnPropertySymbols(s).forEach(keyCopier),i}dt[ht]=!0,dt.withMutations=He.withMutations,dt.asMutable=He.asMutable,dt.asImmutable=He.asImmutable,dt.wasAltered=He.wasAltered,Iterable.Iterator=Iterator,mixin(Iterable,{toArray:function(){assertNotInfinite(this.size);var i=new Array(this.size||0);return this.valueSeq().__iterate((function(s,u){i[u]=s})),i},toIndexedSeq:function(){return new ToIndexedSequence(this)},toJS:function(){return this.toSeq().map((function(i){return i&&"function"==typeof i.toJS?i.toJS():i})).__toJS()},toJSON:function(){return this.toSeq().map((function(i){return i&&"function"==typeof i.toJSON?i.toJSON():i})).__toJS()},toKeyedSeq:function(){return new ToKeyedSequence(this,!0)},toMap:function(){return Map(this.toKeyedSeq())},toObject:function(){assertNotInfinite(this.size);var i={};return this.__iterate((function(s,u){i[u]=s})),i},toOrderedMap:function(){return OrderedMap(this.toKeyedSeq())},toOrderedSet:function(){return OrderedSet(isKeyed(this)?this.valueSeq():this)},toSet:function(){return Set(isKeyed(this)?this.valueSeq():this)},toSetSeq:function(){return new ToSetSequence(this)},toSeq:function(){return isIndexed(this)?this.toIndexedSeq():isKeyed(this)?this.toKeyedSeq():this.toSetSeq()},toStack:function(){return Stack(isKeyed(this)?this.valueSeq():this)},toList:function(){return List(isKeyed(this)?this.valueSeq():this)},toString:function(){return"[Iterable]"},__toString:function(i,s){return 0===this.size?i+s:i+" "+this.toSeq().map(this.__toStringMapper).join(", ")+" "+s},concat:function(){return reify(this,concatFactory(this,i.call(arguments,0)))},includes:function(i){return this.some((function(s){return is(s,i)}))},entries:function(){return this.__iterator(ie)},every:function(i,s){assertNotInfinite(this.size);var u=!0;return this.__iterate((function(m,v,_){if(!i.call(s,m,v,_))return u=!1,!1})),u},filter:function(i,s){return reify(this,filterFactory(this,i,s,!0))},find:function(i,s,u){var m=this.findEntry(i,s);return m?m[1]:u},forEach:function(i,s){return assertNotInfinite(this.size),this.__iterate(s?i.bind(s):i)},join:function(i){assertNotInfinite(this.size),i=void 0!==i?""+i:",";var s="",u=!0;return this.__iterate((function(m){u?u=!1:s+=i,s+=null!=m?m.toString():""})),s},keys:function(){return this.__iterator(Z)},map:function(i,s){return reify(this,mapFactory(this,i,s))},reduce:function(i,s,u){var m,v;return assertNotInfinite(this.size),arguments.length<2?v=!0:m=s,this.__iterate((function(s,_,j){v?(v=!1,m=s):m=i.call(u,m,s,_,j)})),m},reduceRight:function(i,s,u){var m=this.toKeyedSeq().reverse();return m.reduce.apply(m,arguments)},reverse:function(){return reify(this,reverseFactory(this,!0))},slice:function(i,s){return reify(this,sliceFactory(this,i,s,!0))},some:function(i,s){return!this.every(not(i),s)},sort:function(i){return reify(this,sortFactory(this,i))},values:function(){return this.__iterator(ee)},butLast:function(){return this.slice(0,-1)},isEmpty:function(){return void 0!==this.size?0===this.size:!this.some((function(){return!0}))},count:function(i,s){return ensureSize(i?this.toSeq().filter(i,s):this)},countBy:function(i,s){return countByFactory(this,i,s)},equals:function(i){return deepEqual(this,i)},entrySeq:function(){var i=this;if(i._cache)return new ArraySeq(i._cache);var s=i.toSeq().map(entryMapper).toIndexedSeq();return s.fromEntrySeq=function(){return i.toSeq()},s},filterNot:function(i,s){return this.filter(not(i),s)},findEntry:function(i,s,u){var m=u;return this.__iterate((function(u,v,_){if(i.call(s,u,v,_))return m=[v,u],!1})),m},findKey:function(i,s){var u=this.findEntry(i,s);return u&&u[0]},findLast:function(i,s,u){return this.toKeyedSeq().reverse().find(i,s,u)},findLastEntry:function(i,s,u){return this.toKeyedSeq().reverse().findEntry(i,s,u)},findLastKey:function(i,s){return this.toKeyedSeq().reverse().findKey(i,s)},first:function(){return this.find(returnTrue)},flatMap:function(i,s){return reify(this,flatMapFactory(this,i,s))},flatten:function(i){return reify(this,flattenFactory(this,i,!0))},fromEntrySeq:function(){return new FromEntriesSequence(this)},get:function(i,s){return this.find((function(s,u){return is(u,i)}),void 0,s)},getIn:function(i,s){for(var u,m=this,v=forceIterator(i);!(u=v.next()).done;){var _=u.value;if((m=m&&m.get?m.get(_,W):W)===W)return s}return m},groupBy:function(i,s){return groupByFactory(this,i,s)},has:function(i){return this.get(i,W)!==W},hasIn:function(i){return this.getIn(i,W)!==W},isSubset:function(i){return i="function"==typeof i.includes?i:Iterable(i),this.every((function(s){return i.includes(s)}))},isSuperset:function(i){return(i="function"==typeof i.isSubset?i:Iterable(i)).isSubset(this)},keyOf:function(i){return this.findKey((function(s){return is(s,i)}))},keySeq:function(){return this.toSeq().map(keyMapper).toIndexedSeq()},last:function(){return this.toSeq().reverse().first()},lastKeyOf:function(i){return this.toKeyedSeq().reverse().keyOf(i)},max:function(i){return maxFactory(this,i)},maxBy:function(i,s){return maxFactory(this,s,i)},min:function(i){return maxFactory(this,i?neg(i):defaultNegComparator)},minBy:function(i,s){return maxFactory(this,s?neg(s):defaultNegComparator,i)},rest:function(){return this.slice(1)},skip:function(i){return this.slice(Math.max(0,i))},skipLast:function(i){return reify(this,this.toSeq().reverse().skip(i).reverse())},skipWhile:function(i,s){return reify(this,skipWhileFactory(this,i,s,!0))},skipUntil:function(i,s){return this.skipWhile(not(i),s)},sortBy:function(i,s){return reify(this,sortFactory(this,s,i))},take:function(i){return this.slice(0,Math.max(0,i))},takeLast:function(i){return reify(this,this.toSeq().reverse().take(i).reverse())},takeWhile:function(i,s){return reify(this,takeWhileFactory(this,i,s))},takeUntil:function(i,s){return this.takeWhile(not(i),s)},valueSeq:function(){return this.toIndexedSeq()},hashCode:function(){return this.__hash||(this.__hash=hashIterable(this))}});var mt=Iterable.prototype;mt[s]=!0,mt[ce]=mt.values,mt.__toJS=mt.toArray,mt.__toStringMapper=quoteString,mt.inspect=mt.toSource=function(){return this.toString()},mt.chain=mt.flatMap,mt.contains=mt.includes,mixin(KeyedIterable,{flip:function(){return reify(this,flipFactory(this))},mapEntries:function(i,s){var u=this,m=0;return reify(this,this.toSeq().map((function(v,_){return i.call(s,[_,v],m++,u)})).fromEntrySeq())},mapKeys:function(i,s){var u=this;return reify(this,this.toSeq().flip().map((function(m,v){return i.call(s,m,v,u)})).flip())}});var gt=KeyedIterable.prototype;function keyMapper(i,s){return s}function entryMapper(i,s){return[s,i]}function not(i){return function(){return!i.apply(this,arguments)}}function neg(i){return function(){return-i.apply(this,arguments)}}function quoteString(i){return"string"==typeof i?JSON.stringify(i):String(i)}function defaultZipper(){return arrCopy(arguments)}function defaultNegComparator(i,s){return is?-1:0}function hashIterable(i){if(i.size===1/0)return 0;var s=isOrdered(i),u=isKeyed(i),m=s?1:0;return murmurHashOfSize(i.__iterate(u?s?function(i,s){m=31*m+hashMerge(hash(i),hash(s))|0}:function(i,s){m=m+hashMerge(hash(i),hash(s))|0}:s?function(i){m=31*m+hash(i)|0}:function(i){m=m+hash(i)|0}),m)}function murmurHashOfSize(i,s){return s=be(s,3432918353),s=be(s<<15|s>>>-15,461845907),s=be(s<<13|s>>>-13,5),s=be((s=(s+3864292196|0)^i)^s>>>16,2246822507),s=smi((s=be(s^s>>>13,3266489909))^s>>>16)}function hashMerge(i,s){return i^s+2654435769+(i<<6)+(i>>2)|0}return gt[u]=!0,gt[ce]=mt.entries,gt.__toJS=mt.toObject,gt.__toStringMapper=function(i,s){return JSON.stringify(s)+": "+quoteString(i)},mixin(IndexedIterable,{toKeyedSeq:function(){return new ToKeyedSequence(this,!1)},filter:function(i,s){return reify(this,filterFactory(this,i,s,!1))},findIndex:function(i,s){var u=this.findEntry(i,s);return u?u[0]:-1},indexOf:function(i){var s=this.keyOf(i);return void 0===s?-1:s},lastIndexOf:function(i){var s=this.lastKeyOf(i);return void 0===s?-1:s},reverse:function(){return reify(this,reverseFactory(this,!1))},slice:function(i,s){return reify(this,sliceFactory(this,i,s,!1))},splice:function(i,s){var u=arguments.length;if(s=Math.max(0|s,0),0===u||2===u&&!s)return this;i=resolveBegin(i,i<0?this.count():this.size);var m=this.slice(0,i);return reify(this,1===u?m:m.concat(arrCopy(arguments,2),this.slice(i+s)))},findLastIndex:function(i,s){var u=this.findLastEntry(i,s);return u?u[0]:-1},first:function(){return this.get(0)},flatten:function(i){return reify(this,flattenFactory(this,i,!1))},get:function(i,s){return(i=wrapIndex(this,i))<0||this.size===1/0||void 0!==this.size&&i>this.size?s:this.find((function(s,u){return u===i}),void 0,s)},has:function(i){return(i=wrapIndex(this,i))>=0&&(void 0!==this.size?this.size===1/0||i{"function"==typeof Object.create?i.exports=function inherits(i,s){s&&(i.super_=s,i.prototype=Object.create(s.prototype,{constructor:{value:i,enumerable:!1,writable:!0,configurable:!0}}))}:i.exports=function inherits(i,s){if(s){i.super_=s;var TempCtor=function(){};TempCtor.prototype=s.prototype,i.prototype=new TempCtor,i.prototype.constructor=i}}},35823:i=>{i.exports=function(i,s,u,m){var v=new Blob(void 0!==m?[m,i]:[i],{type:u||"application/octet-stream"});if(void 0!==window.navigator.msSaveBlob)window.navigator.msSaveBlob(v,s);else{var _=window.URL&&window.URL.createObjectURL?window.URL.createObjectURL(v):window.webkitURL.createObjectURL(v),j=document.createElement("a");j.style.display="none",j.href=_,j.setAttribute("download",s),void 0===j.download&&j.setAttribute("target","_blank"),document.body.appendChild(j),j.click(),setTimeout((function(){document.body.removeChild(j),window.URL.revokeObjectURL(_)}),200)}}},91296:(i,s,u)=>{var m=NaN,v="[object Symbol]",_=/^\s+|\s+$/g,j=/^[-+]0x[0-9a-f]+$/i,M=/^0b[01]+$/i,$=/^0o[0-7]+$/i,W=parseInt,X="object"==typeof u.g&&u.g&&u.g.Object===Object&&u.g,Y="object"==typeof self&&self&&self.Object===Object&&self,Z=X||Y||Function("return this")(),ee=Object.prototype.toString,ie=Math.max,ae=Math.min,now=function(){return Z.Date.now()};function isObject(i){var s=typeof i;return!!i&&("object"==s||"function"==s)}function toNumber(i){if("number"==typeof i)return i;if(function isSymbol(i){return"symbol"==typeof i||function isObjectLike(i){return!!i&&"object"==typeof i}(i)&&ee.call(i)==v}(i))return m;if(isObject(i)){var s="function"==typeof i.valueOf?i.valueOf():i;i=isObject(s)?s+"":s}if("string"!=typeof i)return 0===i?i:+i;i=i.replace(_,"");var u=M.test(i);return u||$.test(i)?W(i.slice(2),u?2:8):j.test(i)?m:+i}i.exports=function debounce(i,s,u){var m,v,_,j,M,$,W=0,X=!1,Y=!1,Z=!0;if("function"!=typeof i)throw new TypeError("Expected a function");function invokeFunc(s){var u=m,_=v;return m=v=void 0,W=s,j=i.apply(_,u)}function shouldInvoke(i){var u=i-$;return void 0===$||u>=s||u<0||Y&&i-W>=_}function timerExpired(){var i=now();if(shouldInvoke(i))return trailingEdge(i);M=setTimeout(timerExpired,function remainingWait(i){var u=s-(i-$);return Y?ae(u,_-(i-W)):u}(i))}function trailingEdge(i){return M=void 0,Z&&m?invokeFunc(i):(m=v=void 0,j)}function debounced(){var i=now(),u=shouldInvoke(i);if(m=arguments,v=this,$=i,u){if(void 0===M)return function leadingEdge(i){return W=i,M=setTimeout(timerExpired,s),X?invokeFunc(i):j}($);if(Y)return M=setTimeout(timerExpired,s),invokeFunc($)}return void 0===M&&(M=setTimeout(timerExpired,s)),j}return s=toNumber(s)||0,isObject(u)&&(X=!!u.leading,_=(Y="maxWait"in u)?ie(toNumber(u.maxWait)||0,s):_,Z="trailing"in u?!!u.trailing:Z),debounced.cancel=function cancel(){void 0!==M&&clearTimeout(M),W=0,m=$=v=M=void 0},debounced.flush=function flush(){return void 0===M?j:trailingEdge(now())},debounced}},18552:(i,s,u)=>{var m=u(10852)(u(55639),"DataView");i.exports=m},1989:(i,s,u)=>{var m=u(51789),v=u(80401),_=u(57667),j=u(21327),M=u(81866);function Hash(i){var s=-1,u=null==i?0:i.length;for(this.clear();++s{var m=u(3118),v=u(9435);function LazyWrapper(i){this.__wrapped__=i,this.__actions__=[],this.__dir__=1,this.__filtered__=!1,this.__iteratees__=[],this.__takeCount__=4294967295,this.__views__=[]}LazyWrapper.prototype=m(v.prototype),LazyWrapper.prototype.constructor=LazyWrapper,i.exports=LazyWrapper},38407:(i,s,u)=>{var m=u(27040),v=u(14125),_=u(82117),j=u(67518),M=u(54705);function ListCache(i){var s=-1,u=null==i?0:i.length;for(this.clear();++s{var m=u(3118),v=u(9435);function LodashWrapper(i,s){this.__wrapped__=i,this.__actions__=[],this.__chain__=!!s,this.__index__=0,this.__values__=void 0}LodashWrapper.prototype=m(v.prototype),LodashWrapper.prototype.constructor=LodashWrapper,i.exports=LodashWrapper},57071:(i,s,u)=>{var m=u(10852)(u(55639),"Map");i.exports=m},83369:(i,s,u)=>{var m=u(24785),v=u(11285),_=u(96e3),j=u(49916),M=u(95265);function MapCache(i){var s=-1,u=null==i?0:i.length;for(this.clear();++s{var m=u(10852)(u(55639),"Promise");i.exports=m},58525:(i,s,u)=>{var m=u(10852)(u(55639),"Set");i.exports=m},88668:(i,s,u)=>{var m=u(83369),v=u(90619),_=u(72385);function SetCache(i){var s=-1,u=null==i?0:i.length;for(this.__data__=new m;++s{var m=u(38407),v=u(37465),_=u(63779),j=u(67599),M=u(44758),$=u(34309);function Stack(i){var s=this.__data__=new m(i);this.size=s.size}Stack.prototype.clear=v,Stack.prototype.delete=_,Stack.prototype.get=j,Stack.prototype.has=M,Stack.prototype.set=$,i.exports=Stack},62705:(i,s,u)=>{var m=u(55639).Symbol;i.exports=m},11149:(i,s,u)=>{var m=u(55639).Uint8Array;i.exports=m},70577:(i,s,u)=>{var m=u(10852)(u(55639),"WeakMap");i.exports=m},96874:i=>{i.exports=function apply(i,s,u){switch(u.length){case 0:return i.call(s);case 1:return i.call(s,u[0]);case 2:return i.call(s,u[0],u[1]);case 3:return i.call(s,u[0],u[1],u[2])}return i.apply(s,u)}},77412:i=>{i.exports=function arrayEach(i,s){for(var u=-1,m=null==i?0:i.length;++u{i.exports=function arrayFilter(i,s){for(var u=-1,m=null==i?0:i.length,v=0,_=[];++u{var m=u(42118);i.exports=function arrayIncludes(i,s){return!!(null==i?0:i.length)&&m(i,s,0)>-1}},14636:(i,s,u)=>{var m=u(22545),v=u(35694),_=u(1469),j=u(44144),M=u(65776),$=u(36719),W=Object.prototype.hasOwnProperty;i.exports=function arrayLikeKeys(i,s){var u=_(i),X=!u&&v(i),Y=!u&&!X&&j(i),Z=!u&&!X&&!Y&&$(i),ee=u||X||Y||Z,ie=ee?m(i.length,String):[],ae=ie.length;for(var le in i)!s&&!W.call(i,le)||ee&&("length"==le||Y&&("offset"==le||"parent"==le)||Z&&("buffer"==le||"byteLength"==le||"byteOffset"==le)||M(le,ae))||ie.push(le);return ie}},29932:i=>{i.exports=function arrayMap(i,s){for(var u=-1,m=null==i?0:i.length,v=Array(m);++u{i.exports=function arrayPush(i,s){for(var u=-1,m=s.length,v=i.length;++u{i.exports=function arrayReduce(i,s,u,m){var v=-1,_=null==i?0:i.length;for(m&&_&&(u=i[++v]);++v<_;)u=s(u,i[v],v,i);return u}},82908:i=>{i.exports=function arraySome(i,s){for(var u=-1,m=null==i?0:i.length;++u{i.exports=function asciiToArray(i){return i.split("")}},49029:i=>{var s=/[^\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f]+/g;i.exports=function asciiWords(i){return i.match(s)||[]}},86556:(i,s,u)=>{var m=u(89465),v=u(77813);i.exports=function assignMergeValue(i,s,u){(void 0!==u&&!v(i[s],u)||void 0===u&&!(s in i))&&m(i,s,u)}},34865:(i,s,u)=>{var m=u(89465),v=u(77813),_=Object.prototype.hasOwnProperty;i.exports=function assignValue(i,s,u){var j=i[s];_.call(i,s)&&v(j,u)&&(void 0!==u||s in i)||m(i,s,u)}},18470:(i,s,u)=>{var m=u(77813);i.exports=function assocIndexOf(i,s){for(var u=i.length;u--;)if(m(i[u][0],s))return u;return-1}},44037:(i,s,u)=>{var m=u(98363),v=u(3674);i.exports=function baseAssign(i,s){return i&&m(s,v(s),i)}},63886:(i,s,u)=>{var m=u(98363),v=u(81704);i.exports=function baseAssignIn(i,s){return i&&m(s,v(s),i)}},89465:(i,s,u)=>{var m=u(38777);i.exports=function baseAssignValue(i,s,u){"__proto__"==s&&m?m(i,s,{configurable:!0,enumerable:!0,value:u,writable:!0}):i[s]=u}},85990:(i,s,u)=>{var m=u(46384),v=u(77412),_=u(34865),j=u(44037),M=u(63886),$=u(64626),W=u(278),X=u(18805),Y=u(1911),Z=u(58234),ee=u(46904),ie=u(64160),ae=u(43824),le=u(29148),ce=u(38517),pe=u(1469),de=u(44144),fe=u(56688),ye=u(13218),be=u(72928),_e=u(3674),we=u(81704),Se="[object Arguments]",xe="[object Function]",Ie="[object Object]",Pe={};Pe[Se]=Pe["[object Array]"]=Pe["[object ArrayBuffer]"]=Pe["[object DataView]"]=Pe["[object Boolean]"]=Pe["[object Date]"]=Pe["[object Float32Array]"]=Pe["[object Float64Array]"]=Pe["[object Int8Array]"]=Pe["[object Int16Array]"]=Pe["[object Int32Array]"]=Pe["[object Map]"]=Pe["[object Number]"]=Pe[Ie]=Pe["[object RegExp]"]=Pe["[object Set]"]=Pe["[object String]"]=Pe["[object Symbol]"]=Pe["[object Uint8Array]"]=Pe["[object Uint8ClampedArray]"]=Pe["[object Uint16Array]"]=Pe["[object Uint32Array]"]=!0,Pe["[object Error]"]=Pe[xe]=Pe["[object WeakMap]"]=!1,i.exports=function baseClone(i,s,u,Te,Re,qe){var ze,Ve=1&s,We=2&s,He=4&s;if(u&&(ze=Re?u(i,Te,Re,qe):u(i)),void 0!==ze)return ze;if(!ye(i))return i;var Xe=pe(i);if(Xe){if(ze=ae(i),!Ve)return W(i,ze)}else{var Ye=ie(i),Qe=Ye==xe||"[object GeneratorFunction]"==Ye;if(de(i))return $(i,Ve);if(Ye==Ie||Ye==Se||Qe&&!Re){if(ze=We||Qe?{}:ce(i),!Ve)return We?Y(i,M(ze,i)):X(i,j(ze,i))}else{if(!Pe[Ye])return Re?i:{};ze=le(i,Ye,Ve)}}qe||(qe=new m);var et=qe.get(i);if(et)return et;qe.set(i,ze),be(i)?i.forEach((function(m){ze.add(baseClone(m,s,u,m,i,qe))})):fe(i)&&i.forEach((function(m,v){ze.set(v,baseClone(m,s,u,v,i,qe))}));var tt=Xe?void 0:(He?We?ee:Z:We?we:_e)(i);return v(tt||i,(function(m,v){tt&&(m=i[v=m]),_(ze,v,baseClone(m,s,u,v,i,qe))})),ze}},3118:(i,s,u)=>{var m=u(13218),v=Object.create,_=function(){function object(){}return function(i){if(!m(i))return{};if(v)return v(i);object.prototype=i;var s=new object;return object.prototype=void 0,s}}();i.exports=_},89881:(i,s,u)=>{var m=u(47816),v=u(99291)(m);i.exports=v},41848:i=>{i.exports=function baseFindIndex(i,s,u,m){for(var v=i.length,_=u+(m?1:-1);m?_--:++_{var m=u(62488),v=u(37285);i.exports=function baseFlatten(i,s,u,_,j){var M=-1,$=i.length;for(u||(u=v),j||(j=[]);++M<$;){var W=i[M];s>0&&u(W)?s>1?baseFlatten(W,s-1,u,_,j):m(j,W):_||(j[j.length]=W)}return j}},28483:(i,s,u)=>{var m=u(25063)();i.exports=m},47816:(i,s,u)=>{var m=u(28483),v=u(3674);i.exports=function baseForOwn(i,s){return i&&m(i,s,v)}},97786:(i,s,u)=>{var m=u(71811),v=u(40327);i.exports=function baseGet(i,s){for(var u=0,_=(s=m(s,i)).length;null!=i&&u<_;)i=i[v(s[u++])];return u&&u==_?i:void 0}},68866:(i,s,u)=>{var m=u(62488),v=u(1469);i.exports=function baseGetAllKeys(i,s,u){var _=s(i);return v(i)?_:m(_,u(i))}},44239:(i,s,u)=>{var m=u(62705),v=u(89607),_=u(2333),j=m?m.toStringTag:void 0;i.exports=function baseGetTag(i){return null==i?void 0===i?"[object Undefined]":"[object Null]":j&&j in Object(i)?v(i):_(i)}},13:i=>{i.exports=function baseHasIn(i,s){return null!=i&&s in Object(i)}},42118:(i,s,u)=>{var m=u(41848),v=u(62722),_=u(42351);i.exports=function baseIndexOf(i,s,u){return s==s?_(i,s,u):m(i,v,u)}},9454:(i,s,u)=>{var m=u(44239),v=u(37005);i.exports=function baseIsArguments(i){return v(i)&&"[object Arguments]"==m(i)}},90939:(i,s,u)=>{var m=u(2492),v=u(37005);i.exports=function baseIsEqual(i,s,u,_,j){return i===s||(null==i||null==s||!v(i)&&!v(s)?i!=i&&s!=s:m(i,s,u,_,baseIsEqual,j))}},2492:(i,s,u)=>{var m=u(46384),v=u(67114),_=u(18351),j=u(16096),M=u(64160),$=u(1469),W=u(44144),X=u(36719),Y="[object Arguments]",Z="[object Array]",ee="[object Object]",ie=Object.prototype.hasOwnProperty;i.exports=function baseIsEqualDeep(i,s,u,ae,le,ce){var pe=$(i),de=$(s),fe=pe?Z:M(i),ye=de?Z:M(s),be=(fe=fe==Y?ee:fe)==ee,_e=(ye=ye==Y?ee:ye)==ee,we=fe==ye;if(we&&W(i)){if(!W(s))return!1;pe=!0,be=!1}if(we&&!be)return ce||(ce=new m),pe||X(i)?v(i,s,u,ae,le,ce):_(i,s,fe,u,ae,le,ce);if(!(1&u)){var Se=be&&ie.call(i,"__wrapped__"),xe=_e&&ie.call(s,"__wrapped__");if(Se||xe){var Ie=Se?i.value():i,Pe=xe?s.value():s;return ce||(ce=new m),le(Ie,Pe,u,ae,ce)}}return!!we&&(ce||(ce=new m),j(i,s,u,ae,le,ce))}},25588:(i,s,u)=>{var m=u(64160),v=u(37005);i.exports=function baseIsMap(i){return v(i)&&"[object Map]"==m(i)}},2958:(i,s,u)=>{var m=u(46384),v=u(90939);i.exports=function baseIsMatch(i,s,u,_){var j=u.length,M=j,$=!_;if(null==i)return!M;for(i=Object(i);j--;){var W=u[j];if($&&W[2]?W[1]!==i[W[0]]:!(W[0]in i))return!1}for(;++j{i.exports=function baseIsNaN(i){return i!=i}},28458:(i,s,u)=>{var m=u(23560),v=u(15346),_=u(13218),j=u(80346),M=/^\[object .+?Constructor\]$/,$=Function.prototype,W=Object.prototype,X=$.toString,Y=W.hasOwnProperty,Z=RegExp("^"+X.call(Y).replace(/[\\^$.*+?()[\]{}|]/g,"\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$");i.exports=function baseIsNative(i){return!(!_(i)||v(i))&&(m(i)?Z:M).test(j(i))}},29221:(i,s,u)=>{var m=u(64160),v=u(37005);i.exports=function baseIsSet(i){return v(i)&&"[object Set]"==m(i)}},38749:(i,s,u)=>{var m=u(44239),v=u(41780),_=u(37005),j={};j["[object Float32Array]"]=j["[object Float64Array]"]=j["[object Int8Array]"]=j["[object Int16Array]"]=j["[object Int32Array]"]=j["[object Uint8Array]"]=j["[object Uint8ClampedArray]"]=j["[object Uint16Array]"]=j["[object Uint32Array]"]=!0,j["[object Arguments]"]=j["[object Array]"]=j["[object ArrayBuffer]"]=j["[object Boolean]"]=j["[object DataView]"]=j["[object Date]"]=j["[object Error]"]=j["[object Function]"]=j["[object Map]"]=j["[object Number]"]=j["[object Object]"]=j["[object RegExp]"]=j["[object Set]"]=j["[object String]"]=j["[object WeakMap]"]=!1,i.exports=function baseIsTypedArray(i){return _(i)&&v(i.length)&&!!j[m(i)]}},67206:(i,s,u)=>{var m=u(91573),v=u(16432),_=u(6557),j=u(1469),M=u(39601);i.exports=function baseIteratee(i){return"function"==typeof i?i:null==i?_:"object"==typeof i?j(i)?v(i[0],i[1]):m(i):M(i)}},280:(i,s,u)=>{var m=u(25726),v=u(86916),_=Object.prototype.hasOwnProperty;i.exports=function baseKeys(i){if(!m(i))return v(i);var s=[];for(var u in Object(i))_.call(i,u)&&"constructor"!=u&&s.push(u);return s}},10313:(i,s,u)=>{var m=u(13218),v=u(25726),_=u(33498),j=Object.prototype.hasOwnProperty;i.exports=function baseKeysIn(i){if(!m(i))return _(i);var s=v(i),u=[];for(var M in i)("constructor"!=M||!s&&j.call(i,M))&&u.push(M);return u}},9435:i=>{i.exports=function baseLodash(){}},91573:(i,s,u)=>{var m=u(2958),v=u(1499),_=u(42634);i.exports=function baseMatches(i){var s=v(i);return 1==s.length&&s[0][2]?_(s[0][0],s[0][1]):function(u){return u===i||m(u,i,s)}}},16432:(i,s,u)=>{var m=u(90939),v=u(27361),_=u(79095),j=u(15403),M=u(89162),$=u(42634),W=u(40327);i.exports=function baseMatchesProperty(i,s){return j(i)&&M(s)?$(W(i),s):function(u){var j=v(u,i);return void 0===j&&j===s?_(u,i):m(s,j,3)}}},42980:(i,s,u)=>{var m=u(46384),v=u(86556),_=u(28483),j=u(59783),M=u(13218),$=u(81704),W=u(36390);i.exports=function baseMerge(i,s,u,X,Y){i!==s&&_(s,(function(_,$){if(Y||(Y=new m),M(_))j(i,s,$,u,baseMerge,X,Y);else{var Z=X?X(W(i,$),_,$+"",i,s,Y):void 0;void 0===Z&&(Z=_),v(i,$,Z)}}),$)}},59783:(i,s,u)=>{var m=u(86556),v=u(64626),_=u(77133),j=u(278),M=u(38517),$=u(35694),W=u(1469),X=u(29246),Y=u(44144),Z=u(23560),ee=u(13218),ie=u(68630),ae=u(36719),le=u(36390),ce=u(59881);i.exports=function baseMergeDeep(i,s,u,pe,de,fe,ye){var be=le(i,u),_e=le(s,u),we=ye.get(_e);if(we)m(i,u,we);else{var Se=fe?fe(be,_e,u+"",i,s,ye):void 0,xe=void 0===Se;if(xe){var Ie=W(_e),Pe=!Ie&&Y(_e),Te=!Ie&&!Pe&&ae(_e);Se=_e,Ie||Pe||Te?W(be)?Se=be:X(be)?Se=j(be):Pe?(xe=!1,Se=v(_e,!0)):Te?(xe=!1,Se=_(_e,!0)):Se=[]:ie(_e)||$(_e)?(Se=be,$(be)?Se=ce(be):ee(be)&&!Z(be)||(Se=M(_e))):xe=!1}xe&&(ye.set(_e,Se),de(Se,_e,pe,fe,ye),ye.delete(_e)),m(i,u,Se)}}},40371:i=>{i.exports=function baseProperty(i){return function(s){return null==s?void 0:s[i]}}},79152:(i,s,u)=>{var m=u(97786);i.exports=function basePropertyDeep(i){return function(s){return m(s,i)}}},18674:i=>{i.exports=function basePropertyOf(i){return function(s){return null==i?void 0:i[s]}}},10107:i=>{i.exports=function baseReduce(i,s,u,m,v){return v(i,(function(i,v,_){u=m?(m=!1,i):s(u,i,v,_)})),u}},5976:(i,s,u)=>{var m=u(6557),v=u(45357),_=u(30061);i.exports=function baseRest(i,s){return _(v(i,s,m),i+"")}},10611:(i,s,u)=>{var m=u(34865),v=u(71811),_=u(65776),j=u(13218),M=u(40327);i.exports=function baseSet(i,s,u,$){if(!j(i))return i;for(var W=-1,X=(s=v(s,i)).length,Y=X-1,Z=i;null!=Z&&++W{var m=u(6557),v=u(89250),_=v?function(i,s){return v.set(i,s),i}:m;i.exports=_},56560:(i,s,u)=>{var m=u(75703),v=u(38777),_=u(6557),j=v?function(i,s){return v(i,"toString",{configurable:!0,enumerable:!1,value:m(s),writable:!0})}:_;i.exports=j},14259:i=>{i.exports=function baseSlice(i,s,u){var m=-1,v=i.length;s<0&&(s=-s>v?0:v+s),(u=u>v?v:u)<0&&(u+=v),v=s>u?0:u-s>>>0,s>>>=0;for(var _=Array(v);++m{var m=u(89881);i.exports=function baseSome(i,s){var u;return m(i,(function(i,m,v){return!(u=s(i,m,v))})),!!u}},22545:i=>{i.exports=function baseTimes(i,s){for(var u=-1,m=Array(i);++u{var m=u(62705),v=u(29932),_=u(1469),j=u(33448),M=m?m.prototype:void 0,$=M?M.toString:void 0;i.exports=function baseToString(i){if("string"==typeof i)return i;if(_(i))return v(i,baseToString)+"";if(j(i))return $?$.call(i):"";var s=i+"";return"0"==s&&1/i==-Infinity?"-0":s}},27561:(i,s,u)=>{var m=u(67990),v=/^\s+/;i.exports=function baseTrim(i){return i?i.slice(0,m(i)+1).replace(v,""):i}},7518:i=>{i.exports=function baseUnary(i){return function(s){return i(s)}}},57406:(i,s,u)=>{var m=u(71811),v=u(10928),_=u(40292),j=u(40327);i.exports=function baseUnset(i,s){return s=m(s,i),null==(i=_(i,s))||delete i[j(v(s))]}},1757:i=>{i.exports=function baseZipObject(i,s,u){for(var m=-1,v=i.length,_=s.length,j={};++m{i.exports=function cacheHas(i,s){return i.has(s)}},71811:(i,s,u)=>{var m=u(1469),v=u(15403),_=u(55514),j=u(79833);i.exports=function castPath(i,s){return m(i)?i:v(i,s)?[i]:_(j(i))}},40180:(i,s,u)=>{var m=u(14259);i.exports=function castSlice(i,s,u){var v=i.length;return u=void 0===u?v:u,!s&&u>=v?i:m(i,s,u)}},74318:(i,s,u)=>{var m=u(11149);i.exports=function cloneArrayBuffer(i){var s=new i.constructor(i.byteLength);return new m(s).set(new m(i)),s}},64626:(i,s,u)=>{i=u.nmd(i);var m=u(55639),v=s&&!s.nodeType&&s,_=v&&i&&!i.nodeType&&i,j=_&&_.exports===v?m.Buffer:void 0,M=j?j.allocUnsafe:void 0;i.exports=function cloneBuffer(i,s){if(s)return i.slice();var u=i.length,m=M?M(u):new i.constructor(u);return i.copy(m),m}},57157:(i,s,u)=>{var m=u(74318);i.exports=function cloneDataView(i,s){var u=s?m(i.buffer):i.buffer;return new i.constructor(u,i.byteOffset,i.byteLength)}},93147:i=>{var s=/\w*$/;i.exports=function cloneRegExp(i){var u=new i.constructor(i.source,s.exec(i));return u.lastIndex=i.lastIndex,u}},40419:(i,s,u)=>{var m=u(62705),v=m?m.prototype:void 0,_=v?v.valueOf:void 0;i.exports=function cloneSymbol(i){return _?Object(_.call(i)):{}}},77133:(i,s,u)=>{var m=u(74318);i.exports=function cloneTypedArray(i,s){var u=s?m(i.buffer):i.buffer;return new i.constructor(u,i.byteOffset,i.length)}},52157:i=>{var s=Math.max;i.exports=function composeArgs(i,u,m,v){for(var _=-1,j=i.length,M=m.length,$=-1,W=u.length,X=s(j-M,0),Y=Array(W+X),Z=!v;++${var s=Math.max;i.exports=function composeArgsRight(i,u,m,v){for(var _=-1,j=i.length,M=-1,$=m.length,W=-1,X=u.length,Y=s(j-$,0),Z=Array(Y+X),ee=!v;++_{i.exports=function copyArray(i,s){var u=-1,m=i.length;for(s||(s=Array(m));++u{var m=u(34865),v=u(89465);i.exports=function copyObject(i,s,u,_){var j=!u;u||(u={});for(var M=-1,$=s.length;++M<$;){var W=s[M],X=_?_(u[W],i[W],W,u,i):void 0;void 0===X&&(X=i[W]),j?v(u,W,X):m(u,W,X)}return u}},18805:(i,s,u)=>{var m=u(98363),v=u(99551);i.exports=function copySymbols(i,s){return m(i,v(i),s)}},1911:(i,s,u)=>{var m=u(98363),v=u(51442);i.exports=function copySymbolsIn(i,s){return m(i,v(i),s)}},14429:(i,s,u)=>{var m=u(55639)["__core-js_shared__"];i.exports=m},97991:i=>{i.exports=function countHolders(i,s){for(var u=i.length,m=0;u--;)i[u]===s&&++m;return m}},21463:(i,s,u)=>{var m=u(5976),v=u(16612);i.exports=function createAssigner(i){return m((function(s,u){var m=-1,_=u.length,j=_>1?u[_-1]:void 0,M=_>2?u[2]:void 0;for(j=i.length>3&&"function"==typeof j?(_--,j):void 0,M&&v(u[0],u[1],M)&&(j=_<3?void 0:j,_=1),s=Object(s);++m<_;){var $=u[m];$&&i(s,$,m,j)}return s}))}},99291:(i,s,u)=>{var m=u(98612);i.exports=function createBaseEach(i,s){return function(u,v){if(null==u)return u;if(!m(u))return i(u,v);for(var _=u.length,j=s?_:-1,M=Object(u);(s?j--:++j<_)&&!1!==v(M[j],j,M););return u}}},25063:i=>{i.exports=function createBaseFor(i){return function(s,u,m){for(var v=-1,_=Object(s),j=m(s),M=j.length;M--;){var $=j[i?M:++v];if(!1===u(_[$],$,_))break}return s}}},22402:(i,s,u)=>{var m=u(71774),v=u(55639);i.exports=function createBind(i,s,u){var _=1&s,j=m(i);return function wrapper(){return(this&&this!==v&&this instanceof wrapper?j:i).apply(_?u:this,arguments)}}},98805:(i,s,u)=>{var m=u(40180),v=u(62689),_=u(83140),j=u(79833);i.exports=function createCaseFirst(i){return function(s){s=j(s);var u=v(s)?_(s):void 0,M=u?u[0]:s.charAt(0),$=u?m(u,1).join(""):s.slice(1);return M[i]()+$}}},35393:(i,s,u)=>{var m=u(62663),v=u(53816),_=u(58748),j=RegExp("['’]","g");i.exports=function createCompounder(i){return function(s){return m(_(v(s).replace(j,"")),i,"")}}},71774:(i,s,u)=>{var m=u(3118),v=u(13218);i.exports=function createCtor(i){return function(){var s=arguments;switch(s.length){case 0:return new i;case 1:return new i(s[0]);case 2:return new i(s[0],s[1]);case 3:return new i(s[0],s[1],s[2]);case 4:return new i(s[0],s[1],s[2],s[3]);case 5:return new i(s[0],s[1],s[2],s[3],s[4]);case 6:return new i(s[0],s[1],s[2],s[3],s[4],s[5]);case 7:return new i(s[0],s[1],s[2],s[3],s[4],s[5],s[6])}var u=m(i.prototype),_=i.apply(u,s);return v(_)?_:u}}},46347:(i,s,u)=>{var m=u(96874),v=u(71774),_=u(86935),j=u(94487),M=u(20893),$=u(46460),W=u(55639);i.exports=function createCurry(i,s,u){var X=v(i);return function wrapper(){for(var v=arguments.length,Y=Array(v),Z=v,ee=M(wrapper);Z--;)Y[Z]=arguments[Z];var ie=v<3&&Y[0]!==ee&&Y[v-1]!==ee?[]:$(Y,ee);return(v-=ie.length){var m=u(67206),v=u(98612),_=u(3674);i.exports=function createFind(i){return function(s,u,j){var M=Object(s);if(!v(s)){var $=m(u,3);s=_(s),u=function(i){return $(M[i],i,M)}}var W=i(s,u,j);return W>-1?M[$?s[W]:W]:void 0}}},86935:(i,s,u)=>{var m=u(52157),v=u(14054),_=u(97991),j=u(71774),M=u(94487),$=u(20893),W=u(90451),X=u(46460),Y=u(55639);i.exports=function createHybrid(i,s,u,Z,ee,ie,ae,le,ce,pe){var de=128&s,fe=1&s,ye=2&s,be=24&s,_e=512&s,we=ye?void 0:j(i);return function wrapper(){for(var Se=arguments.length,xe=Array(Se),Ie=Se;Ie--;)xe[Ie]=arguments[Ie];if(be)var Pe=$(wrapper),Te=_(xe,Pe);if(Z&&(xe=m(xe,Z,ee,be)),ie&&(xe=v(xe,ie,ae,be)),Se-=Te,be&&Se1&&xe.reverse(),de&&ce{var m=u(96874),v=u(71774),_=u(55639);i.exports=function createPartial(i,s,u,j){var M=1&s,$=v(i);return function wrapper(){for(var s=-1,v=arguments.length,W=-1,X=j.length,Y=Array(X+v),Z=this&&this!==_&&this instanceof wrapper?$:i;++W{var m=u(86528),v=u(258),_=u(69255);i.exports=function createRecurry(i,s,u,j,M,$,W,X,Y,Z){var ee=8&s;s|=ee?32:64,4&(s&=~(ee?64:32))||(s&=-4);var ie=[i,s,M,ee?$:void 0,ee?W:void 0,ee?void 0:$,ee?void 0:W,X,Y,Z],ae=u.apply(void 0,ie);return m(i)&&v(ae,ie),ae.placeholder=j,_(ae,i,s)}},97727:(i,s,u)=>{var m=u(28045),v=u(22402),_=u(46347),j=u(86935),M=u(84375),$=u(66833),W=u(63833),X=u(258),Y=u(69255),Z=u(40554),ee=Math.max;i.exports=function createWrap(i,s,u,ie,ae,le,ce,pe){var de=2&s;if(!de&&"function"!=typeof i)throw new TypeError("Expected a function");var fe=ie?ie.length:0;if(fe||(s&=-97,ie=ae=void 0),ce=void 0===ce?ce:ee(Z(ce),0),pe=void 0===pe?pe:Z(pe),fe-=ae?ae.length:0,64&s){var ye=ie,be=ae;ie=ae=void 0}var _e=de?void 0:$(i),we=[i,s,u,ie,ae,ye,be,le,ce,pe];if(_e&&W(we,_e),i=we[0],s=we[1],u=we[2],ie=we[3],ae=we[4],!(pe=we[9]=void 0===we[9]?de?0:i.length:ee(we[9]-fe,0))&&24&s&&(s&=-25),s&&1!=s)Se=8==s||16==s?_(i,s,pe):32!=s&&33!=s||ae.length?j.apply(void 0,we):M(i,s,u,ie);else var Se=v(i,s,u);return Y((_e?m:X)(Se,we),i,s)}},60696:(i,s,u)=>{var m=u(68630);i.exports=function customOmitClone(i){return m(i)?void 0:i}},69389:(i,s,u)=>{var m=u(18674)({À:"A",Á:"A",Â:"A",Ã:"A",Ä:"A",Å:"A",à:"a",á:"a",â:"a",ã:"a",ä:"a",å:"a",Ç:"C",ç:"c",Ð:"D",ð:"d",È:"E",É:"E",Ê:"E",Ë:"E",è:"e",é:"e",ê:"e",ë:"e",Ì:"I",Í:"I",Î:"I",Ï:"I",ì:"i",í:"i",î:"i",ï:"i",Ñ:"N",ñ:"n",Ò:"O",Ó:"O",Ô:"O",Õ:"O",Ö:"O",Ø:"O",ò:"o",ó:"o",ô:"o",õ:"o",ö:"o",ø:"o",Ù:"U",Ú:"U",Û:"U",Ü:"U",ù:"u",ú:"u",û:"u",ü:"u",Ý:"Y",ý:"y",ÿ:"y",Æ:"Ae",æ:"ae",Þ:"Th",þ:"th",ß:"ss",Ā:"A",Ă:"A",Ą:"A",ā:"a",ă:"a",ą:"a",Ć:"C",Ĉ:"C",Ċ:"C",Č:"C",ć:"c",ĉ:"c",ċ:"c",č:"c",Ď:"D",Đ:"D",ď:"d",đ:"d",Ē:"E",Ĕ:"E",Ė:"E",Ę:"E",Ě:"E",ē:"e",ĕ:"e",ė:"e",ę:"e",ě:"e",Ĝ:"G",Ğ:"G",Ġ:"G",Ģ:"G",ĝ:"g",ğ:"g",ġ:"g",ģ:"g",Ĥ:"H",Ħ:"H",ĥ:"h",ħ:"h",Ĩ:"I",Ī:"I",Ĭ:"I",Į:"I",İ:"I",ĩ:"i",ī:"i",ĭ:"i",į:"i",ı:"i",Ĵ:"J",ĵ:"j",Ķ:"K",ķ:"k",ĸ:"k",Ĺ:"L",Ļ:"L",Ľ:"L",Ŀ:"L",Ł:"L",ĺ:"l",ļ:"l",ľ:"l",ŀ:"l",ł:"l",Ń:"N",Ņ:"N",Ň:"N",Ŋ:"N",ń:"n",ņ:"n",ň:"n",ŋ:"n",Ō:"O",Ŏ:"O",Ő:"O",ō:"o",ŏ:"o",ő:"o",Ŕ:"R",Ŗ:"R",Ř:"R",ŕ:"r",ŗ:"r",ř:"r",Ś:"S",Ŝ:"S",Ş:"S",Š:"S",ś:"s",ŝ:"s",ş:"s",š:"s",Ţ:"T",Ť:"T",Ŧ:"T",ţ:"t",ť:"t",ŧ:"t",Ũ:"U",Ū:"U",Ŭ:"U",Ů:"U",Ű:"U",Ų:"U",ũ:"u",ū:"u",ŭ:"u",ů:"u",ű:"u",ų:"u",Ŵ:"W",ŵ:"w",Ŷ:"Y",ŷ:"y",Ÿ:"Y",Ź:"Z",Ż:"Z",Ž:"Z",ź:"z",ż:"z",ž:"z",IJ:"IJ",ij:"ij",Œ:"Oe",œ:"oe",ʼn:"'n",ſ:"s"});i.exports=m},38777:(i,s,u)=>{var m=u(10852),v=function(){try{var i=m(Object,"defineProperty");return i({},"",{}),i}catch(i){}}();i.exports=v},67114:(i,s,u)=>{var m=u(88668),v=u(82908),_=u(74757);i.exports=function equalArrays(i,s,u,j,M,$){var W=1&u,X=i.length,Y=s.length;if(X!=Y&&!(W&&Y>X))return!1;var Z=$.get(i),ee=$.get(s);if(Z&&ee)return Z==s&&ee==i;var ie=-1,ae=!0,le=2&u?new m:void 0;for($.set(i,s),$.set(s,i);++ie{var m=u(62705),v=u(11149),_=u(77813),j=u(67114),M=u(68776),$=u(21814),W=m?m.prototype:void 0,X=W?W.valueOf:void 0;i.exports=function equalByTag(i,s,u,m,W,Y,Z){switch(u){case"[object DataView]":if(i.byteLength!=s.byteLength||i.byteOffset!=s.byteOffset)return!1;i=i.buffer,s=s.buffer;case"[object ArrayBuffer]":return!(i.byteLength!=s.byteLength||!Y(new v(i),new v(s)));case"[object Boolean]":case"[object Date]":case"[object Number]":return _(+i,+s);case"[object Error]":return i.name==s.name&&i.message==s.message;case"[object RegExp]":case"[object String]":return i==s+"";case"[object Map]":var ee=M;case"[object Set]":var ie=1&m;if(ee||(ee=$),i.size!=s.size&&!ie)return!1;var ae=Z.get(i);if(ae)return ae==s;m|=2,Z.set(i,s);var le=j(ee(i),ee(s),m,W,Y,Z);return Z.delete(i),le;case"[object Symbol]":if(X)return X.call(i)==X.call(s)}return!1}},16096:(i,s,u)=>{var m=u(58234),v=Object.prototype.hasOwnProperty;i.exports=function equalObjects(i,s,u,_,j,M){var $=1&u,W=m(i),X=W.length;if(X!=m(s).length&&!$)return!1;for(var Y=X;Y--;){var Z=W[Y];if(!($?Z in s:v.call(s,Z)))return!1}var ee=M.get(i),ie=M.get(s);if(ee&&ie)return ee==s&&ie==i;var ae=!0;M.set(i,s),M.set(s,i);for(var le=$;++Y{var m=u(85564),v=u(45357),_=u(30061);i.exports=function flatRest(i){return _(v(i,void 0,m),i+"")}},31957:(i,s,u)=>{var m="object"==typeof u.g&&u.g&&u.g.Object===Object&&u.g;i.exports=m},58234:(i,s,u)=>{var m=u(68866),v=u(99551),_=u(3674);i.exports=function getAllKeys(i){return m(i,_,v)}},46904:(i,s,u)=>{var m=u(68866),v=u(51442),_=u(81704);i.exports=function getAllKeysIn(i){return m(i,_,v)}},66833:(i,s,u)=>{var m=u(89250),v=u(50308),_=m?function(i){return m.get(i)}:v;i.exports=_},97658:(i,s,u)=>{var m=u(52060),v=Object.prototype.hasOwnProperty;i.exports=function getFuncName(i){for(var s=i.name+"",u=m[s],_=v.call(m,s)?u.length:0;_--;){var j=u[_],M=j.func;if(null==M||M==i)return j.name}return s}},20893:i=>{i.exports=function getHolder(i){return i.placeholder}},45050:(i,s,u)=>{var m=u(37019);i.exports=function getMapData(i,s){var u=i.__data__;return m(s)?u["string"==typeof s?"string":"hash"]:u.map}},1499:(i,s,u)=>{var m=u(89162),v=u(3674);i.exports=function getMatchData(i){for(var s=v(i),u=s.length;u--;){var _=s[u],j=i[_];s[u]=[_,j,m(j)]}return s}},10852:(i,s,u)=>{var m=u(28458),v=u(47801);i.exports=function getNative(i,s){var u=v(i,s);return m(u)?u:void 0}},85924:(i,s,u)=>{var m=u(5569)(Object.getPrototypeOf,Object);i.exports=m},89607:(i,s,u)=>{var m=u(62705),v=Object.prototype,_=v.hasOwnProperty,j=v.toString,M=m?m.toStringTag:void 0;i.exports=function getRawTag(i){var s=_.call(i,M),u=i[M];try{i[M]=void 0;var m=!0}catch(i){}var v=j.call(i);return m&&(s?i[M]=u:delete i[M]),v}},99551:(i,s,u)=>{var m=u(34963),v=u(70479),_=Object.prototype.propertyIsEnumerable,j=Object.getOwnPropertySymbols,M=j?function(i){return null==i?[]:(i=Object(i),m(j(i),(function(s){return _.call(i,s)})))}:v;i.exports=M},51442:(i,s,u)=>{var m=u(62488),v=u(85924),_=u(99551),j=u(70479),M=Object.getOwnPropertySymbols?function(i){for(var s=[];i;)m(s,_(i)),i=v(i);return s}:j;i.exports=M},64160:(i,s,u)=>{var m=u(18552),v=u(57071),_=u(53818),j=u(58525),M=u(70577),$=u(44239),W=u(80346),X="[object Map]",Y="[object Promise]",Z="[object Set]",ee="[object WeakMap]",ie="[object DataView]",ae=W(m),le=W(v),ce=W(_),pe=W(j),de=W(M),fe=$;(m&&fe(new m(new ArrayBuffer(1)))!=ie||v&&fe(new v)!=X||_&&fe(_.resolve())!=Y||j&&fe(new j)!=Z||M&&fe(new M)!=ee)&&(fe=function(i){var s=$(i),u="[object Object]"==s?i.constructor:void 0,m=u?W(u):"";if(m)switch(m){case ae:return ie;case le:return X;case ce:return Y;case pe:return Z;case de:return ee}return s}),i.exports=fe},47801:i=>{i.exports=function getValue(i,s){return null==i?void 0:i[s]}},58775:i=>{var s=/\{\n\/\* \[wrapped with (.+)\] \*/,u=/,? & /;i.exports=function getWrapDetails(i){var m=i.match(s);return m?m[1].split(u):[]}},222:(i,s,u)=>{var m=u(71811),v=u(35694),_=u(1469),j=u(65776),M=u(41780),$=u(40327);i.exports=function hasPath(i,s,u){for(var W=-1,X=(s=m(s,i)).length,Y=!1;++W{var s=RegExp("[\\u200d\\ud800-\\udfff\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff\\ufe0e\\ufe0f]");i.exports=function hasUnicode(i){return s.test(i)}},93157:i=>{var s=/[a-z][A-Z]|[A-Z]{2}[a-z]|[0-9][a-zA-Z]|[a-zA-Z][0-9]|[^a-zA-Z0-9 ]/;i.exports=function hasUnicodeWord(i){return s.test(i)}},51789:(i,s,u)=>{var m=u(94536);i.exports=function hashClear(){this.__data__=m?m(null):{},this.size=0}},80401:i=>{i.exports=function hashDelete(i){var s=this.has(i)&&delete this.__data__[i];return this.size-=s?1:0,s}},57667:(i,s,u)=>{var m=u(94536),v=Object.prototype.hasOwnProperty;i.exports=function hashGet(i){var s=this.__data__;if(m){var u=s[i];return"__lodash_hash_undefined__"===u?void 0:u}return v.call(s,i)?s[i]:void 0}},21327:(i,s,u)=>{var m=u(94536),v=Object.prototype.hasOwnProperty;i.exports=function hashHas(i){var s=this.__data__;return m?void 0!==s[i]:v.call(s,i)}},81866:(i,s,u)=>{var m=u(94536);i.exports=function hashSet(i,s){var u=this.__data__;return this.size+=this.has(i)?0:1,u[i]=m&&void 0===s?"__lodash_hash_undefined__":s,this}},43824:i=>{var s=Object.prototype.hasOwnProperty;i.exports=function initCloneArray(i){var u=i.length,m=new i.constructor(u);return u&&"string"==typeof i[0]&&s.call(i,"index")&&(m.index=i.index,m.input=i.input),m}},29148:(i,s,u)=>{var m=u(74318),v=u(57157),_=u(93147),j=u(40419),M=u(77133);i.exports=function initCloneByTag(i,s,u){var $=i.constructor;switch(s){case"[object ArrayBuffer]":return m(i);case"[object Boolean]":case"[object Date]":return new $(+i);case"[object DataView]":return v(i,u);case"[object Float32Array]":case"[object Float64Array]":case"[object Int8Array]":case"[object Int16Array]":case"[object Int32Array]":case"[object Uint8Array]":case"[object Uint8ClampedArray]":case"[object Uint16Array]":case"[object Uint32Array]":return M(i,u);case"[object Map]":case"[object Set]":return new $;case"[object Number]":case"[object String]":return new $(i);case"[object RegExp]":return _(i);case"[object Symbol]":return j(i)}}},38517:(i,s,u)=>{var m=u(3118),v=u(85924),_=u(25726);i.exports=function initCloneObject(i){return"function"!=typeof i.constructor||_(i)?{}:m(v(i))}},83112:i=>{var s=/\{(?:\n\/\* \[wrapped with .+\] \*\/)?\n?/;i.exports=function insertWrapDetails(i,u){var m=u.length;if(!m)return i;var v=m-1;return u[v]=(m>1?"& ":"")+u[v],u=u.join(m>2?", ":" "),i.replace(s,"{\n/* [wrapped with "+u+"] */\n")}},37285:(i,s,u)=>{var m=u(62705),v=u(35694),_=u(1469),j=m?m.isConcatSpreadable:void 0;i.exports=function isFlattenable(i){return _(i)||v(i)||!!(j&&i&&i[j])}},65776:i=>{var s=/^(?:0|[1-9]\d*)$/;i.exports=function isIndex(i,u){var m=typeof i;return!!(u=null==u?9007199254740991:u)&&("number"==m||"symbol"!=m&&s.test(i))&&i>-1&&i%1==0&&i{var m=u(77813),v=u(98612),_=u(65776),j=u(13218);i.exports=function isIterateeCall(i,s,u){if(!j(u))return!1;var M=typeof s;return!!("number"==M?v(u)&&_(s,u.length):"string"==M&&s in u)&&m(u[s],i)}},15403:(i,s,u)=>{var m=u(1469),v=u(33448),_=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,j=/^\w*$/;i.exports=function isKey(i,s){if(m(i))return!1;var u=typeof i;return!("number"!=u&&"symbol"!=u&&"boolean"!=u&&null!=i&&!v(i))||(j.test(i)||!_.test(i)||null!=s&&i in Object(s))}},37019:i=>{i.exports=function isKeyable(i){var s=typeof i;return"string"==s||"number"==s||"symbol"==s||"boolean"==s?"__proto__"!==i:null===i}},86528:(i,s,u)=>{var m=u(96425),v=u(66833),_=u(97658),j=u(8111);i.exports=function isLaziable(i){var s=_(i),u=j[s];if("function"!=typeof u||!(s in m.prototype))return!1;if(i===u)return!0;var M=v(u);return!!M&&i===M[0]}},15346:(i,s,u)=>{var m,v=u(14429),_=(m=/[^.]+$/.exec(v&&v.keys&&v.keys.IE_PROTO||""))?"Symbol(src)_1."+m:"";i.exports=function isMasked(i){return!!_&&_ in i}},25726:i=>{var s=Object.prototype;i.exports=function isPrototype(i){var u=i&&i.constructor;return i===("function"==typeof u&&u.prototype||s)}},89162:(i,s,u)=>{var m=u(13218);i.exports=function isStrictComparable(i){return i==i&&!m(i)}},27040:i=>{i.exports=function listCacheClear(){this.__data__=[],this.size=0}},14125:(i,s,u)=>{var m=u(18470),v=Array.prototype.splice;i.exports=function listCacheDelete(i){var s=this.__data__,u=m(s,i);return!(u<0)&&(u==s.length-1?s.pop():v.call(s,u,1),--this.size,!0)}},82117:(i,s,u)=>{var m=u(18470);i.exports=function listCacheGet(i){var s=this.__data__,u=m(s,i);return u<0?void 0:s[u][1]}},67518:(i,s,u)=>{var m=u(18470);i.exports=function listCacheHas(i){return m(this.__data__,i)>-1}},54705:(i,s,u)=>{var m=u(18470);i.exports=function listCacheSet(i,s){var u=this.__data__,v=m(u,i);return v<0?(++this.size,u.push([i,s])):u[v][1]=s,this}},24785:(i,s,u)=>{var m=u(1989),v=u(38407),_=u(57071);i.exports=function mapCacheClear(){this.size=0,this.__data__={hash:new m,map:new(_||v),string:new m}}},11285:(i,s,u)=>{var m=u(45050);i.exports=function mapCacheDelete(i){var s=m(this,i).delete(i);return this.size-=s?1:0,s}},96e3:(i,s,u)=>{var m=u(45050);i.exports=function mapCacheGet(i){return m(this,i).get(i)}},49916:(i,s,u)=>{var m=u(45050);i.exports=function mapCacheHas(i){return m(this,i).has(i)}},95265:(i,s,u)=>{var m=u(45050);i.exports=function mapCacheSet(i,s){var u=m(this,i),v=u.size;return u.set(i,s),this.size+=u.size==v?0:1,this}},68776:i=>{i.exports=function mapToArray(i){var s=-1,u=Array(i.size);return i.forEach((function(i,m){u[++s]=[m,i]})),u}},42634:i=>{i.exports=function matchesStrictComparable(i,s){return function(u){return null!=u&&(u[i]===s&&(void 0!==s||i in Object(u)))}}},24523:(i,s,u)=>{var m=u(88306);i.exports=function memoizeCapped(i){var s=m(i,(function(i){return 500===u.size&&u.clear(),i})),u=s.cache;return s}},63833:(i,s,u)=>{var m=u(52157),v=u(14054),_=u(46460),j="__lodash_placeholder__",M=128,$=Math.min;i.exports=function mergeData(i,s){var u=i[1],W=s[1],X=u|W,Y=X<131,Z=W==M&&8==u||W==M&&256==u&&i[7].length<=s[8]||384==W&&s[7].length<=s[8]&&8==u;if(!Y&&!Z)return i;1&W&&(i[2]=s[2],X|=1&u?0:4);var ee=s[3];if(ee){var ie=i[3];i[3]=ie?m(ie,ee,s[4]):ee,i[4]=ie?_(i[3],j):s[4]}return(ee=s[5])&&(ie=i[5],i[5]=ie?v(ie,ee,s[6]):ee,i[6]=ie?_(i[5],j):s[6]),(ee=s[7])&&(i[7]=ee),W&M&&(i[8]=null==i[8]?s[8]:$(i[8],s[8])),null==i[9]&&(i[9]=s[9]),i[0]=s[0],i[1]=X,i}},89250:(i,s,u)=>{var m=u(70577),v=m&&new m;i.exports=v},94536:(i,s,u)=>{var m=u(10852)(Object,"create");i.exports=m},86916:(i,s,u)=>{var m=u(5569)(Object.keys,Object);i.exports=m},33498:i=>{i.exports=function nativeKeysIn(i){var s=[];if(null!=i)for(var u in Object(i))s.push(u);return s}},31167:(i,s,u)=>{i=u.nmd(i);var m=u(31957),v=s&&!s.nodeType&&s,_=v&&i&&!i.nodeType&&i,j=_&&_.exports===v&&m.process,M=function(){try{var i=_&&_.require&&_.require("util").types;return i||j&&j.binding&&j.binding("util")}catch(i){}}();i.exports=M},2333:i=>{var s=Object.prototype.toString;i.exports=function objectToString(i){return s.call(i)}},5569:i=>{i.exports=function overArg(i,s){return function(u){return i(s(u))}}},45357:(i,s,u)=>{var m=u(96874),v=Math.max;i.exports=function overRest(i,s,u){return s=v(void 0===s?i.length-1:s,0),function(){for(var _=arguments,j=-1,M=v(_.length-s,0),$=Array(M);++j{var m=u(97786),v=u(14259);i.exports=function parent(i,s){return s.length<2?i:m(i,v(s,0,-1))}},52060:i=>{i.exports={}},90451:(i,s,u)=>{var m=u(278),v=u(65776),_=Math.min;i.exports=function reorder(i,s){for(var u=i.length,j=_(s.length,u),M=m(i);j--;){var $=s[j];i[j]=v($,u)?M[$]:void 0}return i}},46460:i=>{var s="__lodash_placeholder__";i.exports=function replaceHolders(i,u){for(var m=-1,v=i.length,_=0,j=[];++m{var m=u(31957),v="object"==typeof self&&self&&self.Object===Object&&self,_=m||v||Function("return this")();i.exports=_},36390:i=>{i.exports=function safeGet(i,s){if(("constructor"!==s||"function"!=typeof i[s])&&"__proto__"!=s)return i[s]}},90619:i=>{i.exports=function setCacheAdd(i){return this.__data__.set(i,"__lodash_hash_undefined__"),this}},72385:i=>{i.exports=function setCacheHas(i){return this.__data__.has(i)}},258:(i,s,u)=>{var m=u(28045),v=u(21275)(m);i.exports=v},21814:i=>{i.exports=function setToArray(i){var s=-1,u=Array(i.size);return i.forEach((function(i){u[++s]=i})),u}},30061:(i,s,u)=>{var m=u(56560),v=u(21275)(m);i.exports=v},69255:(i,s,u)=>{var m=u(58775),v=u(83112),_=u(30061),j=u(87241);i.exports=function setWrapToString(i,s,u){var M=s+"";return _(i,v(M,j(m(M),u)))}},21275:i=>{var s=Date.now;i.exports=function shortOut(i){var u=0,m=0;return function(){var v=s(),_=16-(v-m);if(m=v,_>0){if(++u>=800)return arguments[0]}else u=0;return i.apply(void 0,arguments)}}},37465:(i,s,u)=>{var m=u(38407);i.exports=function stackClear(){this.__data__=new m,this.size=0}},63779:i=>{i.exports=function stackDelete(i){var s=this.__data__,u=s.delete(i);return this.size=s.size,u}},67599:i=>{i.exports=function stackGet(i){return this.__data__.get(i)}},44758:i=>{i.exports=function stackHas(i){return this.__data__.has(i)}},34309:(i,s,u)=>{var m=u(38407),v=u(57071),_=u(83369);i.exports=function stackSet(i,s){var u=this.__data__;if(u instanceof m){var j=u.__data__;if(!v||j.length<199)return j.push([i,s]),this.size=++u.size,this;u=this.__data__=new _(j)}return u.set(i,s),this.size=u.size,this}},42351:i=>{i.exports=function strictIndexOf(i,s,u){for(var m=u-1,v=i.length;++m{var m=u(44286),v=u(62689),_=u(676);i.exports=function stringToArray(i){return v(i)?_(i):m(i)}},55514:(i,s,u)=>{var m=u(24523),v=/[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g,_=/\\(\\)?/g,j=m((function(i){var s=[];return 46===i.charCodeAt(0)&&s.push(""),i.replace(v,(function(i,u,m,v){s.push(m?v.replace(_,"$1"):u||i)})),s}));i.exports=j},40327:(i,s,u)=>{var m=u(33448);i.exports=function toKey(i){if("string"==typeof i||m(i))return i;var s=i+"";return"0"==s&&1/i==-Infinity?"-0":s}},80346:i=>{var s=Function.prototype.toString;i.exports=function toSource(i){if(null!=i){try{return s.call(i)}catch(i){}try{return i+""}catch(i){}}return""}},67990:i=>{var s=/\s/;i.exports=function trimmedEndIndex(i){for(var u=i.length;u--&&s.test(i.charAt(u)););return u}},676:i=>{var s="\\ud800-\\udfff",u="["+s+"]",m="[\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff]",v="\\ud83c[\\udffb-\\udfff]",_="[^"+s+"]",j="(?:\\ud83c[\\udde6-\\uddff]){2}",M="[\\ud800-\\udbff][\\udc00-\\udfff]",$="(?:"+m+"|"+v+")"+"?",W="[\\ufe0e\\ufe0f]?",X=W+$+("(?:\\u200d(?:"+[_,j,M].join("|")+")"+W+$+")*"),Y="(?:"+[_+m+"?",m,j,M,u].join("|")+")",Z=RegExp(v+"(?="+v+")|"+Y+X,"g");i.exports=function unicodeToArray(i){return i.match(Z)||[]}},2757:i=>{var s="\\ud800-\\udfff",u="\\u2700-\\u27bf",m="a-z\\xdf-\\xf6\\xf8-\\xff",v="A-Z\\xc0-\\xd6\\xd8-\\xde",_="\\xac\\xb1\\xd7\\xf7\\x00-\\x2f\\x3a-\\x40\\x5b-\\x60\\x7b-\\xbf\\u2000-\\u206f \\t\\x0b\\f\\xa0\\ufeff\\n\\r\\u2028\\u2029\\u1680\\u180e\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200a\\u202f\\u205f\\u3000",j="["+_+"]",M="\\d+",$="["+u+"]",W="["+m+"]",X="[^"+s+_+M+u+m+v+"]",Y="(?:\\ud83c[\\udde6-\\uddff]){2}",Z="[\\ud800-\\udbff][\\udc00-\\udfff]",ee="["+v+"]",ie="(?:"+W+"|"+X+")",ae="(?:"+ee+"|"+X+")",le="(?:['’](?:d|ll|m|re|s|t|ve))?",ce="(?:['’](?:D|LL|M|RE|S|T|VE))?",pe="(?:[\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff]|\\ud83c[\\udffb-\\udfff])?",de="[\\ufe0e\\ufe0f]?",fe=de+pe+("(?:\\u200d(?:"+["[^"+s+"]",Y,Z].join("|")+")"+de+pe+")*"),ye="(?:"+[$,Y,Z].join("|")+")"+fe,be=RegExp([ee+"?"+W+"+"+le+"(?="+[j,ee,"$"].join("|")+")",ae+"+"+ce+"(?="+[j,ee+ie,"$"].join("|")+")",ee+"?"+ie+"+"+le,ee+"+"+ce,"\\d*(?:1ST|2ND|3RD|(?![123])\\dTH)(?=\\b|[a-z_])","\\d*(?:1st|2nd|3rd|(?![123])\\dth)(?=\\b|[A-Z_])",M,ye].join("|"),"g");i.exports=function unicodeWords(i){return i.match(be)||[]}},87241:(i,s,u)=>{var m=u(77412),v=u(47443),_=[["ary",128],["bind",1],["bindKey",2],["curry",8],["curryRight",16],["flip",512],["partial",32],["partialRight",64],["rearg",256]];i.exports=function updateWrapDetails(i,s){return m(_,(function(u){var m="_."+u[0];s&u[1]&&!v(i,m)&&i.push(m)})),i.sort()}},21913:(i,s,u)=>{var m=u(96425),v=u(7548),_=u(278);i.exports=function wrapperClone(i){if(i instanceof m)return i.clone();var s=new v(i.__wrapped__,i.__chain__);return s.__actions__=_(i.__actions__),s.__index__=i.__index__,s.__values__=i.__values__,s}},39514:(i,s,u)=>{var m=u(97727);i.exports=function ary(i,s,u){return s=u?void 0:s,s=i&&null==s?i.length:s,m(i,128,void 0,void 0,void 0,void 0,s)}},68929:(i,s,u)=>{var m=u(48403),v=u(35393)((function(i,s,u){return s=s.toLowerCase(),i+(u?m(s):s)}));i.exports=v},48403:(i,s,u)=>{var m=u(79833),v=u(11700);i.exports=function capitalize(i){return v(m(i).toLowerCase())}},66678:(i,s,u)=>{var m=u(85990);i.exports=function clone(i){return m(i,4)}},75703:i=>{i.exports=function constant(i){return function(){return i}}},40087:(i,s,u)=>{var m=u(97727);function curry(i,s,u){var v=m(i,8,void 0,void 0,void 0,void 0,void 0,s=u?void 0:s);return v.placeholder=curry.placeholder,v}curry.placeholder={},i.exports=curry},23279:(i,s,u)=>{var m=u(13218),v=u(7771),_=u(14841),j=Math.max,M=Math.min;i.exports=function debounce(i,s,u){var $,W,X,Y,Z,ee,ie=0,ae=!1,le=!1,ce=!0;if("function"!=typeof i)throw new TypeError("Expected a function");function invokeFunc(s){var u=$,m=W;return $=W=void 0,ie=s,Y=i.apply(m,u)}function shouldInvoke(i){var u=i-ee;return void 0===ee||u>=s||u<0||le&&i-ie>=X}function timerExpired(){var i=v();if(shouldInvoke(i))return trailingEdge(i);Z=setTimeout(timerExpired,function remainingWait(i){var u=s-(i-ee);return le?M(u,X-(i-ie)):u}(i))}function trailingEdge(i){return Z=void 0,ce&&$?invokeFunc(i):($=W=void 0,Y)}function debounced(){var i=v(),u=shouldInvoke(i);if($=arguments,W=this,ee=i,u){if(void 0===Z)return function leadingEdge(i){return ie=i,Z=setTimeout(timerExpired,s),ae?invokeFunc(i):Y}(ee);if(le)return clearTimeout(Z),Z=setTimeout(timerExpired,s),invokeFunc(ee)}return void 0===Z&&(Z=setTimeout(timerExpired,s)),Y}return s=_(s)||0,m(u)&&(ae=!!u.leading,X=(le="maxWait"in u)?j(_(u.maxWait)||0,s):X,ce="trailing"in u?!!u.trailing:ce),debounced.cancel=function cancel(){void 0!==Z&&clearTimeout(Z),ie=0,$=ee=W=Z=void 0},debounced.flush=function flush(){return void 0===Z?Y:trailingEdge(v())},debounced}},53816:(i,s,u)=>{var m=u(69389),v=u(79833),_=/[\xc0-\xd6\xd8-\xf6\xf8-\xff\u0100-\u017f]/g,j=RegExp("[\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff]","g");i.exports=function deburr(i){return(i=v(i))&&i.replace(_,m).replace(j,"")}},77813:i=>{i.exports=function eq(i,s){return i===s||i!=i&&s!=s}},13311:(i,s,u)=>{var m=u(67740)(u(30998));i.exports=m},30998:(i,s,u)=>{var m=u(41848),v=u(67206),_=u(40554),j=Math.max;i.exports=function findIndex(i,s,u){var M=null==i?0:i.length;if(!M)return-1;var $=null==u?0:_(u);return $<0&&($=j(M+$,0)),m(i,v(s,3),$)}},85564:(i,s,u)=>{var m=u(21078);i.exports=function flatten(i){return(null==i?0:i.length)?m(i,1):[]}},84599:(i,s,u)=>{var m=u(68836),v=u(69306),_=Array.prototype.push;function baseAry(i,s){return 2==s?function(s,u){return i(s,u)}:function(s){return i(s)}}function cloneArray(i){for(var s=i?i.length:0,u=Array(s);s--;)u[s]=i[s];return u}function wrapImmutable(i,s){return function(){var u=arguments.length;if(u){for(var m=Array(u);u--;)m[u]=arguments[u];var v=m[0]=s.apply(void 0,m);return i.apply(void 0,m),v}}}i.exports=function baseConvert(i,s,u,j){var M="function"==typeof s,$=s===Object(s);if($&&(j=u,u=s,s=void 0),null==u)throw new TypeError;j||(j={});var W={cap:!("cap"in j)||j.cap,curry:!("curry"in j)||j.curry,fixed:!("fixed"in j)||j.fixed,immutable:!("immutable"in j)||j.immutable,rearg:!("rearg"in j)||j.rearg},X=M?u:v,Y="curry"in j&&j.curry,Z="fixed"in j&&j.fixed,ee="rearg"in j&&j.rearg,ie=M?u.runInContext():void 0,ae=M?u:{ary:i.ary,assign:i.assign,clone:i.clone,curry:i.curry,forEach:i.forEach,isArray:i.isArray,isError:i.isError,isFunction:i.isFunction,isWeakMap:i.isWeakMap,iteratee:i.iteratee,keys:i.keys,rearg:i.rearg,toInteger:i.toInteger,toPath:i.toPath},le=ae.ary,ce=ae.assign,pe=ae.clone,de=ae.curry,fe=ae.forEach,ye=ae.isArray,be=ae.isError,_e=ae.isFunction,we=ae.isWeakMap,Se=ae.keys,xe=ae.rearg,Ie=ae.toInteger,Pe=ae.toPath,Te=Se(m.aryMethod),Re={castArray:function(i){return function(){var s=arguments[0];return ye(s)?i(cloneArray(s)):i.apply(void 0,arguments)}},iteratee:function(i){return function(){var s=arguments[1],u=i(arguments[0],s),m=u.length;return W.cap&&"number"==typeof s?(s=s>2?s-2:1,m&&m<=s?u:baseAry(u,s)):u}},mixin:function(i){return function(s){var u=this;if(!_e(u))return i(u,Object(s));var m=[];return fe(Se(s),(function(i){_e(s[i])&&m.push([i,u.prototype[i]])})),i(u,Object(s)),fe(m,(function(i){var s=i[1];_e(s)?u.prototype[i[0]]=s:delete u.prototype[i[0]]})),u}},nthArg:function(i){return function(s){var u=s<0?1:Ie(s)+1;return de(i(s),u)}},rearg:function(i){return function(s,u){var m=u?u.length:0;return de(i(s,u),m)}},runInContext:function(s){return function(u){return baseConvert(i,s(u),j)}}};function castCap(i,s){if(W.cap){var u=m.iterateeRearg[i];if(u)return function iterateeRearg(i,s){return overArg(i,(function(i){var u=s.length;return function baseArity(i,s){return 2==s?function(s,u){return i.apply(void 0,arguments)}:function(s){return i.apply(void 0,arguments)}}(xe(baseAry(i,u),s),u)}))}(s,u);var v=!M&&m.iterateeAry[i];if(v)return function iterateeAry(i,s){return overArg(i,(function(i){return"function"==typeof i?baseAry(i,s):i}))}(s,v)}return s}function castFixed(i,s,u){if(W.fixed&&(Z||!m.skipFixed[i])){var v=m.methodSpread[i],j=v&&v.start;return void 0===j?le(s,u):function flatSpread(i,s){return function(){for(var u=arguments.length,m=u-1,v=Array(u);u--;)v[u]=arguments[u];var j=v[s],M=v.slice(0,s);return j&&_.apply(M,j),s!=m&&_.apply(M,v.slice(s+1)),i.apply(this,M)}}(s,j)}return s}function castRearg(i,s,u){return W.rearg&&u>1&&(ee||!m.skipRearg[i])?xe(s,m.methodRearg[i]||m.aryRearg[u]):s}function cloneByPath(i,s){for(var u=-1,m=(s=Pe(s)).length,v=m-1,_=pe(Object(i)),j=_;null!=j&&++u1?de(s,u):s}(0,v=castCap(_,v),i),!1}})),!v})),v||(v=j),v==s&&(v=Y?de(v,1):function(){return s.apply(this,arguments)}),v.convert=createConverter(_,s),v.placeholder=s.placeholder=u,v}if(!$)return wrap(s,u,X);var qe=u,ze=[];return fe(Te,(function(i){fe(m.aryMethod[i],(function(i){var s=qe[m.remap[i]||i];s&&ze.push([i,wrap(i,s,qe)])}))})),fe(Se(qe),(function(i){var s=qe[i];if("function"==typeof s){for(var u=ze.length;u--;)if(ze[u][0]==i)return;s.convert=createConverter(i,s),ze.push([i,s])}})),fe(ze,(function(i){qe[i[0]]=i[1]})),qe.convert=function convertLib(i){return qe.runInContext.convert(i)(void 0)},qe.placeholder=qe,fe(Se(qe),(function(i){fe(m.realToAlias[i]||[],(function(s){qe[s]=qe[i]}))})),qe}},68836:(i,s)=>{s.aliasToReal={each:"forEach",eachRight:"forEachRight",entries:"toPairs",entriesIn:"toPairsIn",extend:"assignIn",extendAll:"assignInAll",extendAllWith:"assignInAllWith",extendWith:"assignInWith",first:"head",conforms:"conformsTo",matches:"isMatch",property:"get",__:"placeholder",F:"stubFalse",T:"stubTrue",all:"every",allPass:"overEvery",always:"constant",any:"some",anyPass:"overSome",apply:"spread",assoc:"set",assocPath:"set",complement:"negate",compose:"flowRight",contains:"includes",dissoc:"unset",dissocPath:"unset",dropLast:"dropRight",dropLastWhile:"dropRightWhile",equals:"isEqual",identical:"eq",indexBy:"keyBy",init:"initial",invertObj:"invert",juxt:"over",omitAll:"omit",nAry:"ary",path:"get",pathEq:"matchesProperty",pathOr:"getOr",paths:"at",pickAll:"pick",pipe:"flow",pluck:"map",prop:"get",propEq:"matchesProperty",propOr:"getOr",props:"at",symmetricDifference:"xor",symmetricDifferenceBy:"xorBy",symmetricDifferenceWith:"xorWith",takeLast:"takeRight",takeLastWhile:"takeRightWhile",unapply:"rest",unnest:"flatten",useWith:"overArgs",where:"conformsTo",whereEq:"isMatch",zipObj:"zipObject"},s.aryMethod={1:["assignAll","assignInAll","attempt","castArray","ceil","create","curry","curryRight","defaultsAll","defaultsDeepAll","floor","flow","flowRight","fromPairs","invert","iteratee","memoize","method","mergeAll","methodOf","mixin","nthArg","over","overEvery","overSome","rest","reverse","round","runInContext","spread","template","trim","trimEnd","trimStart","uniqueId","words","zipAll"],2:["add","after","ary","assign","assignAllWith","assignIn","assignInAllWith","at","before","bind","bindAll","bindKey","chunk","cloneDeepWith","cloneWith","concat","conformsTo","countBy","curryN","curryRightN","debounce","defaults","defaultsDeep","defaultTo","delay","difference","divide","drop","dropRight","dropRightWhile","dropWhile","endsWith","eq","every","filter","find","findIndex","findKey","findLast","findLastIndex","findLastKey","flatMap","flatMapDeep","flattenDepth","forEach","forEachRight","forIn","forInRight","forOwn","forOwnRight","get","groupBy","gt","gte","has","hasIn","includes","indexOf","intersection","invertBy","invoke","invokeMap","isEqual","isMatch","join","keyBy","lastIndexOf","lt","lte","map","mapKeys","mapValues","matchesProperty","maxBy","meanBy","merge","mergeAllWith","minBy","multiply","nth","omit","omitBy","overArgs","pad","padEnd","padStart","parseInt","partial","partialRight","partition","pick","pickBy","propertyOf","pull","pullAll","pullAt","random","range","rangeRight","rearg","reject","remove","repeat","restFrom","result","sampleSize","some","sortBy","sortedIndex","sortedIndexOf","sortedLastIndex","sortedLastIndexOf","sortedUniqBy","split","spreadFrom","startsWith","subtract","sumBy","take","takeRight","takeRightWhile","takeWhile","tap","throttle","thru","times","trimChars","trimCharsEnd","trimCharsStart","truncate","union","uniqBy","uniqWith","unset","unzipWith","without","wrap","xor","zip","zipObject","zipObjectDeep"],3:["assignInWith","assignWith","clamp","differenceBy","differenceWith","findFrom","findIndexFrom","findLastFrom","findLastIndexFrom","getOr","includesFrom","indexOfFrom","inRange","intersectionBy","intersectionWith","invokeArgs","invokeArgsMap","isEqualWith","isMatchWith","flatMapDepth","lastIndexOfFrom","mergeWith","orderBy","padChars","padCharsEnd","padCharsStart","pullAllBy","pullAllWith","rangeStep","rangeStepRight","reduce","reduceRight","replace","set","slice","sortedIndexBy","sortedLastIndexBy","transform","unionBy","unionWith","update","xorBy","xorWith","zipWith"],4:["fill","setWith","updateWith"]},s.aryRearg={2:[1,0],3:[2,0,1],4:[3,2,0,1]},s.iterateeAry={dropRightWhile:1,dropWhile:1,every:1,filter:1,find:1,findFrom:1,findIndex:1,findIndexFrom:1,findKey:1,findLast:1,findLastFrom:1,findLastIndex:1,findLastIndexFrom:1,findLastKey:1,flatMap:1,flatMapDeep:1,flatMapDepth:1,forEach:1,forEachRight:1,forIn:1,forInRight:1,forOwn:1,forOwnRight:1,map:1,mapKeys:1,mapValues:1,partition:1,reduce:2,reduceRight:2,reject:1,remove:1,some:1,takeRightWhile:1,takeWhile:1,times:1,transform:2},s.iterateeRearg={mapKeys:[1],reduceRight:[1,0]},s.methodRearg={assignInAllWith:[1,0],assignInWith:[1,2,0],assignAllWith:[1,0],assignWith:[1,2,0],differenceBy:[1,2,0],differenceWith:[1,2,0],getOr:[2,1,0],intersectionBy:[1,2,0],intersectionWith:[1,2,0],isEqualWith:[1,2,0],isMatchWith:[2,1,0],mergeAllWith:[1,0],mergeWith:[1,2,0],padChars:[2,1,0],padCharsEnd:[2,1,0],padCharsStart:[2,1,0],pullAllBy:[2,1,0],pullAllWith:[2,1,0],rangeStep:[1,2,0],rangeStepRight:[1,2,0],setWith:[3,1,2,0],sortedIndexBy:[2,1,0],sortedLastIndexBy:[2,1,0],unionBy:[1,2,0],unionWith:[1,2,0],updateWith:[3,1,2,0],xorBy:[1,2,0],xorWith:[1,2,0],zipWith:[1,2,0]},s.methodSpread={assignAll:{start:0},assignAllWith:{start:0},assignInAll:{start:0},assignInAllWith:{start:0},defaultsAll:{start:0},defaultsDeepAll:{start:0},invokeArgs:{start:2},invokeArgsMap:{start:2},mergeAll:{start:0},mergeAllWith:{start:0},partial:{start:1},partialRight:{start:1},without:{start:1},zipAll:{start:0}},s.mutate={array:{fill:!0,pull:!0,pullAll:!0,pullAllBy:!0,pullAllWith:!0,pullAt:!0,remove:!0,reverse:!0},object:{assign:!0,assignAll:!0,assignAllWith:!0,assignIn:!0,assignInAll:!0,assignInAllWith:!0,assignInWith:!0,assignWith:!0,defaults:!0,defaultsAll:!0,defaultsDeep:!0,defaultsDeepAll:!0,merge:!0,mergeAll:!0,mergeAllWith:!0,mergeWith:!0},set:{set:!0,setWith:!0,unset:!0,update:!0,updateWith:!0}},s.realToAlias=function(){var i=Object.prototype.hasOwnProperty,u=s.aliasToReal,m={};for(var v in u){var _=u[v];i.call(m,_)?m[_].push(v):m[_]=[v]}return m}(),s.remap={assignAll:"assign",assignAllWith:"assignWith",assignInAll:"assignIn",assignInAllWith:"assignInWith",curryN:"curry",curryRightN:"curryRight",defaultsAll:"defaults",defaultsDeepAll:"defaultsDeep",findFrom:"find",findIndexFrom:"findIndex",findLastFrom:"findLast",findLastIndexFrom:"findLastIndex",getOr:"get",includesFrom:"includes",indexOfFrom:"indexOf",invokeArgs:"invoke",invokeArgsMap:"invokeMap",lastIndexOfFrom:"lastIndexOf",mergeAll:"merge",mergeAllWith:"mergeWith",padChars:"pad",padCharsEnd:"padEnd",padCharsStart:"padStart",propertyOf:"get",rangeStep:"range",rangeStepRight:"rangeRight",restFrom:"rest",spreadFrom:"spread",trimChars:"trim",trimCharsEnd:"trimEnd",trimCharsStart:"trimStart",zipAll:"zip"},s.skipFixed={castArray:!0,flow:!0,flowRight:!0,iteratee:!0,mixin:!0,rearg:!0,runInContext:!0},s.skipRearg={add:!0,assign:!0,assignIn:!0,bind:!0,bindKey:!0,concat:!0,difference:!0,divide:!0,eq:!0,gt:!0,gte:!0,isEqual:!0,lt:!0,lte:!0,matchesProperty:!0,merge:!0,multiply:!0,overArgs:!0,partial:!0,partialRight:!0,propertyOf:!0,random:!0,range:!0,rangeRight:!0,subtract:!0,zip:!0,zipObject:!0,zipObjectDeep:!0}},4269:(i,s,u)=>{i.exports={ary:u(39514),assign:u(44037),clone:u(66678),curry:u(40087),forEach:u(77412),isArray:u(1469),isError:u(64647),isFunction:u(23560),isWeakMap:u(81018),iteratee:u(72594),keys:u(280),rearg:u(4963),toInteger:u(40554),toPath:u(30084)}},72700:(i,s,u)=>{i.exports=u(28252)},92822:(i,s,u)=>{var m=u(84599),v=u(4269);i.exports=function convert(i,s,u){return m(v,i,s,u)}},69306:i=>{i.exports={}},28252:(i,s,u)=>{var m=u(92822)("set",u(36968));m.placeholder=u(69306),i.exports=m},27361:(i,s,u)=>{var m=u(97786);i.exports=function get(i,s,u){var v=null==i?void 0:m(i,s);return void 0===v?u:v}},79095:(i,s,u)=>{var m=u(13),v=u(222);i.exports=function hasIn(i,s){return null!=i&&v(i,s,m)}},6557:i=>{i.exports=function identity(i){return i}},35694:(i,s,u)=>{var m=u(9454),v=u(37005),_=Object.prototype,j=_.hasOwnProperty,M=_.propertyIsEnumerable,$=m(function(){return arguments}())?m:function(i){return v(i)&&j.call(i,"callee")&&!M.call(i,"callee")};i.exports=$},1469:i=>{var s=Array.isArray;i.exports=s},98612:(i,s,u)=>{var m=u(23560),v=u(41780);i.exports=function isArrayLike(i){return null!=i&&v(i.length)&&!m(i)}},29246:(i,s,u)=>{var m=u(98612),v=u(37005);i.exports=function isArrayLikeObject(i){return v(i)&&m(i)}},51584:(i,s,u)=>{var m=u(44239),v=u(37005);i.exports=function isBoolean(i){return!0===i||!1===i||v(i)&&"[object Boolean]"==m(i)}},44144:(i,s,u)=>{i=u.nmd(i);var m=u(55639),v=u(95062),_=s&&!s.nodeType&&s,j=_&&i&&!i.nodeType&&i,M=j&&j.exports===_?m.Buffer:void 0,$=(M?M.isBuffer:void 0)||v;i.exports=$},41609:(i,s,u)=>{var m=u(280),v=u(64160),_=u(35694),j=u(1469),M=u(98612),$=u(44144),W=u(25726),X=u(36719),Y=Object.prototype.hasOwnProperty;i.exports=function isEmpty(i){if(null==i)return!0;if(M(i)&&(j(i)||"string"==typeof i||"function"==typeof i.splice||$(i)||X(i)||_(i)))return!i.length;var s=v(i);if("[object Map]"==s||"[object Set]"==s)return!i.size;if(W(i))return!m(i).length;for(var u in i)if(Y.call(i,u))return!1;return!0}},18446:(i,s,u)=>{var m=u(90939);i.exports=function isEqual(i,s){return m(i,s)}},64647:(i,s,u)=>{var m=u(44239),v=u(37005),_=u(68630);i.exports=function isError(i){if(!v(i))return!1;var s=m(i);return"[object Error]"==s||"[object DOMException]"==s||"string"==typeof i.message&&"string"==typeof i.name&&!_(i)}},23560:(i,s,u)=>{var m=u(44239),v=u(13218);i.exports=function isFunction(i){if(!v(i))return!1;var s=m(i);return"[object Function]"==s||"[object GeneratorFunction]"==s||"[object AsyncFunction]"==s||"[object Proxy]"==s}},41780:i=>{i.exports=function isLength(i){return"number"==typeof i&&i>-1&&i%1==0&&i<=9007199254740991}},56688:(i,s,u)=>{var m=u(25588),v=u(7518),_=u(31167),j=_&&_.isMap,M=j?v(j):m;i.exports=M},45220:i=>{i.exports=function isNull(i){return null===i}},81763:(i,s,u)=>{var m=u(44239),v=u(37005);i.exports=function isNumber(i){return"number"==typeof i||v(i)&&"[object Number]"==m(i)}},13218:i=>{i.exports=function isObject(i){var s=typeof i;return null!=i&&("object"==s||"function"==s)}},37005:i=>{i.exports=function isObjectLike(i){return null!=i&&"object"==typeof i}},68630:(i,s,u)=>{var m=u(44239),v=u(85924),_=u(37005),j=Function.prototype,M=Object.prototype,$=j.toString,W=M.hasOwnProperty,X=$.call(Object);i.exports=function isPlainObject(i){if(!_(i)||"[object Object]"!=m(i))return!1;var s=v(i);if(null===s)return!0;var u=W.call(s,"constructor")&&s.constructor;return"function"==typeof u&&u instanceof u&&$.call(u)==X}},72928:(i,s,u)=>{var m=u(29221),v=u(7518),_=u(31167),j=_&&_.isSet,M=j?v(j):m;i.exports=M},47037:(i,s,u)=>{var m=u(44239),v=u(1469),_=u(37005);i.exports=function isString(i){return"string"==typeof i||!v(i)&&_(i)&&"[object String]"==m(i)}},33448:(i,s,u)=>{var m=u(44239),v=u(37005);i.exports=function isSymbol(i){return"symbol"==typeof i||v(i)&&"[object Symbol]"==m(i)}},36719:(i,s,u)=>{var m=u(38749),v=u(7518),_=u(31167),j=_&&_.isTypedArray,M=j?v(j):m;i.exports=M},81018:(i,s,u)=>{var m=u(64160),v=u(37005);i.exports=function isWeakMap(i){return v(i)&&"[object WeakMap]"==m(i)}},72594:(i,s,u)=>{var m=u(85990),v=u(67206);i.exports=function iteratee(i){return v("function"==typeof i?i:m(i,1))}},3674:(i,s,u)=>{var m=u(14636),v=u(280),_=u(98612);i.exports=function keys(i){return _(i)?m(i):v(i)}},81704:(i,s,u)=>{var m=u(14636),v=u(10313),_=u(98612);i.exports=function keysIn(i){return _(i)?m(i,!0):v(i)}},10928:i=>{i.exports=function last(i){var s=null==i?0:i.length;return s?i[s-1]:void 0}},88306:(i,s,u)=>{var m=u(83369);function memoize(i,s){if("function"!=typeof i||null!=s&&"function"!=typeof s)throw new TypeError("Expected a function");var memoized=function(){var u=arguments,m=s?s.apply(this,u):u[0],v=memoized.cache;if(v.has(m))return v.get(m);var _=i.apply(this,u);return memoized.cache=v.set(m,_)||v,_};return memoized.cache=new(memoize.Cache||m),memoized}memoize.Cache=m,i.exports=memoize},82492:(i,s,u)=>{var m=u(42980),v=u(21463)((function(i,s,u){m(i,s,u)}));i.exports=v},94885:i=>{i.exports=function negate(i){if("function"!=typeof i)throw new TypeError("Expected a function");return function(){var s=arguments;switch(s.length){case 0:return!i.call(this);case 1:return!i.call(this,s[0]);case 2:return!i.call(this,s[0],s[1]);case 3:return!i.call(this,s[0],s[1],s[2])}return!i.apply(this,s)}}},50308:i=>{i.exports=function noop(){}},7771:(i,s,u)=>{var m=u(55639);i.exports=function(){return m.Date.now()}},57557:(i,s,u)=>{var m=u(29932),v=u(85990),_=u(57406),j=u(71811),M=u(98363),$=u(60696),W=u(99021),X=u(46904),Y=W((function(i,s){var u={};if(null==i)return u;var W=!1;s=m(s,(function(s){return s=j(s,i),W||(W=s.length>1),s})),M(i,X(i),u),W&&(u=v(u,7,$));for(var Y=s.length;Y--;)_(u,s[Y]);return u}));i.exports=Y},39601:(i,s,u)=>{var m=u(40371),v=u(79152),_=u(15403),j=u(40327);i.exports=function property(i){return _(i)?m(j(i)):v(i)}},4963:(i,s,u)=>{var m=u(97727),v=u(99021),_=v((function(i,s){return m(i,256,void 0,void 0,void 0,s)}));i.exports=_},54061:(i,s,u)=>{var m=u(62663),v=u(89881),_=u(67206),j=u(10107),M=u(1469);i.exports=function reduce(i,s,u){var $=M(i)?m:j,W=arguments.length<3;return $(i,_(s,4),u,W,v)}},36968:(i,s,u)=>{var m=u(10611);i.exports=function set(i,s,u){return null==i?i:m(i,s,u)}},59704:(i,s,u)=>{var m=u(82908),v=u(67206),_=u(5076),j=u(1469),M=u(16612);i.exports=function some(i,s,u){var $=j(i)?m:_;return u&&M(i,s,u)&&(s=void 0),$(i,v(s,3))}},70479:i=>{i.exports=function stubArray(){return[]}},95062:i=>{i.exports=function stubFalse(){return!1}},18601:(i,s,u)=>{var m=u(14841),v=1/0;i.exports=function toFinite(i){return i?(i=m(i))===v||i===-1/0?17976931348623157e292*(i<0?-1:1):i==i?i:0:0===i?i:0}},40554:(i,s,u)=>{var m=u(18601);i.exports=function toInteger(i){var s=m(i),u=s%1;return s==s?u?s-u:s:0}},7334:(i,s,u)=>{var m=u(79833);i.exports=function toLower(i){return m(i).toLowerCase()}},14841:(i,s,u)=>{var m=u(27561),v=u(13218),_=u(33448),j=/^[-+]0x[0-9a-f]+$/i,M=/^0b[01]+$/i,$=/^0o[0-7]+$/i,W=parseInt;i.exports=function toNumber(i){if("number"==typeof i)return i;if(_(i))return NaN;if(v(i)){var s="function"==typeof i.valueOf?i.valueOf():i;i=v(s)?s+"":s}if("string"!=typeof i)return 0===i?i:+i;i=m(i);var u=M.test(i);return u||$.test(i)?W(i.slice(2),u?2:8):j.test(i)?NaN:+i}},30084:(i,s,u)=>{var m=u(29932),v=u(278),_=u(1469),j=u(33448),M=u(55514),$=u(40327),W=u(79833);i.exports=function toPath(i){return _(i)?m(i,$):j(i)?[i]:v(M(W(i)))}},59881:(i,s,u)=>{var m=u(98363),v=u(81704);i.exports=function toPlainObject(i){return m(i,v(i))}},79833:(i,s,u)=>{var m=u(80531);i.exports=function toString(i){return null==i?"":m(i)}},11700:(i,s,u)=>{var m=u(98805)("toUpperCase");i.exports=m},58748:(i,s,u)=>{var m=u(49029),v=u(93157),_=u(79833),j=u(2757);i.exports=function words(i,s,u){return i=_(i),void 0===(s=u?void 0:s)?v(i)?j(i):m(i):i.match(s)||[]}},8111:(i,s,u)=>{var m=u(96425),v=u(7548),_=u(9435),j=u(1469),M=u(37005),$=u(21913),W=Object.prototype.hasOwnProperty;function lodash(i){if(M(i)&&!j(i)&&!(i instanceof m)){if(i instanceof v)return i;if(W.call(i,"__wrapped__"))return $(i)}return new v(i)}lodash.prototype=_.prototype,lodash.prototype.constructor=lodash,i.exports=lodash},7287:(i,s,u)=>{var m=u(34865),v=u(1757);i.exports=function zipObject(i,s){return v(i||[],s||[],m)}},96470:(i,s,u)=>{"use strict";var m=u(47802),v=u(21102);s.highlight=highlight,s.highlightAuto=function highlightAuto(i,s){var u,j,M,$,W=s||{},X=W.subset||m.listLanguages(),Y=W.prefix,Z=X.length,ee=-1;null==Y&&(Y=_);if("string"!=typeof i)throw v("Expected `string` for value, got `%s`",i);j={relevance:0,language:null,value:[]},u={relevance:0,language:null,value:[]};for(;++eej.relevance&&(j=M),M.relevance>u.relevance&&(j=u,u=M));j.language&&(u.secondBest=j);return u},s.registerLanguage=function registerLanguage(i,s){m.registerLanguage(i,s)},s.listLanguages=function listLanguages(){return m.listLanguages()},s.registerAlias=function registerAlias(i,s){var u,v=i;s&&((v={})[i]=s);for(u in v)m.registerAliases(v[u],{languageName:u})},Emitter.prototype.addText=function text(i){var s,u,m=this.stack;if(""===i)return;s=m[m.length-1],(u=s.children[s.children.length-1])&&"text"===u.type?u.value+=i:s.children.push({type:"text",value:i})},Emitter.prototype.addKeyword=function addKeyword(i,s){this.openNode(s),this.addText(i),this.closeNode()},Emitter.prototype.addSublanguage=function addSublanguage(i,s){var u=this.stack,m=u[u.length-1],v=i.rootNode.children,_=s?{type:"element",tagName:"span",properties:{className:[s]},children:v}:v;m.children=m.children.concat(_)},Emitter.prototype.openNode=function open(i){var s=this.stack,u=this.options.classPrefix+i,m=s[s.length-1],v={type:"element",tagName:"span",properties:{className:[u]},children:[]};m.children.push(v),s.push(v)},Emitter.prototype.closeNode=function close(){this.stack.pop()},Emitter.prototype.closeAllNodes=noop,Emitter.prototype.finalize=noop,Emitter.prototype.toHTML=function toHtmlNoop(){return""};var _="hljs-";function highlight(i,s,u){var j,M=m.configure({}),$=(u||{}).prefix;if("string"!=typeof i)throw v("Expected `string` for name, got `%s`",i);if(!m.getLanguage(i))throw v("Unknown language: `%s` is not registered",i);if("string"!=typeof s)throw v("Expected `string` for value, got `%s`",s);if(null==$&&($=_),m.configure({__emitter:Emitter,classPrefix:$}),j=m.highlight(s,{language:i,ignoreIllegals:!0}),m.configure(M||{}),j.errorRaised)throw j.errorRaised;return{relevance:j.relevance,language:j.language,value:j.emitter.rootNode.children}}function Emitter(i){this.options=i,this.rootNode={children:[]},this.stack=[this.rootNode]}function noop(){}},42566:(i,s,u)=>{const m=u(94885);function coerceElementMatchingCallback(i){return"string"==typeof i?s=>s.element===i:i.constructor&&i.extend?s=>s instanceof i:i}class ArraySlice{constructor(i){this.elements=i||[]}toValue(){return this.elements.map((i=>i.toValue()))}map(i,s){return this.elements.map(i,s)}flatMap(i,s){return this.map(i,s).reduce(((i,s)=>i.concat(s)),[])}compactMap(i,s){const u=[];return this.forEach((m=>{const v=i.bind(s)(m);v&&u.push(v)})),u}filter(i,s){return i=coerceElementMatchingCallback(i),new ArraySlice(this.elements.filter(i,s))}reject(i,s){return i=coerceElementMatchingCallback(i),new ArraySlice(this.elements.filter(m(i),s))}find(i,s){return i=coerceElementMatchingCallback(i),this.elements.find(i,s)}forEach(i,s){this.elements.forEach(i,s)}reduce(i,s){return this.elements.reduce(i,s)}includes(i){return this.elements.some((s=>s.equals(i)))}shift(){return this.elements.shift()}unshift(i){this.elements.unshift(this.refract(i))}push(i){return this.elements.push(this.refract(i)),this}add(i){this.push(i)}get(i){return this.elements[i]}getValue(i){const s=this.elements[i];if(s)return s.toValue()}get length(){return this.elements.length}get isEmpty(){return 0===this.elements.length}get first(){return this.elements[0]}}"undefined"!=typeof Symbol&&(ArraySlice.prototype[Symbol.iterator]=function symbol(){return this.elements[Symbol.iterator]()}),i.exports=ArraySlice},17645:i=>{class KeyValuePair{constructor(i,s){this.key=i,this.value=s}clone(){const i=new KeyValuePair;return this.key&&(i.key=this.key.clone()),this.value&&(i.value=this.value.clone()),i}}i.exports=KeyValuePair},78520:(i,s,u)=>{const m=u(45220),v=u(47037),_=u(81763),j=u(51584),M=u(13218),$=u(28219),W=u(99829);class Namespace{constructor(i){this.elementMap={},this.elementDetection=[],this.Element=W.Element,this.KeyValuePair=W.KeyValuePair,i&&i.noDefault||this.useDefault(),this._attributeElementKeys=[],this._attributeElementArrayKeys=[]}use(i){return i.namespace&&i.namespace({base:this}),i.load&&i.load({base:this}),this}useDefault(){return this.register("null",W.NullElement).register("string",W.StringElement).register("number",W.NumberElement).register("boolean",W.BooleanElement).register("array",W.ArrayElement).register("object",W.ObjectElement).register("member",W.MemberElement).register("ref",W.RefElement).register("link",W.LinkElement),this.detect(m,W.NullElement,!1).detect(v,W.StringElement,!1).detect(_,W.NumberElement,!1).detect(j,W.BooleanElement,!1).detect(Array.isArray,W.ArrayElement,!1).detect(M,W.ObjectElement,!1),this}register(i,s){return this._elements=void 0,this.elementMap[i]=s,this}unregister(i){return this._elements=void 0,delete this.elementMap[i],this}detect(i,s,u){return void 0===u||u?this.elementDetection.unshift([i,s]):this.elementDetection.push([i,s]),this}toElement(i){if(i instanceof this.Element)return i;let s;for(let u=0;u{const s=i[0].toUpperCase()+i.substr(1);this._elements[s]=this.elementMap[i]}))),this._elements}get serialiser(){return new $(this)}}$.prototype.Namespace=Namespace,i.exports=Namespace},87526:(i,s,u)=>{const m=u(94885),v=u(42566);class ObjectSlice extends v{map(i,s){return this.elements.map((u=>i.bind(s)(u.value,u.key,u)))}filter(i,s){return new ObjectSlice(this.elements.filter((u=>i.bind(s)(u.value,u.key,u))))}reject(i,s){return this.filter(m(i.bind(s)))}forEach(i,s){return this.elements.forEach(((u,m)=>{i.bind(s)(u.value,u.key,u,m)}))}keys(){return this.map(((i,s)=>s.toValue()))}values(){return this.map((i=>i.toValue()))}}i.exports=ObjectSlice},99829:(i,s,u)=>{const m=u(3079),v=u(96295),_=u(16036),j=u(91090),M=u(18866),$=u(35804),W=u(5946),X=u(76735),Y=u(59964),Z=u(38588),ee=u(42566),ie=u(87526),ae=u(17645);function refract(i){if(i instanceof m)return i;if("string"==typeof i)return new _(i);if("number"==typeof i)return new j(i);if("boolean"==typeof i)return new M(i);if(null===i)return new v;if(Array.isArray(i))return new $(i.map(refract));if("object"==typeof i){return new X(i)}return i}m.prototype.ObjectElement=X,m.prototype.RefElement=Z,m.prototype.MemberElement=W,m.prototype.refract=refract,ee.prototype.refract=refract,i.exports={Element:m,NullElement:v,StringElement:_,NumberElement:j,BooleanElement:M,ArrayElement:$,MemberElement:W,ObjectElement:X,LinkElement:Y,RefElement:Z,refract,ArraySlice:ee,ObjectSlice:ie,KeyValuePair:ae}},59964:(i,s,u)=>{const m=u(3079);i.exports=class LinkElement extends m{constructor(i,s,u){super(i||[],s,u),this.element="link"}get relation(){return this.attributes.get("relation")}set relation(i){this.attributes.set("relation",i)}get href(){return this.attributes.get("href")}set href(i){this.attributes.set("href",i)}}},38588:(i,s,u)=>{const m=u(3079);i.exports=class RefElement extends m{constructor(i,s,u){super(i||[],s,u),this.element="ref",this.path||(this.path="element")}get path(){return this.attributes.get("path")}set path(i){this.attributes.set("path",i)}}},43500:(i,s,u)=>{const m=u(78520),v=u(99829);s.lS=m,s.KeyValuePair=u(17645),s.O4=v.ArraySlice,s.rm=v.ObjectSlice,s.W_=v.Element,s.RP=v.StringElement,s.VL=v.NumberElement,s.hh=v.BooleanElement,s.zr=v.NullElement,s.ON=v.ArrayElement,s.Sb=v.ObjectElement,s.c6=v.MemberElement,s.tK=v.RefElement,s.EA=v.LinkElement,s.Qc=v.refract,u(28219),u(3414)},35804:(i,s,u)=>{const m=u(94885),v=u(3079),_=u(42566);class ArrayElement extends v{constructor(i,s,u){super(i||[],s,u),this.element="array"}primitive(){return"array"}get(i){return this.content[i]}getValue(i){const s=this.get(i);if(s)return s.toValue()}getIndex(i){return this.content[i]}set(i,s){return this.content[i]=this.refract(s),this}remove(i){const s=this.content.splice(i,1);return s.length?s[0]:null}map(i,s){return this.content.map(i,s)}flatMap(i,s){return this.map(i,s).reduce(((i,s)=>i.concat(s)),[])}compactMap(i,s){const u=[];return this.forEach((m=>{const v=i.bind(s)(m);v&&u.push(v)})),u}filter(i,s){return new _(this.content.filter(i,s))}reject(i,s){return this.filter(m(i),s)}reduce(i,s){let u,m;void 0!==s?(u=0,m=this.refract(s)):(u=1,m="object"===this.primitive()?this.first.value:this.first);for(let s=u;s{i.bind(s)(u,this.refract(m))}))}shift(){return this.content.shift()}unshift(i){this.content.unshift(this.refract(i))}push(i){return this.content.push(this.refract(i)),this}add(i){this.push(i)}findElements(i,s){const u=s||{},m=!!u.recursive,v=void 0===u.results?[]:u.results;return this.forEach(((s,u,_)=>{m&&void 0!==s.findElements&&s.findElements(i,{results:v,recursive:m}),i(s,u,_)&&v.push(s)})),v}find(i){return new _(this.findElements(i,{recursive:!0}))}findByElement(i){return this.find((s=>s.element===i))}findByClass(i){return this.find((s=>s.classes.includes(i)))}getById(i){return this.find((s=>s.id.toValue()===i)).first}includes(i){return this.content.some((s=>s.equals(i)))}contains(i){return this.includes(i)}empty(){return new this.constructor([])}"fantasy-land/empty"(){return this.empty()}concat(i){return new this.constructor(this.content.concat(i.content))}"fantasy-land/concat"(i){return this.concat(i)}"fantasy-land/map"(i){return new this.constructor(this.map(i))}"fantasy-land/chain"(i){return this.map((s=>i(s)),this).reduce(((i,s)=>i.concat(s)),this.empty())}"fantasy-land/filter"(i){return new this.constructor(this.content.filter(i))}"fantasy-land/reduce"(i,s){return this.content.reduce(i,s)}get length(){return this.content.length}get isEmpty(){return 0===this.content.length}get first(){return this.getIndex(0)}get second(){return this.getIndex(1)}get last(){return this.getIndex(this.length-1)}}ArrayElement.empty=function empty(){return new this},ArrayElement["fantasy-land/empty"]=ArrayElement.empty,"undefined"!=typeof Symbol&&(ArrayElement.prototype[Symbol.iterator]=function symbol(){return this.content[Symbol.iterator]()}),i.exports=ArrayElement},18866:(i,s,u)=>{const m=u(3079);i.exports=class BooleanElement extends m{constructor(i,s,u){super(i,s,u),this.element="boolean"}primitive(){return"boolean"}}},3079:(i,s,u)=>{const m=u(18446),v=u(17645),_=u(42566);class Element{constructor(i,s,u){s&&(this.meta=s),u&&(this.attributes=u),this.content=i}freeze(){Object.isFrozen(this)||(this._meta&&(this.meta.parent=this,this.meta.freeze()),this._attributes&&(this.attributes.parent=this,this.attributes.freeze()),this.children.forEach((i=>{i.parent=this,i.freeze()}),this),this.content&&Array.isArray(this.content)&&Object.freeze(this.content),Object.freeze(this))}primitive(){}clone(){const i=new this.constructor;return i.element=this.element,this.meta.length&&(i._meta=this.meta.clone()),this.attributes.length&&(i._attributes=this.attributes.clone()),this.content?this.content.clone?i.content=this.content.clone():Array.isArray(this.content)?i.content=this.content.map((i=>i.clone())):i.content=this.content:i.content=this.content,i}toValue(){return this.content instanceof Element?this.content.toValue():this.content instanceof v?{key:this.content.key.toValue(),value:this.content.value?this.content.value.toValue():void 0}:this.content&&this.content.map?this.content.map((i=>i.toValue()),this):this.content}toRef(i){if(""===this.id.toValue())throw Error("Cannot create reference to an element that does not contain an ID");const s=new this.RefElement(this.id.toValue());return i&&(s.path=i),s}findRecursive(...i){if(arguments.length>1&&!this.isFrozen)throw new Error("Cannot find recursive with multiple element names without first freezing the element. Call `element.freeze()`");const s=i.pop();let u=new _;const append=(i,s)=>(i.push(s),i),checkElement=(i,u)=>{u.element===s&&i.push(u);const m=u.findRecursive(s);return m&&m.reduce(append,i),u.content instanceof v&&(u.content.key&&checkElement(i,u.content.key),u.content.value&&checkElement(i,u.content.value)),i};return this.content&&(this.content.element&&checkElement(u,this.content),Array.isArray(this.content)&&this.content.reduce(checkElement,u)),i.isEmpty||(u=u.filter((s=>{let u=s.parents.map((i=>i.element));for(const s in i){const m=i[s],v=u.indexOf(m);if(-1===v)return!1;u=u.splice(0,v)}return!0}))),u}set(i){return this.content=i,this}equals(i){return m(this.toValue(),i)}getMetaProperty(i,s){if(!this.meta.hasKey(i)){if(this.isFrozen){const i=this.refract(s);return i.freeze(),i}this.meta.set(i,s)}return this.meta.get(i)}setMetaProperty(i,s){this.meta.set(i,s)}get element(){return this._storedElement||"element"}set element(i){this._storedElement=i}get content(){return this._content}set content(i){if(i instanceof Element)this._content=i;else if(i instanceof _)this.content=i.elements;else if("string"==typeof i||"number"==typeof i||"boolean"==typeof i||"null"===i||null==i)this._content=i;else if(i instanceof v)this._content=i;else if(Array.isArray(i))this._content=i.map(this.refract);else{if("object"!=typeof i)throw new Error("Cannot set content to given value");this._content=Object.keys(i).map((s=>new this.MemberElement(s,i[s])))}}get meta(){if(!this._meta){if(this.isFrozen){const i=new this.ObjectElement;return i.freeze(),i}this._meta=new this.ObjectElement}return this._meta}set meta(i){i instanceof this.ObjectElement?this._meta=i:this.meta.set(i||{})}get attributes(){if(!this._attributes){if(this.isFrozen){const i=new this.ObjectElement;return i.freeze(),i}this._attributes=new this.ObjectElement}return this._attributes}set attributes(i){i instanceof this.ObjectElement?this._attributes=i:this.attributes.set(i||{})}get id(){return this.getMetaProperty("id","")}set id(i){this.setMetaProperty("id",i)}get classes(){return this.getMetaProperty("classes",[])}set classes(i){this.setMetaProperty("classes",i)}get title(){return this.getMetaProperty("title","")}set title(i){this.setMetaProperty("title",i)}get description(){return this.getMetaProperty("description","")}set description(i){this.setMetaProperty("description",i)}get links(){return this.getMetaProperty("links",[])}set links(i){this.setMetaProperty("links",i)}get isFrozen(){return Object.isFrozen(this)}get parents(){let{parent:i}=this;const s=new _;for(;i;)s.push(i),i=i.parent;return s}get children(){if(Array.isArray(this.content))return new _(this.content);if(this.content instanceof v){const i=new _([this.content.key]);return this.content.value&&i.push(this.content.value),i}return this.content instanceof Element?new _([this.content]):new _}get recursiveChildren(){const i=new _;return this.children.forEach((s=>{i.push(s),s.recursiveChildren.forEach((s=>{i.push(s)}))})),i}}i.exports=Element},5946:(i,s,u)=>{const m=u(17645),v=u(3079);i.exports=class MemberElement extends v{constructor(i,s,u,v){super(new m,u,v),this.element="member",this.key=i,this.value=s}get key(){return this.content.key}set key(i){this.content.key=this.refract(i)}get value(){return this.content.value}set value(i){this.content.value=this.refract(i)}}},96295:(i,s,u)=>{const m=u(3079);i.exports=class NullElement extends m{constructor(i,s,u){super(i||null,s,u),this.element="null"}primitive(){return"null"}set(){return new Error("Cannot set the value of null")}}},91090:(i,s,u)=>{const m=u(3079);i.exports=class NumberElement extends m{constructor(i,s,u){super(i,s,u),this.element="number"}primitive(){return"number"}}},76735:(i,s,u)=>{const m=u(94885),v=u(13218),_=u(35804),j=u(5946),M=u(87526);i.exports=class ObjectElement extends _{constructor(i,s,u){super(i||[],s,u),this.element="object"}primitive(){return"object"}toValue(){return this.content.reduce(((i,s)=>(i[s.key.toValue()]=s.value?s.value.toValue():void 0,i)),{})}get(i){const s=this.getMember(i);if(s)return s.value}getMember(i){if(void 0!==i)return this.content.find((s=>s.key.toValue()===i))}remove(i){let s=null;return this.content=this.content.filter((u=>u.key.toValue()!==i||(s=u,!1))),s}getKey(i){const s=this.getMember(i);if(s)return s.key}set(i,s){if(v(i))return Object.keys(i).forEach((s=>{this.set(s,i[s])})),this;const u=i,m=this.getMember(u);return m?m.value=s:this.content.push(new j(u,s)),this}keys(){return this.content.map((i=>i.key.toValue()))}values(){return this.content.map((i=>i.value.toValue()))}hasKey(i){return this.content.some((s=>s.key.equals(i)))}items(){return this.content.map((i=>[i.key.toValue(),i.value.toValue()]))}map(i,s){return this.content.map((u=>i.bind(s)(u.value,u.key,u)))}compactMap(i,s){const u=[];return this.forEach(((m,v,_)=>{const j=i.bind(s)(m,v,_);j&&u.push(j)})),u}filter(i,s){return new M(this.content).filter(i,s)}reject(i,s){return this.filter(m(i),s)}forEach(i,s){return this.content.forEach((u=>i.bind(s)(u.value,u.key,u)))}}},16036:(i,s,u)=>{const m=u(3079);i.exports=class StringElement extends m{constructor(i,s,u){super(i,s,u),this.element="string"}primitive(){return"string"}get length(){return this.content.length}}},3414:(i,s,u)=>{const m=u(28219);i.exports=class JSON06Serialiser extends m{serialise(i){if(!(i instanceof this.namespace.elements.Element))throw new TypeError(`Given element \`${i}\` is not an Element instance`);let s;i._attributes&&i.attributes.get("variable")&&(s=i.attributes.get("variable"));const u={element:i.element};i._meta&&i._meta.length>0&&(u.meta=this.serialiseObject(i.meta));const m="enum"===i.element||-1!==i.attributes.keys().indexOf("enumerations");if(m){const s=this.enumSerialiseAttributes(i);s&&(u.attributes=s)}else if(i._attributes&&i._attributes.length>0){let{attributes:m}=i;m.get("metadata")&&(m=m.clone(),m.set("meta",m.get("metadata")),m.remove("metadata")),"member"===i.element&&s&&(m=m.clone(),m.remove("variable")),m.length>0&&(u.attributes=this.serialiseObject(m))}if(m)u.content=this.enumSerialiseContent(i,u);else if(this[`${i.element}SerialiseContent`])u.content=this[`${i.element}SerialiseContent`](i,u);else if(void 0!==i.content){let m;s&&i.content.key?(m=i.content.clone(),m.key.attributes.set("variable",s),m=this.serialiseContent(m)):m=this.serialiseContent(i.content),this.shouldSerialiseContent(i,m)&&(u.content=m)}else this.shouldSerialiseContent(i,i.content)&&i instanceof this.namespace.elements.Array&&(u.content=[]);return u}shouldSerialiseContent(i,s){return"parseResult"===i.element||"httpRequest"===i.element||"httpResponse"===i.element||"category"===i.element||"link"===i.element||void 0!==s&&(!Array.isArray(s)||0!==s.length)}refSerialiseContent(i,s){return delete s.attributes,{href:i.toValue(),path:i.path.toValue()}}sourceMapSerialiseContent(i){return i.toValue()}dataStructureSerialiseContent(i){return[this.serialiseContent(i.content)]}enumSerialiseAttributes(i){const s=i.attributes.clone(),u=s.remove("enumerations")||new this.namespace.elements.Array([]),m=s.get("default");let v=s.get("samples")||new this.namespace.elements.Array([]);if(m&&m.content&&(m.content.attributes&&m.content.attributes.remove("typeAttributes"),s.set("default",new this.namespace.elements.Array([m.content]))),v.forEach((i=>{i.content&&i.content.element&&i.content.attributes.remove("typeAttributes")})),i.content&&0!==u.length&&v.unshift(i.content),v=v.map((i=>i instanceof this.namespace.elements.Array?[i]:new this.namespace.elements.Array([i.content]))),v.length&&s.set("samples",v),s.length>0)return this.serialiseObject(s)}enumSerialiseContent(i){if(i._attributes){const s=i.attributes.get("enumerations");if(s&&s.length>0)return s.content.map((i=>{const s=i.clone();return s.attributes.remove("typeAttributes"),this.serialise(s)}))}if(i.content){const s=i.content.clone();return s.attributes.remove("typeAttributes"),[this.serialise(s)]}return[]}deserialise(i){if("string"==typeof i)return new this.namespace.elements.String(i);if("number"==typeof i)return new this.namespace.elements.Number(i);if("boolean"==typeof i)return new this.namespace.elements.Boolean(i);if(null===i)return new this.namespace.elements.Null;if(Array.isArray(i))return new this.namespace.elements.Array(i.map(this.deserialise,this));const s=this.namespace.getElementClass(i.element),u=new s;u.element!==i.element&&(u.element=i.element),i.meta&&this.deserialiseObject(i.meta,u.meta),i.attributes&&this.deserialiseObject(i.attributes,u.attributes);const m=this.deserialiseContent(i.content);if(void 0===m&&null!==u.content||(u.content=m),"enum"===u.element){u.content&&u.attributes.set("enumerations",u.content);let i=u.attributes.get("samples");if(u.attributes.remove("samples"),i){const m=i;i=new this.namespace.elements.Array,m.forEach((m=>{m.forEach((m=>{const v=new s(m);v.element=u.element,i.push(v)}))}));const v=i.shift();u.content=v?v.content:void 0,u.attributes.set("samples",i)}else u.content=void 0;let m=u.attributes.get("default");if(m&&m.length>0){m=m.get(0);const i=new s(m);i.element=u.element,u.attributes.set("default",i)}}else if("dataStructure"===u.element&&Array.isArray(u.content))[u.content]=u.content;else if("category"===u.element){const i=u.attributes.get("meta");i&&(u.attributes.set("metadata",i),u.attributes.remove("meta"))}else"member"===u.element&&u.key&&u.key._attributes&&u.key._attributes.getValue("variable")&&(u.attributes.set("variable",u.key.attributes.get("variable")),u.key.attributes.remove("variable"));return u}serialiseContent(i){if(i instanceof this.namespace.elements.Element)return this.serialise(i);if(i instanceof this.namespace.KeyValuePair){const s={key:this.serialise(i.key)};return i.value&&(s.value=this.serialise(i.value)),s}return i&&i.map?i.map(this.serialise,this):i}deserialiseContent(i){if(i){if(i.element)return this.deserialise(i);if(i.key){const s=new this.namespace.KeyValuePair(this.deserialise(i.key));return i.value&&(s.value=this.deserialise(i.value)),s}if(i.map)return i.map(this.deserialise,this)}return i}shouldRefract(i){return!!(i._attributes&&i.attributes.keys().length||i._meta&&i.meta.keys().length)||"enum"!==i.element&&(i.element!==i.primitive()||"member"===i.element)}convertKeyToRefract(i,s){return this.shouldRefract(s)?this.serialise(s):"enum"===s.element?this.serialiseEnum(s):"array"===s.element?s.map((s=>this.shouldRefract(s)||"default"===i?this.serialise(s):"array"===s.element||"object"===s.element||"enum"===s.element?s.children.map((i=>this.serialise(i))):s.toValue())):"object"===s.element?(s.content||[]).map(this.serialise,this):s.toValue()}serialiseEnum(i){return i.children.map((i=>this.serialise(i)))}serialiseObject(i){const s={};return i.forEach(((i,u)=>{if(i){const m=u.toValue();s[m]=this.convertKeyToRefract(m,i)}})),s}deserialiseObject(i,s){Object.keys(i).forEach((u=>{s.set(u,this.deserialise(i[u]))}))}}},28219:i=>{i.exports=class JSONSerialiser{constructor(i){this.namespace=i||new this.Namespace}serialise(i){if(!(i instanceof this.namespace.elements.Element))throw new TypeError(`Given element \`${i}\` is not an Element instance`);const s={element:i.element};i._meta&&i._meta.length>0&&(s.meta=this.serialiseObject(i.meta)),i._attributes&&i._attributes.length>0&&(s.attributes=this.serialiseObject(i.attributes));const u=this.serialiseContent(i.content);return void 0!==u&&(s.content=u),s}deserialise(i){if(!i.element)throw new Error("Given value is not an object containing an element name");const s=new(this.namespace.getElementClass(i.element));s.element!==i.element&&(s.element=i.element),i.meta&&this.deserialiseObject(i.meta,s.meta),i.attributes&&this.deserialiseObject(i.attributes,s.attributes);const u=this.deserialiseContent(i.content);return void 0===u&&null!==s.content||(s.content=u),s}serialiseContent(i){if(i instanceof this.namespace.elements.Element)return this.serialise(i);if(i instanceof this.namespace.KeyValuePair){const s={key:this.serialise(i.key)};return i.value&&(s.value=this.serialise(i.value)),s}if(i&&i.map){if(0===i.length)return;return i.map(this.serialise,this)}return i}deserialiseContent(i){if(i){if(i.element)return this.deserialise(i);if(i.key){const s=new this.namespace.KeyValuePair(this.deserialise(i.key));return i.value&&(s.value=this.deserialise(i.value)),s}if(i.map)return i.map(this.deserialise,this)}return i}serialiseObject(i){const s={};if(i.forEach(((i,u)=>{i&&(s[u.toValue()]=this.serialise(i))})),0!==Object.keys(s).length)return s}deserialiseObject(i,s){Object.keys(i).forEach((u=>{s.set(u,this.deserialise(i[u]))}))}}},27418:i=>{"use strict";var s=Object.getOwnPropertySymbols,u=Object.prototype.hasOwnProperty,m=Object.prototype.propertyIsEnumerable;i.exports=function shouldUseNative(){try{if(!Object.assign)return!1;var i=new String("abc");if(i[5]="de","5"===Object.getOwnPropertyNames(i)[0])return!1;for(var s={},u=0;u<10;u++)s["_"+String.fromCharCode(u)]=u;if("0123456789"!==Object.getOwnPropertyNames(s).map((function(i){return s[i]})).join(""))return!1;var m={};return"abcdefghijklmnopqrst".split("").forEach((function(i){m[i]=i})),"abcdefghijklmnopqrst"===Object.keys(Object.assign({},m)).join("")}catch(i){return!1}}()?Object.assign:function(i,v){for(var _,j,M=function toObject(i){if(null==i)throw new TypeError("Object.assign cannot be called with null or undefined");return Object(i)}(i),$=1;${var m="function"==typeof Map&&Map.prototype,v=Object.getOwnPropertyDescriptor&&m?Object.getOwnPropertyDescriptor(Map.prototype,"size"):null,_=m&&v&&"function"==typeof v.get?v.get:null,j=m&&Map.prototype.forEach,M="function"==typeof Set&&Set.prototype,$=Object.getOwnPropertyDescriptor&&M?Object.getOwnPropertyDescriptor(Set.prototype,"size"):null,W=M&&$&&"function"==typeof $.get?$.get:null,X=M&&Set.prototype.forEach,Y="function"==typeof WeakMap&&WeakMap.prototype?WeakMap.prototype.has:null,Z="function"==typeof WeakSet&&WeakSet.prototype?WeakSet.prototype.has:null,ee="function"==typeof WeakRef&&WeakRef.prototype?WeakRef.prototype.deref:null,ie=Boolean.prototype.valueOf,ae=Object.prototype.toString,le=Function.prototype.toString,ce=String.prototype.match,pe=String.prototype.slice,de=String.prototype.replace,fe=String.prototype.toUpperCase,ye=String.prototype.toLowerCase,be=RegExp.prototype.test,_e=Array.prototype.concat,we=Array.prototype.join,Se=Array.prototype.slice,xe=Math.floor,Ie="function"==typeof BigInt?BigInt.prototype.valueOf:null,Pe=Object.getOwnPropertySymbols,Te="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?Symbol.prototype.toString:null,Re="function"==typeof Symbol&&"object"==typeof Symbol.iterator,qe="function"==typeof Symbol&&Symbol.toStringTag&&(typeof Symbol.toStringTag===Re||"symbol")?Symbol.toStringTag:null,ze=Object.prototype.propertyIsEnumerable,Ve=("function"==typeof Reflect?Reflect.getPrototypeOf:Object.getPrototypeOf)||([].__proto__===Array.prototype?function(i){return i.__proto__}:null);function addNumericSeparator(i,s){if(i===1/0||i===-1/0||i!=i||i&&i>-1e3&&i<1e3||be.call(/e/,s))return s;var u=/[0-9](?=(?:[0-9]{3})+(?![0-9]))/g;if("number"==typeof i){var m=i<0?-xe(-i):xe(i);if(m!==i){var v=String(m),_=pe.call(s,v.length+1);return de.call(v,u,"$&_")+"."+de.call(de.call(_,/([0-9]{3})/g,"$&_"),/_$/,"")}}return de.call(s,u,"$&_")}var We=u(24654),He=We.custom,Xe=isSymbol(He)?He:null;function wrapQuotes(i,s,u){var m="double"===(u.quoteStyle||s)?'"':"'";return m+i+m}function quote(i){return de.call(String(i),/"/g,""")}function isArray(i){return!("[object Array]"!==toStr(i)||qe&&"object"==typeof i&&qe in i)}function isRegExp(i){return!("[object RegExp]"!==toStr(i)||qe&&"object"==typeof i&&qe in i)}function isSymbol(i){if(Re)return i&&"object"==typeof i&&i instanceof Symbol;if("symbol"==typeof i)return!0;if(!i||"object"!=typeof i||!Te)return!1;try{return Te.call(i),!0}catch(i){}return!1}i.exports=function inspect_(i,s,u,m){var v=s||{};if(has(v,"quoteStyle")&&"single"!==v.quoteStyle&&"double"!==v.quoteStyle)throw new TypeError('option "quoteStyle" must be "single" or "double"');if(has(v,"maxStringLength")&&("number"==typeof v.maxStringLength?v.maxStringLength<0&&v.maxStringLength!==1/0:null!==v.maxStringLength))throw new TypeError('option "maxStringLength", if provided, must be a positive integer, Infinity, or `null`');var M=!has(v,"customInspect")||v.customInspect;if("boolean"!=typeof M&&"symbol"!==M)throw new TypeError("option \"customInspect\", if provided, must be `true`, `false`, or `'symbol'`");if(has(v,"indent")&&null!==v.indent&&"\t"!==v.indent&&!(parseInt(v.indent,10)===v.indent&&v.indent>0))throw new TypeError('option "indent" must be "\\t", an integer > 0, or `null`');if(has(v,"numericSeparator")&&"boolean"!=typeof v.numericSeparator)throw new TypeError('option "numericSeparator", if provided, must be `true` or `false`');var $=v.numericSeparator;if(void 0===i)return"undefined";if(null===i)return"null";if("boolean"==typeof i)return i?"true":"false";if("string"==typeof i)return inspectString(i,v);if("number"==typeof i){if(0===i)return 1/0/i>0?"0":"-0";var ae=String(i);return $?addNumericSeparator(i,ae):ae}if("bigint"==typeof i){var fe=String(i)+"n";return $?addNumericSeparator(i,fe):fe}var be=void 0===v.depth?5:v.depth;if(void 0===u&&(u=0),u>=be&&be>0&&"object"==typeof i)return isArray(i)?"[Array]":"[Object]";var xe=function getIndent(i,s){var u;if("\t"===i.indent)u="\t";else{if(!("number"==typeof i.indent&&i.indent>0))return null;u=we.call(Array(i.indent+1)," ")}return{base:u,prev:we.call(Array(s+1),u)}}(v,u);if(void 0===m)m=[];else if(indexOf(m,i)>=0)return"[Circular]";function inspect(i,s,_){if(s&&(m=Se.call(m)).push(s),_){var j={depth:v.depth};return has(v,"quoteStyle")&&(j.quoteStyle=v.quoteStyle),inspect_(i,j,u+1,m)}return inspect_(i,v,u+1,m)}if("function"==typeof i&&!isRegExp(i)){var Pe=function nameOf(i){if(i.name)return i.name;var s=ce.call(le.call(i),/^function\s*([\w$]+)/);if(s)return s[1];return null}(i),He=arrObjKeys(i,inspect);return"[Function"+(Pe?": "+Pe:" (anonymous)")+"]"+(He.length>0?" { "+we.call(He,", ")+" }":"")}if(isSymbol(i)){var Ye=Re?de.call(String(i),/^(Symbol\(.*\))_[^)]*$/,"$1"):Te.call(i);return"object"!=typeof i||Re?Ye:markBoxed(Ye)}if(function isElement(i){if(!i||"object"!=typeof i)return!1;if("undefined"!=typeof HTMLElement&&i instanceof HTMLElement)return!0;return"string"==typeof i.nodeName&&"function"==typeof i.getAttribute}(i)){for(var Qe="<"+ye.call(String(i.nodeName)),et=i.attributes||[],tt=0;tt"}if(isArray(i)){if(0===i.length)return"[]";var rt=arrObjKeys(i,inspect);return xe&&!function singleLineValues(i){for(var s=0;s=0)return!1;return!0}(rt)?"["+indentedJoin(rt,xe)+"]":"[ "+we.call(rt,", ")+" ]"}if(function isError(i){return!("[object Error]"!==toStr(i)||qe&&"object"==typeof i&&qe in i)}(i)){var nt=arrObjKeys(i,inspect);return"cause"in Error.prototype||!("cause"in i)||ze.call(i,"cause")?0===nt.length?"["+String(i)+"]":"{ ["+String(i)+"] "+we.call(nt,", ")+" }":"{ ["+String(i)+"] "+we.call(_e.call("[cause]: "+inspect(i.cause),nt),", ")+" }"}if("object"==typeof i&&M){if(Xe&&"function"==typeof i[Xe]&&We)return We(i,{depth:be-u});if("symbol"!==M&&"function"==typeof i.inspect)return i.inspect()}if(function isMap(i){if(!_||!i||"object"!=typeof i)return!1;try{_.call(i);try{W.call(i)}catch(i){return!0}return i instanceof Map}catch(i){}return!1}(i)){var ot=[];return j&&j.call(i,(function(s,u){ot.push(inspect(u,i,!0)+" => "+inspect(s,i))})),collectionOf("Map",_.call(i),ot,xe)}if(function isSet(i){if(!W||!i||"object"!=typeof i)return!1;try{W.call(i);try{_.call(i)}catch(i){return!0}return i instanceof Set}catch(i){}return!1}(i)){var it=[];return X&&X.call(i,(function(s){it.push(inspect(s,i))})),collectionOf("Set",W.call(i),it,xe)}if(function isWeakMap(i){if(!Y||!i||"object"!=typeof i)return!1;try{Y.call(i,Y);try{Z.call(i,Z)}catch(i){return!0}return i instanceof WeakMap}catch(i){}return!1}(i))return weakCollectionOf("WeakMap");if(function isWeakSet(i){if(!Z||!i||"object"!=typeof i)return!1;try{Z.call(i,Z);try{Y.call(i,Y)}catch(i){return!0}return i instanceof WeakSet}catch(i){}return!1}(i))return weakCollectionOf("WeakSet");if(function isWeakRef(i){if(!ee||!i||"object"!=typeof i)return!1;try{return ee.call(i),!0}catch(i){}return!1}(i))return weakCollectionOf("WeakRef");if(function isNumber(i){return!("[object Number]"!==toStr(i)||qe&&"object"==typeof i&&qe in i)}(i))return markBoxed(inspect(Number(i)));if(function isBigInt(i){if(!i||"object"!=typeof i||!Ie)return!1;try{return Ie.call(i),!0}catch(i){}return!1}(i))return markBoxed(inspect(Ie.call(i)));if(function isBoolean(i){return!("[object Boolean]"!==toStr(i)||qe&&"object"==typeof i&&qe in i)}(i))return markBoxed(ie.call(i));if(function isString(i){return!("[object String]"!==toStr(i)||qe&&"object"==typeof i&&qe in i)}(i))return markBoxed(inspect(String(i)));if(!function isDate(i){return!("[object Date]"!==toStr(i)||qe&&"object"==typeof i&&qe in i)}(i)&&!isRegExp(i)){var at=arrObjKeys(i,inspect),st=Ve?Ve(i)===Object.prototype:i instanceof Object||i.constructor===Object,lt=i instanceof Object?"":"null prototype",ct=!st&&qe&&Object(i)===i&&qe in i?pe.call(toStr(i),8,-1):lt?"Object":"",ut=(st||"function"!=typeof i.constructor?"":i.constructor.name?i.constructor.name+" ":"")+(ct||lt?"["+we.call(_e.call([],ct||[],lt||[]),": ")+"] ":"");return 0===at.length?ut+"{}":xe?ut+"{"+indentedJoin(at,xe)+"}":ut+"{ "+we.call(at,", ")+" }"}return String(i)};var Ye=Object.prototype.hasOwnProperty||function(i){return i in this};function has(i,s){return Ye.call(i,s)}function toStr(i){return ae.call(i)}function indexOf(i,s){if(i.indexOf)return i.indexOf(s);for(var u=0,m=i.length;us.maxStringLength){var u=i.length-s.maxStringLength,m="... "+u+" more character"+(u>1?"s":"");return inspectString(pe.call(i,0,s.maxStringLength),s)+m}return wrapQuotes(de.call(de.call(i,/(['\\])/g,"\\$1"),/[\x00-\x1f]/g,lowbyte),"single",s)}function lowbyte(i){var s=i.charCodeAt(0),u={8:"b",9:"t",10:"n",12:"f",13:"r"}[s];return u?"\\"+u:"\\x"+(s<16?"0":"")+fe.call(s.toString(16))}function markBoxed(i){return"Object("+i+")"}function weakCollectionOf(i){return i+" { ? }"}function collectionOf(i,s,u,m){return i+" ("+s+") {"+(m?indentedJoin(u,m):we.call(u,", "))+"}"}function indentedJoin(i,s){if(0===i.length)return"";var u="\n"+s.prev+s.base;return u+we.call(i,","+u)+"\n"+s.prev}function arrObjKeys(i,s){var u=isArray(i),m=[];if(u){m.length=i.length;for(var v=0;v{var s,u,m=i.exports={};function defaultSetTimout(){throw new Error("setTimeout has not been defined")}function defaultClearTimeout(){throw new Error("clearTimeout has not been defined")}function runTimeout(i){if(s===setTimeout)return setTimeout(i,0);if((s===defaultSetTimout||!s)&&setTimeout)return s=setTimeout,setTimeout(i,0);try{return s(i,0)}catch(u){try{return s.call(null,i,0)}catch(u){return s.call(this,i,0)}}}!function(){try{s="function"==typeof setTimeout?setTimeout:defaultSetTimout}catch(i){s=defaultSetTimout}try{u="function"==typeof clearTimeout?clearTimeout:defaultClearTimeout}catch(i){u=defaultClearTimeout}}();var v,_=[],j=!1,M=-1;function cleanUpNextTick(){j&&v&&(j=!1,v.length?_=v.concat(_):M=-1,_.length&&drainQueue())}function drainQueue(){if(!j){var i=runTimeout(cleanUpNextTick);j=!0;for(var s=_.length;s;){for(v=_,_=[];++M1)for(var u=1;u{"use strict";var m=u(50414);function emptyFunction(){}function emptyFunctionWithReset(){}emptyFunctionWithReset.resetWarningCache=emptyFunction,i.exports=function(){function shim(i,s,u,v,_,j){if(j!==m){var M=new Error("Calling PropTypes validators directly is not supported by the `prop-types` package. Use PropTypes.checkPropTypes() to call them. Read more at http://fb.me/use-check-prop-types");throw M.name="Invariant Violation",M}}function getShim(){return shim}shim.isRequired=shim;var i={array:shim,bigint:shim,bool:shim,func:shim,number:shim,object:shim,string:shim,symbol:shim,any:shim,arrayOf:getShim,element:shim,elementType:shim,instanceOf:getShim,node:shim,objectOf:getShim,oneOf:getShim,oneOfType:getShim,shape:getShim,exact:getShim,checkPropTypes:emptyFunctionWithReset,resetWarningCache:emptyFunction};return i.PropTypes=i,i}},45697:(i,s,u)=>{i.exports=u(92703)()},50414:i=>{"use strict";i.exports="SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED"},55798:i=>{"use strict";var s=String.prototype.replace,u=/%20/g,m="RFC1738",v="RFC3986";i.exports={default:v,formatters:{RFC1738:function(i){return s.call(i,u,"+")},RFC3986:function(i){return String(i)}},RFC1738:m,RFC3986:v}},80129:(i,s,u)=>{"use strict";var m=u(58261),v=u(55235),_=u(55798);i.exports={formats:_,parse:v,stringify:m}},55235:(i,s,u)=>{"use strict";var m=u(12769),v=Object.prototype.hasOwnProperty,_=Array.isArray,j={allowDots:!1,allowPrototypes:!1,allowSparse:!1,arrayLimit:20,charset:"utf-8",charsetSentinel:!1,comma:!1,decoder:m.decode,delimiter:"&",depth:5,ignoreQueryPrefix:!1,interpretNumericEntities:!1,parameterLimit:1e3,parseArrays:!0,plainObjects:!1,strictNullHandling:!1},interpretNumericEntities=function(i){return i.replace(/&#(\d+);/g,(function(i,s){return String.fromCharCode(parseInt(s,10))}))},parseArrayValue=function(i,s){return i&&"string"==typeof i&&s.comma&&i.indexOf(",")>-1?i.split(","):i},M=function parseQueryStringKeys(i,s,u,m){if(i){var _=u.allowDots?i.replace(/\.([^.[]+)/g,"[$1]"):i,j=/(\[[^[\]]*])/g,M=u.depth>0&&/(\[[^[\]]*])/.exec(_),$=M?_.slice(0,M.index):_,W=[];if($){if(!u.plainObjects&&v.call(Object.prototype,$)&&!u.allowPrototypes)return;W.push($)}for(var X=0;u.depth>0&&null!==(M=j.exec(_))&&X=0;--_){var j,M=i[_];if("[]"===M&&u.parseArrays)j=[].concat(v);else{j=u.plainObjects?Object.create(null):{};var $="["===M.charAt(0)&&"]"===M.charAt(M.length-1)?M.slice(1,-1):M,W=parseInt($,10);u.parseArrays||""!==$?!isNaN(W)&&M!==$&&String(W)===$&&W>=0&&u.parseArrays&&W<=u.arrayLimit?(j=[])[W]=v:"__proto__"!==$&&(j[$]=v):j={0:v}}v=j}return v}(W,s,u,m)}};i.exports=function(i,s){var u=function normalizeParseOptions(i){if(!i)return j;if(null!==i.decoder&&void 0!==i.decoder&&"function"!=typeof i.decoder)throw new TypeError("Decoder has to be a function.");if(void 0!==i.charset&&"utf-8"!==i.charset&&"iso-8859-1"!==i.charset)throw new TypeError("The charset option must be either utf-8, iso-8859-1, or undefined");var s=void 0===i.charset?j.charset:i.charset;return{allowDots:void 0===i.allowDots?j.allowDots:!!i.allowDots,allowPrototypes:"boolean"==typeof i.allowPrototypes?i.allowPrototypes:j.allowPrototypes,allowSparse:"boolean"==typeof i.allowSparse?i.allowSparse:j.allowSparse,arrayLimit:"number"==typeof i.arrayLimit?i.arrayLimit:j.arrayLimit,charset:s,charsetSentinel:"boolean"==typeof i.charsetSentinel?i.charsetSentinel:j.charsetSentinel,comma:"boolean"==typeof i.comma?i.comma:j.comma,decoder:"function"==typeof i.decoder?i.decoder:j.decoder,delimiter:"string"==typeof i.delimiter||m.isRegExp(i.delimiter)?i.delimiter:j.delimiter,depth:"number"==typeof i.depth||!1===i.depth?+i.depth:j.depth,ignoreQueryPrefix:!0===i.ignoreQueryPrefix,interpretNumericEntities:"boolean"==typeof i.interpretNumericEntities?i.interpretNumericEntities:j.interpretNumericEntities,parameterLimit:"number"==typeof i.parameterLimit?i.parameterLimit:j.parameterLimit,parseArrays:!1!==i.parseArrays,plainObjects:"boolean"==typeof i.plainObjects?i.plainObjects:j.plainObjects,strictNullHandling:"boolean"==typeof i.strictNullHandling?i.strictNullHandling:j.strictNullHandling}}(s);if(""===i||null==i)return u.plainObjects?Object.create(null):{};for(var $="string"==typeof i?function parseQueryStringValues(i,s){var u,M={},$=s.ignoreQueryPrefix?i.replace(/^\?/,""):i,W=s.parameterLimit===1/0?void 0:s.parameterLimit,X=$.split(s.delimiter,W),Y=-1,Z=s.charset;if(s.charsetSentinel)for(u=0;u-1&&(ie=_(ie)?[ie]:ie),v.call(M,ee)?M[ee]=m.combine(M[ee],ie):M[ee]=ie}return M}(i,u):i,W=u.plainObjects?Object.create(null):{},X=Object.keys($),Y=0;Y{"use strict";var m=u(37478),v=u(12769),_=u(55798),j=Object.prototype.hasOwnProperty,M={brackets:function brackets(i){return i+"[]"},comma:"comma",indices:function indices(i,s){return i+"["+s+"]"},repeat:function repeat(i){return i}},$=Array.isArray,W=String.prototype.split,X=Array.prototype.push,pushToArray=function(i,s){X.apply(i,$(s)?s:[s])},Y=Date.prototype.toISOString,Z=_.default,ee={addQueryPrefix:!1,allowDots:!1,charset:"utf-8",charsetSentinel:!1,delimiter:"&",encode:!0,encoder:v.encode,encodeValuesOnly:!1,format:Z,formatter:_.formatters[Z],indices:!1,serializeDate:function serializeDate(i){return Y.call(i)},skipNulls:!1,strictNullHandling:!1},ie={},ae=function stringify(i,s,u,_,j,M,X,Y,Z,ae,le,ce,pe,de,fe,ye){for(var be=i,_e=ye,we=0,Se=!1;void 0!==(_e=_e.get(ie))&&!Se;){var xe=_e.get(i);if(we+=1,void 0!==xe){if(xe===we)throw new RangeError("Cyclic object value");Se=!0}void 0===_e.get(ie)&&(we=0)}if("function"==typeof Y?be=Y(s,be):be instanceof Date?be=le(be):"comma"===u&&$(be)&&(be=v.maybeMap(be,(function(i){return i instanceof Date?le(i):i}))),null===be){if(j)return X&&!de?X(s,ee.encoder,fe,"key",ce):s;be=""}if(function isNonNullishPrimitive(i){return"string"==typeof i||"number"==typeof i||"boolean"==typeof i||"symbol"==typeof i||"bigint"==typeof i}(be)||v.isBuffer(be)){if(X){var Ie=de?s:X(s,ee.encoder,fe,"key",ce);if("comma"===u&&de){for(var Pe=W.call(String(be),","),Te="",Re=0;Re0?be.join(",")||null:void 0}];else if($(Y))qe=Y;else{var Ve=Object.keys(be);qe=Z?Ve.sort(Z):Ve}for(var We=_&&$(be)&&1===be.length?s+"[]":s,He=0;He0?fe+de:""}},12769:(i,s,u)=>{"use strict";var m=u(55798),v=Object.prototype.hasOwnProperty,_=Array.isArray,j=function(){for(var i=[],s=0;s<256;++s)i.push("%"+((s<16?"0":"")+s.toString(16)).toUpperCase());return i}(),M=function arrayToObject(i,s){for(var u=s&&s.plainObjects?Object.create(null):{},m=0;m1;){var s=i.pop(),u=s.obj[s.prop];if(_(u)){for(var m=[],v=0;v=48&&X<=57||X>=65&&X<=90||X>=97&&X<=122||_===m.RFC1738&&(40===X||41===X)?$+=M.charAt(W):X<128?$+=j[X]:X<2048?$+=j[192|X>>6]+j[128|63&X]:X<55296||X>=57344?$+=j[224|X>>12]+j[128|X>>6&63]+j[128|63&X]:(W+=1,X=65536+((1023&X)<<10|1023&M.charCodeAt(W)),$+=j[240|X>>18]+j[128|X>>12&63]+j[128|X>>6&63]+j[128|63&X])}return $},isBuffer:function isBuffer(i){return!(!i||"object"!=typeof i)&&!!(i.constructor&&i.constructor.isBuffer&&i.constructor.isBuffer(i))},isRegExp:function isRegExp(i){return"[object RegExp]"===Object.prototype.toString.call(i)},maybeMap:function maybeMap(i,s){if(_(i)){for(var u=[],m=0;m{"use strict";var u=Object.prototype.hasOwnProperty;function decode(i){try{return decodeURIComponent(i.replace(/\+/g," "))}catch(i){return null}}function encode(i){try{return encodeURIComponent(i)}catch(i){return null}}s.stringify=function querystringify(i,s){s=s||"";var m,v,_=[];for(v in"string"!=typeof s&&(s="?"),i)if(u.call(i,v)){if((m=i[v])||null!=m&&!isNaN(m)||(m=""),v=encode(v),m=encode(m),null===v||null===m)continue;_.push(v+"="+m)}return _.length?s+_.join("&"):""},s.parse=function querystring(i){for(var s,u=/([^=?#&]+)=?([^&]*)/g,m={};s=u.exec(i);){var v=decode(s[1]),_=decode(s[2]);null===v||null===_||v in m||(m[v]=_)}return m}},14419:(i,s,u)=>{const m=u(60697),v=u(69450),_=m.types;i.exports=class RandExp{constructor(i,s){if(this._setDefaults(i),i instanceof RegExp)this.ignoreCase=i.ignoreCase,this.multiline=i.multiline,i=i.source;else{if("string"!=typeof i)throw new Error("Expected a regexp or string");this.ignoreCase=s&&-1!==s.indexOf("i"),this.multiline=s&&-1!==s.indexOf("m")}this.tokens=m(i)}_setDefaults(i){this.max=null!=i.max?i.max:null!=RandExp.prototype.max?RandExp.prototype.max:100,this.defaultRange=i.defaultRange?i.defaultRange:this.defaultRange.clone(),i.randInt&&(this.randInt=i.randInt)}gen(){return this._gen(this.tokens,[])}_gen(i,s){var u,m,v,j,M;switch(i.type){case _.ROOT:case _.GROUP:if(i.followedBy||i.notFollowedBy)return"";for(i.remember&&void 0===i.groupNumber&&(i.groupNumber=s.push(null)-1),m="",j=0,M=(u=i.options?this._randSelect(i.options):i.stack).length;j{"use strict";var m=u(34155),v=65536,_=4294967295;var j=u(89509).Buffer,M=u.g.crypto||u.g.msCrypto;M&&M.getRandomValues?i.exports=function randomBytes(i,s){if(i>_)throw new RangeError("requested too many random bytes");var u=j.allocUnsafe(i);if(i>0)if(i>v)for(var $=0;${"use strict";function _typeof(i){return _typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(i){return typeof i}:function(i){return i&&"function"==typeof Symbol&&i.constructor===Symbol&&i!==Symbol.prototype?"symbol":typeof i},_typeof(i)}Object.defineProperty(s,"__esModule",{value:!0}),s.CopyToClipboard=void 0;var m=_interopRequireDefault(u(67294)),v=_interopRequireDefault(u(20640)),_=["text","onCopy","options","children"];function _interopRequireDefault(i){return i&&i.__esModule?i:{default:i}}function ownKeys(i,s){var u=Object.keys(i);if(Object.getOwnPropertySymbols){var m=Object.getOwnPropertySymbols(i);s&&(m=m.filter((function(s){return Object.getOwnPropertyDescriptor(i,s).enumerable}))),u.push.apply(u,m)}return u}function _objectSpread(i){for(var s=1;s=0||(v[u]=i[u]);return v}(i,s);if(Object.getOwnPropertySymbols){var _=Object.getOwnPropertySymbols(i);for(m=0;m<_.length;m++)u=_[m],s.indexOf(u)>=0||Object.prototype.propertyIsEnumerable.call(i,u)&&(v[u]=i[u])}return v}function _defineProperties(i,s){for(var u=0;u{"use strict";var m=u(74300).CopyToClipboard;m.CopyToClipboard=m,i.exports=m},53441:(i,s,u)=>{"use strict";function _typeof(i){return _typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(i){return typeof i}:function(i){return i&&"function"==typeof Symbol&&i.constructor===Symbol&&i!==Symbol.prototype?"symbol":typeof i},_typeof(i)}Object.defineProperty(s,"__esModule",{value:!0}),s.DebounceInput=void 0;var m=_interopRequireDefault(u(67294)),v=_interopRequireDefault(u(91296)),_=["element","onChange","value","minLength","debounceTimeout","forceNotifyByEnter","forceNotifyOnBlur","onKeyDown","onBlur","inputRef"];function _interopRequireDefault(i){return i&&i.__esModule?i:{default:i}}function _objectWithoutProperties(i,s){if(null==i)return{};var u,m,v=function _objectWithoutPropertiesLoose(i,s){if(null==i)return{};var u,m,v={},_=Object.keys(i);for(m=0;m<_.length;m++)u=_[m],s.indexOf(u)>=0||(v[u]=i[u]);return v}(i,s);if(Object.getOwnPropertySymbols){var _=Object.getOwnPropertySymbols(i);for(m=0;m<_.length;m++)u=_[m],s.indexOf(u)>=0||Object.prototype.propertyIsEnumerable.call(i,u)&&(v[u]=i[u])}return v}function ownKeys(i,s){var u=Object.keys(i);if(Object.getOwnPropertySymbols){var m=Object.getOwnPropertySymbols(i);s&&(m=m.filter((function(s){return Object.getOwnPropertyDescriptor(i,s).enumerable}))),u.push.apply(u,m)}return u}function _objectSpread(i){for(var s=1;s=m?u.notify(i):s.length>v.length&&u.notify(_objectSpread(_objectSpread({},i),{},{target:_objectSpread(_objectSpread({},i.target),{},{value:""})}))}))})),_defineProperty(_assertThisInitialized(u),"onKeyDown",(function(i){"Enter"===i.key&&u.forceNotify(i);var s=u.props.onKeyDown;s&&(i.persist(),s(i))})),_defineProperty(_assertThisInitialized(u),"onBlur",(function(i){u.forceNotify(i);var s=u.props.onBlur;s&&(i.persist(),s(i))})),_defineProperty(_assertThisInitialized(u),"createNotifier",(function(i){if(i<0)u.notify=function(){return null};else if(0===i)u.notify=u.doNotify;else{var s=(0,v.default)((function(i){u.isDebouncing=!1,u.doNotify(i)}),i);u.notify=function(i){u.isDebouncing=!0,s(i)},u.flush=function(){return s.flush()},u.cancel=function(){u.isDebouncing=!1,s.cancel()}}})),_defineProperty(_assertThisInitialized(u),"doNotify",(function(){u.props.onChange.apply(void 0,arguments)})),_defineProperty(_assertThisInitialized(u),"forceNotify",(function(i){var s=u.props.debounceTimeout;if(u.isDebouncing||!(s>0)){u.cancel&&u.cancel();var m=u.state.value,v=u.props.minLength;m.length>=v?u.doNotify(i):u.doNotify(_objectSpread(_objectSpread({},i),{},{target:_objectSpread(_objectSpread({},i.target),{},{value:m})}))}})),u.isDebouncing=!1,u.state={value:void 0===i.value||null===i.value?"":i.value};var m=u.props.debounceTimeout;return u.createNotifier(m),u}return function _createClass(i,s,u){return s&&_defineProperties(i.prototype,s),u&&_defineProperties(i,u),Object.defineProperty(i,"prototype",{writable:!1}),i}(DebounceInput,[{key:"componentDidUpdate",value:function componentDidUpdate(i){if(!this.isDebouncing){var s=this.props,u=s.value,m=s.debounceTimeout,v=i.debounceTimeout,_=i.value,j=this.state.value;void 0!==u&&_!==u&&j!==u&&this.setState({value:u}),m!==v&&this.createNotifier(m)}}},{key:"componentWillUnmount",value:function componentWillUnmount(){this.flush&&this.flush()}},{key:"render",value:function render(){var i,s,u=this.props,v=u.element,j=(u.onChange,u.value,u.minLength,u.debounceTimeout,u.forceNotifyByEnter),M=u.forceNotifyOnBlur,$=u.onKeyDown,W=u.onBlur,X=u.inputRef,Y=_objectWithoutProperties(u,_),Z=this.state.value;i=j?{onKeyDown:this.onKeyDown}:$?{onKeyDown:$}:{},s=M?{onBlur:this.onBlur}:W?{onBlur:W}:{};var ee=X?{ref:X}:{};return m.default.createElement(v,_objectSpread(_objectSpread(_objectSpread(_objectSpread({},Y),{},{onChange:this.onChange,value:Z},i),s),ee))}}]),DebounceInput}(m.default.PureComponent);s.DebounceInput=j,_defineProperty(j,"defaultProps",{element:"input",type:"text",onKeyDown:void 0,onBlur:void 0,value:void 0,minLength:0,debounceTimeout:100,forceNotifyByEnter:!0,forceNotifyOnBlur:!0,inputRef:void 0})},775:(i,s,u)=>{"use strict";var m=u(53441).DebounceInput;m.DebounceInput=m,i.exports=m},64448:(i,s,u)=>{"use strict";var m=u(67294),v=u(27418),_=u(63840);function y(i){for(var s="https://reactjs.org/docs/error-decoder.html?invariant="+i,u=1;u