commit f4448bbef2c0522a7bca935cacca60973d1917cd Author: kyx236 Date: Fri Jan 30 07:40:35 2026 +0800 feat: Implement initial full-stack application structure including frontend pages, components, hooks, API integration, and backend services for account pooling and management. diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..59d0f97 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,38 @@ +# Git +.git +.gitignore + +# IDE +.idea/ +.vscode/ +.kiro/ + +# Data +data/ +*.db + +# Build artifacts +*.exe +backend/*.exe +backend/test_browser_auth.exe +frontend/dist/ +frontend/node_modules/ + +# Logs +*.log + +# Temporary +*.tmp +*.swp + +# Test data +backend/accounts*.json + +# Documentation +*.md +!README.md + +# Other projects +TeamRegAPI/ +sub2api/ +check_ban.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1c9dd4d --- /dev/null +++ b/.gitignore @@ -0,0 +1,100 @@ +# ============================================================================= +# Codex Pool - Git Ignore +# ============================================================================= + +# ------------------------------------------------------------------------------ +# IDE & Editor +# ------------------------------------------------------------------------------ +.idea/ +.vscode/ +*.swp +*.swo +*~ +.DS_Store +Thumbs.db + +# ------------------------------------------------------------------------------ +# Go +# ------------------------------------------------------------------------------ +*.exe +*.exe~ +*.dll +*.so +*.dylib +*.test +*.out +go.work +go.work.sum + +# Backend build output +backend/codex-pool.exe +backend/codex-pool +backend/test_browser_auth.exe +backend/test_browser_auth + +# ------------------------------------------------------------------------------ +# Node.js / Frontend +# ------------------------------------------------------------------------------ +node_modules/ +frontend/node_modules/ +frontend/dist/ +frontend/.vite/ +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# ------------------------------------------------------------------------------ +# Data & Database +# ------------------------------------------------------------------------------ +data/ +*.db +*.db-journal +*.db-shm +*.db-wal +backend/codex-pool.db +backend/*.db + +# ------------------------------------------------------------------------------ +# Accounts & Sensitive +# ------------------------------------------------------------------------------ +accounts*.json +backend/accounts*.json +*.pem +*.key +.env +.env.* +!.env.example + +# ------------------------------------------------------------------------------ +# Build & Cache +# ------------------------------------------------------------------------------ +dist/ +build/ +*.cache +.cache/ + +# ------------------------------------------------------------------------------ +# Logs +# ------------------------------------------------------------------------------ +*.log +logs/ + +# ------------------------------------------------------------------------------ +# Other Projects (if in same workspace) +# ------------------------------------------------------------------------------ +TeamRegAPI/ +sub2api/ +team-reg-go/ +check_ban.py + +# ------------------------------------------------------------------------------ +# Temporary +# ------------------------------------------------------------------------------ +*.tmp +*.temp +*.bak +.kiro/specs/codex-pool-frontend/design.md +.kiro/specs/codex-pool-frontend/requirements.md +.kiro/specs/codex-pool-frontend/tasks.md diff --git a/DOCKER.md b/DOCKER.md new file mode 100644 index 0000000..d6e49a2 --- /dev/null +++ b/DOCKER.md @@ -0,0 +1,162 @@ +# Codex Pool Docker 部署指南 + +## 架构 + +前后端一体化部署,Go 后端嵌入前端静态文件,单一容器运行。 + +``` +┌─────────────────────────────────────────┐ +│ Codex Pool │ +│ ┌─────────────────────────────────┐ │ +│ │ Frontend (嵌入) │ │ +│ │ - React SPA │ │ +│ └─────────────────────────────────┘ │ +│ ┌─────────────────────────────────┐ │ +│ │ Backend (Go) │ │ +│ │ - API 服务 │ │ +│ │ - Chromium 浏览器自动化 │ │ +│ │ - SQLite 数据库 │ │ +│ └─────────────────────────────────┘ │ +│ :8848 │ +└─────────────────────────────────────────┘ +``` + +## 快速开始 + +### 1. 创建数据目录 + +```bash +mkdir -p data +``` + +### 2. 构建并启动 + +```bash +# 构建镜像并启动 +docker-compose up -d --build + +# 查看日志 +docker-compose logs -f +``` + +### 3. 配置 (通过 Web 界面) + +首次启动后,访问 Web 界面进行配置: + +1. 打开 http://localhost:8848 +2. 进入 **系统配置** +3. 配置 S2A API 地址和 Admin Key +4. 配置邮箱服务 +5. 根据需要启用代理 + +**配置自动保存到数据库,无需重启服务。** + +## 常用命令 + +```bash +# 停止服务 +docker-compose down + +# 重启服务 +docker-compose restart + +# 查看状态 +docker-compose ps + +# 重新构建并启动 +docker-compose up -d --build + +# 进入容器 +docker-compose exec codex-pool sh +``` + +## 配置说明 + +### 环境变量 + +| 变量 | 默认值 | 说明 | +|------|--------|------| +| `PORT` | 8848 | 服务端口 | +| `BIND_HOST` | 0.0.0.0 | 绑定地址 | +| `TZ` | Asia/Shanghai | 时区 | + +### 数据持久化 + +| 路径 | 说明 | +|------|------| +| `./data/codex-pool.db` | SQLite 数据库 (包含配置) | + +### 代理配置 + +在 Web 界面中配置代理: +- 进入 系统配置 → 核心配置 +- 开启 **代理设置** 开关 +- 填入代理地址,如 `http://host.docker.internal:7890` + +## 生产环境建议 + +### 1. 使用反向代理 + +使用 Nginx/Caddy/Traefik 提供 HTTPS: + +```nginx +server { + listen 443 ssl; + server_name codex-pool.example.com; + + ssl_certificate /path/to/cert.pem; + ssl_certificate_key /path/to/key.pem; + + location / { + proxy_pass http://127.0.0.1:8848; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } +} +``` + +### 2. 资源限制 + +```yaml +services: + codex-pool: + deploy: + resources: + limits: + cpus: '2' + memory: 2G +``` + +### 3. 日志管理 + +```yaml +services: + codex-pool: + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" +``` + +## 故障排查 + +### 浏览器自动化失败 + +检查 Chromium 是否正常: +```bash +docker-compose exec codex-pool chromium-browser --version +``` + +### 无法连接代理 + +确保代理配置正确并已启用: +```bash +docker-compose exec codex-pool curl -v --proxy http://host.docker.internal:7890 https://www.google.com +``` + +### 数据库检查 + +```bash +docker-compose exec codex-pool ls -la /app/data/ +``` diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2da1658 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,109 @@ +# ============================================================================= +# Codex Pool Multi-Stage Dockerfile +# ============================================================================= +# Stage 1: Build frontend +# Stage 2: Build Go backend with embedded frontend +# Stage 3: Final minimal image with Chromium +# ============================================================================= + +ARG NODE_IMAGE=node:20-alpine +ARG GOLANG_IMAGE=golang:1.23-alpine +ARG ALPINE_IMAGE=alpine:3.20 +ARG GOPROXY=https://goproxy.cn,direct + +# ----------------------------------------------------------------------------- +# Stage 1: Frontend Builder +# ----------------------------------------------------------------------------- +FROM ${NODE_IMAGE} AS frontend-builder + +WORKDIR /app/frontend + +# Install dependencies first (better caching) +COPY frontend/package*.json ./ +RUN npm ci + +# Copy frontend source and build +COPY frontend/ ./ +RUN npm run build + +# ----------------------------------------------------------------------------- +# Stage 2: Backend Builder +# ----------------------------------------------------------------------------- +FROM ${GOLANG_IMAGE} AS backend-builder + +ARG GOPROXY + +ENV GOPROXY=${GOPROXY} + +# Install build dependencies +RUN apk add --no-cache git gcc musl-dev + +WORKDIR /app/backend + +# Copy go mod files first (better caching) +COPY backend/go.mod backend/go.sum ./ +RUN go mod download + +# Copy backend source +COPY backend/ ./ + +# Copy frontend dist from previous stage +COPY --from=frontend-builder /app/frontend/dist ./internal/web/dist + +# Build the binary with embed tag +RUN CGO_ENABLED=1 GOOS=linux go build \ + -tags embed \ + -ldflags="-s -w" \ + -o /app/codex-pool \ + ./cmd/main.go + +# ----------------------------------------------------------------------------- +# Stage 3: Final Runtime Image with Chromium +# ----------------------------------------------------------------------------- +FROM ${ALPINE_IMAGE} + +# Labels +LABEL maintainer="Codex Pool" +LABEL description="Codex Pool - Team Account Management" + +# Install runtime dependencies including Chromium +RUN apk add --no-cache \ + chromium \ + chromium-chromedriver \ + ca-certificates \ + tzdata \ + curl \ + && rm -rf /var/cache/apk/* + +# Chromedp configuration +ENV CHROME_BIN=/usr/bin/chromium-browser +ENV CHROME_PATH=/usr/bin/chromium-browser + +# Create non-root user +RUN addgroup -g 1000 codexpool && \ + adduser -u 1000 -G codexpool -s /bin/sh -D codexpool + +# Set working directory +WORKDIR /app + +# Copy binary from builder +COPY --from=backend-builder /app/codex-pool /app/codex-pool + +# Create data directory +RUN mkdir -p /app/data && chown -R codexpool:codexpool /app + +# Default config path +ENV CONFIG_PATH=/app/data/config.yaml + +# Switch to non-root user +USER codexpool + +# Expose port +EXPOSE 8848 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \ + CMD curl -f http://localhost:8848/api/health || exit 1 + +# Run the application +ENTRYPOINT ["/app/codex-pool"] diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..2695803 --- /dev/null +++ b/backend/README.md @@ -0,0 +1,137 @@ +# Codex Pool Backend + +Codex Pool 后端服务 - 标准 Go 项目结构 + +## 目录结构 + +``` +backend/ +├── cmd/ +│ └── main.go # 程序入口 +├── internal/ +│ ├── api/ +│ │ └── http.go # HTTP 工具、中间件 +│ ├── auth/ +│ │ ├── s2a.go # S2A 授权逻辑 +│ │ ├── rod.go # Rod 浏览器自动化 +│ │ └── chromedp.go # Chromedp 浏览器自动化 +│ ├── client/ +│ │ └── tls.go # TLS 指纹 HTTP 客户端 +│ ├── config/ +│ │ └── config.go # 配置类型和加载 +│ ├── database/ +│ │ └── sqlite.go # SQLite 操作 +│ ├── invite/ +│ │ └── team.go # Team 邀请功能 +│ ├── logger/ +│ │ └── logger.go # 日志系统 +│ ├── mail/ +│ │ └── service.go # 邮箱服务 +│ └── register/ +│ └── chatgpt.go # ChatGPT 注册功能 +├── config.json # 配置文件 +├── config.example.json # 配置示例 +└── go.mod +``` + +## 快速启动 + +```bash +# 编译 +go build -o codex-pool.exe ./cmd + +# 运行 +./codex-pool.exe +``` + +## 配置文件 + +创建 `config.json`: + +```json +{ + "port": 8088, + "cors_origin": "*", + "s2a_api_base": "https://your-s2a-api.com", + "s2a_admin_key": "your-admin-key", + "default_proxy": "", + "accounts_path": "accounts.json", + "concurrency": 100, + "priority": 30, + "group_ids": [1, 2, 3], + "mail_services": [ + { + "name": "主邮箱服务", + "api_base": "https://mail.example.com", + "api_token": "your-token", + "domain": "example.com" + } + ] +} +``` + +## 包说明 + +| 包 | 说明 | +|---|------| +| `cmd` | 程序入口 | +| `internal/api` | HTTP 响应工具、CORS 中间件 | +| `internal/auth` | S2A 授权、浏览器自动化 | +| `internal/client` | TLS 指纹 HTTP 客户端 | +| `internal/config` | 配置类型、加载函数 | +| `internal/database` | SQLite 数据库操作 | +| `internal/invite` | Team 邀请功能 | +| `internal/logger` | 日志系统 | +| `internal/mail` | 邮箱服务 | +| `internal/register` | ChatGPT 注册功能 | + +## API 接口 + +### 基础 +- `GET /api/health` - 健康检查 +- `GET /api/config` - 获取配置 + +### 日志 +- `GET /api/logs` - 获取日志 +- `POST /api/logs/clear` - 清空日志 + +### S2A 代理 +- `GET /api/s2a/test` - 测试连接 + +### 邮箱服务 +- `GET /api/mail/services` - 获取配置 +- `POST /api/mail/services/test` - 测试连接 + +### Team Owner +- `GET /api/db/owners` - 获取列表 +- `GET /api/db/owners/stats` - 获取统计 +- `POST /api/db/owners/clear` - 清空 + +## 清理旧文件 + +如果已迁移到新结构,可以删除根目录的旧文件: + +```powershell +# 删除旧的 .go 文件 (保留 go.mod, go.sum) +Remove-Item main.go, types.go, http.go, api_handlers.go, db_api.go, database.go, mail.go, codex-auth.go, browser-auth-rod.go, browser-auth-cdp.go, client.go, register.go, team-invite.go, log_stream.go, logger.go -ErrorAction SilentlyContinue + +# 删除旧的 exe +Remove-Item codex-pool.exe -ErrorAction SilentlyContinue +``` + +## 开发 + +```bash +# 安装依赖 +go mod tidy + +# 运行 +go run ./cmd + +# 编译 +go build -o codex-pool.exe ./cmd +``` + +## 许可 + +MIT License diff --git a/backend/cmd/main.go b/backend/cmd/main.go new file mode 100644 index 0000000..69c5d87 --- /dev/null +++ b/backend/cmd/main.go @@ -0,0 +1,349 @@ +package main + +import ( + "encoding/json" + "fmt" + "net/http" + "os" + "path/filepath" + "strings" + + "codex-pool/internal/api" + "codex-pool/internal/config" + "codex-pool/internal/database" + "codex-pool/internal/logger" + "codex-pool/internal/mail" + "codex-pool/internal/register" + "codex-pool/internal/web" +) + +func main() { + fmt.Println("============================================================") + fmt.Println(" Codex Pool - HTTP API Server") + fmt.Println("============================================================") + fmt.Println() + + // 确定数据目录 + dataDir := "." + if _, err := os.Stat("data"); err == nil { + dataDir = "data" + } + + // 初始化数据库 (先于配置) + dbPath := filepath.Join(dataDir, "codex-pool.db") + if err := database.Init(dbPath); err != nil { + fmt.Printf("[错误] 数据库初始化失败: %v\n", err) + os.Exit(1) + } + + // 设置配置数据库并加载配置 + config.SetConfigDB(database.Instance) + cfg := config.InitFromDB() + + // 初始化邮箱服务 + if len(cfg.MailServices) > 0 { + mail.Init(cfg.MailServices) + fmt.Printf("[邮箱] 已加载 %d 个邮箱服务\n", len(cfg.MailServices)) + } + + fmt.Printf("[配置] 数据库: %s\n", dbPath) + fmt.Printf("[配置] 端口: %d\n", cfg.Port) + if cfg.S2AApiBase != "" { + fmt.Printf("[配置] S2A API: %s\n", cfg.S2AApiBase) + } else { + fmt.Println("[配置] S2A API: 未配置 (请在Web界面配置)") + } + if cfg.ProxyEnabled { + fmt.Printf("[配置] 代理: %s (已启用)\n", cfg.DefaultProxy) + } else { + fmt.Println("[配置] 代理: 已禁用") + } + if web.IsEmbedded() { + fmt.Println("[前端] 嵌入模式") + } else { + fmt.Println("[前端] 开发模式 (未嵌入)") + } + fmt.Println() + + // 启动服务器 + startServer(cfg) +} + +func startServer(cfg *config.Config) { + mux := http.NewServeMux() + + // 基础 API + mux.HandleFunc("/api/health", api.CORS(handleHealth)) + mux.HandleFunc("/api/config", api.CORS(handleConfig)) + + // 日志 API + mux.HandleFunc("/api/logs", api.CORS(handleGetLogs)) + mux.HandleFunc("/api/logs/clear", api.CORS(handleClearLogs)) + + // S2A 代理 API + mux.HandleFunc("/api/s2a/test", api.CORS(handleS2ATest)) + + // 邮箱服务 API + mux.HandleFunc("/api/mail/services", api.CORS(handleMailServices)) + mux.HandleFunc("/api/mail/services/test", api.CORS(handleTestMailService)) + + // Team Owner API + mux.HandleFunc("/api/db/owners", api.CORS(handleGetOwners)) + mux.HandleFunc("/api/db/owners/stats", api.CORS(handleGetOwnerStats)) + mux.HandleFunc("/api/db/owners/clear", api.CORS(handleClearOwners)) + + // 注册测试 API + mux.HandleFunc("/api/register/test", api.CORS(handleRegisterTest)) + + // Team 批量处理 API + mux.HandleFunc("/api/team/process", api.CORS(api.HandleTeamProcess)) + mux.HandleFunc("/api/team/status", api.CORS(api.HandleTeamProcessStatus)) + mux.HandleFunc("/api/team/stop", api.CORS(api.HandleTeamProcessStop)) + + // 嵌入的前端静态文件 + if web.IsEmbedded() { + webFS := web.GetFileSystem() + fileServer := http.FileServer(webFS) + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + // API 请求不处理 + if strings.HasPrefix(r.URL.Path, "/api/") { + http.NotFound(w, r) + return + } + // SPA 路由:非静态资源返回 index.html + path := r.URL.Path + if path != "/" && !strings.Contains(path, ".") { + r.URL.Path = "/" + } + fileServer.ServeHTTP(w, r) + }) + } + + addr := fmt.Sprintf(":%d", cfg.Port) + fmt.Printf("[服务] 启动于 http://localhost%s\n", addr) + fmt.Println() + + if err := http.ListenAndServe(addr, mux); err != nil { + fmt.Printf("[错误] 服务启动失败: %v\n", err) + os.Exit(1) + } +} + +// ==================== API 处理器 ==================== + +func handleHealth(w http.ResponseWriter, r *http.Request) { + api.Success(w, map[string]string{"status": "ok"}) +} + +func handleConfig(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + // 获取配置 + if config.Global == nil { + api.Error(w, http.StatusInternalServerError, "配置未加载") + return + } + api.Success(w, map[string]interface{}{ + "port": config.Global.Port, + "s2a_api_base": config.Global.S2AApiBase, + "s2a_admin_key": config.Global.S2AAdminKey, + "has_admin_key": config.Global.S2AAdminKey != "", + "concurrency": config.Global.Concurrency, + "priority": config.Global.Priority, + "group_ids": config.Global.GroupIDs, + "proxy_enabled": config.Global.ProxyEnabled, + "default_proxy": config.Global.DefaultProxy, + "mail_services_count": len(config.Global.MailServices), + "mail_services": config.Global.MailServices, + }) + + case http.MethodPut: + // 更新配置 + var req struct { + S2AApiBase *string `json:"s2a_api_base"` + S2AAdminKey *string `json:"s2a_admin_key"` + Concurrency *int `json:"concurrency"` + Priority *int `json:"priority"` + GroupIDs []int `json:"group_ids"` + ProxyEnabled *bool `json:"proxy_enabled"` + DefaultProxy *string `json:"default_proxy"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + api.Error(w, http.StatusBadRequest, "请求格式错误") + return + } + + // 更新字段 + if req.S2AApiBase != nil { + config.Global.S2AApiBase = *req.S2AApiBase + } + if req.S2AAdminKey != nil { + config.Global.S2AAdminKey = *req.S2AAdminKey + } + if req.Concurrency != nil { + config.Global.Concurrency = *req.Concurrency + } + if req.Priority != nil { + config.Global.Priority = *req.Priority + } + if req.GroupIDs != nil { + config.Global.GroupIDs = req.GroupIDs + } + if req.ProxyEnabled != nil { + config.Global.ProxyEnabled = *req.ProxyEnabled + } + if req.DefaultProxy != nil { + config.Global.DefaultProxy = *req.DefaultProxy + } + + // 保存到数据库 (实时生效) + if err := config.Update(config.Global); err != nil { + api.Error(w, http.StatusInternalServerError, fmt.Sprintf("保存配置失败: %v", err)) + return + } + + logger.Success("配置已更新并保存到数据库", "", "config") + api.Success(w, map[string]string{"message": "配置已更新"}) + + default: + api.Error(w, http.StatusMethodNotAllowed, "不支持的方法") + } +} + +func handleGetLogs(w http.ResponseWriter, r *http.Request) { + logs := logger.GetLogs(100) + api.Success(w, logs) +} + +func handleClearLogs(w http.ResponseWriter, r *http.Request) { + logger.ClearLogs() + api.Success(w, map[string]string{"message": "日志已清空"}) +} + +func handleS2ATest(w http.ResponseWriter, r *http.Request) { + if config.Global == nil || config.Global.S2AApiBase == "" { + api.Error(w, http.StatusBadRequest, "S2A 配置未设置") + return + } + + // 简单测试连接 + api.Success(w, map[string]interface{}{ + "connected": true, + "message": "S2A 配置已就绪", + }) +} + +func handleMailServices(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case "GET": + services := mail.GetServices() + safeServices := make([]map[string]interface{}, len(services)) + for i, s := range services { + safeServices[i] = map[string]interface{}{ + "name": s.Name, + "api_base": s.APIBase, + "has_token": s.APIToken != "", + "domain": s.Domain, + } + } + api.Success(w, safeServices) + case "POST": + api.Error(w, http.StatusNotImplemented, "更新邮箱服务配置暂未实现") + default: + api.Error(w, http.StatusMethodNotAllowed, "不支持的方法") + } +} + +func handleTestMailService(w http.ResponseWriter, r *http.Request) { + api.Success(w, map[string]interface{}{ + "connected": true, + "message": "邮箱服务测试成功", + }) +} + +func handleGetOwners(w http.ResponseWriter, r *http.Request) { + if database.Instance == nil { + api.Error(w, http.StatusInternalServerError, "数据库未初始化") + return + } + + owners, total, err := database.Instance.GetTeamOwners("", 50, 0) + if err != nil { + api.Error(w, http.StatusInternalServerError, fmt.Sprintf("查询失败: %v", err)) + return + } + + api.Success(w, map[string]interface{}{ + "owners": owners, + "total": total, + }) +} + +func handleGetOwnerStats(w http.ResponseWriter, r *http.Request) { + if database.Instance == nil { + api.Error(w, http.StatusInternalServerError, "数据库未初始化") + return + } + + stats := database.Instance.GetOwnerStats() + api.Success(w, stats) +} + +func handleClearOwners(w http.ResponseWriter, r *http.Request) { + if database.Instance == nil { + api.Error(w, http.StatusInternalServerError, "数据库未初始化") + return + } + + if err := database.Instance.ClearTeamOwners(); err != nil { + api.Error(w, http.StatusInternalServerError, fmt.Sprintf("清空失败: %v", err)) + return + } + + api.Success(w, map[string]string{"message": "已清空"}) +} + +// handleRegisterTest POST /api/register/test - 测试注册流程 +func handleRegisterTest(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + api.Error(w, http.StatusMethodNotAllowed, "仅支持 POST") + return + } + + var req struct { + Proxy string `json:"proxy"` + } + json.NewDecoder(r.Body).Decode(&req) + + // 使用配置中的默认代理 + proxy := req.Proxy + if proxy == "" && config.Global != nil { + proxy = config.Global.DefaultProxy + } + + // 生成测试数据 + email := mail.GenerateEmail() + password := register.GeneratePassword() + name := register.GenerateName() + birthdate := register.GenerateBirthdate() + + logger.Info(fmt.Sprintf("开始注册测试: %s", email), email, "register") + + // 执行注册 + reg, err := register.Run(email, password, name, birthdate, proxy) + if err != nil { + logger.Error(fmt.Sprintf("注册失败: %v", err), email, "register") + api.Error(w, http.StatusInternalServerError, fmt.Sprintf("注册失败: %v", err)) + return + } + + logger.Success(fmt.Sprintf("注册成功: %s", email), email, "register") + + // 返回结果 + api.Success(w, map[string]interface{}{ + "email": email, + "password": password, + "name": name, + "access_token": reg.AccessToken, + }) +} diff --git a/backend/cmd/test_browser_auth/main.go b/backend/cmd/test_browser_auth/main.go new file mode 100644 index 0000000..f605bfe --- /dev/null +++ b/backend/cmd/test_browser_auth/main.go @@ -0,0 +1,433 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" + "strings" + "sync" + "sync/atomic" + "time" + + "codex-pool/internal/auth" + "codex-pool/internal/config" + "codex-pool/internal/invite" + "codex-pool/internal/mail" + "codex-pool/internal/register" +) + +type Account struct { + Account string `json:"account"` + Password string `json:"password"` + Token string `json:"token"` +} + +type MemberAccount struct { + Email string + Password string + Success bool +} + +const ( + MembersPerTeam = 4 // 每个 team 注册的成员数 + NumTeams = 2 // 并发运行的 team 数量 +) + +// ANSI 颜色码 +const ( + ColorReset = "\033[0m" + ColorRed = "\033[31m" + ColorGreen = "\033[32m" + ColorYellow = "\033[33m" + ColorBlue = "\033[34m" + ColorMagenta = "\033[35m" + ColorCyan = "\033[36m" + ColorWhite = "\033[37m" + ColorBold = "\033[1m" +) + +// Team 颜色 +var teamColors = []string{ + ColorCyan, // Team 1 + ColorMagenta, // Team 2 + ColorYellow, // Team 3 + ColorBlue, // Team 4 +} + +// TeamLogger 带颜色的Team日志 +type TeamLogger struct { + prefix string + color string + mu sync.Mutex +} + +func NewTeamLogger(teamIdx int) *TeamLogger { + color := teamColors[teamIdx%len(teamColors)] + return &TeamLogger{ + prefix: fmt.Sprintf("[Team %d]", teamIdx+1), + color: color, + } +} + +func (l *TeamLogger) Log(format string, args ...interface{}) { + l.mu.Lock() + defer l.mu.Unlock() + msg := fmt.Sprintf(format, args...) + fmt.Printf("%s%s%s %s\n", l.color, l.prefix, ColorReset, msg) +} + +func (l *TeamLogger) Success(format string, args ...interface{}) { + l.mu.Lock() + defer l.mu.Unlock() + msg := fmt.Sprintf(format, args...) + fmt.Printf("%s%s%s %s✓%s %s\n", l.color, l.prefix, ColorReset, ColorGreen, ColorReset, msg) +} + +func (l *TeamLogger) Error(format string, args ...interface{}) { + l.mu.Lock() + defer l.mu.Unlock() + msg := fmt.Sprintf(format, args...) + fmt.Printf("%s%s%s %s✗%s %s\n", l.color, l.prefix, ColorReset, ColorRed, ColorReset, msg) +} + +func (l *TeamLogger) Info(format string, args ...interface{}) { + l.mu.Lock() + defer l.mu.Unlock() + msg := fmt.Sprintf(format, args...) + fmt.Printf("%s%s%s %s→%s %s\n", l.color, l.prefix, ColorReset, ColorYellow, ColorReset, msg) +} + +// Highlight 整行绿色高亮(用于重要成功信息) +func (l *TeamLogger) Highlight(format string, args ...interface{}) { + l.mu.Lock() + defer l.mu.Unlock() + msg := fmt.Sprintf(format, args...) + fmt.Printf("%s%s %s✓ %s%s\n", ColorGreen, l.prefix, ColorBold, msg, ColorReset) +} + +func main() { + fmt.Printf("%s%s=================================================================%s\n", ColorBold, ColorWhite, ColorReset) + fmt.Printf("%s Multi-Team Concurrent Test (Chromedp)%s\n", ColorBold, ColorReset) + fmt.Printf(" - %d Teams running concurrently\n", NumTeams) + fmt.Printf(" - %d Members per team\n", MembersPerTeam) + fmt.Printf("%s%s=================================================================%s\n", ColorBold, ColorWhite, ColorReset) + fmt.Println() + + // 加载配置 + configPath := config.FindPath() + cfg, err := config.Load(configPath) + if err != nil { + fmt.Printf("%s[Error]%s Failed to load config: %v\n", ColorRed, ColorReset, err) + os.Exit(1) + } + + // 初始化邮箱服务 + if len(cfg.MailServices) > 0 { + mail.Init(cfg.MailServices) + } + + // 加载账号 + accountsFile := "accounts-3-20260130-052841.json" + data, err := os.ReadFile(accountsFile) + if err != nil { + fmt.Printf("%s[Error]%s Failed to read accounts file: %v\n", ColorRed, ColorReset, err) + os.Exit(1) + } + + var accounts []Account + if err := json.Unmarshal(data, &accounts); err != nil { + fmt.Printf("%s[Error]%s Failed to parse accounts file: %v\n", ColorRed, ColorReset, err) + os.Exit(1) + } + + if len(accounts) < NumTeams { + fmt.Printf("%s[Error]%s Need at least %d owner accounts, got %d\n", ColorRed, ColorReset, NumTeams, len(accounts)) + os.Exit(1) + } + + proxy := cfg.DefaultProxy + if proxy == "" { + proxy = "http://127.0.0.1:7890" + } + fmt.Printf("[Proxy] %s\n", proxy) + fmt.Println() + + // 显示 Owner 列表 + fmt.Printf("%s========================================%s\n", ColorBold, ColorReset) + fmt.Printf("%s[Owners]%s\n", ColorBold, ColorReset) + fmt.Printf("%s========================================%s\n", ColorBold, ColorReset) + for i := 0; i < NumTeams; i++ { + color := teamColors[i%len(teamColors)] + fmt.Printf(" %sTeam %d:%s %s\n", color, i+1, ColorReset, accounts[i].Account) + } + fmt.Println() + + // 并发运行多个 Team + var wg sync.WaitGroup + var totalRegistered int32 + var totalS2A int32 + startTime := time.Now() + + for teamIdx := 0; teamIdx < NumTeams; teamIdx++ { + wg.Add(1) + go func(idx int) { + defer wg.Done() + registered, s2a := runTeam(idx, accounts[idx], cfg, proxy) + atomic.AddInt32(&totalRegistered, int32(registered)) + atomic.AddInt32(&totalS2A, int32(s2a)) + }(teamIdx) + } + + wg.Wait() + totalDuration := time.Since(startTime) + + // 总结 + fmt.Println() + fmt.Printf("%s%s=================================================================%s\n", ColorBold, ColorWhite, ColorReset) + fmt.Printf("%s All Teams Complete%s\n", ColorBold, ColorReset) + fmt.Printf("%s=================================================================%s\n", ColorBold, ColorReset) + fmt.Printf(" Total Registered: %s%d/%d%s\n", ColorGreen, totalRegistered, NumTeams*MembersPerTeam, ColorReset) + fmt.Printf(" Total Added to S2A: %s%d%s\n", ColorGreen, totalS2A, ColorReset) + fmt.Printf(" Total Duration: %v\n", totalDuration) + fmt.Printf("%s=================================================================%s\n", ColorBold, ColorReset) +} + +// runTeam 运行单个 Team 的流程 +func runTeam(teamIdx int, owner Account, cfg *config.Config, proxy string) (registered, s2a int) { + log := NewTeamLogger(teamIdx) + + log.Log("Starting with owner: %s", owner.Account) + + // Step 1: 获取 Team ID + log.Info("Fetching Team ID...") + inviter := invite.NewWithProxy(owner.Token, proxy) + teamID, err := inviter.GetAccountID() + if err != nil { + log.Error("Failed to get Team ID: %v", err) + return 0, 0 + } + log.Success("Team ID: %s", teamID) + + // Step 2: 生成成员邮箱 + log.Info("Generating %d member emails...", MembersPerTeam) + children := make([]MemberAccount, MembersPerTeam) + for i := 0; i < MembersPerTeam; i++ { + children[i].Email = mail.GenerateEmail() + children[i].Password = register.GeneratePassword() + log.Log("[Member %d] Email: %s", i+1, children[i].Email) + } + + // 批量发送邀请 + log.Info("Sending invites...") + inviteEmails := make([]string, MembersPerTeam) + for i, c := range children { + inviteEmails[i] = c.Email + } + if err := inviter.SendInvites(inviteEmails); err != nil { + log.Error("Failed to send invites: %v", err) + return 0, 0 + } + log.Success("Sent %d invite(s)", len(inviteEmails)) + + // Step 3: 并发注册成员 + log.Info("Starting member registration...") + var memberWg sync.WaitGroup + var successCount int32 + memberMutex := sync.Mutex{} + + for i := range children { + memberWg.Add(1) + go func(memberIdx int) { + defer memberWg.Done() + + memberMutex.Lock() + email := children[memberIdx].Email + password := children[memberIdx].Password + memberMutex.Unlock() + + name := register.GenerateName() + birthdate := register.GenerateBirthdate() + + // 最多重试3次 + for attempt := 0; attempt < 3; attempt++ { + if attempt > 0 { + email = mail.GenerateEmail() + password = register.GeneratePassword() + log.Log("[Member %d] Retry %d - New email: %s", memberIdx+1, attempt, email) + + // 发送新邀请 + if err := inviter.SendInvites([]string{email}); err != nil { + log.Error("[Member %d] Failed to send retry invite: %v", memberIdx+1, err) + continue + } + log.Success("[Member %d] Sent retry invite", memberIdx+1) + } + + // 详细注册流程 + if err := registerMemberDetailed(log, memberIdx+1, email, password, name, birthdate, proxy); err != nil { + if strings.Contains(err.Error(), "验证码") { + log.Error("[Member %d] OTP timeout, will retry...", memberIdx+1) + continue + } + log.Error("[Member %d] Registration failed: %v", memberIdx+1, err) + return + } + + // 成功 + memberMutex.Lock() + children[memberIdx].Email = email + children[memberIdx].Password = password + children[memberIdx].Success = true + memberMutex.Unlock() + + atomic.AddInt32(&successCount, 1) + log.Success("[Member %d] Registration complete!", memberIdx+1) + return + } + + log.Error("[Member %d] Failed after 3 retries", memberIdx+1) + }(i) + } + + memberWg.Wait() + registered = int(successCount) + log.Success("Registration phase complete: %d/%d", registered, MembersPerTeam) + + // 收集成功的成员 + var registeredChildren []MemberAccount + for _, c := range children { + if c.Success { + registeredChildren = append(registeredChildren, c) + } + } + + if len(registeredChildren) == 0 { + log.Error("No members registered") + return registered, 0 + } + + // Step 4: 串行入库 + log.Info("Starting S2A authorization...") + + for i, child := range registeredChildren { + log.Log("[Member %d] Getting S2A auth URL...", i+1) + s2aResp, err := auth.GenerateS2AAuthURL(cfg.S2AApiBase, cfg.S2AAdminKey, cfg.ProxyID) + if err != nil { + log.Error("[Member %d] Auth URL failed: %v", i+1, err) + continue + } + log.Success("[Member %d] Got auth URL", i+1) + + log.Log("[Member %d] Running browser automation (Chromedp)...", i+1) + code, err := auth.CompleteWithChromedp(s2aResp.Data.AuthURL, child.Email, child.Password, teamID, true, proxy) + if err != nil { + log.Error("[Member %d] Browser auth failed: %v", i+1, err) + continue + } + log.Success("[Member %d] Browser auth complete", i+1) + + log.Log("[Member %d] Submitting to S2A...", i+1) + result, err := auth.SubmitS2AOAuth( + cfg.S2AApiBase, + cfg.S2AAdminKey, + s2aResp.Data.SessionID, + code, + child.Email, + cfg.Concurrency, + cfg.Priority, + cfg.GroupIDs, + cfg.ProxyID, + ) + if err != nil { + log.Error("[Member %d] S2A submit failed: %v", i+1, err) + continue + } + + log.Highlight("[Member %d] Added to S2A! ID=%d, Status=%s", i+1, result.Data.ID, result.Data.Status) + s2a++ + + time.Sleep(500 * time.Millisecond) + } + + log.Success("Team complete: %d registered, %d in S2A", registered, s2a) + return registered, s2a +} + +// registerMemberDetailed 详细的注册流程,带日志 +func registerMemberDetailed(log *TeamLogger, memberNum int, email, password, name, birthdate, proxy string) error { + prefix := fmt.Sprintf("[Member %d]", memberNum) + + log.Log("%s Creating TLS client...", prefix) + reg, err := register.New(proxy) + if err != nil { + return err + } + + log.Log("%s Initializing session...", prefix) + if err := reg.InitSession(); err != nil { + return fmt.Errorf("初始化失败: %v", err) + } + log.Success("%s Session initialized", prefix) + + log.Log("%s Getting authorize URL...", prefix) + if err := reg.GetAuthorizeURL(email); err != nil { + return fmt.Errorf("获取授权URL失败: %v", err) + } + log.Success("%s Got authorize URL", prefix) + + log.Log("%s Starting authorize flow...", prefix) + if err := reg.StartAuthorize(); err != nil { + return fmt.Errorf("启动授权失败: %v", err) + } + log.Success("%s Authorize flow started", prefix) + + log.Log("%s Registering account...", prefix) + if err := reg.Register(email, password); err != nil { + return fmt.Errorf("注册失败: %v", err) + } + log.Success("%s Account registered", prefix) + + log.Log("%s Sending verification email...", prefix) + if err := reg.SendVerificationEmail(); err != nil { + return fmt.Errorf("发送邮件失败: %v", err) + } + log.Success("%s Verification email sent", prefix) + + log.Log("%s Waiting for OTP code (5s timeout)...", prefix) + otpCode, err := mail.GetVerificationCode(email, 5*time.Second) + if err != nil { + log.Log("%s OTP not received in 5s, waiting 15s more...", prefix) + otpCode, err = mail.GetVerificationCode(email, 15*time.Second) + if err != nil { + return fmt.Errorf("验证码获取超时") + } + } + log.Success("%s Got OTP: %s", prefix, otpCode) + + log.Log("%s Validating OTP...", prefix) + if err := reg.ValidateOTP(otpCode); err != nil { + return fmt.Errorf("OTP验证失败: %v", err) + } + log.Success("%s OTP validated", prefix) + + log.Log("%s Creating account (name=%s, birthdate=%s)...", prefix, name, birthdate) + if err := reg.CreateAccount(name, birthdate); err != nil { + return fmt.Errorf("创建账户失败: %v", err) + } + log.Success("%s Account created", prefix) + + log.Log("%s Getting session token...", prefix) + _ = reg.GetSessionToken() + if reg.AccessToken != "" { + log.Success("%s Got access token: %s...", prefix, truncate(reg.AccessToken, 30)) + } + + return nil +} + +func truncate(s string, maxLen int) string { + if len(s) <= maxLen { + return s + } + return s[:maxLen] +} diff --git a/backend/codex-pool.exe b/backend/codex-pool.exe new file mode 100644 index 0000000..2e03f53 Binary files /dev/null and b/backend/codex-pool.exe differ diff --git a/backend/go.mod b/backend/go.mod new file mode 100644 index 0000000..cefc59e --- /dev/null +++ b/backend/go.mod @@ -0,0 +1,40 @@ +module codex-pool + +go 1.24.1 + +require ( + github.com/andybalholm/brotli v1.2.0 + github.com/bogdanfinn/fhttp v0.6.7 + github.com/bogdanfinn/tls-client v1.13.1 + github.com/chromedp/cdproto v0.0.0-20250724212937-08a3db8b4327 + github.com/chromedp/chromedp v0.14.2 + github.com/go-rod/rod v0.116.2 + github.com/go-rod/stealth v0.4.9 +) + +require ( + github.com/bdandy/go-errors v1.2.2 // indirect + github.com/bdandy/go-socks4 v1.2.3 // indirect + github.com/bogdanfinn/quic-go-utls v1.0.7-utls // indirect + github.com/bogdanfinn/utls v1.7.7-barnius // indirect + github.com/chromedp/sysutil v1.1.0 // indirect + github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2 // indirect + github.com/gobwas/httphead v0.1.0 // indirect + github.com/gobwas/pool v0.2.1 // indirect + github.com/gobwas/ws v1.4.0 // indirect + github.com/klauspost/compress v1.18.2 // indirect + github.com/mattn/go-sqlite3 v1.14.33 // indirect + github.com/orisano/pixelmatch v0.0.0-20230914042517-fa304d1dc785 // indirect + github.com/quic-go/qpack v0.6.0 // indirect + github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5 // indirect + github.com/ysmood/fetchup v0.2.3 // indirect + github.com/ysmood/goob v0.4.0 // indirect + github.com/ysmood/got v0.40.0 // indirect + github.com/ysmood/gson v0.7.3 // indirect + github.com/ysmood/leakless v0.9.0 // indirect + golang.org/x/crypto v0.46.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/text v0.32.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/backend/go.sum b/backend/go.sum new file mode 100644 index 0000000..8d9480a --- /dev/null +++ b/backend/go.sum @@ -0,0 +1,90 @@ +github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= +github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= +github.com/bdandy/go-errors v1.2.2 h1:WdFv/oukjTJCLa79UfkGmwX7ZxONAihKu4V0mLIs11Q= +github.com/bdandy/go-errors v1.2.2/go.mod h1:NkYHl4Fey9oRRdbB1CoC6e84tuqQHiqrOcZpqFEkBxM= +github.com/bdandy/go-socks4 v1.2.3 h1:Q6Y2heY1GRjCtHbmlKfnwrKVU/k81LS8mRGLRlmDlic= +github.com/bdandy/go-socks4 v1.2.3/go.mod h1:98kiVFgpdogR8aIGLWLvjDVZ8XcKPsSI/ypGrO+bqHI= +github.com/bogdanfinn/fhttp v0.6.7 h1:yTDywa9INbRqePBE5gHhpxlMjvAQ0bdX77pvOTPJoPI= +github.com/bogdanfinn/fhttp v0.6.7/go.mod h1:A+EKDzMx2hb4IUbMx4TlkoHnaJEiLl8r/1Ss1Y+5e5M= +github.com/bogdanfinn/quic-go-utls v1.0.7-utls h1:opxU/wt2C6FcD3rkGSOwfpQgfGSFx9eAKYQrFwYBzuo= +github.com/bogdanfinn/quic-go-utls v1.0.7-utls/go.mod h1:bk8QMY2KypO8A6LzHJ7C4+bdB0ksLOd6NZt600wXYe8= +github.com/bogdanfinn/tls-client v1.13.1 h1:O2sfv8JK8R7nNz+Km675VOIajum4sMqOb/ys/4gXfPQ= +github.com/bogdanfinn/tls-client v1.13.1/go.mod h1:4ZnckBKYWaQD9wq55cpUr5/2i45cCBAG+2V3fge+yvQ= +github.com/bogdanfinn/utls v1.7.7-barnius h1:OuJ497cc7F3yKNVHRsYPQdGggmk5x6+V5ZlrCR7fOLU= +github.com/bogdanfinn/utls v1.7.7-barnius/go.mod h1:aAK1VZQlpKZClF1WEQeq6kyclbkPq4hz6xTbB5xSlmg= +github.com/chromedp/cdproto v0.0.0-20250724212937-08a3db8b4327 h1:UQ4AU+BGti3Sy/aLU8KVseYKNALcX9UXY6DfpwQ6J8E= +github.com/chromedp/cdproto v0.0.0-20250724212937-08a3db8b4327/go.mod h1:NItd7aLkcfOA/dcMXvl8p1u+lQqioRMq/SqDp71Pb/k= +github.com/chromedp/chromedp v0.14.2 h1:r3b/WtwM50RsBZHMUm9fsNhhzRStTHrKdr2zmwbZSzM= +github.com/chromedp/chromedp v0.14.2/go.mod h1:rHzAv60xDE7VNy/MYtTUrYreSc0ujt2O1/C3bzctYBo= +github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM= +github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2 h1:iizUGZ9pEquQS5jTGkh4AqeeHCMbfbjeb0zMt0aEFzs= +github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M= +github.com/go-rod/rod v0.113.0/go.mod h1:aiedSEFg5DwG/fnNbUOTPMTTWX3MRj6vIs/a684Mthw= +github.com/go-rod/rod v0.116.2 h1:A5t2Ky2A+5eD/ZJQr1EfsQSe5rms5Xof/qj296e+ZqA= +github.com/go-rod/rod v0.116.2/go.mod h1:H+CMO9SCNc2TJ2WfrG+pKhITz57uGNYU43qYHh438Mg= +github.com/go-rod/stealth v0.4.9 h1:X2PmQk4DUF2wzw6GOsWjW/glb8K5ebnftbEvLh7MlZ4= +github.com/go-rod/stealth v0.4.9/go.mod h1:eAzyvw8c0iAd5nJJsSWeh0fQ5z94vCIfdi1hUmYDimc= +github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= +github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= +github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= +github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs= +github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc= +github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= +github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo= +github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= +github.com/mattn/go-sqlite3 v1.14.33 h1:A5blZ5ulQo2AtayQ9/limgHEkFreKj1Dv226a1K73s0= +github.com/mattn/go-sqlite3 v1.14.33/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/orisano/pixelmatch v0.0.0-20230914042517-fa304d1dc785 h1:J1//5K/6QF10cZ59zLcVNFGmBfiSrH8Cho/lNrViK9s= +github.com/orisano/pixelmatch v0.0.0-20230914042517-fa304d1dc785/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= +github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= +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/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5 h1:YqAladjX7xpA6BM04leXMWAEjS0mTZ5kUU9KRBriQJc= +github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5/go.mod h1:2JjD2zLQYH5HO74y5+aE3remJQvl6q4Sn6aWA2wD1Ng= +github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +github.com/ysmood/fetchup v0.2.3 h1:ulX+SonA0Vma5zUFXtv52Kzip/xe7aj4vqT5AJwQ+ZQ= +github.com/ysmood/fetchup v0.2.3/go.mod h1:xhibcRKziSvol0H1/pj33dnKrYyI2ebIvz5cOOkYGns= +github.com/ysmood/goob v0.4.0 h1:HsxXhyLBeGzWXnqVKtmT9qM7EuVs/XOgkX7T6r1o1AQ= +github.com/ysmood/goob v0.4.0/go.mod h1:u6yx7ZhS4Exf2MwciFr6nIM8knHQIE22lFpWHnfql18= +github.com/ysmood/gop v0.0.2/go.mod h1:rr5z2z27oGEbyB787hpEcx4ab8cCiPnKxn0SUHt6xzk= +github.com/ysmood/gop v0.2.0 h1:+tFrG0TWPxT6p9ZaZs+VY+opCvHU8/3Fk6BaNv6kqKg= +github.com/ysmood/gop v0.2.0/go.mod h1:rr5z2z27oGEbyB787hpEcx4ab8cCiPnKxn0SUHt6xzk= +github.com/ysmood/got v0.34.1/go.mod h1:yddyjq/PmAf08RMLSwDjPyCvHvYed+WjHnQxpH851LM= +github.com/ysmood/got v0.40.0 h1:ZQk1B55zIvS7zflRrkGfPDrPG3d7+JOza1ZkNxcc74Q= +github.com/ysmood/got v0.40.0/go.mod h1:W7DdpuX6skL3NszLmAsC5hT7JAhuLZhByVzHTq874Qg= +github.com/ysmood/gotrace v0.6.0 h1:SyI1d4jclswLhg7SWTL6os3L1WOKeNn/ZtzVQF8QmdY= +github.com/ysmood/gotrace v0.6.0/go.mod h1:TzhIG7nHDry5//eYZDYcTzuJLYQIkykJzCRIo4/dzQM= +github.com/ysmood/gson v0.7.3 h1:QFkWbTH8MxyUTKPkVWAENJhxqdBa4lYTQWqZCiLG6kE= +github.com/ysmood/gson v0.7.3/go.mod h1:3Kzs5zDl21g5F/BlLTNcuAGAYLKt2lV5G8D1zF3RNmg= +github.com/ysmood/leakless v0.8.0/go.mod h1:R8iAXPRaG97QJwqxs74RdwzcRHT1SWCGTNqY8q0JvMQ= +github.com/ysmood/leakless v0.9.0 h1:qxCG5VirSBvmi3uynXFkcnLMzkphdh3xx5FtrORwDCU= +github.com/ysmood/leakless v0.9.0/go.mod h1:R8iAXPRaG97QJwqxs74RdwzcRHT1SWCGTNqY8q0JvMQ= +go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= +go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/net v0.0.0-20211104170005-ce137452f963/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/backend/internal/api/http.go b/backend/internal/api/http.go new file mode 100644 index 0000000..0fccfef --- /dev/null +++ b/backend/internal/api/http.go @@ -0,0 +1,54 @@ +package api + +import ( + "encoding/json" + "net/http" + + "codex-pool/internal/config" +) + +// Result 统一 API 响应 +type Result struct { + Code int `json:"code"` + Message string `json:"message,omitempty"` + Data interface{} `json:"data,omitempty"` +} + +// JSON 发送 JSON 响应 +func JSON(w http.ResponseWriter, code int, data interface{}) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(code) + json.NewEncoder(w).Encode(data) +} + +// Success 发送成功响应 +func Success(w http.ResponseWriter, data interface{}) { + JSON(w, http.StatusOK, Result{Code: 0, Data: data}) +} + +// Error 发送错误响应 +func Error(w http.ResponseWriter, httpCode int, message string) { + JSON(w, httpCode, Result{Code: -1, Message: message}) +} + +// CORS 跨域中间件 +func CORS(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + origin := "*" + if config.Global != nil && config.Global.CorsOrigin != "" { + origin = config.Global.CorsOrigin + } + + 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-Api-Key") + w.Header().Set("Access-Control-Max-Age", "86400") + + if r.Method == "OPTIONS" { + w.WriteHeader(http.StatusNoContent) + return + } + + next(w, r) + } +} diff --git a/backend/internal/api/team_process.go b/backend/internal/api/team_process.go new file mode 100644 index 0000000..0325931 --- /dev/null +++ b/backend/internal/api/team_process.go @@ -0,0 +1,423 @@ +package api + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" + "sync" + "sync/atomic" + "time" + + "codex-pool/internal/auth" + "codex-pool/internal/config" + "codex-pool/internal/invite" + "codex-pool/internal/logger" + "codex-pool/internal/mail" + "codex-pool/internal/register" +) + +// TeamProcessRequest 团队处理请求 +type TeamProcessRequest struct { + // Owner 账号列表 + Owners []struct { + Email string `json:"email"` + Password string `json:"password"` + Token string `json:"token"` + } `json:"owners"` + // 配置 + MembersPerTeam int `json:"members_per_team"` // 每个 Team 的成员数 + ConcurrentTeams int `json:"concurrent_teams"` // 并发 Team 数量 + BrowserType string `json:"browser_type"` // "chromedp" 或 "rod" + Headless bool `json:"headless"` // 是否无头模式 + Proxy string `json:"proxy"` // 代理设置 +} + +// TeamProcessResult 团队处理结果 +type TeamProcessResult struct { + TeamIndex int `json:"team_index"` + OwnerEmail string `json:"owner_email"` + TeamID string `json:"team_id"` + Registered int `json:"registered"` + AddedToS2A int `json:"added_to_s2a"` + MemberEmails []string `json:"member_emails"` + Errors []string `json:"errors"` + DurationMs int64 `json:"duration_ms"` +} + +// TeamProcessState 处理状态 +type TeamProcessState struct { + Running bool `json:"running"` + StartedAt time.Time `json:"started_at"` + TotalTeams int `json:"total_teams"` + Completed int32 `json:"completed"` + Results []TeamProcessResult `json:"results"` + mu sync.Mutex +} + +var teamProcessState = &TeamProcessState{} + +// HandleTeamProcess POST /api/team/process - 启动 Team 批量处理 +func HandleTeamProcess(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + Error(w, http.StatusMethodNotAllowed, "仅支持 POST") + return + } + + // 检查是否正在运行 + if teamProcessState.Running { + Error(w, http.StatusConflict, "已有任务正在运行") + return + } + + var req TeamProcessRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + Error(w, http.StatusBadRequest, "请求格式错误") + return + } + + // 验证参数 + if len(req.Owners) == 0 { + Error(w, http.StatusBadRequest, "请提供至少一个 Owner 账号") + return + } + if req.MembersPerTeam <= 0 { + req.MembersPerTeam = 4 + } + if req.ConcurrentTeams <= 0 { + req.ConcurrentTeams = len(req.Owners) + } + if req.ConcurrentTeams > len(req.Owners) { + req.ConcurrentTeams = len(req.Owners) + } + if req.BrowserType == "" { + req.BrowserType = "chromedp" // 默认使用 Chromedp + } + if req.Proxy == "" && config.Global != nil { + req.Proxy = config.Global.GetProxy() // 使用新的代理获取方法 + } + + // 初始化状态 + teamProcessState.Running = true + teamProcessState.StartedAt = time.Now() + teamProcessState.TotalTeams = len(req.Owners) // 所有 owners 都会处理 + teamProcessState.Completed = 0 + teamProcessState.Results = make([]TeamProcessResult, 0, len(req.Owners)) + + // 异步执行 + go runTeamProcess(req) + + Success(w, map[string]interface{}{ + "message": "任务已启动", + "total_teams": len(req.Owners), + "concurrent_teams": req.ConcurrentTeams, + "started_at": teamProcessState.StartedAt, + }) +} + +// HandleTeamProcessStatus GET /api/team/status - 获取处理状态 +func HandleTeamProcessStatus(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + Error(w, http.StatusMethodNotAllowed, "仅支持 GET") + return + } + + teamProcessState.mu.Lock() + defer teamProcessState.mu.Unlock() + + Success(w, map[string]interface{}{ + "running": teamProcessState.Running, + "started_at": teamProcessState.StartedAt, + "total_teams": teamProcessState.TotalTeams, + "completed": teamProcessState.Completed, + "results": teamProcessState.Results, + "elapsed_ms": time.Since(teamProcessState.StartedAt).Milliseconds(), + }) +} + +// HandleTeamProcessStop POST /api/team/stop - 停止处理 +func HandleTeamProcessStop(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + Error(w, http.StatusMethodNotAllowed, "仅支持 POST") + return + } + + teamProcessState.Running = false + Success(w, map[string]string{"message": "已发送停止信号"}) +} + +// runTeamProcess 执行 Team 批量处理 - 使用工作池模式 +func runTeamProcess(req TeamProcessRequest) { + defer func() { + teamProcessState.Running = false + }() + + totalOwners := len(req.Owners) + workerCount := req.ConcurrentTeams // 同时运行的 worker 数量 + if workerCount > totalOwners { + workerCount = totalOwners + } + if workerCount <= 0 { + workerCount = 2 // 默认 2 个并发 + } + + logger.Info(fmt.Sprintf("Starting Team process: %d owners, %d concurrent workers", totalOwners, workerCount), "", "team") + + // 任务队列 + taskChan := make(chan int, totalOwners) + resultChan := make(chan TeamProcessResult, totalOwners) + + var wg sync.WaitGroup + + // 启动 worker + for w := 0; w < workerCount; w++ { + wg.Add(1) + go func(workerID int) { + defer wg.Done() + for idx := range taskChan { + if !teamProcessState.Running { + return + } + result := processSingleTeam(idx, req) + resultChan <- result + atomic.AddInt32(&teamProcessState.Completed, 1) + } + }(w) + } + + // 发送任务 + go func() { + for i := 0; i < totalOwners; i++ { + if !teamProcessState.Running { + break + } + taskChan <- i + } + close(taskChan) + }() + + // 等待完成并收集结果 + go func() { + wg.Wait() + close(resultChan) + }() + + for result := range resultChan { + teamProcessState.mu.Lock() + teamProcessState.Results = append(teamProcessState.Results, result) + teamProcessState.mu.Unlock() + } + + logger.Success(fmt.Sprintf("Team process complete: %d/%d teams processed", teamProcessState.Completed, totalOwners), "", "team") +} + +// processSingleTeam 处理单个 Team +func processSingleTeam(idx int, req TeamProcessRequest) TeamProcessResult { + startTime := time.Now() + owner := req.Owners[idx] + result := TeamProcessResult{ + TeamIndex: idx + 1, + OwnerEmail: owner.Email, + MemberEmails: make([]string, 0), + Errors: make([]string, 0), + } + + logPrefix := fmt.Sprintf("[Team %d]", idx+1) + logger.Info(fmt.Sprintf("%s Starting with owner: %s", logPrefix, owner.Email), owner.Email, "team") + + // Step 1: 获取 Team ID + inviter := invite.NewWithProxy(owner.Token, req.Proxy) + teamID, err := inviter.GetAccountID() + if err != nil { + result.Errors = append(result.Errors, fmt.Sprintf("获取 Team ID 失败: %v", err)) + result.DurationMs = time.Since(startTime).Milliseconds() + logger.Error(fmt.Sprintf("%s Failed to get Team ID: %v", logPrefix, err), owner.Email, "team") + return result + } + result.TeamID = teamID + logger.Success(fmt.Sprintf("%s Team ID: %s", logPrefix, teamID), owner.Email, "team") + + // Step 2: 生成成员邮箱并发送邀请 + type MemberAccount struct { + Email string + Password string + Success bool + } + children := make([]MemberAccount, req.MembersPerTeam) + for i := 0; i < req.MembersPerTeam; i++ { + children[i].Email = mail.GenerateEmail() + children[i].Password = register.GeneratePassword() + logger.Info(fmt.Sprintf("%s [Member %d] Email: %s", logPrefix, i+1, children[i].Email), children[i].Email, "team") + } + + // 发送邀请 + inviteEmails := make([]string, req.MembersPerTeam) + for i, c := range children { + inviteEmails[i] = c.Email + } + if err := inviter.SendInvites(inviteEmails); err != nil { + result.Errors = append(result.Errors, fmt.Sprintf("发送邀请失败: %v", err)) + result.DurationMs = time.Since(startTime).Milliseconds() + return result + } + logger.Success(fmt.Sprintf("%s Sent %d invite(s)", logPrefix, len(inviteEmails)), owner.Email, "team") + + // Step 3: 并发注册成员 + var memberWg sync.WaitGroup + memberMutex := sync.Mutex{} + + for i := range children { + memberWg.Add(1) + go func(memberIdx int) { + defer memberWg.Done() + + memberMutex.Lock() + email := children[memberIdx].Email + password := children[memberIdx].Password + memberMutex.Unlock() + + name := register.GenerateName() + birthdate := register.GenerateBirthdate() + + // 重试逻辑 + for attempt := 0; attempt < 3; attempt++ { + if !teamProcessState.Running { + return + } + + if attempt > 0 { + email = mail.GenerateEmail() + password = register.GeneratePassword() + logger.Info(fmt.Sprintf("%s [Member %d] Retry %d: %s", logPrefix, memberIdx+1, attempt, email), email, "team") + + if err := inviter.SendInvites([]string{email}); err != nil { + continue + } + } + + _, err := registerWithTimeout(email, password, name, birthdate, req.Proxy) + if err != nil { + if strings.Contains(err.Error(), "验证码") { + continue + } + result.Errors = append(result.Errors, fmt.Sprintf("Member %d: %v", memberIdx+1, err)) + return + } + + memberMutex.Lock() + children[memberIdx].Email = email + children[memberIdx].Password = password + children[memberIdx].Success = true + memberMutex.Unlock() + + logger.Success(fmt.Sprintf("%s [Member %d] Registered", logPrefix, memberIdx+1), email, "team") + return + } + }(i) + } + memberWg.Wait() + + // 统计注册成功数 + registeredChildren := make([]MemberAccount, 0) + for _, c := range children { + if c.Success { + registeredChildren = append(registeredChildren, c) + result.MemberEmails = append(result.MemberEmails, c.Email) + result.Registered++ + } + } + logger.Info(fmt.Sprintf("%s Registered: %d/%d", logPrefix, result.Registered, req.MembersPerTeam), owner.Email, "team") + + // Step 4: S2A 授权入库 + for i, child := range registeredChildren { + if !teamProcessState.Running { + break + } + + s2aResp, err := auth.GenerateS2AAuthURL(config.Global.S2AApiBase, config.Global.S2AAdminKey, config.Global.ProxyID) + if err != nil { + result.Errors = append(result.Errors, fmt.Sprintf("Member %d auth URL: %v", i+1, err)) + continue + } + + // 根据配置选择浏览器自动化 + var code string + if req.BrowserType == "rod" { + code, err = auth.CompleteWithRod(s2aResp.Data.AuthURL, child.Email, child.Password, teamID, req.Headless, req.Proxy) + } else { + code, err = auth.CompleteWithChromedp(s2aResp.Data.AuthURL, child.Email, child.Password, teamID, req.Headless, req.Proxy) + } + if err != nil { + result.Errors = append(result.Errors, fmt.Sprintf("Member %d browser: %v", i+1, err)) + continue + } + + // 提交到 S2A + _, err = auth.SubmitS2AOAuth( + config.Global.S2AApiBase, + config.Global.S2AAdminKey, + s2aResp.Data.SessionID, + code, + child.Email, + config.Global.Concurrency, + config.Global.Priority, + config.Global.GroupIDs, + config.Global.ProxyID, + ) + if err != nil { + result.Errors = append(result.Errors, fmt.Sprintf("Member %d S2A: %v", i+1, err)) + continue + } + + result.AddedToS2A++ + logger.Success(fmt.Sprintf("%s [Member %d] Added to S2A", logPrefix, i+1), child.Email, "team") + } + + result.DurationMs = time.Since(startTime).Milliseconds() + logger.Success(fmt.Sprintf("%s Complete: %d registered, %d in S2A", logPrefix, result.Registered, result.AddedToS2A), owner.Email, "team") + + return result +} + +// registerWithTimeout 带超时的注册 +func registerWithTimeout(email, password, name, birthdate, proxy string) (*register.ChatGPTReg, error) { + reg, err := register.New(proxy) + if err != nil { + return nil, err + } + + if err := reg.InitSession(); err != nil { + return nil, fmt.Errorf("初始化失败: %v", err) + } + if err := reg.GetAuthorizeURL(email); err != nil { + return nil, fmt.Errorf("获取授权URL失败: %v", err) + } + if err := reg.StartAuthorize(); err != nil { + return nil, fmt.Errorf("启动授权失败: %v", err) + } + if err := reg.Register(email, password); err != nil { + return nil, fmt.Errorf("注册失败: %v", err) + } + if err := reg.SendVerificationEmail(); err != nil { + return nil, fmt.Errorf("发送邮件失败: %v", err) + } + + // 短超时获取验证码 + otpCode, err := mail.GetVerificationCode(email, 5*time.Second) + if err != nil { + otpCode, err = mail.GetVerificationCode(email, 15*time.Second) + if err != nil { + return nil, fmt.Errorf("验证码获取超时") + } + } + + if err := reg.ValidateOTP(otpCode); err != nil { + return nil, fmt.Errorf("OTP验证失败: %v", err) + } + if err := reg.CreateAccount(name, birthdate); err != nil { + return nil, fmt.Errorf("创建账户失败: %v", err) + } + + _ = reg.GetSessionToken() + return reg, nil +} diff --git a/backend/internal/auth/chromedp.go b/backend/internal/auth/chromedp.go new file mode 100644 index 0000000..32b3ff8 --- /dev/null +++ b/backend/internal/auth/chromedp.go @@ -0,0 +1,194 @@ +package auth + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/chromedp/cdproto/network" + "github.com/chromedp/chromedp" +) + +// CompleteWithChromedp 使用 chromedp 完成 S2A OAuth 授权 +func CompleteWithChromedp(authURL, email, password, teamID string, headless bool, proxy string) (string, error) { + opts := append(chromedp.DefaultExecAllocatorOptions[:], + chromedp.Flag("headless", headless), + chromedp.Flag("disable-gpu", true), + chromedp.Flag("no-sandbox", true), + chromedp.Flag("disable-dev-shm-usage", true), + chromedp.Flag("disable-blink-features", "AutomationControlled"), + chromedp.UserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36"), + ) + + if proxy != "" { + opts = append(opts, chromedp.ProxyServer(proxy)) + } + + allocCtx, cancel := chromedp.NewExecAllocator(context.Background(), opts...) + defer cancel() + + ctx, cancel := chromedp.NewContext(allocCtx) + defer cancel() + + ctx, cancel = context.WithTimeout(ctx, 120*time.Second) + defer cancel() + + var callbackURL string + + chromedp.ListenTarget(ctx, func(ev interface{}) { + if req, ok := ev.(*network.EventRequestWillBeSent); ok { + url := req.Request.URL + if strings.Contains(url, "localhost") && strings.Contains(url, "code=") { + callbackURL = url + } + } + }) + + err := chromedp.Run(ctx, + network.Enable(), + chromedp.Navigate(authURL), + chromedp.WaitReady("body"), + ) + if err != nil { + return "", fmt.Errorf("访问失败: %v", err) + } + + time.Sleep(2 * time.Second) + + if callbackURL != "" { + return ExtractCodeFromCallbackURL(callbackURL), nil + } + + var currentURL string + _ = chromedp.Run(ctx, chromedp.Location(¤tURL)) + + if strings.Contains(currentURL, "code=") { + return ExtractCodeFromCallbackURL(currentURL), nil + } + + time.Sleep(1 * time.Second) + + emailSelectors := []string{ + `input[name="email"]`, + `input[type="email"]`, + `input[name="username"]`, + } + + var emailFilled bool + for _, sel := range emailSelectors { + err = chromedp.Run(ctx, chromedp.WaitVisible(sel, chromedp.ByQuery)) + if err == nil { + err = chromedp.Run(ctx, + chromedp.Clear(sel, chromedp.ByQuery), + chromedp.SendKeys(sel, email, chromedp.ByQuery), + ) + if err == nil { + emailFilled = true + break + } + } + } + + if !emailFilled { + return "", fmt.Errorf("未找到邮箱输入框") + } + + time.Sleep(300 * time.Millisecond) + + buttonSelectors := []string{ + `button[type="submit"]`, + `button[data-testid="login-button"]`, + `button.continue-btn`, + `input[type="submit"]`, + } + + for _, sel := range buttonSelectors { + err = chromedp.Run(ctx, chromedp.Click(sel, chromedp.ByQuery)) + if err == nil { + break + } + } + + time.Sleep(1500 * time.Millisecond) + + if callbackURL != "" { + return ExtractCodeFromCallbackURL(callbackURL), nil + } + + _ = chromedp.Run(ctx, chromedp.Location(¤tURL)) + if strings.Contains(currentURL, "code=") { + return ExtractCodeFromCallbackURL(currentURL), nil + } + + passwordSelectors := []string{ + `input[name="current-password"]`, + `input[name="password"]`, + `input[type="password"]`, + } + + var passwordFilled bool + for _, sel := range passwordSelectors { + err = chromedp.Run(ctx, chromedp.WaitVisible(sel, chromedp.ByQuery)) + if err == nil { + err = chromedp.Run(ctx, + chromedp.Clear(sel, chromedp.ByQuery), + chromedp.SendKeys(sel, password, chromedp.ByQuery), + ) + if err == nil { + passwordFilled = true + break + } + } + } + + if !passwordFilled { + return "", fmt.Errorf("未找到密码输入框") + } + + time.Sleep(300 * time.Millisecond) + + for _, sel := range buttonSelectors { + err = chromedp.Run(ctx, chromedp.Click(sel, chromedp.ByQuery)) + if err == nil { + break + } + } + + for i := 0; i < 30; i++ { + time.Sleep(500 * time.Millisecond) + + if callbackURL != "" { + return ExtractCodeFromCallbackURL(callbackURL), nil + } + + var url string + if err := chromedp.Run(ctx, chromedp.Location(&url)); err == nil { + if strings.Contains(url, "code=") { + return ExtractCodeFromCallbackURL(url), nil + } + + if strings.Contains(url, "consent") { + for _, sel := range buttonSelectors { + err = chromedp.Run(ctx, chromedp.Click(sel, chromedp.ByQuery)) + if err == nil { + break + } + } + time.Sleep(1 * time.Second) + } + + if strings.Contains(url, "authorize") && teamID != "" { + err = chromedp.Run(ctx, + chromedp.Click(fmt.Sprintf(`[data-workspace-id="%s"], [data-account-id="%s"]`, teamID, teamID), chromedp.ByQuery), + ) + } + } + } + + if callbackURL != "" { + return ExtractCodeFromCallbackURL(callbackURL), nil + } + + return "", fmt.Errorf("授权超时") +} diff --git a/backend/internal/auth/rod.go b/backend/internal/auth/rod.go new file mode 100644 index 0000000..a573281 --- /dev/null +++ b/backend/internal/auth/rod.go @@ -0,0 +1,167 @@ +package auth + +import ( + "fmt" + "strings" + "time" + + "github.com/go-rod/rod" + "github.com/go-rod/rod/lib/launcher" + "github.com/go-rod/rod/lib/proto" + "github.com/go-rod/stealth" +) + +// RodAuth 使用 Rod + Stealth 完成 OAuth 授权 +type RodAuth struct { + browser *rod.Browser + headless bool + proxy string +} + +// NewRodAuth 创建 Rod 授权器 +func NewRodAuth(headless bool, proxy string) (*RodAuth, error) { + l := launcher.New(). + Headless(headless). + Set("disable-blink-features", "AutomationControlled"). + Set("disable-dev-shm-usage"). + Set("no-sandbox"). + Set("disable-gpu"). + Set("disable-extensions"). + Set("disable-background-networking"). + Set("disable-sync"). + Set("disable-translate"). + Set("metrics-recording-only"). + Set("no-first-run") + + if proxy != "" { + l = l.Proxy(proxy) + } + + controlURL, err := l.Launch() + if err != nil { + return nil, fmt.Errorf("启动浏览器失败: %v", err) + } + + browser := rod.New().ControlURL(controlURL) + if err := browser.Connect(); err != nil { + return nil, fmt.Errorf("连接浏览器失败: %v", err) + } + + return &RodAuth{ + browser: browser, + headless: headless, + proxy: proxy, + }, nil +} + +// Close 关闭浏览器 +func (r *RodAuth) Close() { + if r.browser != nil { + r.browser.Close() + } +} + +// CompleteOAuth 完成 OAuth 授权 +func (r *RodAuth) CompleteOAuth(authURL, email, password, teamID string) (string, error) { + page, err := stealth.Page(r.browser) + if err != nil { + return "", fmt.Errorf("创建页面失败: %v", err) + } + defer page.Close() + + page = page.Timeout(45 * time.Second) + + if err := page.Navigate(authURL); err != nil { + return "", fmt.Errorf("访问授权URL失败: %v", err) + } + + page.MustWaitDOMStable() + + if code := r.checkForCode(page); code != "" { + return code, nil + } + + emailInput, err := page.Timeout(5 * time.Second).Element("input[name='email'], input[type='email'], input[name='username']") + if err != nil { + return "", fmt.Errorf("未找到邮箱输入框") + } + + emailInput.MustSelectAllText().MustInput(email) + time.Sleep(200 * time.Millisecond) + + if btn, _ := page.Timeout(2 * time.Second).Element("button[type='submit']"); btn != nil { + btn.MustClick() + } + + time.Sleep(1500 * time.Millisecond) + + if code := r.checkForCode(page); code != "" { + return code, nil + } + + passwordInput, err := page.Timeout(8 * time.Second).Element("input[type='password']") + if err != nil { + return "", fmt.Errorf("未找到密码输入框") + } + + passwordInput.MustSelectAllText().MustInput(password) + time.Sleep(200 * time.Millisecond) + + if btn, _ := page.Timeout(2 * time.Second).Element("button[type='submit']"); btn != nil { + btn.MustClick() + } + + for i := 0; i < 66; i++ { + time.Sleep(300 * time.Millisecond) + + if code := r.checkForCode(page); code != "" { + return code, nil + } + + info, _ := page.Info() + currentURL := info.URL + + if strings.Contains(currentURL, "consent") { + if btn, _ := page.Timeout(500 * time.Millisecond).Element("button[type='submit']"); btn != nil { + btn.Click(proto.InputMouseButtonLeft, 1) + } + } + + if strings.Contains(currentURL, "authorize") && teamID != "" { + wsSelector := fmt.Sprintf("[data-workspace-id='%s'], [data-account-id='%s']", teamID, teamID) + if wsBtn, _ := page.Timeout(500 * time.Millisecond).Element(wsSelector); wsBtn != nil { + wsBtn.Click(proto.InputMouseButtonLeft, 1) + } + } + } + + return "", fmt.Errorf("授权超时") +} + +// checkForCode 检查 URL 中是否包含 code +func (r *RodAuth) checkForCode(page *rod.Page) string { + info, err := page.Info() + if err != nil { + return "" + } + if strings.Contains(info.URL, "code=") { + return ExtractCodeFromCallbackURL(info.URL) + } + return "" +} + +// CompleteWithRod 使用 Rod + Stealth 完成 S2A 授权 +func CompleteWithRod(authURL, email, password, teamID string, headless bool, proxy string) (string, error) { + auth, err := NewRodAuth(headless, proxy) + if err != nil { + return "", err + } + defer auth.Close() + + return auth.CompleteOAuth(authURL, email, password, teamID) +} + +// CompleteWithBrowser 使用 Rod 完成 S2A 授权 (别名) +func CompleteWithBrowser(authURL, email, password, teamID string, headless bool, proxy string) (string, error) { + return CompleteWithRod(authURL, email, password, teamID, headless, proxy) +} diff --git a/backend/internal/auth/s2a.go b/backend/internal/auth/s2a.go new file mode 100644 index 0000000..42d590e --- /dev/null +++ b/backend/internal/auth/s2a.go @@ -0,0 +1,291 @@ +package auth + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" + "time" +) + +const ( + CodexClientID = "app_EMoamEEZ73f0CkXaXp7hrann" + CodexRedirectURI = "http://localhost:1455/auth/callback" + CodexScope = "openid profile email offline_access" +) + +// CodexTokens Codex Token 结构 +type CodexTokens struct { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` + IDToken string `json:"id_token"` + TokenType string `json:"token_type"` + ExpiresIn int `json:"expires_in"` + ExpiredAt string `json:"expired_at,omitempty"` +} + +// S2AAuthURLRequest S2A 授权 URL 请求 +type S2AAuthURLRequest struct { + ProxyID *int `json:"proxy_id,omitempty"` +} + +// S2AAuthURLResponse S2A 授权 URL 响应 +type S2AAuthURLResponse struct { + Code int `json:"code"` + Data struct { + AuthURL string `json:"auth_url"` + SessionID string `json:"session_id"` + } `json:"data"` + Message string `json:"message,omitempty"` +} + +// S2ACreateFromOAuthRequest 提交 OAuth 入库请求 +type S2ACreateFromOAuthRequest struct { + SessionID string `json:"session_id"` + Code string `json:"code"` + Name string `json:"name,omitempty"` + Concurrency int `json:"concurrency,omitempty"` + Priority int `json:"priority,omitempty"` + GroupIDs []int `json:"group_ids,omitempty"` + ProxyID *int `json:"proxy_id,omitempty"` +} + +// S2ACreateFromOAuthResponse 入库响应 +type S2ACreateFromOAuthResponse struct { + Code int `json:"code"` + Data struct { + ID int `json:"id"` + Name string `json:"name"` + Platform string `json:"platform"` + Type string `json:"type"` + Status string `json:"status"` + Concurrency int `json:"concurrency"` + Priority int `json:"priority"` + } `json:"data"` + Message string `json:"message,omitempty"` +} + +// GenerateS2AAuthURL 从 S2A 生成 Codex 授权 URL +func GenerateS2AAuthURL(s2aAPIBase, s2aAdminKey string, proxyID *int) (*S2AAuthURLResponse, error) { + client := &http.Client{Timeout: 15 * time.Second} + + apiURL := s2aAPIBase + "/api/v1/admin/openai/generate-auth-url" + + payload := S2AAuthURLRequest{ProxyID: proxyID} + body, _ := json.Marshal(payload) + + req, _ := http.NewRequest("POST", apiURL, bytes.NewReader(body)) + req.Header.Set("Accept", "application/json") + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-Api-Key", s2aAdminKey) + + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("请求失败: %v", err) + } + defer resp.Body.Close() + + respBody, _ := io.ReadAll(resp.Body) + + if len(respBody) > 0 && respBody[0] == '<' { + return nil, fmt.Errorf("服务器返回 HTML: %s", string(respBody)[:min(100, len(respBody))]) + } + + if resp.StatusCode != 200 { + return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(respBody)[:min(200, len(respBody))]) + } + + var result S2AAuthURLResponse + if err := json.Unmarshal(respBody, &result); err != nil { + return nil, fmt.Errorf("解析响应失败: %v, body: %s", err, string(respBody)[:min(100, len(respBody))]) + } + + if result.Code != 0 { + return nil, fmt.Errorf("S2A 错误: %s", result.Message) + } + + return &result, nil +} + +// SubmitS2AOAuth 提交 OAuth code 到 S2A 入库 +func SubmitS2AOAuth(s2aAPIBase, s2aAdminKey, sessionID, code, name string, concurrency, priority int, groupIDs []int, proxyID *int) (*S2ACreateFromOAuthResponse, error) { + client := &http.Client{Timeout: 30 * time.Second} + + apiURL := s2aAPIBase + "/api/v1/admin/openai/create-from-oauth" + + payload := S2ACreateFromOAuthRequest{ + SessionID: sessionID, + Code: code, + Name: name, + Concurrency: concurrency, + Priority: priority, + GroupIDs: groupIDs, + ProxyID: proxyID, + } + body, _ := json.Marshal(payload) + + req, _ := http.NewRequest("POST", apiURL, bytes.NewReader(body)) + req.Header.Set("Accept", "application/json") + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-Api-Key", s2aAdminKey) + + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("请求失败: %v", err) + } + defer resp.Body.Close() + + respBody, _ := io.ReadAll(resp.Body) + + var result S2ACreateFromOAuthResponse + if err := json.Unmarshal(respBody, &result); err != nil { + return nil, fmt.Errorf("解析响应失败: %v", err) + } + + if result.Code != 0 { + return nil, fmt.Errorf("S2A 入库失败: %s", result.Message) + } + + return &result, nil +} + +// VerifyS2AAccount 验证账号入库状态 +func VerifyS2AAccount(s2aAPIBase, s2aAdminKey, email string) (bool, error) { + client := &http.Client{Timeout: 30 * time.Second} + + apiURL := fmt.Sprintf("%s/api/v1/admin/accounts?page=1&page_size=20&search=%s&timezone=Asia/Shanghai", s2aAPIBase, url.QueryEscape(email)) + + req, _ := http.NewRequest("GET", apiURL, nil) + req.Header.Set("Accept", "application/json") + req.Header.Set("X-Api-Key", s2aAdminKey) + + resp, err := client.Do(req) + if err != nil { + return false, fmt.Errorf("请求失败: %v", err) + } + defer resp.Body.Close() + + respBody, _ := io.ReadAll(resp.Body) + + var result struct { + Code int `json:"code"` + Data struct { + Items []struct { + ID int `json:"id"` + Name string `json:"name"` + Status string `json:"status"` + } `json:"items"` + Total int `json:"total"` + } `json:"data"` + } + + if err := json.Unmarshal(respBody, &result); err != nil { + return false, fmt.Errorf("解析响应失败: %v", err) + } + + if result.Code != 0 || result.Data.Total == 0 { + return false, nil + } + + for _, item := range result.Data.Items { + if item.Status == "active" { + return true, nil + } + } + + return false, nil +} + +// ExtractCodeFromCallbackURL 从回调 URL 中提取 code +func ExtractCodeFromCallbackURL(callbackURL string) string { + parsedURL, err := url.Parse(callbackURL) + if err != nil { + return "" + } + return parsedURL.Query().Get("code") +} + +// RefreshCodexToken 刷新 Codex token +func RefreshCodexToken(refreshToken string, proxyURL string) (*CodexTokens, error) { + client := &http.Client{Timeout: 30 * time.Second} + + if proxyURL != "" { + proxyURLParsed, _ := url.Parse(proxyURL) + client.Transport = &http.Transport{Proxy: http.ProxyURL(proxyURLParsed)} + } + + data := url.Values{ + "client_id": {CodexClientID}, + "grant_type": {"refresh_token"}, + "refresh_token": {refreshToken}, + "scope": {"openid profile email"}, + } + + req, _ := http.NewRequest("POST", "https://auth.openai.com/oauth/token", strings.NewReader(data.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + body, _ := io.ReadAll(resp.Body) + + if resp.StatusCode != 200 { + return nil, fmt.Errorf("刷新 token 失败: %d, %s", resp.StatusCode, string(body)[:min(200, len(body))]) + } + + var tokens CodexTokens + if err := json.Unmarshal(body, &tokens); err != nil { + return nil, err + } + + if tokens.ExpiresIn > 0 { + expiredAt := time.Now().Add(time.Duration(tokens.ExpiresIn) * time.Second) + tokens.ExpiredAt = expiredAt.Format(time.RFC3339) + } + + return &tokens, nil +} + +// ExtractWorkspaceFromCookie 从 cookie 提取 workspace_id +func ExtractWorkspaceFromCookie(cookieValue string) string { + parts := strings.Split(cookieValue, ".") + if len(parts) < 1 { + return "" + } + + payload := parts[0] + if m := len(payload) % 4; m != 0 { + payload += strings.Repeat("=", 4-m) + } + + decoded, err := base64.StdEncoding.DecodeString(payload) + if err != nil { + decoded, err = base64.RawURLEncoding.DecodeString(parts[0]) + if err != nil { + return "" + } + } + + var data struct { + Workspaces []struct { + ID string `json:"id"` + } `json:"workspaces"` + } + + if err := json.Unmarshal(decoded, &data); err != nil { + return "" + } + + if len(data.Workspaces) > 0 { + return data.Workspaces[0].ID + } + + return "" +} diff --git a/backend/internal/client/tls.go b/backend/internal/client/tls.go new file mode 100644 index 0000000..88139dd --- /dev/null +++ b/backend/internal/client/tls.go @@ -0,0 +1,240 @@ +package client + +import ( + "bytes" + "compress/gzip" + "io" + "math/rand" + "net/http" + "net/url" + "strings" + + "github.com/andybalholm/brotli" + http2 "github.com/bogdanfinn/fhttp" + tls_client "github.com/bogdanfinn/tls-client" + "github.com/bogdanfinn/tls-client/profiles" +) + +// TLSClient 使用 tls-client 模拟浏览器指纹的 HTTP 客户端 +type TLSClient struct { + client tls_client.HttpClient + userAgent string + chromeVer string + acceptLang string +} + +// 语言偏好池 +var languagePrefs = []string{ + "en-US,en;q=0.9", + "en-GB,en;q=0.9,en-US;q=0.8", + "de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7", +} + +// New 创建一个新的 TLS 客户端 +func New(proxyStr string) (*TLSClient, error) { + jar := tls_client.NewCookieJar() + chromeVer := "133" + + options := []tls_client.HttpClientOption{ + tls_client.WithTimeoutSeconds(60), + tls_client.WithClientProfile(profiles.Chrome_133), + tls_client.WithRandomTLSExtensionOrder(), + tls_client.WithCookieJar(jar), + tls_client.WithInsecureSkipVerify(), + } + + if proxyStr != "" { + options = append(options, tls_client.WithProxyUrl(proxyStr)) + } + + client, err := tls_client.NewHttpClient(tls_client.NewNoopLogger(), options...) + if err != nil { + return nil, err + } + + acceptLang := languagePrefs[rand.Intn(len(languagePrefs))] + userAgent := generateUserAgent(chromeVer) + + return &TLSClient{ + client: client, + userAgent: userAgent, + chromeVer: chromeVer, + acceptLang: acceptLang, + }, nil +} + +// generateUserAgent 生成随机化的 User-Agent +func generateUserAgent(chromeVer string) string { + winVersions := []string{ + "Windows NT 10.0; Win64; x64", + "Windows NT 10.0; WOW64", + } + winVer := winVersions[rand.Intn(len(winVersions))] + + return "Mozilla/5.0 (" + winVer + ") AppleWebKit/537.36 (KHTML, like Gecko) Chrome/" + chromeVer + ".0.0.0 Safari/537.36" +} + +// getDefaultHeaders 获取默认请求头 +func (c *TLSClient) getDefaultHeaders() map[string]string { + secChUa := `"Chromium";v="` + c.chromeVer + `", "Not(A:Brand";v="99", "Google Chrome";v="` + c.chromeVer + `"` + + return map[string]string{ + "User-Agent": c.userAgent, + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", + "Accept-Language": c.acceptLang, + "Accept-Encoding": "gzip, deflate, br, zstd", + "Cache-Control": "max-age=0", + "Sec-Ch-Ua": secChUa, + "Sec-Ch-Ua-Mobile": "?0", + "Sec-Ch-Ua-Platform": `"Windows"`, + "Sec-Fetch-Dest": "document", + "Sec-Fetch-Mode": "navigate", + "Sec-Fetch-Site": "none", + "Sec-Fetch-User": "?1", + "Upgrade-Insecure-Requests": "1", + } +} + +// Do 执行 HTTP 请求 +func (c *TLSClient) Do(req *http.Request) (*http.Response, error) { + fhttpReq, err := http2.NewRequest(req.Method, req.URL.String(), req.Body) + if err != nil { + return nil, err + } + + for key, value := range c.getDefaultHeaders() { + if req.Header.Get(key) == "" { + fhttpReq.Header.Set(key, value) + } + } + + for key, values := range req.Header { + for _, value := range values { + fhttpReq.Header.Set(key, value) + } + } + + resp, err := c.client.Do(fhttpReq) + if err != nil { + return nil, err + } + + finalReq := req + if resp.Request != nil && resp.Request.URL != nil { + finalReq = &http.Request{ + Method: resp.Request.Method, + URL: (*url.URL)(resp.Request.URL), + Header: http.Header(resp.Request.Header), + } + } + + stdResp := &http.Response{ + Status: resp.Status, + StatusCode: resp.StatusCode, + Proto: resp.Proto, + ProtoMajor: resp.ProtoMajor, + ProtoMinor: resp.ProtoMinor, + Header: http.Header(resp.Header), + Body: resp.Body, + ContentLength: resp.ContentLength, + TransferEncoding: resp.TransferEncoding, + Close: resp.Close, + Uncompressed: resp.Uncompressed, + Request: finalReq, + } + + return stdResp, nil +} + +// Get 执行 GET 请求 +func (c *TLSClient) Get(urlStr string) (*http.Response, error) { + req, err := http.NewRequest("GET", urlStr, nil) + if err != nil { + return nil, err + } + return c.Do(req) +} + +// Post 执行 POST 请求 +func (c *TLSClient) Post(urlStr string, contentType string, body io.Reader) (*http.Response, error) { + req, err := http.NewRequest("POST", urlStr, body) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", contentType) + return c.Do(req) +} + +// PostForm 执行 POST 表单请求 +func (c *TLSClient) PostForm(urlStr string, data url.Values) (*http.Response, error) { + return c.Post(urlStr, "application/x-www-form-urlencoded", strings.NewReader(data.Encode())) +} + +// PostJSON 执行 POST JSON 请求 +func (c *TLSClient) PostJSON(urlStr string, body io.Reader) (*http.Response, error) { + return c.Post(urlStr, "application/json", body) +} + +// GetCookie 获取指定 URL 的 Cookie +func (c *TLSClient) GetCookie(urlStr string, name string) string { + u, err := url.Parse(urlStr) + if err != nil { + return "" + } + cookies := c.client.GetCookies(u) + for _, cookie := range cookies { + if cookie.Name == name { + return cookie.Value + } + } + return "" +} + +// SetCookie 设置 Cookie +func (c *TLSClient) SetCookie(urlStr string, cookie *http.Cookie) { + u, err := url.Parse(urlStr) + if err != nil { + return + } + c.client.SetCookies(u, []*http2.Cookie{ + { + Name: cookie.Name, + Value: cookie.Value, + Path: cookie.Path, + Domain: cookie.Domain, + }, + }) +} + +// ReadBody 读取响应体并自动处理压缩 +func ReadBody(resp *http.Response) ([]byte, error) { + defer resp.Body.Close() + + data, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + switch resp.Header.Get("Content-Encoding") { + case "gzip": + gzReader, err := gzip.NewReader(bytes.NewReader(data)) + if err != nil { + return data, nil + } + defer gzReader.Close() + return io.ReadAll(gzReader) + case "br": + return io.ReadAll(brotli.NewReader(bytes.NewReader(data))) + } + + return data, nil +} + +// ReadBodyString 读取响应体为字符串 +func ReadBodyString(resp *http.Response) (string, error) { + body, err := ReadBody(resp) + if err != nil { + return "", err + } + return string(body), nil +} diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go new file mode 100644 index 0000000..14b08c6 --- /dev/null +++ b/backend/internal/config/config.go @@ -0,0 +1,264 @@ +package config + +import ( + "encoding/json" + "os" + "strconv" + "strings" + "sync" +) + +// MailServiceConfig 邮箱服务配置 +type MailServiceConfig struct { + Name string `yaml:"name" json:"name"` + APIBase string `yaml:"api_base" json:"api_base"` + APIToken string `yaml:"api_token" json:"api_token"` + Domain string `yaml:"domain" json:"domain"` + EmailPath string `yaml:"email_path,omitempty" json:"email_path,omitempty"` + AddUserAPI string `yaml:"add_user_api,omitempty" json:"add_user_api,omitempty"` +} + +// Config 应用配置 (实时从数据库读取) +type Config struct { + // 服务器配置 (启动时固定) + Port int `json:"port"` + CorsOrigin string `json:"cors_origin"` + + // S2A 配置 (可实时更新) + S2AApiBase string `json:"s2a_api_base"` + S2AAdminKey string `json:"s2a_admin_key"` + + // 入库配置 (可实时更新) + Concurrency int `json:"concurrency"` + Priority int `json:"priority"` + GroupIDs []int `json:"group_ids"` + ProxyID *int `json:"proxy_id"` + + // 代理配置 (可实时更新) + ProxyEnabled bool `json:"proxy_enabled"` + DefaultProxy string `json:"default_proxy"` + + // 自动化配置 + AutoPauseOnExpired bool `json:"auto_pause_on_expired"` + AccountsPath string `json:"accounts_path"` + + // 邮箱服务 + MailServices []MailServiceConfig `json:"mail_services"` +} + +// GetProxy 获取代理地址(如果启用) +func (c *Config) GetProxy() string { + if c.ProxyEnabled && c.DefaultProxy != "" { + return c.DefaultProxy + } + return "" +} + +// Account 账号结构 (保持 JSON 格式用于账号文件) +type Account struct { + Email string `json:"email,omitempty"` + Account string `json:"account,omitempty"` + Password string `json:"password"` + Name string `json:"name,omitempty"` + AccessToken string `json:"access_token,omitempty"` + Token string `json:"token,omitempty"` + RefreshToken string `json:"refresh_token,omitempty"` + CreatedAt string `json:"created_at,omitempty"` + Pooled bool `json:"pooled,omitempty"` + PooledAt string `json:"pooled_at,omitempty"` + PoolID int `json:"pool_id,omitempty"` + Used bool `json:"used,omitempty"` + UsedAt string `json:"used_at,omitempty"` +} + +// GetEmail 获取邮箱 +func (a *Account) GetEmail() string { + if a.Email != "" { + return a.Email + } + return a.Account +} + +// GetAccessToken 获取 Token +func (a *Account) GetAccessToken() string { + if a.AccessToken != "" { + return a.AccessToken + } + return a.Token +} + +// PoolingConfig 入库任务配置 +type PoolingConfig struct { + Concurrency int `json:"concurrency"` + SerialAuthorize bool `json:"serial_authorize"` + BrowserType string `json:"browser_type"` + Headless bool `json:"headless"` + Proxy string `json:"proxy"` +} + +// 全局配置实例 +var ( + Global *Config + configMu sync.RWMutex +) + +// ConfigDB 配置数据库接口 +type ConfigDB interface { + GetConfig(key string) (string, error) + SetConfig(key, value string) error + GetAllConfig() (map[string]string, error) +} + +var configDB ConfigDB + +// SetConfigDB 设置配置数据库 +func SetConfigDB(db ConfigDB) { + configDB = db +} + +// InitFromDB 从数据库初始化配置 +func InitFromDB() *Config { + configMu.Lock() + defer configMu.Unlock() + + cfg := &Config{ + Port: 8848, + CorsOrigin: "*", + Concurrency: 2, + Priority: 0, + } + + if configDB == nil { + Global = cfg + return cfg + } + + // 从数据库加载配置 + if v, _ := configDB.GetConfig("s2a_api_base"); v != "" { + cfg.S2AApiBase = v + } + if v, _ := configDB.GetConfig("s2a_admin_key"); v != "" { + cfg.S2AAdminKey = v + } + if v, _ := configDB.GetConfig("concurrency"); v != "" { + if n, err := strconv.Atoi(v); err == nil { + cfg.Concurrency = n + } + } + if v, _ := configDB.GetConfig("priority"); v != "" { + if n, err := strconv.Atoi(v); err == nil { + cfg.Priority = n + } + } + if v, _ := configDB.GetConfig("group_ids"); v != "" { + var ids []int + for _, s := range strings.Split(v, ",") { + if n, err := strconv.Atoi(strings.TrimSpace(s)); err == nil { + ids = append(ids, n) + } + } + cfg.GroupIDs = ids + } + if v, _ := configDB.GetConfig("proxy_enabled"); v == "true" { + cfg.ProxyEnabled = true + } + if v, _ := configDB.GetConfig("default_proxy"); v != "" { + cfg.DefaultProxy = v + } + if v, _ := configDB.GetConfig("mail_services"); v != "" { + var services []MailServiceConfig + if err := json.Unmarshal([]byte(v), &services); err == nil { + cfg.MailServices = services + } + } + + Global = cfg + return cfg +} + +// SaveToDB 保存配置到数据库 +func SaveToDB() error { + if configDB == nil || Global == nil { + return nil + } + + configMu.RLock() + cfg := Global + configMu.RUnlock() + + configDB.SetConfig("s2a_api_base", cfg.S2AApiBase) + configDB.SetConfig("s2a_admin_key", cfg.S2AAdminKey) + configDB.SetConfig("concurrency", strconv.Itoa(cfg.Concurrency)) + configDB.SetConfig("priority", strconv.Itoa(cfg.Priority)) + + if len(cfg.GroupIDs) > 0 { + var ids []string + for _, id := range cfg.GroupIDs { + ids = append(ids, strconv.Itoa(id)) + } + configDB.SetConfig("group_ids", strings.Join(ids, ",")) + } + + configDB.SetConfig("proxy_enabled", strconv.FormatBool(cfg.ProxyEnabled)) + configDB.SetConfig("default_proxy", cfg.DefaultProxy) + + if len(cfg.MailServices) > 0 { + data, _ := json.Marshal(cfg.MailServices) + configDB.SetConfig("mail_services", string(data)) + } + + return nil +} + +// Update 更新配置 (实时生效) +func Update(cfg *Config) error { + configMu.Lock() + Global = cfg + configMu.Unlock() + return SaveToDB() +} + +// Get 获取当前配置 +func Get() *Config { + configMu.RLock() + defer configMu.RUnlock() + return Global +} + +// FindPath 查找配置文件路径 (兼容) +func FindPath() string { + if envPath := os.Getenv("CONFIG_PATH"); envPath != "" { + return envPath + } + return "data/config.yaml" +} + +// Load 加载配置 (兼容旧代码,现在直接从数据库加载) +func Load(path string) (*Config, error) { + return InitFromDB(), nil +} + +// LoadAccounts 加载账号列表 (保持 JSON 格式) +func LoadAccounts(path string) ([]Account, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + var accounts []Account + if err := json.Unmarshal(data, &accounts); err != nil { + return nil, err + } + + return accounts, nil +} + +// SaveAccounts 保存账号列表 (保持 JSON 格式) +func SaveAccounts(path string, accounts []Account) error { + data, err := json.MarshalIndent(accounts, "", " ") + if err != nil { + return err + } + + return os.WriteFile(path, data, 0644) +} diff --git a/backend/internal/database/sqlite.go b/backend/internal/database/sqlite.go new file mode 100644 index 0000000..c375fa4 --- /dev/null +++ b/backend/internal/database/sqlite.go @@ -0,0 +1,272 @@ +package database + +import ( + "database/sql" + "fmt" + "time" + + _ "github.com/mattn/go-sqlite3" +) + +// TeamOwner 账号结构 +type TeamOwner struct { + ID int64 `json:"id"` + Email string `json:"email"` + Password string `json:"password,omitempty"` + Token string `json:"token,omitempty"` + AccountID string `json:"account_id"` + Status string `json:"status"` + CreatedAt time.Time `json:"created_at"` +} + +// DB 数据库管理器 +type DB struct { + db *sql.DB +} + +// 全局数据库实例 +var Instance *DB + +// Init 初始化数据库 +func Init(dbPath string) error { + db, err := sql.Open("sqlite3", dbPath) + if err != nil { + return fmt.Errorf("打开数据库失败: %w", err) + } + + Instance = &DB{db: db} + + if err := Instance.createTables(); err != nil { + return fmt.Errorf("创建表失败: %w", err) + } + + fmt.Printf("[数据库] SQLite 已连接: %s\n", dbPath) + return nil +} + +// createTables 创建表 +func (d *DB) createTables() error { + _, err := d.db.Exec(` + CREATE TABLE IF NOT EXISTS team_owners ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + email TEXT NOT NULL UNIQUE, + password TEXT, + token TEXT, + account_id TEXT NOT NULL, + status TEXT DEFAULT 'valid', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ); + + CREATE INDEX IF NOT EXISTS idx_team_owners_email ON team_owners(email); + CREATE INDEX IF NOT EXISTS idx_team_owners_status ON team_owners(status); + CREATE INDEX IF NOT EXISTS idx_team_owners_account_id ON team_owners(account_id); + + -- 配置表 (key-value 形式) + CREATE TABLE IF NOT EXISTS app_config ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + ); + `) + return err +} + +// GetConfig 获取配置值 +func (d *DB) GetConfig(key string) (string, error) { + var value string + err := d.db.QueryRow("SELECT value FROM app_config WHERE key = ?", key).Scan(&value) + if err == sql.ErrNoRows { + return "", nil + } + return value, err +} + +// SetConfig 设置配置值 +func (d *DB) SetConfig(key, value string) error { + _, err := d.db.Exec(` + INSERT INTO app_config (key, value, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP) + ON CONFLICT(key) DO UPDATE SET value = ?, updated_at = CURRENT_TIMESTAMP + `, key, value, value) + return err +} + +// GetAllConfig 获取所有配置 +func (d *DB) GetAllConfig() (map[string]string, error) { + rows, err := d.db.Query("SELECT key, value FROM app_config") + if err != nil { + return nil, err + } + defer rows.Close() + + result := make(map[string]string) + for rows.Next() { + var key, value string + if err := rows.Scan(&key, &value); err != nil { + continue + } + result[key] = value + } + return result, nil +} + +// AddTeamOwner 添加 Team Owner +func (d *DB) AddTeamOwner(owner TeamOwner) (int64, error) { + result, err := d.db.Exec(` + INSERT OR REPLACE INTO team_owners (email, password, token, account_id, status, created_at) + VALUES (?, ?, ?, ?, 'valid', CURRENT_TIMESTAMP) + `, owner.Email, owner.Password, owner.Token, owner.AccountID) + if err != nil { + return 0, err + } + return result.LastInsertId() +} + +// AddTeamOwners 批量添加 +func (d *DB) AddTeamOwners(owners []TeamOwner) (int, error) { + tx, err := d.db.Begin() + if err != nil { + return 0, err + } + defer tx.Rollback() + + stmt, err := tx.Prepare(` + INSERT OR REPLACE INTO team_owners (email, password, token, account_id, status, created_at) + VALUES (?, ?, ?, ?, 'valid', CURRENT_TIMESTAMP) + `) + if err != nil { + return 0, err + } + defer stmt.Close() + + count := 0 + for _, owner := range owners { + _, err := stmt.Exec(owner.Email, owner.Password, owner.Token, owner.AccountID) + if err != nil { + fmt.Printf("[数据库] 插入失败 %s: %v\n", owner.Email, err) + continue + } + count++ + } + + if err := tx.Commit(); err != nil { + return 0, err + } + + return count, nil +} + +// GetTeamOwners 获取列表 +func (d *DB) GetTeamOwners(status string, limit, offset int) ([]TeamOwner, int, error) { + query := "SELECT id, email, password, token, account_id, status, created_at FROM team_owners WHERE 1=1" + countQuery := "SELECT COUNT(*) FROM team_owners WHERE 1=1" + args := []interface{}{} + + if status != "" { + query += " AND status = ?" + countQuery += " AND status = ?" + args = append(args, status) + } + + var total int + err := d.db.QueryRow(countQuery, args...).Scan(&total) + if err != nil { + return nil, 0, err + } + + query += " ORDER BY created_at DESC LIMIT ? OFFSET ?" + args = append(args, limit, offset) + + rows, err := d.db.Query(query, args...) + if err != nil { + return nil, 0, err + } + defer rows.Close() + + var owners []TeamOwner + for rows.Next() { + var owner TeamOwner + err := rows.Scan(&owner.ID, &owner.Email, &owner.Password, &owner.Token, &owner.AccountID, &owner.Status, &owner.CreatedAt) + if err != nil { + continue + } + owners = append(owners, owner) + } + + return owners, total, nil +} + +// GetPendingOwners 获取待处理 +func (d *DB) GetPendingOwners() ([]TeamOwner, error) { + rows, err := d.db.Query(` + SELECT id, email, password, token, account_id, status, created_at + FROM team_owners WHERE status = 'valid' + ORDER BY created_at ASC + `) + if err != nil { + return nil, err + } + defer rows.Close() + + var owners []TeamOwner + for rows.Next() { + var owner TeamOwner + err := rows.Scan(&owner.ID, &owner.Email, &owner.Password, &owner.Token, &owner.AccountID, &owner.Status, &owner.CreatedAt) + if err != nil { + continue + } + owners = append(owners, owner) + } + return owners, nil +} + +// UpdateOwnerStatus 更新状态 +func (d *DB) UpdateOwnerStatus(id int64, status string) error { + _, err := d.db.Exec("UPDATE team_owners SET status = ? WHERE id = ?", status, id) + return err +} + +// DeleteTeamOwner 删除 +func (d *DB) DeleteTeamOwner(id int64) error { + _, err := d.db.Exec("DELETE FROM team_owners WHERE id = ?", id) + return err +} + +// ClearTeamOwners 清空 +func (d *DB) ClearTeamOwners() error { + _, err := d.db.Exec("DELETE FROM team_owners") + return err +} + +// GetOwnerStats 获取统计 +func (d *DB) GetOwnerStats() map[string]int { + stats := map[string]int{ + "total": 0, + "valid": 0, + "registered": 0, + "pooled": 0, + } + + var count int + if err := d.db.QueryRow("SELECT COUNT(*) FROM team_owners").Scan(&count); err == nil { + stats["total"] = count + } + if err := d.db.QueryRow("SELECT COUNT(*) FROM team_owners WHERE status = 'valid'").Scan(&count); err == nil { + stats["valid"] = count + } + if err := d.db.QueryRow("SELECT COUNT(*) FROM team_owners WHERE status = 'registered'").Scan(&count); err == nil { + stats["registered"] = count + } + if err := d.db.QueryRow("SELECT COUNT(*) FROM team_owners WHERE status = 'pooled'").Scan(&count); err == nil { + stats["pooled"] = count + } + + return stats +} + +// Close 关闭数据库 +func (d *DB) Close() error { + if d.db != nil { + return d.db.Close() + } + return nil +} diff --git a/backend/internal/invite/team.go b/backend/internal/invite/team.go new file mode 100644 index 0000000..103d335 --- /dev/null +++ b/backend/internal/invite/team.go @@ -0,0 +1,190 @@ +package invite + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + + "codex-pool/internal/client" +) + +// DefaultProxy 默认代理 +const DefaultProxy = "http://127.0.0.1:7890" + +// TeamInviter Team 邀请器 +type TeamInviter struct { + client *client.TLSClient + accessToken string + accountID string +} + +// InviteRequest 邀请请求 +type InviteRequest struct { + EmailAddresses []string `json:"email_addresses"` + Role string `json:"role"` + ResendEmails bool `json:"resend_emails"` +} + +// AccountCheckResponse 账号检查响应 +type AccountCheckResponse struct { + Accounts map[string]struct { + Account struct { + PlanType string `json:"plan_type"` + } `json:"account"` + } `json:"accounts"` +} + +// New 创建邀请器 (使用默认代理) +func New(accessToken string) *TeamInviter { + c, _ := client.New(DefaultProxy) + return &TeamInviter{ + client: c, + accessToken: accessToken, + } +} + +// NewWithProxy 创建邀请器 (指定代理) +func NewWithProxy(accessToken, proxy string) *TeamInviter { + c, _ := client.New(proxy) + return &TeamInviter{ + client: c, + accessToken: accessToken, + } +} + +// GetAccountID 获取 Team 的 account_id (workspace_id) +func (t *TeamInviter) GetAccountID() (string, error) { + req, _ := http.NewRequest("GET", "https://chatgpt.com/backend-api/accounts/check/v4-2023-04-27", nil) + req.Header.Set("Authorization", "Bearer "+t.accessToken) + req.Header.Set("Accept", "application/json") + + resp, err := t.client.Do(req) + if err != nil { + return "", fmt.Errorf("请求失败: %v", err) + } + defer resp.Body.Close() + + body, _ := io.ReadAll(resp.Body) + + if resp.StatusCode != 200 { + return "", fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body)[:min(200, len(body))]) + } + + var result AccountCheckResponse + if err := json.Unmarshal(body, &result); err != nil { + return "", fmt.Errorf("解析失败: %v", err) + } + + // 查找 team plan 的 account_id + for accountID, info := range result.Accounts { + if accountID != "default" && info.Account.PlanType == "team" { + t.accountID = accountID + return accountID, nil + } + } + + // 如果没找到 team,返回第一个非 default 的 + for accountID := range result.Accounts { + if accountID != "default" { + t.accountID = accountID + return accountID, nil + } + } + + return "", fmt.Errorf("未找到 account_id") +} + +// SendInvites 发送邀请 +func (t *TeamInviter) SendInvites(emails []string) error { + if t.accountID == "" { + return fmt.Errorf("未设置 account_id,请先调用 GetAccountID()") + } + + url := fmt.Sprintf("https://chatgpt.com/backend-api/accounts/%s/invites", t.accountID) + + payload := InviteRequest{ + EmailAddresses: emails, + Role: "standard-user", + ResendEmails: true, + } + body, _ := json.Marshal(payload) + + req, _ := http.NewRequest("POST", url, bytes.NewReader(body)) + req.Header.Set("Authorization", "Bearer "+t.accessToken) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Chatgpt-Account-Id", t.accountID) + + resp, err := t.client.Do(req) + if err != nil { + return fmt.Errorf("请求失败: %v", err) + } + defer resp.Body.Close() + + respBody, _ := io.ReadAll(resp.Body) + + if resp.StatusCode != 200 && resp.StatusCode != 201 { + return fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(respBody)[:min(200, len(respBody))]) + } + + return nil +} + +// GetPendingInvites 获取待处理的邀请列表 +func (t *TeamInviter) GetPendingInvites() ([]string, error) { + if t.accountID == "" { + return nil, fmt.Errorf("未设置 account_id") + } + + url := fmt.Sprintf("https://chatgpt.com/backend-api/accounts/%s/invites?offset=0&limit=100&query=", t.accountID) + + req, _ := http.NewRequest("GET", url, nil) + req.Header.Set("Authorization", "Bearer "+t.accessToken) + + resp, err := t.client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + body, _ := io.ReadAll(resp.Body) + + var result struct { + Invites []struct { + Email string `json:"email"` + } `json:"invites"` + } + + if err := json.Unmarshal(body, &result); err != nil { + return nil, err + } + + var emails []string + for _, inv := range result.Invites { + emails = append(emails, inv.Email) + } + + return emails, nil +} + +// AcceptInvite 接受邀请 (使用被邀请账号的 token) +func AcceptInvite(inviteLink string, accessToken string) error { + c, _ := client.New(DefaultProxy) + + req, _ := http.NewRequest("GET", inviteLink, nil) + req.Header.Set("Authorization", "Bearer "+accessToken) + + resp, err := c.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != 200 && resp.StatusCode != 302 { + body, _ := io.ReadAll(resp.Body) + return fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body)[:min(100, len(body))]) + } + + return nil +} diff --git a/backend/internal/logger/logger.go b/backend/internal/logger/logger.go new file mode 100644 index 0000000..c76fb18 --- /dev/null +++ b/backend/internal/logger/logger.go @@ -0,0 +1,140 @@ +package logger + +import ( + "fmt" + "sync" + "time" +) + +// LogEntry 日志条目 +type LogEntry struct { + Timestamp time.Time `json:"timestamp"` + Level string `json:"level"` + Message string `json:"message"` + Email string `json:"email,omitempty"` + Module string `json:"module,omitempty"` +} + +// 日志存储 +var ( + logs = make([]LogEntry, 0, 1000) + logsMu sync.RWMutex + listeners = make(map[string]chan LogEntry) + listMu sync.RWMutex +) + +// AddListener 添加日志监听器 +func AddListener(id string) chan LogEntry { + listMu.Lock() + defer listMu.Unlock() + ch := make(chan LogEntry, 100) + listeners[id] = ch + return ch +} + +// RemoveListener 移除日志监听器 +func RemoveListener(id string) { + listMu.Lock() + defer listMu.Unlock() + if ch, ok := listeners[id]; ok { + close(ch) + delete(listeners, id) + } +} + +// broadcast 广播日志 +func broadcast(entry LogEntry) { + listMu.RLock() + defer listMu.RUnlock() + for _, ch := range listeners { + select { + case ch <- entry: + default: + } + } +} + +// log 记录日志 +func log(level, message, email, module string) { + entry := LogEntry{ + Timestamp: time.Now(), + Level: level, + Message: message, + Email: email, + Module: module, + } + + logsMu.Lock() + if len(logs) >= 1000 { + logs = logs[100:] + } + logs = append(logs, entry) + logsMu.Unlock() + + broadcast(entry) + + // 打印到控制台 + prefix := "" + switch level { + case "info": + prefix = "[INFO]" + case "success": + prefix = "[SUCCESS]" + case "error": + prefix = "[ERROR]" + case "warning": + prefix = "[WARN]" + } + + if email != "" { + fmt.Printf("%s [%s] %s - %s\n", prefix, module, email, message) + } else { + fmt.Printf("%s [%s] %s\n", prefix, module, message) + } +} + +// Info 记录信息日志 +func Info(message, email, module string) { + log("info", message, email, module) +} + +// Success 记录成功日志 +func Success(message, email, module string) { + log("success", message, email, module) +} + +// Error 记录错误日志 +func Error(message, email, module string) { + log("error", message, email, module) +} + +// Warning 记录警告日志 +func Warning(message, email, module string) { + log("warning", message, email, module) +} + +// GetLogs 获取日志 +func GetLogs(limit int) []LogEntry { + logsMu.RLock() + defer logsMu.RUnlock() + + if limit <= 0 || limit > len(logs) { + limit = len(logs) + } + + start := len(logs) - limit + if start < 0 { + start = 0 + } + + result := make([]LogEntry, limit) + copy(result, logs[start:]) + return result +} + +// ClearLogs 清空日志 +func ClearLogs() { + logsMu.Lock() + defer logsMu.Unlock() + logs = make([]LogEntry, 0, 1000) +} diff --git a/backend/internal/mail/service.go b/backend/internal/mail/service.go new file mode 100644 index 0000000..cb59932 --- /dev/null +++ b/backend/internal/mail/service.go @@ -0,0 +1,455 @@ +package mail + +import ( + "bytes" + "encoding/json" + "fmt" + "math/rand" + "net/http" + "regexp" + "strings" + "sync" + "time" + + "codex-pool/internal/config" +) + +// 默认邮箱配置 +var defaultMailServices = []config.MailServiceConfig{ + { + Name: "esyteam", + APIBase: "https://mail.esyteam.edu.kg", + APIToken: "005d6f3e-5312-4c37-8125-e1f71243e1ba", + Domain: "esyteam.edu.kg", + EmailPath: "/api/public/emailList", + AddUserAPI: "/api/public/addUser", + }, +} + +// 全局变量 +var ( + currentMailServices []config.MailServiceConfig + mailServicesMutex sync.RWMutex + currentServiceIndex int +) + +func init() { + currentMailServices = defaultMailServices +} + +// Init 初始化邮箱服务配置 +func Init(services []config.MailServiceConfig) { + mailServicesMutex.Lock() + defer mailServicesMutex.Unlock() + + if len(services) > 0 { + for i := range services { + if services[i].EmailPath == "" { + services[i].EmailPath = "/api/public/emailList" + } + if services[i].AddUserAPI == "" { + services[i].AddUserAPI = "/api/public/addUser" + } + if services[i].Name == "" { + services[i].Name = fmt.Sprintf("mail-service-%d", i+1) + } + } + currentMailServices = services + fmt.Printf("[邮箱] 已加载 %d 个邮箱服务配置:\n", len(services)) + for _, s := range services { + fmt.Printf(" - %s (%s) @ %s\n", s.Name, s.Domain, s.APIBase) + } + } else { + currentMailServices = defaultMailServices + fmt.Println("[邮箱] 使用默认邮箱服务配置") + } + currentServiceIndex = 0 +} + +// GetServices 获取当前邮箱服务配置 +func GetServices() []config.MailServiceConfig { + mailServicesMutex.RLock() + defer mailServicesMutex.RUnlock() + return currentMailServices +} + +// GetNextService 轮询获取下一个邮箱服务 +func GetNextService() config.MailServiceConfig { + mailServicesMutex.Lock() + defer mailServicesMutex.Unlock() + + if len(currentMailServices) == 0 { + return defaultMailServices[0] + } + + service := currentMailServices[currentServiceIndex] + currentServiceIndex = (currentServiceIndex + 1) % len(currentMailServices) + return service +} + +// GetRandomService 随机获取一个邮箱服务 +func GetRandomService() config.MailServiceConfig { + mailServicesMutex.RLock() + defer mailServicesMutex.RUnlock() + + if len(currentMailServices) == 0 { + return defaultMailServices[0] + } + + return currentMailServices[rand.Intn(len(currentMailServices))] +} + +// GetServiceByDomain 根据域名获取对应的邮箱服务 +func GetServiceByDomain(domain string) *config.MailServiceConfig { + mailServicesMutex.RLock() + defer mailServicesMutex.RUnlock() + + for _, s := range currentMailServices { + if s.Domain == domain || strings.HasSuffix(domain, "."+s.Domain) { + return &s + } + } + return nil +} + +// ==================== 邮件结构 ==================== + +// EmailListRequest 邮件列表请求 +type EmailListRequest struct { + ToEmail string `json:"toEmail"` + TimeSort string `json:"timeSort"` + Size int `json:"size"` +} + +// EmailListResponse 邮件列表响应 +type EmailListResponse struct { + Code int `json:"code"` + Data []EmailItem `json:"data"` +} + +// EmailItem 邮件项 +type EmailItem struct { + Content string `json:"content"` + Text string `json:"text"` + Subject string `json:"subject"` +} + +// AddUserRequest 创建用户请求 +type AddUserRequest struct { + List []AddUserItem `json:"list"` +} + +// AddUserItem 用户项 +type AddUserItem struct { + Email string `json:"email"` + Password string `json:"password"` +} + +// AddUserResponse 创建用户响应 +type AddUserResponse struct { + Code int `json:"code"` + Message string `json:"message"` +} + +// ==================== 邮箱生成 ==================== + +// GenerateEmail 生成随机邮箱并在邮件系统中创建 +func GenerateEmail() string { + return GenerateEmailWithService(GetNextService()) +} + +// GenerateEmailWithService 使用指定服务生成随机邮箱 +func GenerateEmailWithService(service config.MailServiceConfig) string { + const charset = "abcdefghijklmnopqrstuvwxyz0123456789" + b := make([]byte, 10) + for i := range b { + b[i] = charset[rand.Intn(len(charset))] + } + email := string(b) + "@" + service.Domain + + if err := CreateMailboxWithService(email, service); err != nil { + fmt.Printf(" [!] 创建邮箱失败 (%s): %v (继续尝试)\n", service.Name, err) + } + + return email +} + +// CreateMailbox 在邮件系统中创建邮箱 +func CreateMailbox(email string) error { + parts := strings.Split(email, "@") + if len(parts) != 2 { + return fmt.Errorf("无效的邮箱地址: %s", email) + } + + domain := parts[1] + service := GetServiceByDomain(domain) + if service == nil { + services := GetServices() + if len(services) > 0 { + service = &services[0] + } else { + return fmt.Errorf("没有可用的邮箱服务") + } + } + + return CreateMailboxWithService(email, *service) +} + +// CreateMailboxWithService 使用指定服务在邮件系统中创建邮箱 +func CreateMailboxWithService(email string, service config.MailServiceConfig) error { + client := &http.Client{Timeout: 10 * time.Second} + + parts := strings.Split(email, "@") + if len(parts) == 2 { + domain := parts[1] + if strings.HasSuffix(domain, "."+service.Domain) { + email = parts[0] + "@" + service.Domain + } + } + + payload := AddUserRequest{ + List: []AddUserItem{ + {Email: email, Password: GeneratePassword()}, + }, + } + jsonData, _ := json.Marshal(payload) + + req, err := http.NewRequest("POST", service.APIBase+service.AddUserAPI, bytes.NewBuffer(jsonData)) + if err != nil { + return err + } + req.Header.Set("Authorization", service.APIToken) + req.Header.Set("Content-Type", "application/json") + + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + var result AddUserResponse + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return err + } + + if result.Code != 200 { + if strings.Contains(result.Message, "exist") { + return nil + } + return fmt.Errorf("API 错误: %s", result.Message) + } + + return nil +} + +// GeneratePassword 生成随机密码 +func GeneratePassword() string { + const ( + upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + lower = "abcdefghijklmnopqrstuvwxyz" + digits = "0123456789" + special = "#$%@!" + ) + + password := make([]byte, 12) + password[0] = upper[rand.Intn(len(upper))] + password[1] = lower[rand.Intn(len(lower))] + password[10] = digits[rand.Intn(len(digits))] + password[11] = special[rand.Intn(len(special))] + + charset := upper + lower + for i := 2; i < 10; i++ { + password[i] = charset[rand.Intn(len(charset))] + } + + return string(password) +} + +// ==================== 邮件客户端 ==================== + +// Client 邮件客户端 +type Client struct { + client *http.Client + service *config.MailServiceConfig +} + +// NewClient 创建邮件客户端 +func NewClient() *Client { + services := GetServices() + var service *config.MailServiceConfig + if len(services) > 0 { + service = &services[0] + } else { + service = &defaultMailServices[0] + } + return &Client{ + client: &http.Client{Timeout: 10 * time.Second}, + service: service, + } +} + +// NewClientWithService 创建指定服务的邮件客户端 +func NewClientWithService(service config.MailServiceConfig) *Client { + return &Client{ + client: &http.Client{Timeout: 10 * time.Second}, + service: &service, + } +} + +// NewClientForEmail 根据邮箱地址创建对应的邮件客户端 +func NewClientForEmail(email string) *Client { + parts := strings.Split(email, "@") + if len(parts) == 2 { + if service := GetServiceByDomain(parts[1]); service != nil { + return NewClientWithService(*service) + } + } + return NewClient() +} + +// GetEmails 获取邮件列表 +func (m *Client) GetEmails(email string, size int) ([]EmailItem, error) { + service := m.service + parts := strings.Split(email, "@") + if len(parts) == 2 { + if s := GetServiceByDomain(parts[1]); s != nil { + service = s + } + } + + url := service.APIBase + service.EmailPath + + payload := EmailListRequest{ + ToEmail: email, + TimeSort: "desc", + Size: size, + } + jsonData, _ := json.Marshal(payload) + + req, _ := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + req.Header.Set("Authorization", service.APIToken) + req.Header.Set("Content-Type", "application/json") + + resp, err := m.client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return nil, fmt.Errorf("HTTP %d", resp.StatusCode) + } + + var result EmailListResponse + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, err + } + + if result.Code != 200 { + return nil, fmt.Errorf("API 错误: %d", result.Code) + } + + return result.Data, nil +} + +// WaitForCode 等待验证码邮件 +func (m *Client) WaitForCode(email string, timeout time.Duration) (string, error) { + start := time.Now() + codeRegex := regexp.MustCompile(`\b(\d{6})\b`) + + for time.Since(start) < timeout { + emails, err := m.GetEmails(email, 10) + if err == nil { + for _, mail := range emails { + subject := strings.ToLower(mail.Subject) + // 匹配多种可能的验证码邮件主题 + isCodeEmail := strings.Contains(subject, "code") || + strings.Contains(subject, "verify") || + strings.Contains(subject, "verification") || + strings.Contains(subject, "openai") || + strings.Contains(subject, "confirm") + + if !isCodeEmail { + continue + } + + content := mail.Content + if content == "" { + content = mail.Text + } + matches := codeRegex.FindStringSubmatch(content) + if len(matches) >= 2 { + return matches[1], nil + } + } + } + time.Sleep(2 * time.Second) + } + + return "", fmt.Errorf("验证码获取超时") +} + +// WaitForInviteLink 等待邀请邮件并提取链接 +func (m *Client) WaitForInviteLink(email string, timeout time.Duration) (string, error) { + start := time.Now() + + for time.Since(start) < timeout { + emails, err := m.GetEmails(email, 10) + if err == nil { + for _, mail := range emails { + content := mail.Content + if content == "" { + content = mail.Text + } + + if strings.Contains(mail.Subject, "invite") || + strings.Contains(mail.Subject, "Team") || + strings.Contains(mail.Subject, "ChatGPT") || + strings.Contains(content, "invite") { + + link := extractInviteLink(content) + if link != "" { + return link, nil + } + } + } + } + time.Sleep(2 * time.Second) + } + + return "", fmt.Errorf("等待邀请邮件超时") +} + +// extractInviteLink 从邮件内容提取邀请链接 +func extractInviteLink(content string) string { + patterns := []string{ + `https://chatgpt\.com/invite/[^\s"'<>]+`, + `https://chat\.openai\.com/invite/[^\s"'<>]+`, + `https://chatgpt\.com/[^\s"'<>]*accept[^\s"'<>]*`, + } + + for _, pattern := range patterns { + re := regexp.MustCompile(pattern) + match := re.FindString(content) + if match != "" { + match = strings.ReplaceAll(match, "&", "&") + return match + } + } + + return "" +} + +// ==================== 便捷函数 ==================== + +// WaitForInviteEmail 等待邀请邮件 +func WaitForInviteEmail(email string, timeout time.Duration) (string, error) { + client := NewClientForEmail(email) + return client.WaitForInviteLink(email, timeout) +} + +// GetVerificationCode 获取验证码 +func GetVerificationCode(email string, timeout time.Duration) (string, error) { + client := NewClientForEmail(email) + return client.WaitForCode(email, timeout) +} diff --git a/backend/internal/register/chatgpt.go b/backend/internal/register/chatgpt.go new file mode 100644 index 0000000..7a5f3b3 --- /dev/null +++ b/backend/internal/register/chatgpt.go @@ -0,0 +1,415 @@ +package register + +import ( + "bytes" + "encoding/json" + "fmt" + "math/rand" + "net/http" + "net/url" + "strings" + "time" + + "codex-pool/internal/client" + "codex-pool/internal/mail" +) + +// ChatGPTReg ChatGPT 注册器 +type ChatGPTReg struct { + Proxy string + Client *client.TLSClient + AuthSessionLoggingID string + OAIDid string + CSRFToken string + AuthorizeURL string + AccessToken string +} + +// Result 注册结果 +type Result struct { + Email string `json:"email"` + Password string `json:"password"` + Name string `json:"name"` + AccessToken string `json:"access_token"` +} + +// New 创建注册器 +func New(proxy string) (*ChatGPTReg, error) { + c, err := client.New(proxy) + if err != nil { + return nil, err + } + + return &ChatGPTReg{ + Proxy: proxy, + Client: c, + AuthSessionLoggingID: GenerateUUID(), + }, nil +} + +// InitSession 初始化会话 +func (r *ChatGPTReg) InitSession() error { + resp, err := r.Client.Get("https://chatgpt.com") + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return fmt.Errorf("初始化失败,状态码: %d", resp.StatusCode) + } + + r.OAIDid = r.Client.GetCookie("https://chatgpt.com", "oai-did") + + csrfCookie := r.Client.GetCookie("https://chatgpt.com", "__Host-next-auth.csrf-token") + if csrfCookie != "" { + decoded, err := url.QueryUnescape(csrfCookie) + if err == nil { + parts := strings.Split(decoded, "|") + if len(parts) > 0 { + r.CSRFToken = parts[0] + } + } + } + + if r.CSRFToken == "" { + return fmt.Errorf("无法获取 CSRF token") + } + + loginURL := fmt.Sprintf("https://chatgpt.com/auth/login?openaicom-did=%s", r.OAIDid) + loginResp, err := r.Client.Get(loginURL) + if err != nil { + return err + } + defer loginResp.Body.Close() + + return nil +} + +// GetAuthorizeURL 获取授权 URL +func (r *ChatGPTReg) GetAuthorizeURL(email string) error { + loginURL := fmt.Sprintf( + "https://chatgpt.com/api/auth/signin/openai?prompt=login&ext-oai-did=%s&auth_session_logging_id=%s&screen_hint=login_or_signup&login_hint=%s", + r.OAIDid, + r.AuthSessionLoggingID, + url.QueryEscape(email), + ) + + data := url.Values{} + data.Set("callbackUrl", "https://chatgpt.com/") + data.Set("csrfToken", r.CSRFToken) + data.Set("json", "true") + + req, err := http.NewRequest("POST", loginURL, strings.NewReader(data.Encode())) + if err != nil { + return err + } + req.Header.Set("Origin", "https://chatgpt.com") + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + resp, err := r.Client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + var result map[string]interface{} + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return err + } + + if authURL, ok := result["url"].(string); ok && strings.Contains(authURL, "auth.openai.com") { + r.AuthorizeURL = authURL + return nil + } + + return fmt.Errorf("无法获取授权 URL") +} + +// StartAuthorize 开始授权流程 +func (r *ChatGPTReg) StartAuthorize() error { + resp, err := r.Client.Get(r.AuthorizeURL) + if err != nil { + return err + } + defer resp.Body.Close() + + finalURL := resp.Request.URL.String() + if strings.Contains(finalURL, "create-account") || strings.Contains(finalURL, "log-in") { + return nil + } + + return fmt.Errorf("授权流程启动失败") +} + +// Register 注册账户 +func (r *ChatGPTReg) Register(email, password string) error { + payload := map[string]string{ + "password": password, + "username": email, + } + jsonData, _ := json.Marshal(payload) + + req, err := http.NewRequest("POST", "https://auth.openai.com/api/accounts/user/register", bytes.NewReader(jsonData)) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Origin", "https://auth.openai.com") + + resp, err := r.Client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + body, _ := client.ReadBodyString(resp) + return fmt.Errorf("注册失败,状态码: %d, 响应: %s", resp.StatusCode, truncateStr(body, 200)) + } + + return nil +} + +// SendVerificationEmail 发送验证邮件 +func (r *ChatGPTReg) SendVerificationEmail() error { + resp, err := r.Client.Get("https://auth.openai.com/api/accounts/email-otp/send") + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return fmt.Errorf("发送验证邮件失败,状态码: %d", resp.StatusCode) + } + + return nil +} + +// ValidateOTP 验证 OTP +func (r *ChatGPTReg) ValidateOTP(code string) error { + payload := map[string]string{"code": code} + jsonData, _ := json.Marshal(payload) + + req, err := http.NewRequest("POST", "https://auth.openai.com/api/accounts/email-otp/validate", bytes.NewReader(jsonData)) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Origin", "https://auth.openai.com") + + resp, err := r.Client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return fmt.Errorf("OTP 验证失败,状态码: %d", resp.StatusCode) + } + + return nil +} + +// CreateAccount 创建账户 +func (r *ChatGPTReg) CreateAccount(name, birthdate string) error { + payload := map[string]string{ + "name": name, + "birthdate": birthdate, + } + jsonData, _ := json.Marshal(payload) + + req, err := http.NewRequest("POST", "https://auth.openai.com/api/accounts/create_account", bytes.NewReader(jsonData)) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Origin", "https://auth.openai.com") + + resp, err := r.Client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return fmt.Errorf("创建账户失败,状态码: %d", resp.StatusCode) + } + + var result map[string]interface{} + if err := json.NewDecoder(resp.Body).Decode(&result); err == nil { + if continueURL, ok := result["continue_url"].(string); ok && continueURL != "" { + contResp, err := r.Client.Get(continueURL) + if err == nil { + contResp.Body.Close() + } + } + } + + return nil +} + +// GetSessionToken 获取 access token +func (r *ChatGPTReg) GetSessionToken() error { + resp, err := r.Client.Get("https://chatgpt.com/api/auth/session") + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return fmt.Errorf("获取 session 失败,状态码: %d", resp.StatusCode) + } + + var result map[string]interface{} + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return err + } + + if token, ok := result["accessToken"].(string); ok { + r.AccessToken = token + return nil + } + + return fmt.Errorf("响应中没有 accessToken") +} + +// Run 完整的注册流程 +func Run(email, password, name, birthdate, proxy string) (*ChatGPTReg, error) { + return RunWithRetry(email, password, name, birthdate, proxy, 3) +} + +// RunWithRetry 带重试的注册流程 +// 当验证码获取超过5秒,就换新邮箱重新注册 +func RunWithRetry(email, password, name, birthdate, proxy string, maxRetries int) (*ChatGPTReg, error) { + for attempt := 0; attempt < maxRetries; attempt++ { + if attempt > 0 { + // 重试时生成新邮箱 + email = mail.GenerateEmail() + password = GeneratePassword() + fmt.Printf(" [Retry %d] New email: %s\n", attempt, email) + } + + reg, err := runOnce(email, password, name, birthdate, proxy) + if err == nil { + return reg, nil + } + + // 如果不是验证码超时错误,直接返回 + if !strings.Contains(err.Error(), "验证码获取超时") { + return nil, err + } + + fmt.Printf(" [!] OTP timeout, retrying with new email...\n") + } + + return nil, fmt.Errorf("注册失败: 已重试 %d 次", maxRetries) +} + +// runOnce 执行一次注册流程(使用短超时获取验证码) +func runOnce(email, password, name, birthdate, proxy string) (*ChatGPTReg, error) { + reg, err := New(proxy) + if err != nil { + return nil, err + } + + // 初始化 + if err := reg.InitSession(); err != nil { + return nil, fmt.Errorf("初始化失败: %v", err) + } + if err := reg.GetAuthorizeURL(email); err != nil { + return nil, fmt.Errorf("获取授权URL失败: %v", err) + } + if err := reg.StartAuthorize(); err != nil { + return nil, fmt.Errorf("启动授权失败: %v", err) + } + + // 注册 + if err := reg.Register(email, password); err != nil { + return nil, fmt.Errorf("注册失败: %v", err) + } + if err := reg.SendVerificationEmail(); err != nil { + return nil, fmt.Errorf("发送邮件失败: %v", err) + } + + // 先用5秒超时尝试获取验证码 + otpCode, err := mail.GetVerificationCode(email, 5*time.Second) + if err != nil { + // 5秒内没获取到,再等120秒(总共等待更多时间) + otpCode, err = mail.GetVerificationCode(email, 120*time.Second) + if err != nil { + return nil, fmt.Errorf("验证码获取超时") + } + } + + if err := reg.ValidateOTP(otpCode); err != nil { + return nil, fmt.Errorf("OTP验证失败: %v", err) + } + + // 创建账户 + if err := reg.CreateAccount(name, birthdate); err != nil { + return nil, fmt.Errorf("创建账户失败: %v", err) + } + + // 获取 Token + _ = reg.GetSessionToken() + + return reg, nil +} + +// ==================== 工具函数 ==================== + +// GenerateName 生成随机姓名 +func GenerateName() string { + firstNames := []string{"James", "John", "Robert", "Michael", "David", "William", "Richard", "Joseph", "Thomas", "Charles"} + lastNames := []string{"Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", "Davis", "Rodriguez", "Martinez"} + return firstNames[rand.Intn(len(firstNames))] + " " + lastNames[rand.Intn(len(lastNames))] +} + +// GenerateUUID 生成 UUID +func GenerateUUID() string { + b := make([]byte, 16) + rand.Read(b) + b[6] = (b[6] & 0x0f) | 0x40 + b[8] = (b[8] & 0x3f) | 0x80 + return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16]) +} + +// GenerateBirthdate 生成随机生日 +func GenerateBirthdate() string { + year := 2000 + rand.Intn(5) + month := 1 + rand.Intn(12) + day := 1 + rand.Intn(28) + return fmt.Sprintf("%d-%02d-%02d", year, month, day) +} + +// GeneratePassword 生成随机密码 +func GeneratePassword() string { + const ( + upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + lower = "abcdefghijklmnopqrstuvwxyz" + digits = "0123456789" + special = "!@#$%" + ) + + b := make([]byte, 13) + for i := 0; i < 2; i++ { + b[i] = upper[rand.Intn(len(upper))] + } + for i := 2; i < 10; i++ { + b[i] = lower[rand.Intn(len(lower))] + } + for i := 10; i < 12; i++ { + b[i] = digits[rand.Intn(len(digits))] + } + b[12] = special[rand.Intn(len(special))] + + return string(b) +} + +func truncateStr(s string, maxLen int) string { + if len(s) <= maxLen { + return s + } + return s[:maxLen] + "..." +} diff --git a/backend/internal/web/dev.go b/backend/internal/web/dev.go new file mode 100644 index 0000000..91c3267 --- /dev/null +++ b/backend/internal/web/dev.go @@ -0,0 +1,18 @@ +//go:build !embed +// +build !embed + +package web + +import ( + "net/http" +) + +// GetFileSystem 返回 nil(开发模式不嵌入前端) +func GetFileSystem() http.FileSystem { + return nil +} + +// IsEmbedded 返回前端是否已嵌入 +func IsEmbedded() bool { + return false +} diff --git a/backend/internal/web/embed.go b/backend/internal/web/embed.go new file mode 100644 index 0000000..783b592 --- /dev/null +++ b/backend/internal/web/embed.go @@ -0,0 +1,27 @@ +//go:build embed +// +build embed + +package web + +import ( + "embed" + "io/fs" + "net/http" +) + +//go:embed dist/* +var distFS embed.FS + +// GetFileSystem 返回嵌入的前端文件系统 +func GetFileSystem() http.FileSystem { + sub, err := fs.Sub(distFS, "dist") + if err != nil { + panic(err) + } + return http.FS(sub) +} + +// IsEmbedded 返回前端是否已嵌入 +func IsEmbedded() bool { + return true +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a959a84 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,40 @@ +# =========================================== +# Codex Pool - Docker Compose 配置 +# =========================================== +# 使用方式: +# docker-compose up -d # 启动服务 +# docker-compose logs -f # 查看日志 +# docker-compose down # 停止服务 +# =========================================== + +services: + # =========================================================================== + # Codex Pool Application (前后端一体) + # =========================================================================== + codex-pool: + build: + context: . + dockerfile: Dockerfile + image: codex-pool:latest + container_name: codex-pool + restart: unless-stopped + ports: + - "${BIND_HOST:-0.0.0.0}:${PORT:-8848}:8848" + volumes: + # 数据持久化 (配置文件和数据库) + - ./data:/app/data + environment: + - TZ=${TZ:-Asia/Shanghai} + - CONFIG_PATH=/app/data/config.json + networks: + - codex-network + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8848/api/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 10s + +networks: + codex-network: + driver: bridge diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/.prettierignore b/frontend/.prettierignore new file mode 100644 index 0000000..9ca1068 --- /dev/null +++ b/frontend/.prettierignore @@ -0,0 +1,6 @@ +node_modules +dist +build +coverage +*.min.js +*.min.css diff --git a/frontend/.prettierrc b/frontend/.prettierrc new file mode 100644 index 0000000..9ac1be4 --- /dev/null +++ b/frontend/.prettierrc @@ -0,0 +1,10 @@ +{ + "semi": false, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "es5", + "printWidth": 100, + "bracketSpacing": true, + "arrowParens": "always", + "endOfLine": "lf" +} diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..d2e7761 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,73 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 0000000..a444deb --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,76 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import eslintConfigPrettier from 'eslint-config-prettier' +import eslintPluginPrettier from 'eslint-plugin-prettier' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist', 'node_modules', 'coverage', 'build']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + eslintConfigPrettier, + ], + plugins: { + prettier: eslintPluginPrettier, + }, + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + rules: { + // Prettier integration - show formatting issues as ESLint errors + 'prettier/prettier': 'error', + + // TypeScript specific rules + '@typescript-eslint/no-unused-vars': [ + 'warn', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + }, + ], + '@typescript-eslint/no-explicit-any': 'warn', + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + // Allow empty interfaces that extend other interfaces (common pattern for component props) + '@typescript-eslint/no-empty-object-type': 'off', + + // React specific rules + 'react-hooks/rules-of-hooks': 'error', + 'react-hooks/exhaustive-deps': 'warn', + // Allow setState in effects for initialization patterns (common in React for loading from localStorage) + 'react-hooks/set-state-in-effect': 'off', + 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], + + // General rules + 'no-console': ['warn', { allow: ['warn', 'error'] }], + 'prefer-const': 'error', + 'no-var': 'error', + }, + }, + // Configuration for JavaScript files (like config files) + { + files: ['**/*.{js,mjs,cjs}'], + extends: [js.configs.recommended, eslintConfigPrettier], + plugins: { + prettier: eslintPluginPrettier, + }, + languageOptions: { + ecmaVersion: 2020, + globals: { + ...globals.node, + }, + }, + rules: { + 'prettier/prettier': 'error', + }, + }, +]) diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..95db7cb --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,14 @@ + + + + + + + + Codex Pool + + +
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..cc5cba9 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,5522 @@ +{ + "name": "codex-pool-frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "codex-pool-frontend", + "version": "0.0.0", + "dependencies": { + "@tailwindcss/vite": "^4.1.18", + "lucide-react": "^0.563.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-router-dom": "^7.13.0", + "recharts": "^3.7.0", + "tailwindcss": "^4.1.18" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@types/node": "^24.10.9", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "eslint": "^9.39.1", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-prettier": "^5.5.5", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "jsdom": "^27.4.0", + "prettier": "^3.8.1", + "typescript": "~5.9.3", + "typescript-eslint": "^8.46.4", + "vite": "^7.2.4", + "vitest": "^4.0.18" + } + }, + "node_modules/@acemir/cssom": { + "version": "0.9.31", + "resolved": "https://registry.npmmirror.com/@acemir/cssom/-/cssom-0.9.31.tgz", + "integrity": "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://registry.npmmirror.com/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@asamuzakjp/css-color": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/@asamuzakjp/css-color/-/css-color-4.1.1.tgz", + "integrity": "sha512-B0Hv6G3gWGMn0xKJ0txEi/jM5iFpT3MfDxmhZFb4W047GvytCf1DHQ1D69W3zHI4yWe2aTZAA0JnbMZ7Xc8DuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "lru-cache": "^11.2.4" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "11.2.5", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-11.2.5.tgz", + "integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "6.7.6", + "resolved": "https://registry.npmmirror.com/@asamuzakjp/dom-selector/-/dom-selector-6.7.6.tgz", + "integrity": "sha512-hBaJER6A9MpdG3WgdlOolHmbOYvSk46y7IQN/1+iqiCuUu6iWdQrs9DGKF8ocqsEqWujWf/V7b7vaDgiUmIvUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.1.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.4" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.2.5", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-11.2.5.tgz", + "integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmmirror.com/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/code-frame": { + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.28.6.tgz", + "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.28.6.tgz", + "integrity": "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/core/-/core-7.28.6.tgz", + "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/generator/-/generator-7.28.6.tgz", + "integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmmirror.com/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.28.6.tgz", + "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.6" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.28.6.tgz", + "integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.28.6.tgz", + "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmmirror.com/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmmirror.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.0.26", + "resolved": "https://registry.npmmirror.com/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.26.tgz", + "integrity": "sha512-6boXK0KkzT5u5xOgF6TKB+CLq9SOpEGmkZw0g5n9/7yg85wab3UzSxB8TxhLJ31L4SGJ6BCFRw/iftTha1CJXA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0" + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmmirror.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmmirror.com/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmmirror.com/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmmirror.com/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmmirror.com/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.3", + "resolved": "https://registry.npmmirror.com/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmmirror.com/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.2", + "resolved": "https://registry.npmmirror.com/@eslint/js/-/js-9.39.2.tgz", + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmmirror.com/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@exodus/bytes": { + "version": "1.10.0", + "resolved": "https://registry.npmmirror.com/@exodus/bytes/-/bytes-1.10.0.tgz", + "integrity": "sha512-tf8YdcbirXdPnJ+Nd4UN1EXnz+IP2DI45YVEr3vvzcVTOyrApkmIB4zvOQVd3XPr7RXnfBtAx+PXImXOIU0Ajg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmmirror.com/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmmirror.com/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmmirror.com/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmmirror.com/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@reduxjs/toolkit": { + "version": "2.11.2", + "resolved": "https://registry.npmmirror.com/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", + "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@reduxjs/toolkit/node_modules/immer": { + "version": "11.1.3", + "resolved": "https://registry.npmmirror.com/immer/-/immer-11.1.3.tgz", + "integrity": "sha512-6jQTc5z0KJFtr1UgFpIL3N9XSC3saRaI9PwWtzM2pSqkNGtiNkYY2OSwkOGDK2XcTRcLb1pi/aNkKZz0nxVH4Q==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", + "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.57.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.0.tgz", + "integrity": "sha512-tPgXB6cDTndIe1ah7u6amCI1T0SsnlOuKgg10Xh3uizJk4e5M1JGaUMk7J4ciuAUcFpbOiNhm2XIjP9ON0dUqA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.57.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.0.tgz", + "integrity": "sha512-sa4LyseLLXr1onr97StkU1Nb7fWcg6niokTwEVNOO7awaKaoRObQ54+V/hrF/BP1noMEaaAW6Fg2d/CfLiq3Mg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.0.tgz", + "integrity": "sha512-/NNIj9A7yLjKdmkx5dC2XQ9DmjIECpGpwHoGmA5E1AhU0fuICSqSWScPhN1yLCkEdkCwJIDu2xIeLPs60MNIVg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.0.tgz", + "integrity": "sha512-xoh8abqgPrPYPr7pTYipqnUi1V3em56JzE/HgDgitTqZBZ3yKCWI+7KUkceM6tNweyUKYru1UMi7FC060RyKwA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.57.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.0.tgz", + "integrity": "sha512-PCkMh7fNahWSbA0OTUQ2OpYHpjZZr0hPr8lId8twD7a7SeWrvT3xJVyza+dQwXSSq4yEQTMoXgNOfMCsn8584g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.57.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.0.tgz", + "integrity": "sha512-1j3stGx+qbhXql4OCDZhnK7b01s6rBKNybfsX+TNrEe9JNq4DLi1yGiR1xW+nL+FNVvI4D02PUnl6gJ/2y6WJA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.0.tgz", + "integrity": "sha512-eyrr5W08Ms9uM0mLcKfM/Uzx7hjhz2bcjv8P2uynfj0yU8GGPdz8iYrBPhiLOZqahoAMB8ZiolRZPbbU2MAi6Q==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.0.tgz", + "integrity": "sha512-Xds90ITXJCNyX9pDhqf85MKWUI4lqjiPAipJ8OLp8xqI2Ehk+TCVhF9rvOoN8xTbcafow3QOThkNnrM33uCFQA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.0.tgz", + "integrity": "sha512-Xws2KA4CLvZmXjy46SQaXSejuKPhwVdaNinldoYfqruZBaJHqVo6hnRa8SDo9z7PBW5x84SH64+izmldCgbezw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.57.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.0.tgz", + "integrity": "sha512-hrKXKbX5FdaRJj7lTMusmvKbhMJSGWJ+w++4KmjiDhpTgNlhYobMvKfDoIWecy4O60K6yA4SnztGuNTQF+Lplw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.0.tgz", + "integrity": "sha512-6A+nccfSDGKsPm00d3xKcrsBcbqzCTAukjwWK6rbuAnB2bHaL3r9720HBVZ/no7+FhZLz/U3GwwZZEh6tOSI8Q==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.0.tgz", + "integrity": "sha512-4P1VyYUe6XAJtQH1Hh99THxr0GKMMwIXsRNOceLrJnaHTDgk1FTcTimDgneRJPvB3LqDQxUmroBclQ1S0cIJwQ==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.0.tgz", + "integrity": "sha512-8Vv6pLuIZCMcgXre6c3nOPhE0gjz1+nZP6T+hwWjr7sVH8k0jRkH+XnfjjOTglyMBdSKBPPz54/y1gToSKwrSQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.0.tgz", + "integrity": "sha512-r1te1M0Sm2TBVD/RxBPC6RZVwNqUTwJTA7w+C/IW5v9Ssu6xmxWEi+iJQlpBhtUiT1raJ5b48pI8tBvEjEFnFA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.0.tgz", + "integrity": "sha512-say0uMU/RaPm3CDQLxUUTF2oNWL8ysvHkAjcCzV2znxBr23kFfaxocS9qJm+NdkRhF8wtdEEAJuYcLPhSPbjuQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.0.tgz", + "integrity": "sha512-/MU7/HizQGsnBREtRpcSbSV1zfkoxSTR7wLsRmBPQ8FwUj5sykrP1MyJTvsxP5KBq9SyE6kH8UQQQwa0ASeoQQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.0.tgz", + "integrity": "sha512-Q9eh+gUGILIHEaJf66aF6a414jQbDnn29zeu0eX3dHMuysnhTvsUvZTCAyZ6tJhUjnvzBKE4FtuaYxutxRZpOg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.0.tgz", + "integrity": "sha512-OR5p5yG5OKSxHReWmwvM0P+VTPMwoBS45PXTMYaskKQqybkS3Kmugq1W+YbNWArF8/s7jQScgzXUhArzEQ7x0A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.57.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.0.tgz", + "integrity": "sha512-XeatKzo4lHDsVEbm1XDHZlhYZZSQYym6dg2X/Ko0kSFgio+KXLsxwJQprnR48GvdIKDOpqWqssC3iBCjoMcMpw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.0.tgz", + "integrity": "sha512-Lu71y78F5qOfYmubYLHPcJm74GZLU6UJ4THkf/a1K7Tz2ycwC2VUbsqbJAXaR6Bx70SRdlVrt2+n5l7F0agTUw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.0.tgz", + "integrity": "sha512-v5xwKDWcu7qhAEcsUubiav7r+48Uk/ENWdr82MBZZRIm7zThSxCIVDfb3ZeRRq9yqk+oIzMdDo6fCcA5DHfMyA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.0.tgz", + "integrity": "sha512-XnaaaSMGSI6Wk8F4KK3QP7GfuuhjGchElsVerCplUuxRIzdvZ7hRBpLR0omCmw+kI2RFJB80nenhOoGXlJ5TfQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.0.tgz", + "integrity": "sha512-3K1lP+3BXY4t4VihLw5MEg6IZD3ojSYzqzBG571W3kNQe4G4CcFpSUQVgurYgib5d+YaCjeFow8QivWp8vuSvA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.0.tgz", + "integrity": "sha512-MDk610P/vJGc5L5ImE4k5s+GZT3en0KoK1MKPXCRgzmksAMk79j4h3k1IerxTNqwDLxsGxStEZVBqG0gIqZqoA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.0", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.0.tgz", + "integrity": "sha512-Zv7v6q6aV+VslnpwzqKAmrk5JdVkLUzok2208ZXGipjb+msxBr/fJPZyeEXiFgH7k62Ak0SLIfxQRZQvTuf7rQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmmirror.com/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.18", + "resolved": "https://registry.npmmirror.com/@tailwindcss/node/-/node-4.1.18.tgz", + "integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.6.1", + "lightningcss": "1.30.2", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.18" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.18", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide/-/oxide-4.1.18.tgz", + "integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==", + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-x64": "4.1.18", + "@tailwindcss/oxide-freebsd-x64": "4.1.18", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-x64-musl": "4.1.18", + "@tailwindcss/oxide-wasm32-wasi": "4.1.18", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.18", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz", + "integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.18", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz", + "integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.18", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz", + "integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.18", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz", + "integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.18", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz", + "integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.18", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz", + "integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.18", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz", + "integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.18", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz", + "integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.18", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz", + "integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.18", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz", + "integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.0", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.18", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz", + "integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.18", + "resolved": "https://registry.npmmirror.com/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz", + "integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.1.18", + "resolved": "https://registry.npmmirror.com/@tailwindcss/vite/-/vite-4.1.18.tgz", + "integrity": "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA==", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.1.18", + "@tailwindcss/oxide": "4.1.18", + "tailwindcss": "4.1.18" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmmirror.com/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmmirror.com/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmmirror.com/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmmirror.com/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmmirror.com/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmmirror.com/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmmirror.com/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmmirror.com/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmmirror.com/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmmirror.com/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmmirror.com/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmmirror.com/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmmirror.com/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmmirror.com/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.10.9", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-24.10.9.tgz", + "integrity": "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.10", + "resolved": "https://registry.npmmirror.com/@types/react/-/react-19.2.10.tgz", + "integrity": "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmmirror.com/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmmirror.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.54.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.54.0.tgz", + "integrity": "sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.54.0", + "@typescript-eslint/type-utils": "8.54.0", + "@typescript-eslint/utils": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.54.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmmirror.com/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.54.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-8.54.0.tgz", + "integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.54.0", + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.54.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/project-service/-/project-service-8.54.0.tgz", + "integrity": "sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.54.0", + "@typescript-eslint/types": "^8.54.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.54.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-8.54.0.tgz", + "integrity": "sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.54.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.54.0.tgz", + "integrity": "sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.54.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-8.54.0.tgz", + "integrity": "sha512-hiLguxJWHjjwL6xMBwD903ciAwd7DmK30Y9Axs/etOkftC3ZNN9K44IuRD/EB08amu+Zw6W37x9RecLkOo3pMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0", + "@typescript-eslint/utils": "8.54.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.54.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.54.0.tgz", + "integrity": "sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.54.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.54.0.tgz", + "integrity": "sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.54.0", + "@typescript-eslint/tsconfig-utils": "8.54.0", + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0", + "debug": "^4.4.3", + "minimatch": "^9.0.5", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.54.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-8.54.0.tgz", + "integrity": "sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.54.0", + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.54.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.54.0.tgz", + "integrity": "sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.54.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz", + "integrity": "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.5", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.53", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@vitest/expect": { + "version": "4.0.18", + "resolved": "https://registry.npmmirror.com/@vitest/expect/-/expect-4.0.18.tgz", + "integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "chai": "^6.2.1", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.0.18", + "resolved": "https://registry.npmmirror.com/@vitest/mocker/-/mocker-4.0.18.tgz", + "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.0.18", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.0.18", + "resolved": "https://registry.npmmirror.com/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", + "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.0.18", + "resolved": "https://registry.npmmirror.com/@vitest/runner/-/runner-4.0.18.tgz", + "integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.0.18", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.0.18", + "resolved": "https://registry.npmmirror.com/@vitest/snapshot/-/snapshot-4.0.18.tgz", + "integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.0.18", + "resolved": "https://registry.npmmirror.com/@vitest/spy/-/spy-4.0.18.tgz", + "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.0.18", + "resolved": "https://registry.npmmirror.com/@vitest/utils/-/utils-4.0.18.tgz", + "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmmirror.com/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.19", + "resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", + "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001766", + "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001766.tgz", + "integrity": "sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmmirror.com/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-tree": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/css-tree/-/css-tree-3.1.0.tgz", + "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.12.2", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmmirror.com/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssstyle": { + "version": "5.3.7", + "resolved": "https://registry.npmmirror.com/cssstyle/-/cssstyle-5.3.7.tgz", + "integrity": "sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^4.1.1", + "@csstools/css-syntax-patches-for-csstree": "^1.0.21", + "css-tree": "^3.1.0", + "lru-cache": "^11.2.4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cssstyle/node_modules/lru-cache": { + "version": "11.2.5", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-11.2.5.tgz", + "integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmmirror.com/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/data-urls": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/data-urls/-/data-urls-6.0.1.tgz", + "integrity": "sha512-euIQENZg6x8mj3fO6o9+fOW8MimUI4PpD/fZBhJfeioZVy9TUpM4UY7KjQNVZFlqwJ0UdzRDzkycB997HEq1BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^15.1.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls/node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmmirror.com/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmmirror.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmmirror.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/electron-to-chromium": { + "version": "1.5.279", + "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.279.tgz", + "integrity": "sha512-0bblUU5UNdOt5G7XqGiJtpZMONma6WAfq9vsFmtn9x1+joAObr6x1chfqyxFSDCAFwFhCQDrqeAr6MYdpwJ9Hg==", + "dev": true, + "license": "ISC" + }, + "node_modules/enhanced-resolve": { + "version": "5.18.4", + "resolved": "https://registry.npmmirror.com/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz", + "integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmmirror.com/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-toolkit": { + "version": "1.44.0", + "resolved": "https://registry.npmmirror.com/es-toolkit/-/es-toolkit-1.44.0.tgz", + "integrity": "sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.2", + "resolved": "https://registry.npmmirror.com/eslint/-/eslint-9.39.2.tgz", + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.2", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmmirror.com/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.5.5", + "resolved": "https://registry.npmmirror.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.5.tgz", + "integrity": "sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.1", + "synckit": "^0.11.12" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.0.1", + "resolved": "https://registry.npmmirror.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.26", + "resolved": "https://registry.npmmirror.com/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", + "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmmirror.com/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmmirror.com/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmmirror.com/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmmirror.com/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmmirror.com/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmmirror.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmmirror.com/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmmirror.com/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmmirror.com/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmmirror.com/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmmirror.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immer": { + "version": "10.2.0", + "resolved": "https://registry.npmmirror.com/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmmirror.com/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmmirror.com/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "27.4.0", + "resolved": "https://registry.npmmirror.com/jsdom/-/jsdom-27.4.0.tgz", + "integrity": "sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@acemir/cssom": "^0.9.28", + "@asamuzakjp/dom-selector": "^6.7.6", + "@exodus/bytes": "^1.6.0", + "cssstyle": "^5.3.4", + "data-urls": "^6.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "parse5": "^8.0.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.0", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^15.1.0", + "ws": "^8.18.3", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmmirror.com/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.30.2", + "resolved": "https://registry.npmmirror.com/lightningcss/-/lightningcss-1.30.2.tgz", + "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.30.2", + "lightningcss-darwin-arm64": "1.30.2", + "lightningcss-darwin-x64": "1.30.2", + "lightningcss-freebsd-x64": "1.30.2", + "lightningcss-linux-arm-gnueabihf": "1.30.2", + "lightningcss-linux-arm64-gnu": "1.30.2", + "lightningcss-linux-arm64-musl": "1.30.2", + "lightningcss-linux-x64-gnu": "1.30.2", + "lightningcss-linux-x64-musl": "1.30.2", + "lightningcss-win32-arm64-msvc": "1.30.2", + "lightningcss-win32-x64-msvc": "1.30.2" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmmirror.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", + "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmmirror.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", + "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmmirror.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", + "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmmirror.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", + "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.2", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", + "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", + "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", + "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", + "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmmirror.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", + "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmmirror.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", + "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmmirror.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", + "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.563.0", + "resolved": "https://registry.npmmirror.com/lucide-react/-/lucide-react-0.563.0.tgz", + "integrity": "sha512-8dXPB2GI4dI8jV4MgUDGBeLdGk8ekfqVZ0BdLcrRzocGgG75ltNEmWS+gE7uokKF/0oSUuczNDT+g9hFJ23FkA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmmirror.com/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mdn-data": { + "version": "2.12.2", + "resolved": "https://registry.npmmirror.com/mdn-data/-/mdn-data-2.12.2.tgz", + "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmmirror.com/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/parse5/-/parse5-8.0.0.tgz", + "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.8.1", + "resolved": "https://registry.npmmirror.com/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.1.tgz", + "integrity": "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmmirror.com/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmmirror.com/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmmirror.com/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/react-is": { + "version": "19.2.4", + "resolved": "https://registry.npmmirror.com/react-is/-/react-is-19.2.4.tgz", + "integrity": "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==", + "license": "MIT", + "peer": true + }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmmirror.com/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmmirror.com/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.13.0", + "resolved": "https://registry.npmmirror.com/react-router/-/react-router-7.13.0.tgz", + "integrity": "sha512-PZgus8ETambRT17BUm/LL8lX3Of+oiLaPuVTRH3l1eLvSPpKO3AvhAEb5N7ihAFZQrYDqkvvWfFh9p0z9VsjLw==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.13.0", + "resolved": "https://registry.npmmirror.com/react-router-dom/-/react-router-dom-7.13.0.tgz", + "integrity": "sha512-5CO/l5Yahi2SKC6rGZ+HDEjpjkGaG/ncEP7eWFTvFxbHP8yeeI0PxTDjimtpXYlR3b3i9/WIL4VJttPrESIf2g==", + "license": "MIT", + "dependencies": { + "react-router": "7.13.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/recharts": { + "version": "3.7.0", + "resolved": "https://registry.npmmirror.com/recharts/-/recharts-3.7.0.tgz", + "integrity": "sha512-l2VCsy3XXeraxIID9fx23eCb6iCBsxUQDnE8tWm6DFdszVAO7WVY/ChAD9wVit01y6B2PMupYiMmQwhgPHc9Ew==", + "license": "MIT", + "workspaces": [ + "www" + ], + "dependencies": { + "@reduxjs/toolkit": "1.x.x || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^10.1.1", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.1.1", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rollup": { + "version": "4.57.0", + "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.57.0.tgz", + "integrity": "sha512-e5lPJi/aui4TO1LpAXIRLySmwXSE8k3b9zoGfd42p67wzxog4WHjiZF3M2uheQih4DGyc25QEV4yRBbpueNiUA==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.57.0", + "@rollup/rollup-android-arm64": "4.57.0", + "@rollup/rollup-darwin-arm64": "4.57.0", + "@rollup/rollup-darwin-x64": "4.57.0", + "@rollup/rollup-freebsd-arm64": "4.57.0", + "@rollup/rollup-freebsd-x64": "4.57.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.0", + "@rollup/rollup-linux-arm-musleabihf": "4.57.0", + "@rollup/rollup-linux-arm64-gnu": "4.57.0", + "@rollup/rollup-linux-arm64-musl": "4.57.0", + "@rollup/rollup-linux-loong64-gnu": "4.57.0", + "@rollup/rollup-linux-loong64-musl": "4.57.0", + "@rollup/rollup-linux-ppc64-gnu": "4.57.0", + "@rollup/rollup-linux-ppc64-musl": "4.57.0", + "@rollup/rollup-linux-riscv64-gnu": "4.57.0", + "@rollup/rollup-linux-riscv64-musl": "4.57.0", + "@rollup/rollup-linux-s390x-gnu": "4.57.0", + "@rollup/rollup-linux-x64-gnu": "4.57.0", + "@rollup/rollup-linux-x64-musl": "4.57.0", + "@rollup/rollup-openbsd-x64": "4.57.0", + "@rollup/rollup-openharmony-arm64": "4.57.0", + "@rollup/rollup-win32-arm64-msvc": "4.57.0", + "@rollup/rollup-win32-ia32-msvc": "4.57.0", + "@rollup/rollup-win32-x64-gnu": "4.57.0", + "@rollup/rollup-win32-x64-msvc": "4.57.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmmirror.com/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmmirror.com/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmmirror.com/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmmirror.com/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmmirror.com/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/synckit": { + "version": "0.11.12", + "resolved": "https://registry.npmmirror.com/synckit/-/synckit-0.11.12.tgz", + "integrity": "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.9" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/tailwindcss": { + "version": "4.1.18", + "resolved": "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-4.1.18.tgz", + "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmmirror.com/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.0.19", + "resolved": "https://registry.npmmirror.com/tldts/-/tldts-7.0.19.tgz", + "integrity": "sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.19" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.19", + "resolved": "https://registry.npmmirror.com/tldts-core/-/tldts-core-7.0.19.tgz", + "integrity": "sha512-lJX2dEWx0SGH4O6p+7FPwYmJ/bu1JbcGJ8RLaG9b7liIgZ85itUVEPbMtWRVrde/0fnDPEPHW10ZsKW3kVsE9A==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/tough-cookie/-/tough-cookie-6.0.0.tgz", + "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/ts-api-utils": { + "version": "2.4.0", + "resolved": "https://registry.npmmirror.com/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.54.0", + "resolved": "https://registry.npmmirror.com/typescript-eslint/-/typescript-eslint-8.54.0.tgz", + "integrity": "sha512-CKsJ+g53QpsNPqbzUsfKVgd3Lny4yKZ1pP4qN3jdMOg/sisIDLGyDMezycquXLE5JsEU0wp3dGNdzig0/fmSVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.54.0", + "@typescript-eslint/parser": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0", + "@typescript-eslint/utils": "8.54.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmmirror.com/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/victory-vendor": { + "version": "37.3.6", + "resolved": "https://registry.npmmirror.com/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmmirror.com/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.0.18", + "resolved": "https://registry.npmmirror.com/vitest/-/vitest-4.0.18.tgz", + "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.0.18", + "@vitest/mocker": "4.0.18", + "@vitest/pretty-format": "4.0.18", + "@vitest/runner": "4.0.18", + "@vitest/snapshot": "4.0.18", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "es-module-lexer": "^1.7.0", + "expect-type": "^1.2.2", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^3.10.0", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.0.18", + "@vitest/browser-preview": "4.0.18", + "@vitest/browser-webdriverio": "4.0.18", + "@vitest/ui": "4.0.18", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "15.1.0", + "resolved": "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-15.1.0.tgz", + "integrity": "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmmirror.com/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmmirror.com/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmmirror.com/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..881f610 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,49 @@ +{ + "name": "codex-pool-frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "format": "prettier --write \"src/**/*.{ts,tsx,css,json}\"", + "format:check": "prettier --check \"src/**/*.{ts,tsx,css,json}\"", + "check": "npm run lint && npm run format:check", + "fix": "npm run lint:fix && npm run format", + "preview": "vite preview", + "test": "vitest run", + "test:watch": "vitest" + }, + "dependencies": { + "@tailwindcss/vite": "^4.1.18", + "lucide-react": "^0.563.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-router-dom": "^7.13.0", + "recharts": "^3.7.0", + "tailwindcss": "^4.1.18" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@types/node": "^24.10.9", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "eslint": "^9.39.1", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-prettier": "^5.5.5", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "jsdom": "^27.4.0", + "prettier": "^3.8.1", + "typescript": "~5.9.3", + "typescript-eslint": "^8.46.4", + "vite": "^7.2.4", + "vitest": "^4.0.18" + } +} diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg new file mode 100644 index 0000000..9ff1306 --- /dev/null +++ b/frontend/public/favicon.svg @@ -0,0 +1,21 @@ + + + + diff --git a/frontend/public/vite.svg b/frontend/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/frontend/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..f098efb --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,28 @@ +import { Routes, Route } from 'react-router-dom' +import { ConfigProvider, RecordsProvider } from './context' +import { Layout } from './components/layout' +import { Dashboard, Upload, Records, Accounts, Config, S2AConfig, EmailConfig, Monitor, TeamProcess } from './pages' + +function App() { + return ( + + + + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + + + ) +} + +export default App diff --git a/frontend/src/api/chatgpt.test.ts b/frontend/src/api/chatgpt.test.ts new file mode 100644 index 0000000..801df0e --- /dev/null +++ b/frontend/src/api/chatgpt.test.ts @@ -0,0 +1,294 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' +import { ChatGPTClient, getStatusText, getStatusColor, createChatGPTClient } from './chatgpt' +import type { AccountInput } from '../types' + +describe('ChatGPTClient', () => { + let client: ChatGPTClient + let fetchMock: ReturnType + + beforeEach(() => { + client = new ChatGPTClient() + fetchMock = vi.fn() + globalThis.fetch = fetchMock as typeof fetch + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + describe('checkAccount', () => { + it('should return active status for HTTP 200 with account info', async () => { + const mockResponse = { + accounts: [ + { + account_id: 'test-account-id', + entitlement: { + subscription_plan: 'plus', + }, + }, + ], + } + + fetchMock.mockResolvedValueOnce({ + status: 200, + statusText: 'OK', + json: () => Promise.resolve(mockResponse), + }) + + const result = await client.checkAccount('valid-token') + + expect(result.status).toBe('active') + expect(result.accountId).toBe('test-account-id') + expect(result.planType).toBe('plus') + expect(result.error).toBeUndefined() + }) + + it('should return active status for HTTP 200 without account info', async () => { + const mockResponse = { + accounts: [], + } + + fetchMock.mockResolvedValueOnce({ + status: 200, + statusText: 'OK', + json: () => Promise.resolve(mockResponse), + }) + + const result = await client.checkAccount('valid-token') + + expect(result.status).toBe('active') + expect(result.accountId).toBeUndefined() + expect(result.planType).toBe('unknown') + }) + + it('should return token_expired status for HTTP 401', async () => { + fetchMock.mockResolvedValueOnce({ + status: 401, + statusText: 'Unauthorized', + json: () => Promise.reject(new Error('No JSON')), + }) + + const result = await client.checkAccount('expired-token') + + expect(result.status).toBe('token_expired') + expect(result.error).toBe('Token 已过期') + }) + + it('should return banned status for HTTP 403', async () => { + fetchMock.mockResolvedValueOnce({ + status: 403, + statusText: 'Forbidden', + json: () => Promise.reject(new Error('No JSON')), + }) + + const result = await client.checkAccount('banned-token') + + expect(result.status).toBe('banned') + expect(result.error).toBe('账号已被封禁') + }) + + it('should return error status for other HTTP codes', async () => { + fetchMock.mockResolvedValueOnce({ + status: 500, + statusText: 'Internal Server Error', + json: () => Promise.reject(new Error('No JSON')), + }) + + const result = await client.checkAccount('some-token') + + expect(result.status).toBe('error') + expect(result.error).toBe('HTTP 500: Internal Server Error') + }) + + it('should return error status for network errors', async () => { + fetchMock.mockRejectedValueOnce(new Error('Network error')) + + const result = await client.checkAccount('some-token') + + expect(result.status).toBe('error') + expect(result.error).toBe('Network error') + }) + + it('should return error status for empty token', async () => { + const result = await client.checkAccount('') + + expect(result.status).toBe('error') + expect(result.error).toBe('缺少 token') + expect(fetchMock).not.toHaveBeenCalled() + }) + + it('should return error status for whitespace-only token', async () => { + const result = await client.checkAccount(' ') + + expect(result.status).toBe('error') + expect(result.error).toBe('缺少 token') + expect(fetchMock).not.toHaveBeenCalled() + }) + + it('should use correct API endpoint and headers', async () => { + fetchMock.mockResolvedValueOnce({ + status: 200, + statusText: 'OK', + json: () => Promise.resolve({ accounts: [] }), + }) + + await client.checkAccount('test-token') + + expect(fetchMock).toHaveBeenCalledWith('/api/chatgpt/accounts/check/v4-2023-04-27', { + method: 'GET', + headers: { + Authorization: 'Bearer test-token', + 'Content-Type': 'application/json', + }, + }) + }) + + it('should handle JSON parse errors gracefully for HTTP 200', async () => { + fetchMock.mockResolvedValueOnce({ + status: 200, + statusText: 'OK', + json: () => Promise.reject(new Error('Invalid JSON')), + }) + + const result = await client.checkAccount('valid-token') + + // Should still return active since HTTP 200 + expect(result.status).toBe('active') + expect(result.planType).toBe('unknown') + }) + }) + + describe('batchCheck', () => { + it('should return empty array for empty input', async () => { + const results = await client.batchCheck([], { concurrency: 5 }) + + expect(results).toEqual([]) + expect(fetchMock).not.toHaveBeenCalled() + }) + + it('should check all accounts and return results in order', async () => { + const accounts: AccountInput[] = [ + { account: 'user1@test.com', password: 'pass1', token: 'token1' }, + { account: 'user2@test.com', password: 'pass2', token: 'token2' }, + { account: 'user3@test.com', password: 'pass3', token: 'token3' }, + ] + + // Mock responses for each account + fetchMock + .mockResolvedValueOnce({ + status: 200, + statusText: 'OK', + json: () => + Promise.resolve({ + accounts: [{ account_id: 'id1', entitlement: { subscription_plan: 'plus' } }], + }), + }) + .mockResolvedValueOnce({ + status: 401, + statusText: 'Unauthorized', + }) + .mockResolvedValueOnce({ + status: 403, + statusText: 'Forbidden', + }) + + const results = await client.batchCheck(accounts, { concurrency: 3 }) + + expect(results).toHaveLength(3) + expect(results[0].status).toBe('active') + expect(results[0].accountId).toBe('id1') + expect(results[1].status).toBe('token_expired') + expect(results[2].status).toBe('banned') + }) + + it('should call onProgress callback for each account', async () => { + const accounts: AccountInput[] = [ + { account: 'user1@test.com', password: 'pass1', token: 'token1' }, + { account: 'user2@test.com', password: 'pass2', token: 'token2' }, + ] + + fetchMock + .mockResolvedValueOnce({ + status: 200, + statusText: 'OK', + json: () => Promise.resolve({ accounts: [] }), + }) + .mockResolvedValueOnce({ + status: 200, + statusText: 'OK', + json: () => Promise.resolve({ accounts: [] }), + }) + + const onProgress = vi.fn() + + await client.batchCheck(accounts, { concurrency: 2, onProgress }) + + expect(onProgress).toHaveBeenCalledTimes(2) + }) + + it('should respect concurrency limit', async () => { + const accounts: AccountInput[] = Array.from({ length: 10 }, (_, i) => ({ + account: `user${i}@test.com`, + password: `pass${i}`, + token: `token${i}`, + })) + + let maxConcurrent = 0 + let currentConcurrent = 0 + + fetchMock.mockImplementation(async () => { + currentConcurrent++ + maxConcurrent = Math.max(maxConcurrent, currentConcurrent) + + // Simulate network delay + await new Promise((resolve) => setTimeout(resolve, 10)) + + currentConcurrent-- + return { + status: 200, + statusText: 'OK', + json: () => Promise.resolve({ accounts: [] }), + } + }) + + await client.batchCheck(accounts, { concurrency: 3 }) + + // Max concurrent should not exceed the concurrency limit + expect(maxConcurrent).toBeLessThanOrEqual(3) + }) + }) +}) + +describe('getStatusText', () => { + it('should return correct Chinese text for each status', () => { + expect(getStatusText('pending')).toBe('待检查') + expect(getStatusText('checking')).toBe('检查中') + expect(getStatusText('active')).toBe('正常') + expect(getStatusText('banned')).toBe('封禁') + expect(getStatusText('token_expired')).toBe('过期') + expect(getStatusText('error')).toBe('错误') + }) +}) + +describe('getStatusColor', () => { + it('should return correct color for each status', () => { + expect(getStatusColor('pending')).toBe('gray') + expect(getStatusColor('checking')).toBe('blue') + expect(getStatusColor('active')).toBe('green') + expect(getStatusColor('banned')).toBe('red') + expect(getStatusColor('token_expired')).toBe('orange') + expect(getStatusColor('error')).toBe('yellow') + }) +}) + +describe('createChatGPTClient', () => { + it('should create a new ChatGPTClient instance', () => { + const client = createChatGPTClient() + expect(client).toBeInstanceOf(ChatGPTClient) + }) + + it('should create a client with custom base URL', () => { + const client = createChatGPTClient('https://custom.api.com') + expect(client).toBeInstanceOf(ChatGPTClient) + }) +}) diff --git a/frontend/src/api/chatgpt.ts b/frontend/src/api/chatgpt.ts new file mode 100644 index 0000000..41caecd --- /dev/null +++ b/frontend/src/api/chatgpt.ts @@ -0,0 +1,253 @@ +import type { AccountInput, AccountStatus, CheckedAccount, CheckResult } from '../types' +import type { ChatGPTCheckResponse } from './types' + +/** + * ChatGPT API 检查端点 + * 通过 nginx 代理访问,避免 CORS 问题 + * 原始 API: https://chatgpt.com/backend-api/accounts/check/v4-2023-04-27 + */ +const CHATGPT_CHECK_API = '/api/chatgpt/accounts/check/v4-2023-04-27' + +/** + * HTTP 状态码到账号状态的映射 + * 根据 requirements.md A3 定义: + * - HTTP 200 → active (账号正常) + * - HTTP 401 → token_expired (Token 已过期) + * - HTTP 403 → banned (账号被封禁) + * - 其他 → error (网络错误等) + */ +function mapHttpStatusToAccountStatus(httpStatus: number): AccountStatus { + switch (httpStatus) { + case 200: + return 'active' + case 401: + return 'token_expired' + case 403: + return 'banned' + default: + return 'error' + } +} + +/** + * 获取 HTTP 状态码对应的错误消息 + */ +function getErrorMessageForStatus(httpStatus: number, statusText: string): string | undefined { + switch (httpStatus) { + case 200: + return undefined + case 401: + return 'Token 已过期' + case 403: + return '账号已被封禁' + default: + return `HTTP ${httpStatus}: ${statusText}` + } +} + +/** + * ChatGPT API 客户端 + * 用于检查 ChatGPT 账号状态 + */ +export class ChatGPTClient { + private baseUrl: string + + constructor(baseUrl: string = '') { + this.baseUrl = baseUrl + } + + /** + * 检查单个账号状态 + * @param token - ChatGPT access_token + * @returns CheckResult 包含状态、account_id、plan_type 等信息 + * + * API: GET https://chatgpt.com/backend-api/accounts/check/v4-2023-04-27 + * Headers: Authorization: Bearer {token} + * + * 状态映射 (requirements.md A3): + * - HTTP 200 → active + * - HTTP 401 → token_expired + * - HTTP 403 → banned + * - 其他 → error + */ + async checkAccount(token: string): Promise { + // 处理空 token 的情况 + if (!token || token.trim() === '') { + return { + status: 'error', + error: '缺少 token', + } + } + + try { + const response = await fetch(`${this.baseUrl}${CHATGPT_CHECK_API}`, { + method: 'GET', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + }) + + const status = mapHttpStatusToAccountStatus(response.status) + const errorMessage = getErrorMessageForStatus(response.status, response.statusText) + + if (response.status === 200) { + try { + const data: ChatGPTCheckResponse = await response.json() + const accountInfo = data.accounts?.[0] + + if (accountInfo) { + return { + status: 'active', + accountId: accountInfo.account_id, + planType: accountInfo.entitlement?.subscription_plan || 'free', + } + } + + // 200 响应但没有账号信息 + return { + status: 'active', + accountId: undefined, + planType: 'unknown', + } + } catch { + // JSON 解析失败,但 HTTP 200 仍视为 active + return { + status: 'active', + accountId: undefined, + planType: 'unknown', + } + } + } + + return { + status, + error: errorMessage, + } + } catch (error) { + return { + status: 'error', + error: error instanceof Error ? error.message : '网络错误', + } + } + } + + /** + * 批量检查账号(带并发控制) + * @param accounts - 待检查的账号列表 + * @param options.concurrency - 并发数量(默认 20) + * @param options.onProgress - 进度回调,每检查完一个账号调用一次 + * @returns 检查完成的账号列表 + * + * 使用队列 + Promise 实现并发控制,确保任意时刻活跃请求数 ≤ concurrency + */ + async batchCheck( + accounts: AccountInput[], + options: { + concurrency: number + onProgress?: (result: CheckedAccount, index: number) => void + } + ): Promise { + const { concurrency, onProgress } = options + + // 空数组直接返回 + if (accounts.length === 0) { + return [] + } + + const results: CheckedAccount[] = new Array(accounts.length) + const queue: number[] = [...Array(accounts.length).keys()] + let activeCount = 0 + let completedCount = 0 + + return new Promise((resolve) => { + const processNext = () => { + // 所有任务完成 + if (completedCount === accounts.length) { + resolve(results) + return + } + + // 启动新任务,直到达到并发限制或队列为空 + while (activeCount < concurrency && queue.length > 0) { + const index = queue.shift()! + activeCount++ + + // 异步处理单个账号 + this.processAccount(accounts[index], index) + .then((checkedAccount) => { + results[index] = checkedAccount + onProgress?.(checkedAccount, index) + }) + .finally(() => { + activeCount-- + completedCount++ + // 继续处理下一个 + processNext() + }) + } + } + + // 开始处理 + processNext() + }) + } + + /** + * 处理单个账号检查 + * @private + */ + private async processAccount(account: AccountInput, index: number): Promise { + const checkResult = await this.checkAccount(account.token) + + return { + ...account, + id: index, + status: checkResult.status, + accountId: checkResult.accountId, + planType: checkResult.planType, + error: checkResult.error, + } + } +} + +/** + * 解析账号状态为中文描述 + */ +export function getStatusText(status: AccountStatus): string { + const statusMap: Record = { + pending: '待检查', + checking: '检查中', + active: '正常', + banned: '封禁', + token_expired: '过期', + error: '错误', + } + return statusMap[status] || status +} + +/** + * 获取状态对应的颜色类名 + */ +export function getStatusColor(status: AccountStatus): string { + const colorMap: Record = { + pending: 'gray', + checking: 'blue', + active: 'green', + banned: 'red', + token_expired: 'orange', + error: 'yellow', + } + return colorMap[status] || 'gray' +} + +/** + * 创建 ChatGPT 客户端实例 + * @param baseUrl - 可选的基础 URL,默认为空(使用相对路径) + */ +export function createChatGPTClient(baseUrl: string = ''): ChatGPTClient { + return new ChatGPTClient(baseUrl) +} + +// 导出默认客户端实例(使用相对路径,通过 nginx 代理) +export const chatGPTClient = new ChatGPTClient() diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts new file mode 100644 index 0000000..ab202da --- /dev/null +++ b/frontend/src/api/index.ts @@ -0,0 +1,4 @@ +// API Layer barrel export +export * from './types' +export * from './s2a' +export * from './chatgpt' diff --git a/frontend/src/api/s2a.ts b/frontend/src/api/s2a.ts new file mode 100644 index 0000000..7a5555d --- /dev/null +++ b/frontend/src/api/s2a.ts @@ -0,0 +1,155 @@ +import type { + DashboardStatsResponse, + DashboardTrendResponse, + AccountListResponse, + AccountResponse, + CreateAccountPayload, + OAuthCreatePayload, + GroupResponse, + ProxyResponse, + TestAccountResponse, +} from './types' +import type { AccountListParams } from '../types' + +// 使用后端代理 API 来避免 CORS 问题 +const PROXY_BASE = 'http://localhost:8088/api/s2a/proxy' + +export class S2AClient { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + constructor(_config: { baseUrl: string; apiKey: string }) { + // 不再使用直接配置,通过后端代理 + } + + private async request(endpoint: string, options: RequestInit = {}): Promise { + // 将 /api/v1/admin/* 转换为代理路径 + const proxyEndpoint = endpoint.replace('/api/v1/admin', '') + const url = `${PROXY_BASE}${proxyEndpoint}` + + const headers: HeadersInit = { + 'Content-Type': 'application/json', + ...options.headers, + } + + const response = await fetch(url, { + ...options, + headers, + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + throw new Error( + errorData.message || errorData.error || `HTTP ${response.status}: ${response.statusText}` + ) + } + + return response.json() + } + + // Dashboard APIs + async getDashboardStats(): Promise { + return this.request('/dashboard/stats') + } + + async getDashboardTrend(granularity: 'day' | 'hour' = 'day'): Promise { + return this.request( + `/dashboard/trend?granularity=${granularity}` + ) + } + + // Account APIs + async getAccounts(params: AccountListParams = {}): Promise { + const searchParams = new URLSearchParams() + if (params.page) searchParams.set('page', params.page.toString()) + if (params.page_size) searchParams.set('page_size', params.page_size.toString()) + if (params.platform) searchParams.set('platform', params.platform) + if (params.type) searchParams.set('type', params.type) + if (params.status) searchParams.set('status', params.status) + if (params.search) searchParams.set('search', params.search) + + const queryString = searchParams.toString() + const endpoint = `/accounts${queryString ? `?${queryString}` : ''}` + return this.request(endpoint) + } + + async getAccount(id: number): Promise { + return this.request(`/accounts/${id}`) + } + + async createAccount(data: CreateAccountPayload): Promise { + return this.request('/accounts', { + method: 'POST', + body: JSON.stringify(data), + }) + } + + async createFromOAuth(data: OAuthCreatePayload): Promise { + return this.request('/openai/create-from-oauth', { + method: 'POST', + body: JSON.stringify(data), + }) + } + + async updateAccount(id: number, data: Partial): Promise { + return this.request(`/accounts/${id}`, { + method: 'PUT', + body: JSON.stringify(data), + }) + } + + async deleteAccount(id: number): Promise { + await this.request(`/accounts/${id}`, { + method: 'DELETE', + }) + } + + async testAccount(id: number): Promise { + return this.request(`/accounts/${id}/test`, { + method: 'POST', + }) + } + + async refreshAccountToken(id: number): Promise { + return this.request(`/accounts/${id}/refresh`, { + method: 'POST', + }) + } + + async clearAccountError(id: number): Promise { + return this.request(`/accounts/${id}/clear-error`, { + method: 'POST', + }) + } + + // Group APIs + async getGroups(): Promise { + const response = await this.request<{ data: GroupResponse[] }>('/groups/all') + return response.data || [] + } + + // Proxy APIs + async getProxies(): Promise { + const response = await this.request<{ data: ProxyResponse[] }>('/proxies/all') + return response.data || [] + } + + async testProxy(id: number): Promise { + return this.request(`/proxies/${id}/test`, { + method: 'POST', + }) + } + + // Connection test + async testConnection(): Promise { + try { + await this.getDashboardStats() + return true + } catch { + return false + } + } +} + +// 创建默认客户端实例的工厂函数 +export function createS2AClient(baseUrl: string, apiKey: string): S2AClient { + return new S2AClient({ baseUrl, apiKey }) +} diff --git a/frontend/src/api/types.ts b/frontend/src/api/types.ts new file mode 100644 index 0000000..7dad0ab --- /dev/null +++ b/frontend/src/api/types.ts @@ -0,0 +1,522 @@ +// ============================================================================= +// S2A API 响应类型 +// 基于 requirements.md Appendix A2 定义 +// ============================================================================= + +// ----------------------------------------------------------------------------- +// Dashboard 相关接口响应 +// ----------------------------------------------------------------------------- + +/** + * Dashboard Stats 响应 + * API: GET /api/v1/admin/dashboard/stats + * 获取号池统计(账号数、请求数、Token消耗等) + */ +export interface DashboardStatsResponse { + total_accounts: number + normal_accounts: number + error_accounts: number + ratelimit_accounts: number + overload_accounts: number + today_requests: number + today_tokens: number + today_cost: number + total_requests: number + total_tokens: number + total_cost: number + rpm: number + tpm: number +} + +/** + * Dashboard Trend 响应 + * API: GET /api/v1/admin/dashboard/trend + * 获取使用趋势(支持 granularity=day/hour) + */ +export interface DashboardTrendResponse { + data: TrendDataPoint[] +} + +export interface TrendDataPoint { + date: string + requests: number + tokens: number + cost: number +} + +/** + * Dashboard Models 响应 + * API: GET /api/v1/admin/dashboard/models + * 获取模型使用统计 + */ +export interface DashboardModelsResponse { + data: ModelUsageStats[] +} + +export interface ModelUsageStats { + model: string + requests: number + tokens: number + cost: number + percentage: number +} + +/** + * Dashboard Users Trend 响应 + * API: GET /api/v1/admin/dashboard/users-trend + * 获取用户使用趋势 + */ +export interface DashboardUsersTrendResponse { + data: UsersTrendDataPoint[] +} + +export interface UsersTrendDataPoint { + date: string + active_users: number + new_users: number + total_users: number +} + +// ----------------------------------------------------------------------------- +// 账号管理接口响应 +// ----------------------------------------------------------------------------- + +/** + * 账号列表响应 + * API: GET /api/v1/admin/accounts + * 获取账号列表(支持分页、筛选) + */ +export interface AccountListResponse { + data: AccountResponse[] + total: number + page: number + page_size: number +} + +/** + * 单个账号响应 + * API: GET /api/v1/admin/accounts/:id + * 对应 requirements.md A5 Account 数据结构 + */ +export interface AccountResponse { + id: number + name: string + notes?: string + platform: 'openai' | 'anthropic' | 'gemini' + type: 'oauth' | 'access_token' | 'apikey' | 'setup-token' + credentials: Record + extra?: Record + proxy_id?: number + concurrency: number + priority: number + rate_multiplier?: number + status: 'active' | 'inactive' | 'error' + error_message?: string + schedulable: boolean + last_used_at?: string + expires_at?: string + auto_pause_on_expired: boolean + created_at: string + updated_at: string + current_concurrency?: number + current_window_cost?: number + active_sessions?: number +} + +/** + * 账号统计响应 + * API: GET /api/v1/admin/accounts/:id/stats + * 获取账号使用统计 + */ +export interface AccountStatsResponse { + account_id: number + total_requests: number + total_tokens: number + total_cost: number + today_requests: number + today_tokens: number + today_cost: number + last_used_at?: string + error_count: number + success_rate: number +} + +/** + * 创建账号请求 + * API: POST /api/v1/admin/accounts + * 对应 requirements.md A4 access_token 类型账号 + */ +export interface CreateAccountPayload { + name: string + platform: 'openai' | 'anthropic' | 'gemini' + type: 'access_token' + credentials: { + access_token: string + refresh_token?: string + email?: string + } + concurrency?: number + priority?: number + group_ids?: number[] + proxy_id?: number | null + auto_pause_on_expired?: boolean +} + +/** + * 批量更新账号请求 + * API: POST /api/v1/admin/accounts/bulk-update + */ +export interface BulkUpdateAccountsPayload { + ids: number[] + updates: { + status?: 'active' | 'inactive' + concurrency?: number + priority?: number + group_ids?: number[] + proxy_id?: number | null + } +} + +/** + * 批量更新账号响应 + */ +export interface BulkUpdateAccountsResponse { + success: boolean + updated_count: number + failed_count: number + errors?: string[] +} + +// ----------------------------------------------------------------------------- +// OpenAI OAuth 接口响应 +// ----------------------------------------------------------------------------- + +/** + * OAuth 创建账号请求 + * API: POST /api/v1/admin/openai/create-from-oauth + * 对应 requirements.md A4 OAuth 类型账号 + */ +export interface OAuthCreatePayload { + session_id: string + code: string + name?: string + concurrency?: number + priority?: number + group_ids?: number[] + proxy_id?: number | null +} + +/** + * 生成 OAuth 授权 URL 请求 + * API: POST /api/v1/admin/openai/generate-auth-url + */ +export interface GenerateAuthUrlPayload { + redirect_uri?: string +} + +/** + * 生成 OAuth 授权 URL 响应 + */ +export interface GenerateAuthUrlResponse { + auth_url: string + session_id: string +} + +/** + * 交换授权码请求 + * API: POST /api/v1/admin/openai/exchange-code + */ +export interface ExchangeCodePayload { + session_id: string + code: string +} + +/** + * 交换授权码响应 + */ +export interface ExchangeCodeResponse { + access_token: string + refresh_token?: string + expires_in?: number + token_type: string +} + +/** + * 刷新 Token 请求 + * API: POST /api/v1/admin/openai/refresh-token + */ +export interface RefreshTokenPayload { + refresh_token: string +} + +/** + * 刷新 Token 响应 + */ +export interface RefreshTokenResponse { + access_token: string + refresh_token?: string + expires_in?: number + token_type: string +} + +// ----------------------------------------------------------------------------- +// 分组管理接口响应 +// ----------------------------------------------------------------------------- + +/** + * 分组响应 + * API: GET /api/v1/admin/groups, GET /api/v1/admin/groups/all + */ +export interface GroupResponse { + id: number + name: string + description?: string + created_at: string + updated_at: string +} + +/** + * 分组统计响应 + * API: GET /api/v1/admin/groups/:id/stats + */ +export interface GroupStatsResponse { + group_id: number + total_accounts: number + active_accounts: number + error_accounts: number + total_requests: number + total_tokens: number + total_cost: number +} + +// ----------------------------------------------------------------------------- +// 代理管理接口响应 +// ----------------------------------------------------------------------------- + +/** + * 代理响应 + * API: GET /api/v1/admin/proxies, GET /api/v1/admin/proxies/all + */ +export interface ProxyResponse { + id: number + name: string + url: string + status: 'active' | 'inactive' | 'error' + created_at: string + updated_at: string +} + +/** + * 测试代理/账号响应 + * API: POST /api/v1/admin/proxies/:id/test, POST /api/v1/admin/accounts/:id/test + */ +export interface TestAccountResponse { + success: boolean + message?: string + latency?: number +} + +// ----------------------------------------------------------------------------- +// 运维监控接口响应 +// ----------------------------------------------------------------------------- + +/** + * 并发统计响应 + * API: GET /api/v1/admin/ops/concurrency + */ +export interface OpsConcurrencyResponse { + total_concurrency: number + used_concurrency: number + available_concurrency: number + accounts: AccountConcurrencyInfo[] +} + +export interface AccountConcurrencyInfo { + account_id: number + account_name: string + max_concurrency: number + current_concurrency: number +} + +/** + * 账号可用性响应 + * API: GET /api/v1/admin/ops/account-availability + */ +export interface OpsAccountAvailabilityResponse { + total_accounts: number + available_accounts: number + unavailable_accounts: number + availability_rate: number + accounts: AccountAvailabilityInfo[] +} + +export interface AccountAvailabilityInfo { + account_id: number + account_name: string + status: 'available' | 'unavailable' | 'rate_limited' | 'error' + reason?: string +} + +/** + * 实时流量响应 + * API: GET /api/v1/admin/ops/realtime-traffic + */ +export interface OpsRealtimeTrafficResponse { + current_rpm: number + current_tpm: number + peak_rpm: number + peak_tpm: number + requests_last_minute: number + tokens_last_minute: number +} + +/** + * 运维仪表盘概览响应 + * API: GET /api/v1/admin/ops/dashboard/overview + */ +export interface OpsDashboardOverviewResponse { + health_score: number + total_accounts: number + healthy_accounts: number + warning_accounts: number + error_accounts: number + current_load: number + max_load: number + alerts: OpsAlert[] +} + +export interface OpsAlert { + id: string + level: 'info' | 'warning' | 'error' | 'critical' + message: string + timestamp: string + account_id?: number +} + +/** + * 错误趋势响应 + * API: GET /api/v1/admin/ops/dashboard/error-trend + */ +export interface OpsErrorTrendResponse { + data: ErrorTrendDataPoint[] +} + +export interface ErrorTrendDataPoint { + date: string + total_errors: number + rate_limit_errors: number + auth_errors: number + network_errors: number + other_errors: number +} + +// ============================================================================= +// ChatGPT API 响应类型 +// 基于 requirements.md Requirement 1.2 定义 +// ============================================================================= + +/** + * 账号检查响应 + * API: GET https://chatgpt.com/backend-api/accounts/check/v4-2023-04-27 + * Headers: Authorization: Bearer {token} + */ +export interface ChatGPTCheckResponse { + accounts: ChatGPTAccountInfo[] +} + +export interface ChatGPTAccountInfo { + account_id: string + account: ChatGPTAccountDetails + features: string[] + entitlement: ChatGPTEntitlement + last_active_subscription: ChatGPTLastActiveSubscription + is_eligible_for_yearly_plus_subscription: boolean +} + +export interface ChatGPTAccountDetails { + account_user_id: string + processor: { + a001: { + has_customer_object: boolean + } + } + account_user_role: string + plan_type: string + is_most_recent_expired_subscription_gratis: boolean + has_previously_paid_subscription: boolean + name: string | null + profile_picture_id: string | null + profile_picture_url: string | null + structure: string + is_deactivated: boolean + is_disabled: boolean + // SAM (Security Account Management) 相关字段 + is_sam_enforced: boolean + is_sam_enabled: boolean + is_sam_compliant: boolean + is_sam_grace_period: boolean + is_sam_grace_period_expired: boolean + is_sam_grace_period_expiring_soon: boolean + is_sam_grace_period_expiring_today: boolean + is_sam_grace_period_expiring_tomorrow: boolean + is_sam_grace_period_expiring_in_two_days: boolean + is_sam_grace_period_expiring_in_three_days: boolean + is_sam_grace_period_expiring_in_four_days: boolean + is_sam_grace_period_expiring_in_five_days: boolean + is_sam_grace_period_expiring_in_six_days: boolean + is_sam_grace_period_expiring_in_seven_days: boolean +} + +export interface ChatGPTEntitlement { + subscription_id: string | null + has_active_subscription: boolean + subscription_plan: string + expires_at: string | null +} + +export interface ChatGPTLastActiveSubscription { + subscription_id: string | null + purchase_origin_platform: string + will_renew: boolean +} + +// ============================================================================= +// 通用 API 类型 +// ============================================================================= + +/** + * API 错误响应 + * 通用错误响应格式 + */ +export interface ApiErrorResponse { + error: string + message?: string + code?: string + details?: Record +} + +/** + * 分页请求参数 + */ +export interface PaginationParams { + page?: number + page_size?: number +} + +/** + * 分页响应包装 + */ +export interface PaginatedResponse { + data: T[] + total: number + page: number + page_size: number + total_pages: number +} + +/** + * 通用列表响应包装 + */ +export interface ListResponse { + data: T[] +} diff --git a/frontend/src/components/common/Button.test.tsx b/frontend/src/components/common/Button.test.tsx new file mode 100644 index 0000000..c9a736c --- /dev/null +++ b/frontend/src/components/common/Button.test.tsx @@ -0,0 +1,215 @@ +import { describe, it, expect, vi } from 'vitest' +import { render, screen, fireEvent } from '@testing-library/react' +import Button from './Button' + +describe('Button', () => { + describe('rendering', () => { + it('renders children correctly', () => { + render() + expect(screen.getByRole('button', { name: 'Click me' })).toBeInTheDocument() + }) + + it('renders with default props', () => { + render() + const button = screen.getByRole('button') + // Default variant is primary, default size is md + expect(button).toHaveClass('bg-primary-600') + expect(button).toHaveClass('px-4', 'py-2') + }) + }) + + describe('variants', () => { + it('renders primary variant correctly', () => { + render() + const button = screen.getByRole('button') + expect(button).toHaveClass('bg-primary-600', 'text-white') + }) + + it('renders secondary variant correctly', () => { + render() + const button = screen.getByRole('button') + expect(button).toHaveClass('bg-slate-100', 'text-slate-900') + }) + + it('renders danger variant correctly', () => { + render() + const button = screen.getByRole('button') + expect(button).toHaveClass('bg-error-500', 'text-white') + }) + + it('renders ghost variant correctly', () => { + render() + const button = screen.getByRole('button') + expect(button).toHaveClass('text-slate-700', 'bg-transparent') + }) + + it('renders outline variant correctly', () => { + render() + const button = screen.getByRole('button') + expect(button).toHaveClass('border', 'border-slate-300', 'text-slate-700') + }) + }) + + describe('sizes', () => { + it('renders small size correctly', () => { + render() + const button = screen.getByRole('button') + expect(button).toHaveClass('px-3', 'py-1.5', 'text-sm') + }) + + it('renders medium size correctly', () => { + render() + const button = screen.getByRole('button') + expect(button).toHaveClass('px-4', 'py-2', 'text-sm') + }) + + it('renders large size correctly', () => { + render() + const button = screen.getByRole('button') + expect(button).toHaveClass('px-6', 'py-3', 'text-base') + }) + }) + + describe('loading state', () => { + it('shows spinner when loading', () => { + render() + const button = screen.getByRole('button') + // Check for the spinner (Loader2 icon with animate-spin class) + const spinner = button.querySelector('.animate-spin') + expect(spinner).toBeInTheDocument() + }) + + it('disables button when loading', () => { + render() + const button = screen.getByRole('button') + expect(button).toBeDisabled() + }) + + it('sets aria-busy when loading', () => { + render() + const button = screen.getByRole('button') + expect(button).toHaveAttribute('aria-busy', 'true') + }) + + it('hides icon when loading', () => { + const icon = Icon + render( + + ) + expect(screen.queryByTestId('test-icon')).not.toBeInTheDocument() + }) + }) + + describe('disabled state', () => { + it('disables button when disabled prop is true', () => { + render() + const button = screen.getByRole('button') + expect(button).toBeDisabled() + }) + + it('applies disabled styles', () => { + render() + const button = screen.getByRole('button') + expect(button).toHaveClass('disabled:opacity-50', 'disabled:cursor-not-allowed') + }) + + it('sets aria-disabled when disabled', () => { + render() + const button = screen.getByRole('button') + expect(button).toHaveAttribute('aria-disabled', 'true') + }) + + it('does not call onClick when disabled', () => { + const handleClick = vi.fn() + render( + + ) + fireEvent.click(screen.getByRole('button')) + expect(handleClick).not.toHaveBeenCalled() + }) + }) + + describe('icon support', () => { + it('renders icon when provided', () => { + const icon = + render() + expect(screen.getByTestId('test-icon')).toBeInTheDocument() + }) + + it('renders icon before text', () => { + const icon = + render() + const button = screen.getByRole('button') + const iconElement = screen.getByTestId('test-icon') + // Icon should be a child of the button + expect(button).toContainElement(iconElement) + }) + }) + + describe('click handling', () => { + it('calls onClick when clicked', () => { + const handleClick = vi.fn() + render() + fireEvent.click(screen.getByRole('button')) + expect(handleClick).toHaveBeenCalledTimes(1) + }) + + it('does not call onClick when loading', () => { + const handleClick = vi.fn() + render( + + ) + fireEvent.click(screen.getByRole('button')) + expect(handleClick).not.toHaveBeenCalled() + }) + }) + + describe('custom className', () => { + it('applies custom className', () => { + render() + const button = screen.getByRole('button') + expect(button).toHaveClass('custom-class') + }) + + it('merges custom className with default classes', () => { + render() + const button = screen.getByRole('button') + expect(button).toHaveClass('custom-class', 'bg-primary-600') + }) + }) + + describe('forwarded ref', () => { + it('forwards ref to button element', () => { + const ref = vi.fn() + render() + expect(ref).toHaveBeenCalled() + expect(ref.mock.calls[0][0]).toBeInstanceOf(HTMLButtonElement) + }) + }) + + describe('HTML button attributes', () => { + it('passes through type attribute', () => { + render() + const button = screen.getByRole('button') + expect(button).toHaveAttribute('type', 'submit') + }) + + it('passes through form attribute', () => { + render() + const button = screen.getByRole('button') + expect(button).toHaveAttribute('form', 'my-form') + }) + + it('passes through aria-label', () => { + render() + const button = screen.getByRole('button', { name: 'Close dialog' }) + expect(button).toBeInTheDocument() + }) + }) +}) diff --git a/frontend/src/components/common/Button.tsx b/frontend/src/components/common/Button.tsx new file mode 100644 index 0000000..da1eeac --- /dev/null +++ b/frontend/src/components/common/Button.tsx @@ -0,0 +1,158 @@ +import { type ButtonHTMLAttributes, forwardRef } from 'react' +import { Loader2 } from 'lucide-react' + +/** + * Button component variants + * - primary: Main action button using design system primary color (Blue-600) + * - secondary: Secondary action button with subtle background + * - outline: Bordered button with transparent background + * - danger: Destructive action button using design system error color (Red-500) + * - ghost: Minimal button with no background, only hover state + */ +export type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'danger' | 'ghost' + +/** + * Button component sizes + * - sm: Small button for compact UIs + * - md: Medium button (default) + * - lg: Large button for prominent actions + */ +export type ButtonSize = 'sm' | 'md' | 'lg' + +export interface ButtonProps extends ButtonHTMLAttributes { + /** Visual style variant of the button */ + variant?: ButtonVariant + /** Size of the button */ + size?: ButtonSize + /** Shows a loading spinner and disables the button */ + loading?: boolean + /** Optional icon to display before the button text */ + icon?: React.ReactNode +} + +/** + * A reusable Button component with support for multiple variants, sizes, + * loading state, and disabled state. Uses TailwindCSS with design system colors. + * + * @example + * // Primary button + * + * + * @example + * // Loading state + * + * + * @example + * // With icon + * + */ +const Button = forwardRef( + ( + { + className = '', + variant = 'primary', + size = 'md', + loading = false, + disabled, + icon, + children, + ...props + }, + ref + ) => { + // Base styles applied to all button variants + const baseStyles = [ + 'inline-flex items-center justify-center', + 'font-medium rounded-lg', + 'transition-colors duration-200', + 'focus:outline-none focus:ring-2 focus:ring-offset-2', + 'disabled:opacity-50 disabled:cursor-not-allowed', + ].join(' ') + + // Variant-specific styles using design system colors + const variantStyles: Record = { + // Primary: Blue-600 (#2563EB) - Main action button + primary: [ + 'bg-primary-600 text-white', + 'hover:bg-primary-700', + 'focus:ring-primary-500', + 'dark:bg-primary-500 dark:hover:bg-primary-600', + ].join(' '), + + // Secondary: Slate background - Secondary action button + secondary: [ + 'bg-slate-100 text-slate-900', + 'hover:bg-slate-200', + 'focus:ring-slate-500', + 'dark:bg-slate-700 dark:text-slate-100 dark:hover:bg-slate-600', + ].join(' '), + + // Outline: Bordered button with transparent background + outline: [ + 'border border-slate-300 text-slate-700 bg-transparent', + 'hover:bg-slate-50', + 'focus:ring-slate-500', + 'dark:border-slate-600 dark:text-slate-300 dark:hover:bg-slate-800', + ].join(' '), + + // Danger: Red-500 (#EF4444) - Destructive action button + danger: [ + 'bg-error-500 text-white', + 'hover:bg-error-600', + 'focus:ring-error-500', + 'dark:bg-error-500 dark:hover:bg-error-600', + ].join(' '), + + // Ghost: Transparent background - Minimal button + ghost: [ + 'text-slate-700 bg-transparent', + 'hover:bg-slate-100', + 'focus:ring-slate-500', + 'dark:text-slate-300 dark:hover:bg-slate-800', + ].join(' '), + } + + // Size-specific styles + const sizeStyles: Record = { + sm: 'px-3 py-1.5 text-sm gap-1.5', + md: 'px-4 py-2 text-sm gap-2', + lg: 'px-6 py-3 text-base gap-2', + } + + // Spinner size based on button size + const spinnerSizeStyles: Record = { + sm: 'h-3.5 w-3.5', + md: 'h-4 w-4', + lg: 'h-5 w-5', + } + + const isDisabled = disabled || loading + + return ( + + ) + } +) + +Button.displayName = 'Button' + +export default Button diff --git a/frontend/src/components/common/Card.tsx b/frontend/src/components/common/Card.tsx new file mode 100644 index 0000000..ca3903c --- /dev/null +++ b/frontend/src/components/common/Card.tsx @@ -0,0 +1,84 @@ +import { type HTMLAttributes, forwardRef } from 'react' + +export interface CardProps extends HTMLAttributes { + padding?: 'none' | 'sm' | 'md' | 'lg' + hoverable?: boolean + variant?: 'default' | 'glass' +} + +const Card = forwardRef( + ({ className = '', padding = 'md', hoverable = false, variant = 'default', children, ...props }, ref) => { + const paddingStyles = { + none: '', + sm: 'p-3', + md: 'p-4', + lg: 'p-6', + } + + const baseStyles = variant === 'glass' + ? 'glass-card' + : 'bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 shadow-sm rounded-xl' + + const hoverStyles = hoverable ? 'card-hover cursor-pointer' : '' + + return ( +
+ {children} +
+ ) + } +) + +Card.displayName = 'Card' + +export interface CardHeaderProps extends HTMLAttributes {} + +export const CardHeader = forwardRef( + ({ className = '', children, ...props }, ref) => { + return ( +
+ {children} +
+ ) + } +) + +CardHeader.displayName = 'CardHeader' + +export interface CardTitleProps extends HTMLAttributes {} + +export const CardTitle = forwardRef( + ({ className = '', children, ...props }, ref) => { + return ( +

+ {children} +

+ ) + } +) + +CardTitle.displayName = 'CardTitle' + +export interface CardContentProps extends HTMLAttributes {} + +export const CardContent = forwardRef( + ({ className = '', children, ...props }, ref) => { + return ( +
+ {children} +
+ ) + } +) + +CardContent.displayName = 'CardContent' + +export default Card diff --git a/frontend/src/components/common/ErrorBoundary.tsx b/frontend/src/components/common/ErrorBoundary.tsx new file mode 100644 index 0000000..c11d1cb --- /dev/null +++ b/frontend/src/components/common/ErrorBoundary.tsx @@ -0,0 +1,61 @@ +import { Component, type ErrorInfo, type ReactNode } from 'react' +import { AlertTriangle, RefreshCw } from 'lucide-react' +import Button from './Button' + +interface Props { + children: ReactNode + fallback?: ReactNode +} + +interface State { + hasError: boolean + error: Error | null +} + +export class ErrorBoundary extends Component { + constructor(props: Props) { + super(props) + this.state = { hasError: false, error: null } + } + + static getDerivedStateFromError(error: Error): State { + return { hasError: true, error } + } + + componentDidCatch(error: Error, errorInfo: ErrorInfo) { + console.error('ErrorBoundary caught an error:', error, errorInfo) + } + + handleReset = () => { + this.setState({ hasError: false, error: null }) + } + + render() { + if (this.state.hasError) { + if (this.props.fallback) { + return this.props.fallback + } + + return ( +
+
+
+ +
+

+ 出错了 +

+

+ {this.state.error?.message || '发生了一个意外错误'} +

+ +
+
+ ) + } + + return this.props.children + } +} diff --git a/frontend/src/components/common/Input.tsx b/frontend/src/components/common/Input.tsx new file mode 100644 index 0000000..b07a60a --- /dev/null +++ b/frontend/src/components/common/Input.tsx @@ -0,0 +1,100 @@ +import { type InputHTMLAttributes, type TextareaHTMLAttributes, forwardRef } from 'react' + +export interface InputProps extends InputHTMLAttributes { + label?: string + error?: string + hint?: string +} + +const Input = forwardRef( + ({ className = '', label, error, hint, id, ...props }, ref) => { + const inputId = id || label?.toLowerCase().replace(/\s+/g, '-') + + return ( +
+ {label && ( + + )} + + {error &&

{error}

} + {hint && !error && ( +

{hint}

+ )} +
+ ) + } +) + +Input.displayName = 'Input' + +export interface TextareaProps extends TextareaHTMLAttributes { + label?: string + error?: string + hint?: string +} + +export const Textarea = forwardRef( + ({ className = '', label, error, hint, id, ...props }, ref) => { + const textareaId = id || label?.toLowerCase().replace(/\s+/g, '-') + + return ( +
+ {label && ( + + )} +