From 6323e70e4986dbff4617f6d742cf62de53ad12ea Mon Sep 17 00:00:00 2001 From: wangbo Date: Sat, 9 May 2026 14:36:35 +0800 Subject: [PATCH] Initial project scaffold Co-authored-by: Cursor --- .env.example | 25 + .gitignore | 12 + README.md | 60 + apps/api/cmd/gateway/main.go | 56 + apps/api/cmd/migrate/main.go | 85 + apps/api/go.mod | 8 + apps/api/internal/auth/auth.go | 222 ++ apps/api/internal/config/config.go | 99 + apps/api/internal/httpapi/handlers.go | 218 ++ apps/api/internal/httpapi/response.go | 28 + apps/api/internal/httpapi/server.go | 78 + apps/api/internal/store/postgres.go | 508 +++ apps/api/migrations/0001_init.sql | 333 ++ apps/api/project.json | 39 + apps/web/index.html | 12 + apps/web/package.json | 24 + apps/web/project.json | 38 + apps/web/src/App.tsx | 227 ++ apps/web/src/api.ts | 60 + apps/web/src/main.tsx | 10 + apps/web/src/styles.css | 286 ++ apps/web/src/vite-env.d.ts | 1 + apps/web/tsconfig.json | 21 + apps/web/vite.config.ts | 9 + docs/design.md | 1584 ++++++++++ docs/migration-plan.md | 49 + docs/server-main-integration.md | 87 + go.work | 5 + go.work.sum | 16 + nx.json | 19 + package.json | 21 + packages/contracts/package.json | 17 + packages/contracts/project.json | 23 + packages/contracts/src/index.ts | 163 + packages/contracts/tsconfig.json | 11 + pnpm-lock.yaml | 4172 +++++++++++++++++++++++++ pnpm-workspace.yaml | 3 + scripts/create-database.sh | 22 + scripts/dev.sh | 13 + 39 files changed, 8664 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 README.md create mode 100644 apps/api/cmd/gateway/main.go create mode 100644 apps/api/cmd/migrate/main.go create mode 100644 apps/api/go.mod create mode 100644 apps/api/internal/auth/auth.go create mode 100644 apps/api/internal/config/config.go create mode 100644 apps/api/internal/httpapi/handlers.go create mode 100644 apps/api/internal/httpapi/response.go create mode 100644 apps/api/internal/httpapi/server.go create mode 100644 apps/api/internal/store/postgres.go create mode 100644 apps/api/migrations/0001_init.sql create mode 100644 apps/api/project.json create mode 100644 apps/web/index.html create mode 100644 apps/web/package.json create mode 100644 apps/web/project.json create mode 100644 apps/web/src/App.tsx create mode 100644 apps/web/src/api.ts create mode 100644 apps/web/src/main.tsx create mode 100644 apps/web/src/styles.css create mode 100644 apps/web/src/vite-env.d.ts create mode 100644 apps/web/tsconfig.json create mode 100644 apps/web/vite.config.ts create mode 100644 docs/design.md create mode 100644 docs/migration-plan.md create mode 100644 docs/server-main-integration.md create mode 100644 go.work create mode 100644 go.work.sum create mode 100644 nx.json create mode 100644 package.json create mode 100644 packages/contracts/package.json create mode 100644 packages/contracts/project.json create mode 100644 packages/contracts/src/index.ts create mode 100644 packages/contracts/tsconfig.json create mode 100644 pnpm-lock.yaml create mode 100644 pnpm-workspace.yaml create mode 100755 scripts/create-database.sh create mode 100755 scripts/dev.sh diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..54744b8 --- /dev/null +++ b/.env.example @@ -0,0 +1,25 @@ +APP_ENV=development +HTTP_ADDR=:8088 + +# Reuse the same PostgreSQL instance as Agent memory, but use an independent +# database. When running from the host, use the externally reachable host/port. +AI_GATEWAY_DATABASE_NAME=easyai_ai_gateway +AI_GATEWAY_DATABASE_URL=postgresql://easyai:easyai2025@localhost:5432/easyai_ai_gateway?sslmode=disable + +# When running inside the EasyAI docker network, use the container DNS name: +# AI_GATEWAY_DATABASE_URL=postgresql://easyai:easyai2025@easyai-pgvector:5432/easyai_ai_gateway?schema=public +# +# If AI_GATEWAY_DATABASE_URL is omitted, the service can derive host/user/pass +# from MEMORY_DATABASE_URL but will still replace the database with +# AI_GATEWAY_DATABASE_NAME. +# MEMORY_DATABASE_URL=postgresql://easyai:easyai2025@easyai-pgvector:5432/easyai_memory?schema=public + +# Keep this aligned with easyai-server-main CONFIG_JWT_SECRET in the first migration phase. +CONFIG_JWT_SECRET=this is a very secret secret + +# Used when the gateway delegates OpenAPI sk-* validation, file upload, and settlement callbacks. +SERVER_MAIN_BASE_URL=http://localhost:3000 +SERVER_MAIN_INTERNAL_TOKEN=change-me + +CORS_ALLOWED_ORIGIN=http://localhost:5178 +VITE_GATEWAY_API_BASE_URL=http://localhost:8088 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..54811f2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +dist/ +node_modules/ +.nx/ +.turbo/ +.DS_Store +.env +*.log + +apps/api/bin/ +apps/api/tmp/ + +coverage/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..798f860 --- /dev/null +++ b/README.md @@ -0,0 +1,60 @@ +# EasyAI AI Gateway + +独立的 AI 网关中台脚手架,用于把现有 `integration-platform` 的平台管理、模型路由、计费预估、队列执行、Chat / 生图 / 生视频等生成能力逐步从 `easyai-server-main` 拆成可独立运行的项目。 + +## 技术选型 + +- 后端:Go + PostgreSQL,复用 Agent memory 的 `easyai-pgvector`,保留 `server-main` 的 JWT / API Key 授权语义。 +- 前端:React + TypeScript + TSX,UI 体系按 `shadcn-ui` / Radix / Tailwind 方向沉淀,先提供运维控制台骨架。 +- Monorepo:Nx 负责任务编排,Go 使用 `go.work` 管理模块。 +- 集成:完成后由 `easyai-server-main` 通过内部 HTTP SDK 直连本服务,前端经网关访问本服务。 + +## 目录 + +```text +apps/ + api/ Go HTTP API, auth middleware, PG store, migrations + web/ React TSX admin console +packages/ + contracts/ Shared TypeScript DTO contracts +docs/ + design.md Detailed architecture and migration design +``` + +## 本地启动 + +```bash +cp .env.example .env +pnpm install +pnpm dev +``` + +服务默认地址: + +- API: `http://localhost:8088` +- Web: `http://localhost:5178` +- PostgreSQL: 默认使用宿主机 `localhost:5432` 上的 `postgres` 容器,并使用独立库 `easyai_ai_gateway` + +默认 EasyAI 部署里,`easyai-pgvector` 在容器网络内的连接串是: + +```dotenv +AI_GATEWAY_DATABASE_URL=postgresql://easyai:easyai2025@easyai-pgvector:5432/easyai_ai_gateway?schema=public +``` + +宿主机直跑时需要使用宿主机可访问的 Postgres 地址。如果 `easyai-pgvector` 将 `5432` 映射到了本机,可使用: + +```dotenv +AI_GATEWAY_DATABASE_URL=postgresql://easyai:easyai2025@localhost:5432/easyai_ai_gateway?sslmode=disable +``` + +如果现有 `easyai-pgvector` 没有把 `5432` 映射到宿主机,就需要补端口映射,或者把 AI Gateway 后端容器化后接入同一个 `easyai` Docker network。 + +## 迁移原则 + +1. 新服务先并行运行,不直接删除 `easyai-server-main` 内现有模块。 +2. 授权先复用 `server-main` 的 JWT secret、claim、角色权限模型。 +3. OpenAPI `sk-*` 校验、文件上传、扣费结算仍由 `server-main` 承担。 +4. 网关服务负责基准模型库、平台模型路由、TPM/RPM/并发限流、任务队列、三方平台执行、任务进度推送。 +5. 切流时优先让 `server-main` 的 `OpenaiService` 变成薄门面,内部调用本服务。 + +详细设计见 [docs/design.md](docs/design.md)。 diff --git a/apps/api/cmd/gateway/main.go b/apps/api/cmd/gateway/main.go new file mode 100644 index 0000000..255e296 --- /dev/null +++ b/apps/api/cmd/gateway/main.go @@ -0,0 +1,56 @@ +package main + +import ( + "context" + "errors" + "log/slog" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "github.com/easyai/easyai-ai-gateway/apps/api/internal/config" + "github.com/easyai/easyai-ai-gateway/apps/api/internal/httpapi" + "github.com/easyai/easyai-ai-gateway/apps/api/internal/store" +) + +func main() { + cfg := config.Load() + logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ + Level: cfg.LogLevel, + })) + + ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + defer stop() + + db, err := store.Connect(ctx, cfg.DatabaseURL) + if err != nil { + logger.Error("connect postgres failed", "error", err) + os.Exit(1) + } + defer db.Close() + + server := &http.Server{ + Addr: cfg.HTTPAddr, + Handler: httpapi.NewServer(cfg, db, logger), + ReadHeaderTimeout: 10 * time.Second, + } + + go func() { + logger.Info("easyai ai gateway api started", "addr", cfg.HTTPAddr) + if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { + logger.Error("http server failed", "error", err) + os.Exit(1) + } + }() + + <-ctx.Done() + shutdownCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + if err := server.Shutdown(shutdownCtx); err != nil { + logger.Error("http shutdown failed", "error", err) + os.Exit(1) + } + logger.Info("easyai ai gateway api stopped") +} diff --git a/apps/api/cmd/migrate/main.go b/apps/api/cmd/migrate/main.go new file mode 100644 index 0000000..1a4d4b9 --- /dev/null +++ b/apps/api/cmd/migrate/main.go @@ -0,0 +1,85 @@ +package main + +import ( + "context" + "fmt" + "log/slog" + "os" + "path/filepath" + "sort" + "strings" + + "github.com/easyai/easyai-ai-gateway/apps/api/internal/config" + "github.com/jackc/pgx/v5" +) + +func main() { + cfg := config.Load() + logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) + ctx := context.Background() + + conn, err := pgx.Connect(ctx, cfg.DatabaseURL) + if err != nil { + logger.Error("connect postgres failed", "error", err) + os.Exit(1) + } + defer conn.Close(ctx) + + if _, err := conn.Exec(ctx, ` +CREATE TABLE IF NOT EXISTS schema_migrations ( + version text PRIMARY KEY, + applied_at timestamptz NOT NULL DEFAULT now() +);`); err != nil { + logger.Error("ensure schema_migrations failed", "error", err) + os.Exit(1) + } + + files, err := filepath.Glob("migrations/*.sql") + if err != nil { + logger.Error("read migrations failed", "error", err) + os.Exit(1) + } + sort.Strings(files) + + for _, file := range files { + version := strings.TrimSuffix(filepath.Base(file), filepath.Ext(file)) + var exists bool + if err := conn.QueryRow(ctx, "SELECT EXISTS (SELECT 1 FROM schema_migrations WHERE version=$1)", version).Scan(&exists); err != nil { + logger.Error("check migration failed", "version", version, "error", err) + os.Exit(1) + } + if exists { + logger.Info("migration skipped", "version", version) + continue + } + + sqlBytes, err := os.ReadFile(file) + if err != nil { + logger.Error("read migration file failed", "file", file, "error", err) + os.Exit(1) + } + + tx, err := conn.Begin(ctx) + if err != nil { + logger.Error("begin migration failed", "version", version, "error", err) + os.Exit(1) + } + if _, err := tx.Exec(ctx, string(sqlBytes)); err != nil { + _ = tx.Rollback(ctx) + logger.Error("execute migration failed", "version", version, "error", err) + os.Exit(1) + } + if _, err := tx.Exec(ctx, "INSERT INTO schema_migrations(version) VALUES($1)", version); err != nil { + _ = tx.Rollback(ctx) + logger.Error("record migration failed", "version", version, "error", err) + os.Exit(1) + } + if err := tx.Commit(ctx); err != nil { + logger.Error("commit migration failed", "version", version, "error", err) + os.Exit(1) + } + logger.Info("migration applied", "version", version) + } + + fmt.Println("migrations complete") +} diff --git a/apps/api/go.mod b/apps/api/go.mod new file mode 100644 index 0000000..8de3e49 --- /dev/null +++ b/apps/api/go.mod @@ -0,0 +1,8 @@ +module github.com/easyai/easyai-ai-gateway/apps/api + +go 1.23 + +require ( + github.com/golang-jwt/jwt/v5 v5.2.2 + github.com/jackc/pgx/v5 v5.7.2 +) diff --git a/apps/api/internal/auth/auth.go b/apps/api/internal/auth/auth.go new file mode 100644 index 0000000..62f6dda --- /dev/null +++ b/apps/api/internal/auth/auth.go @@ -0,0 +1,222 @@ +package auth + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "strings" + "time" + + "github.com/golang-jwt/jwt/v5" +) + +type Permission string + +const ( + PermissionPublic Permission = "public" + PermissionBasic Permission = "basic" + PermissionCreat Permission = "creat" + PermissionPower Permission = "power" + PermissionManager Permission = "manager" +) + +type User struct { + ID string `json:"sub"` + Username string `json:"username"` + Roles []string `json:"role,omitempty"` + TenantID string `json:"tenantId,omitempty"` + SSOID string `json:"sso_id,omitempty"` + APIKeyID string `json:"apiKeyId,omitempty"` + APIKeySecret string `json:"apiKeySecret,omitempty"` + APIKeyName string `json:"apiKeyName,omitempty"` +} + +type contextKey string + +const userContextKey contextKey = "easyai-auth-user" + +var ErrUnauthorized = errors.New("unauthorized") + +type Authenticator struct { + JWTSecret string + ServerMainBaseURL string + ServerMainInternalToken string + HTTPClient *http.Client +} + +func New(jwtSecret string, serverMainBaseURL string, internalToken string) *Authenticator { + return &Authenticator{ + JWTSecret: jwtSecret, + ServerMainBaseURL: strings.TrimRight(serverMainBaseURL, "/"), + ServerMainInternalToken: internalToken, + HTTPClient: &http.Client{ + Timeout: 10 * time.Second, + }, + } +} + +func UserFromContext(ctx context.Context) (*User, bool) { + user, ok := ctx.Value(userContextKey).(*User) + return user, ok +} + +func (a *Authenticator) Require(permission Permission, next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + user, err := a.Authenticate(r) + if err != nil { + if permission == PermissionPublic { + next.ServeHTTP(w, r) + return + } + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + if !hasPermission(user.Roles, permission) { + http.Error(w, "forbidden", http.StatusForbidden) + return + } + next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), userContextKey, user))) + }) +} + +func (a *Authenticator) Authenticate(r *http.Request) (*User, error) { + token := extractBearer(r.Header.Get("Authorization")) + if token == "" { + token = strings.TrimSpace(r.Header.Get("x-comfy-api-key")) + } + if token == "" { + return nil, ErrUnauthorized + } + if strings.HasPrefix(token, "sk-") { + return a.verifyAPIKey(r.Context(), token) + } + return a.verifyJWT(token) +} + +func (a *Authenticator) verifyJWT(tokenString string) (*User, error) { + token, err := jwt.ParseWithClaims(tokenString, jwt.MapClaims{}, func(token *jwt.Token) (any, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + return []byte(a.JWTSecret), nil + }) + if err != nil || !token.Valid { + return nil, ErrUnauthorized + } + claims, ok := token.Claims.(jwt.MapClaims) + if !ok { + return nil, ErrUnauthorized + } + + user := &User{ + ID: stringClaim(claims, "sub"), + Username: stringClaim(claims, "username"), + Roles: stringSliceClaim(claims, "role"), + TenantID: stringClaim(claims, "tenantId"), + SSOID: stringClaim(claims, "sso_id"), + APIKeyID: stringClaim(claims, "apiKeyId"), + APIKeySecret: stringClaim(claims, "apiKeySecret"), + APIKeyName: stringClaim(claims, "apiKeyName"), + } + if user.ID == "" { + return nil, ErrUnauthorized + } + return user, nil +} + +func (a *Authenticator) verifyAPIKey(ctx context.Context, apiKey string) (*User, error) { + if a.ServerMainBaseURL == "" || a.ServerMainInternalToken == "" { + return nil, ErrUnauthorized + } + body, _ := json.Marshal(map[string]string{"apiKey": apiKey}) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, a.ServerMainBaseURL+"/internal/platform/auth/verify-api-key", bytes.NewReader(body)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+a.ServerMainInternalToken) + + resp, err := a.HTTPClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, ErrUnauthorized + } + var user User + if err := json.NewDecoder(resp.Body).Decode(&user); err != nil { + return nil, err + } + if user.ID == "" { + return nil, ErrUnauthorized + } + return &user, nil +} + +func extractBearer(value string) string { + fields := strings.Fields(value) + if len(fields) == 2 && strings.EqualFold(fields[0], "bearer") { + return fields[1] + } + return "" +} + +func hasPermission(roles []string, required Permission) bool { + if required == PermissionPublic { + return true + } + granted := map[Permission]bool{PermissionPublic: true} + for _, role := range roles { + for _, permission := range permissionsForRole(role) { + granted[permission] = true + } + } + return granted[required] +} + +func permissionsForRole(role string) []Permission { + switch role { + case "admin", "manager": + return []Permission{PermissionPublic, PermissionBasic, PermissionCreat, PermissionPower, PermissionManager} + case "operator": + return []Permission{PermissionPublic, PermissionBasic, PermissionCreat, PermissionPower} + case "creator": + return []Permission{PermissionPublic, PermissionBasic, PermissionCreat} + case "user": + return []Permission{PermissionPublic, PermissionBasic} + default: + return []Permission{PermissionPublic} + } +} + +func stringClaim(claims jwt.MapClaims, key string) string { + value, _ := claims[key].(string) + return value +} + +func stringSliceClaim(claims jwt.MapClaims, key string) []string { + value := claims[key] + switch typed := value.(type) { + case []string: + return typed + case []any: + out := make([]string, 0, len(typed)) + for _, item := range typed { + if s, ok := item.(string); ok && s != "" { + out = append(out, s) + } + } + return out + case string: + if typed == "" { + return nil + } + return []string{typed} + default: + return nil + } +} diff --git a/apps/api/internal/config/config.go b/apps/api/internal/config/config.go new file mode 100644 index 0000000..1bd7d7a --- /dev/null +++ b/apps/api/internal/config/config.go @@ -0,0 +1,99 @@ +package config + +import ( + "log/slog" + "net/url" + "os" + "strings" +) + +type Config struct { + AppEnv string + HTTPAddr string + DatabaseURL string + JWTSecret string + ServerMainBaseURL string + ServerMainInternalToken string + CORSAllowedOrigin string + LogLevel slog.Level +} + +func Load() Config { + return Config{ + AppEnv: env("APP_ENV", "development"), + HTTPAddr: env("HTTP_ADDR", ":8088"), + DatabaseURL: gatewayDatabaseURL(), + JWTSecret: env("CONFIG_JWT_SECRET", "this is a very secret secret"), + ServerMainBaseURL: strings.TrimRight( + env("SERVER_MAIN_BASE_URL", "http://localhost:3000"), + "/", + ), + ServerMainInternalToken: env("SERVER_MAIN_INTERNAL_TOKEN", ""), + CORSAllowedOrigin: env("CORS_ALLOWED_ORIGIN", "http://localhost:5178"), + LogLevel: logLevel(env("LOG_LEVEL", "info")), + } +} + +func gatewayDatabaseURL() string { + if value := envValue("AI_GATEWAY_DATABASE_URL"); value != "" { + return normalizePostgresURL(value) + } + if value := envValue("DATABASE_URL"); value != "" { + return normalizePostgresURL(value) + } + if memoryURL := envValue("MEMORY_DATABASE_URL"); memoryURL != "" { + return normalizePostgresURL(withDatabase(memoryURL, env("AI_GATEWAY_DATABASE_NAME", "easyai_ai_gateway"))) + } + return normalizePostgresURL("postgresql://easyai:easyai2025@localhost:5432/easyai_ai_gateway?sslmode=disable") +} + +func normalizePostgresURL(raw string) string { + parsed, err := url.Parse(raw) + if err != nil { + return raw + } + values := parsed.Query() + schema := values.Get("schema") + if schema == "" { + return raw + } + values.Del("schema") + if values.Get("search_path") == "" { + values.Set("search_path", schema) + } + parsed.RawQuery = values.Encode() + return parsed.String() +} + +func withDatabase(raw string, databaseName string) string { + parsed, err := url.Parse(raw) + if err != nil || databaseName == "" { + return raw + } + parsed.Path = "/" + databaseName + return parsed.String() +} + +func envValue(key string) string { + return strings.TrimSpace(os.Getenv(key)) +} + +func env(key string, fallback string) string { + if value := envValue(key); value != "" { + return value + } + return fallback +} + +func logLevel(value string) slog.Level { + switch strings.ToLower(value) { + case "debug": + return slog.LevelDebug + case "warn", "warning": + return slog.LevelWarn + case "error": + return slog.LevelError + default: + return slog.LevelInfo + } +} diff --git a/apps/api/internal/httpapi/handlers.go b/apps/api/internal/httpapi/handlers.go new file mode 100644 index 0000000..414b8dd --- /dev/null +++ b/apps/api/internal/httpapi/handlers.go @@ -0,0 +1,218 @@ +package httpapi + +import ( + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/easyai/easyai-ai-gateway/apps/api/internal/auth" + "github.com/easyai/easyai-ai-gateway/apps/api/internal/store" +) + +func (s *Server) health(w http.ResponseWriter, r *http.Request) { + writeJSON(w, http.StatusOK, map[string]any{ + "ok": true, + "service": "easyai-ai-gateway", + "env": s.cfg.AppEnv, + }) +} + +func (s *Server) ready(w http.ResponseWriter, r *http.Request) { + if err := s.store.Ping(r.Context()); err != nil { + writeError(w, http.StatusServiceUnavailable, "postgres unavailable") + return + } + writeJSON(w, http.StatusOK, map[string]any{"ok": true}) +} + +func (s *Server) me(w http.ResponseWriter, r *http.Request) { + user, _ := auth.UserFromContext(r.Context()) + writeJSON(w, http.StatusOK, user) +} + +func (s *Server) listPlatforms(w http.ResponseWriter, r *http.Request) { + platforms, err := s.store.ListPlatforms(r.Context()) + if err != nil { + s.logger.Error("list platforms failed", "error", err) + writeError(w, http.StatusInternalServerError, "list platforms failed") + return + } + writeJSON(w, http.StatusOK, map[string]any{"items": platforms}) +} + +func (s *Server) createPlatform(w http.ResponseWriter, r *http.Request) { + var input store.CreatePlatformInput + if err := json.NewDecoder(r.Body).Decode(&input); err != nil { + writeError(w, http.StatusBadRequest, "invalid json body") + return + } + if input.Provider == "" || input.Name == "" { + writeError(w, http.StatusBadRequest, "provider and name are required") + return + } + if input.AuthType == "" { + input.AuthType = "bearer" + } + platform, err := s.store.CreatePlatform(r.Context(), input) + if err != nil { + s.logger.Error("create platform failed", "error", err) + writeError(w, http.StatusInternalServerError, "create platform failed") + return + } + writeJSON(w, http.StatusCreated, platform) +} + +func (s *Server) listModels(w http.ResponseWriter, r *http.Request) { + models, err := s.store.ListModels(r.Context()) + if err != nil { + s.logger.Error("list models failed", "error", err) + writeError(w, http.StatusInternalServerError, "list models failed") + return + } + writeJSON(w, http.StatusOK, map[string]any{"items": models}) +} + +func (s *Server) listCatalogProviders(w http.ResponseWriter, r *http.Request) { + items, err := s.store.ListCatalogProviders(r.Context()) + if err != nil { + s.logger.Error("list catalog providers failed", "error", err) + writeError(w, http.StatusInternalServerError, "list catalog providers failed") + return + } + writeJSON(w, http.StatusOK, map[string]any{"items": items}) +} + +func (s *Server) listBaseModels(w http.ResponseWriter, r *http.Request) { + items, err := s.store.ListBaseModels(r.Context()) + if err != nil { + s.logger.Error("list base models failed", "error", err) + writeError(w, http.StatusInternalServerError, "list base models failed") + return + } + writeJSON(w, http.StatusOK, map[string]any{"items": items}) +} + +func (s *Server) listPricingRules(w http.ResponseWriter, r *http.Request) { + items, err := s.store.ListPricingRules(r.Context()) + if err != nil { + s.logger.Error("list pricing rules failed", "error", err) + writeError(w, http.StatusInternalServerError, "list pricing rules failed") + return + } + writeJSON(w, http.StatusOK, map[string]any{"items": items}) +} + +func (s *Server) estimatePricing(w http.ResponseWriter, r *http.Request) { + var body map[string]any + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + writeError(w, http.StatusBadRequest, "invalid json body") + return + } + writeJSON(w, http.StatusOK, map[string]any{ + "items": []any{}, + "resolver": "effective-pricing-placeholder", + "request": body, + }) +} + +func (s *Server) listRateLimitWindows(w http.ResponseWriter, r *http.Request) { + items, err := s.store.ListRateLimitWindows(r.Context()) + if err != nil { + s.logger.Error("list rate limit windows failed", "error", err) + writeError(w, http.StatusInternalServerError, "list rate limit windows failed") + return + } + writeJSON(w, http.StatusOK, map[string]any{"items": items}) +} + +func (s *Server) createTask(kind string) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + user, ok := auth.UserFromContext(r.Context()) + if !ok { + writeError(w, http.StatusUnauthorized, "unauthorized") + return + } + + var body map[string]any + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + writeError(w, http.StatusBadRequest, "invalid json body") + return + } + model, _ := body["model"].(string) + if model == "" { + writeError(w, http.StatusBadRequest, "model is required") + return + } + + task, err := s.store.CreateTask(r.Context(), store.CreateTaskInput{ + Kind: kind, + Model: model, + Request: body, + }, user) + if err != nil { + s.logger.Error("create task failed", "kind", kind, "error", err) + writeError(w, http.StatusInternalServerError, "create task failed") + return + } + + writeJSON(w, http.StatusAccepted, map[string]any{ + "task": task, + "next": map[string]string{ + "events": fmt.Sprintf("/api/v1/tasks/%s/events", task.ID), + "detail": fmt.Sprintf("/api/v1/tasks/%s", task.ID), + }, + }) + }) +} + +func (s *Server) getTask(w http.ResponseWriter, r *http.Request) { + task, err := s.store.GetTask(r.Context(), r.PathValue("taskID")) + if err == nil { + writeJSON(w, http.StatusOK, task) + return + } + if store.IsNotFound(err) { + writeError(w, http.StatusNotFound, "task not found") + return + } + s.logger.Error("get task failed", "error", err) + writeError(w, http.StatusInternalServerError, "get task failed") +} + +func (s *Server) taskEvents(w http.ResponseWriter, r *http.Request) { + task, err := s.store.GetTask(r.Context(), r.PathValue("taskID")) + if err != nil { + if store.IsNotFound(err) { + writeError(w, http.StatusNotFound, "task not found") + return + } + writeError(w, http.StatusInternalServerError, "get task failed") + return + } + + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("Connection", "keep-alive") + + sendSSE(w, "task.accepted", map[string]any{ + "taskId": task.ID, + "status": task.Status, + }) + if flusher, ok := w.(http.Flusher); ok { + flusher.Flush() + } + + timer := time.NewTimer(250 * time.Millisecond) + defer timer.Stop() + + select { + case <-r.Context().Done(): + return + case <-timer.C: + sendSSE(w, "task.placeholder", map[string]any{ + "taskId": task.ID, + "message": "runtime worker is not wired yet", + }) + } +} diff --git a/apps/api/internal/httpapi/response.go b/apps/api/internal/httpapi/response.go new file mode 100644 index 0000000..f19eede --- /dev/null +++ b/apps/api/internal/httpapi/response.go @@ -0,0 +1,28 @@ +package httpapi + +import ( + "encoding/json" + "fmt" + "net/http" +) + +func writeJSON(w http.ResponseWriter, status int, value any) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(status) + _ = json.NewEncoder(w).Encode(value) +} + +func writeError(w http.ResponseWriter, status int, message string) { + writeJSON(w, status, map[string]any{ + "error": map[string]any{ + "message": message, + "status": status, + }, + }) +} + +func sendSSE(w http.ResponseWriter, event string, payload any) { + bytes, _ := json.Marshal(payload) + _, _ = fmt.Fprintf(w, "event: %s\n", event) + _, _ = fmt.Fprintf(w, "data: %s\n\n", bytes) +} diff --git a/apps/api/internal/httpapi/server.go b/apps/api/internal/httpapi/server.go new file mode 100644 index 0000000..d103423 --- /dev/null +++ b/apps/api/internal/httpapi/server.go @@ -0,0 +1,78 @@ +package httpapi + +import ( + "log/slog" + "net/http" + "strings" + + "github.com/easyai/easyai-ai-gateway/apps/api/internal/auth" + "github.com/easyai/easyai-ai-gateway/apps/api/internal/config" + "github.com/easyai/easyai-ai-gateway/apps/api/internal/store" +) + +type Server struct { + cfg config.Config + store *store.Store + auth *auth.Authenticator + logger *slog.Logger +} + +func NewServer(cfg config.Config, db *store.Store, logger *slog.Logger) http.Handler { + server := &Server{ + cfg: cfg, + store: db, + auth: auth.New(cfg.JWTSecret, cfg.ServerMainBaseURL, cfg.ServerMainInternalToken), + logger: logger, + } + + mux := http.NewServeMux() + mux.HandleFunc("GET /healthz", server.health) + mux.HandleFunc("GET /readyz", server.ready) + + mux.Handle("GET /api/v1/me", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.me))) + mux.Handle("GET /api/v1/catalog/providers", server.auth.Require(auth.PermissionPower, http.HandlerFunc(server.listCatalogProviders))) + mux.Handle("GET /api/v1/catalog/base-models", server.auth.Require(auth.PermissionPower, http.HandlerFunc(server.listBaseModels))) + mux.Handle("GET /api/v1/pricing/rules", server.auth.Require(auth.PermissionPower, http.HandlerFunc(server.listPricingRules))) + mux.Handle("POST /api/v1/pricing/estimate", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.estimatePricing))) + mux.Handle("GET /api/v1/platforms", server.auth.Require(auth.PermissionPower, http.HandlerFunc(server.listPlatforms))) + mux.Handle("POST /api/v1/platforms", server.auth.Require(auth.PermissionPower, http.HandlerFunc(server.createPlatform))) + mux.Handle("GET /api/v1/models", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.listModels))) + mux.Handle("GET /api/v1/runtime/rate-limit-windows", server.auth.Require(auth.PermissionPower, http.HandlerFunc(server.listRateLimitWindows))) + mux.Handle("POST /api/v1/chat/completions", server.auth.Require(auth.PermissionBasic, server.createTask("chat.completions"))) + mux.Handle("POST /api/v1/images/generations", server.auth.Require(auth.PermissionBasic, server.createTask("images.generations"))) + mux.Handle("POST /api/v1/videos/generations", server.auth.Require(auth.PermissionBasic, server.createTask("videos.generations"))) + mux.Handle("GET /api/v1/tasks/{taskID}", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.getTask))) + mux.Handle("GET /api/v1/tasks/{taskID}/events", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.taskEvents))) + + return server.recover(server.cors(mux)) +} + +func (s *Server) cors(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + origin := r.Header.Get("Origin") + if origin != "" && (s.cfg.CORSAllowedOrigin == "*" || strings.EqualFold(origin, s.cfg.CORSAllowedOrigin)) { + w.Header().Set("Access-Control-Allow-Origin", origin) + w.Header().Set("Vary", "Origin") + w.Header().Set("Access-Control-Allow-Credentials", "true") + w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type, X-Comfy-Api-Key") + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PATCH, DELETE, OPTIONS") + } + if r.Method == http.MethodOptions { + w.WriteHeader(http.StatusNoContent) + return + } + next.ServeHTTP(w, r) + }) +} + +func (s *Server) recover(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer func() { + if err := recover(); err != nil { + s.logger.Error("panic recovered", "error", err, "path", r.URL.Path) + writeError(w, http.StatusInternalServerError, "internal server error") + } + }() + next.ServeHTTP(w, r) + }) +} diff --git a/apps/api/internal/store/postgres.go b/apps/api/internal/store/postgres.go new file mode 100644 index 0000000..28b644e --- /dev/null +++ b/apps/api/internal/store/postgres.go @@ -0,0 +1,508 @@ +package store + +import ( + "context" + "encoding/json" + "time" + + "github.com/easyai/easyai-ai-gateway/apps/api/internal/auth" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgxpool" +) + +type Store struct { + pool *pgxpool.Pool +} + +func Connect(ctx context.Context, databaseURL string) (*Store, error) { + pool, err := pgxpool.New(ctx, databaseURL) + if err != nil { + return nil, err + } + if err := pool.Ping(ctx); err != nil { + pool.Close() + return nil, err + } + return &Store{pool: pool}, nil +} + +func (s *Store) Close() { + s.pool.Close() +} + +func (s *Store) Ping(ctx context.Context) error { + return s.pool.Ping(ctx) +} + +type Platform struct { + ID string `json:"id"` + Provider string `json:"provider"` + PlatformKey string `json:"platformKey"` + Name string `json:"name"` + BaseURL string `json:"baseUrl,omitempty"` + AuthType string `json:"authType"` + Status string `json:"status"` + Priority int `json:"priority"` + DefaultPricingMode string `json:"defaultPricingMode"` + DefaultDiscountFactor float64 `json:"defaultDiscountFactor"` + Config map[string]any `json:"config,omitempty"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} + +type CreatePlatformInput struct { + Provider string `json:"provider"` + PlatformKey string `json:"platformKey"` + Name string `json:"name"` + BaseURL string `json:"baseUrl"` + AuthType string `json:"authType"` + Credentials map[string]any `json:"credentials"` + Config map[string]any `json:"config"` + DefaultPricingMode string `json:"defaultPricingMode"` + DefaultDiscountFactor float64 `json:"defaultDiscountFactor"` + Priority int `json:"priority"` +} + +type PlatformModel struct { + ID string `json:"id"` + PlatformID string `json:"platformId"` + BaseModelID string `json:"baseModelId,omitempty"` + Provider string `json:"provider,omitempty"` + PlatformName string `json:"platformName,omitempty"` + ModelName string `json:"modelName"` + ModelAlias string `json:"modelAlias,omitempty"` + ModelType string `json:"modelType"` + DisplayName string `json:"displayName"` + CapabilityOverride map[string]any `json:"capabilityOverride,omitempty"` + Capabilities map[string]any `json:"capabilities,omitempty"` + PricingMode string `json:"pricingMode"` + DiscountFactor float64 `json:"discountFactor,omitempty"` + BillingConfigOverride map[string]any `json:"billingConfigOverride,omitempty"` + BillingConfig map[string]any `json:"billingConfig,omitempty"` + Enabled bool `json:"enabled"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} + +type CatalogProvider struct { + ID string `json:"id"` + ProviderKey string `json:"providerKey"` + DisplayName string `json:"displayName"` + ProviderType string `json:"providerType"` + CapabilitySchema map[string]any `json:"capabilitySchema,omitempty"` + DefaultRateLimitPolicy map[string]any `json:"defaultRateLimitPolicy,omitempty"` + Status string `json:"status"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} + +type BaseModel struct { + ID string `json:"id"` + ProviderKey string `json:"providerKey"` + CanonicalModelKey string `json:"canonicalModelKey"` + ProviderModelName string `json:"providerModelName"` + ModelType string `json:"modelType"` + DisplayName string `json:"displayName"` + Capabilities map[string]any `json:"capabilities,omitempty"` + BaseBillingConfig map[string]any `json:"baseBillingConfig,omitempty"` + DefaultRateLimitPolicy map[string]any `json:"defaultRateLimitPolicy,omitempty"` + PricingVersion int `json:"pricingVersion"` + Status string `json:"status"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} + +type PricingRule struct { + ID string `json:"id"` + ScopeType string `json:"scopeType"` + ScopeID string `json:"scopeId,omitempty"` + ResourceType string `json:"resourceType"` + Unit string `json:"unit"` + BasePrice float64 `json:"basePrice"` + Currency string `json:"currency"` + BaseWeight map[string]any `json:"baseWeight,omitempty"` + DynamicWeight map[string]any `json:"dynamicWeight,omitempty"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} + +type RateLimitWindow struct { + ScopeType string `json:"scopeType"` + ScopeKey string `json:"scopeKey"` + Metric string `json:"metric"` + WindowStart time.Time `json:"windowStart"` + LimitValue float64 `json:"limitValue"` + UsedValue float64 `json:"usedValue"` + ReservedValue float64 `json:"reservedValue"` + ResetAt time.Time `json:"resetAt"` + UpdatedAt time.Time `json:"updatedAt"` +} + +type CreateTaskInput struct { + Kind string `json:"kind"` + Model string `json:"model"` + Request map[string]any `json:"request"` +} + +type GatewayTask struct { + ID string `json:"id"` + Kind string `json:"kind"` + UserID string `json:"userId"` + TenantID string `json:"tenantId,omitempty"` + Model string `json:"model"` + Request map[string]any `json:"request,omitempty"` + Status string `json:"status"` + Result map[string]any `json:"result,omitempty"` + Billings []any `json:"billings,omitempty"` + Error string `json:"error,omitempty"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} + +func (s *Store) ListPlatforms(ctx context.Context) ([]Platform, error) { + rows, err := s.pool.Query(ctx, ` +SELECT id::text, provider, platform_key, name, COALESCE(base_url, ''), auth_type, status, priority, + default_pricing_mode, default_discount_factor::float8, config, created_at, updated_at +FROM integration_platforms +ORDER BY priority ASC, created_at DESC`) + if err != nil { + return nil, err + } + defer rows.Close() + + platforms := make([]Platform, 0) + for rows.Next() { + var platform Platform + var configBytes []byte + if err := rows.Scan( + &platform.ID, + &platform.Provider, + &platform.PlatformKey, + &platform.Name, + &platform.BaseURL, + &platform.AuthType, + &platform.Status, + &platform.Priority, + &platform.DefaultPricingMode, + &platform.DefaultDiscountFactor, + &configBytes, + &platform.CreatedAt, + &platform.UpdatedAt, + ); err != nil { + return nil, err + } + platform.Config = decodeObject(configBytes) + platforms = append(platforms, platform) + } + return platforms, rows.Err() +} + +func (s *Store) CreatePlatform(ctx context.Context, input CreatePlatformInput) (Platform, error) { + credentials, _ := json.Marshal(input.Credentials) + config, _ := json.Marshal(input.Config) + if input.DefaultPricingMode == "" { + input.DefaultPricingMode = "inherit_discount" + } + if input.DefaultDiscountFactor == 0 { + input.DefaultDiscountFactor = 1 + } + if input.Priority == 0 { + input.Priority = 100 + } + var platform Platform + var configBytes []byte + err := s.pool.QueryRow(ctx, ` +INSERT INTO integration_platforms (provider, platform_key, name, base_url, auth_type, credentials, config, default_pricing_mode, default_discount_factor, priority) +VALUES ($1, COALESCE(NULLIF($2, ''), 'platform_' || replace(gen_random_uuid()::text, '-', '')), $3, $4, $5, $6, $7, $8, $9, $10) +RETURNING id::text, provider, platform_key, name, COALESCE(base_url, ''), auth_type, status, priority, + default_pricing_mode, default_discount_factor::float8, config, created_at, updated_at`, + input.Provider, input.PlatformKey, input.Name, input.BaseURL, input.AuthType, credentials, config, input.DefaultPricingMode, input.DefaultDiscountFactor, input.Priority, + ).Scan( + &platform.ID, + &platform.Provider, + &platform.PlatformKey, + &platform.Name, + &platform.BaseURL, + &platform.AuthType, + &platform.Status, + &platform.Priority, + &platform.DefaultPricingMode, + &platform.DefaultDiscountFactor, + &configBytes, + &platform.CreatedAt, + &platform.UpdatedAt, + ) + if err != nil { + return Platform{}, err + } + platform.Config = decodeObject(configBytes) + return platform, nil +} + +func (s *Store) ListModels(ctx context.Context) ([]PlatformModel, error) { + rows, err := s.pool.Query(ctx, ` +SELECT m.id::text, m.platform_id::text, COALESCE(m.base_model_id::text, ''), p.provider, p.name, + m.model_name, COALESCE(m.model_alias, ''), m.model_type, m.display_name, + m.capability_override, m.capabilities, m.pricing_mode, COALESCE(m.discount_factor, 0)::float8, + m.billing_config_override, m.billing_config, m.enabled, m.created_at, m.updated_at +FROM platform_models m +JOIN integration_platforms p ON p.id = m.platform_id +ORDER BY m.model_type ASC, m.model_name ASC`) + if err != nil { + return nil, err + } + defer rows.Close() + + models := make([]PlatformModel, 0) + for rows.Next() { + var model PlatformModel + var capabilityOverride []byte + var capabilities []byte + var billingConfigOverride []byte + var billingConfig []byte + if err := rows.Scan( + &model.ID, + &model.PlatformID, + &model.BaseModelID, + &model.Provider, + &model.PlatformName, + &model.ModelName, + &model.ModelAlias, + &model.ModelType, + &model.DisplayName, + &capabilityOverride, + &capabilities, + &model.PricingMode, + &model.DiscountFactor, + &billingConfigOverride, + &billingConfig, + &model.Enabled, + &model.CreatedAt, + &model.UpdatedAt, + ); err != nil { + return nil, err + } + model.CapabilityOverride = decodeObject(capabilityOverride) + model.Capabilities = decodeObject(capabilities) + model.BillingConfigOverride = decodeObject(billingConfigOverride) + model.BillingConfig = decodeObject(billingConfig) + models = append(models, model) + } + return models, rows.Err() +} + +func (s *Store) ListCatalogProviders(ctx context.Context) ([]CatalogProvider, error) { + rows, err := s.pool.Query(ctx, ` +SELECT id::text, provider_key, display_name, provider_type, capability_schema, + default_rate_limit_policy, status, created_at, updated_at +FROM model_catalog_providers +ORDER BY provider_key ASC`) + if err != nil { + return nil, err + } + defer rows.Close() + + items := make([]CatalogProvider, 0) + for rows.Next() { + var item CatalogProvider + var capabilitySchema []byte + var rateLimitPolicy []byte + if err := rows.Scan( + &item.ID, + &item.ProviderKey, + &item.DisplayName, + &item.ProviderType, + &capabilitySchema, + &rateLimitPolicy, + &item.Status, + &item.CreatedAt, + &item.UpdatedAt, + ); err != nil { + return nil, err + } + item.CapabilitySchema = decodeObject(capabilitySchema) + item.DefaultRateLimitPolicy = decodeObject(rateLimitPolicy) + items = append(items, item) + } + return items, rows.Err() +} + +func (s *Store) ListBaseModels(ctx context.Context) ([]BaseModel, error) { + rows, err := s.pool.Query(ctx, ` +SELECT id::text, provider_key, canonical_model_key, provider_model_name, model_type, display_name, + capabilities, base_billing_config, default_rate_limit_policy, pricing_version, + status, created_at, updated_at +FROM base_model_catalog +ORDER BY provider_key ASC, model_type ASC, canonical_model_key ASC`) + if err != nil { + return nil, err + } + defer rows.Close() + + items := make([]BaseModel, 0) + for rows.Next() { + var item BaseModel + var capabilities []byte + var billingConfig []byte + var rateLimitPolicy []byte + if err := rows.Scan( + &item.ID, + &item.ProviderKey, + &item.CanonicalModelKey, + &item.ProviderModelName, + &item.ModelType, + &item.DisplayName, + &capabilities, + &billingConfig, + &rateLimitPolicy, + &item.PricingVersion, + &item.Status, + &item.CreatedAt, + &item.UpdatedAt, + ); err != nil { + return nil, err + } + item.Capabilities = decodeObject(capabilities) + item.BaseBillingConfig = decodeObject(billingConfig) + item.DefaultRateLimitPolicy = decodeObject(rateLimitPolicy) + items = append(items, item) + } + return items, rows.Err() +} + +func (s *Store) ListPricingRules(ctx context.Context) ([]PricingRule, error) { + rows, err := s.pool.Query(ctx, ` +SELECT id::text, scope_type, COALESCE(scope_id::text, ''), resource_type, unit, + base_price::float8, currency, base_weight, dynamic_weight, created_at, updated_at +FROM model_pricing_rules +ORDER BY scope_type ASC, resource_type ASC, created_at DESC`) + if err != nil { + return nil, err + } + defer rows.Close() + + items := make([]PricingRule, 0) + for rows.Next() { + var item PricingRule + var baseWeight []byte + var dynamicWeight []byte + if err := rows.Scan( + &item.ID, + &item.ScopeType, + &item.ScopeID, + &item.ResourceType, + &item.Unit, + &item.BasePrice, + &item.Currency, + &baseWeight, + &dynamicWeight, + &item.CreatedAt, + &item.UpdatedAt, + ); err != nil { + return nil, err + } + item.BaseWeight = decodeObject(baseWeight) + item.DynamicWeight = decodeObject(dynamicWeight) + items = append(items, item) + } + return items, rows.Err() +} + +func (s *Store) ListRateLimitWindows(ctx context.Context) ([]RateLimitWindow, error) { + rows, err := s.pool.Query(ctx, ` +SELECT scope_type, scope_key, metric, window_start, limit_value::float8, used_value::float8, + reserved_value::float8, reset_at, updated_at +FROM gateway_rate_limit_counters +WHERE reset_at >= now() - interval '5 minutes' +ORDER BY window_start DESC, scope_type ASC, scope_key ASC, metric ASC`) + if err != nil { + return nil, err + } + defer rows.Close() + + items := make([]RateLimitWindow, 0) + for rows.Next() { + var item RateLimitWindow + if err := rows.Scan( + &item.ScopeType, + &item.ScopeKey, + &item.Metric, + &item.WindowStart, + &item.LimitValue, + &item.UsedValue, + &item.ReservedValue, + &item.ResetAt, + &item.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, item) + } + return items, rows.Err() +} + +func (s *Store) CreateTask(ctx context.Context, input CreateTaskInput, user *auth.User) (GatewayTask, error) { + requestBody, _ := json.Marshal(input.Request) + var task GatewayTask + var requestBytes []byte + var resultBytes []byte + var billingsBytes []byte + err := s.pool.QueryRow(ctx, ` +INSERT INTO gateway_tasks (kind, user_id, tenant_id, model, request, status) +VALUES ($1, $2, NULLIF($3, ''), $4, $5, 'queued') +RETURNING id::text, kind, user_id, COALESCE(tenant_id, ''), model, request, status, result, billings, COALESCE(error, ''), created_at, updated_at`, + input.Kind, user.ID, user.TenantID, input.Model, requestBody, + ).Scan(&task.ID, &task.Kind, &task.UserID, &task.TenantID, &task.Model, &requestBytes, &task.Status, &resultBytes, &billingsBytes, &task.Error, &task.CreatedAt, &task.UpdatedAt) + if err != nil { + return GatewayTask{}, err + } + task.Request = decodeObject(requestBytes) + task.Result = decodeObject(resultBytes) + task.Billings = decodeArray(billingsBytes) + return task, nil +} + +func (s *Store) GetTask(ctx context.Context, taskID string) (GatewayTask, error) { + var task GatewayTask + var requestBytes []byte + var resultBytes []byte + var billingsBytes []byte + err := s.pool.QueryRow(ctx, ` +SELECT id::text, kind, user_id, COALESCE(tenant_id, ''), model, request, status, result, billings, COALESCE(error, ''), created_at, updated_at +FROM gateway_tasks +WHERE id=$1`, taskID, + ).Scan(&task.ID, &task.Kind, &task.UserID, &task.TenantID, &task.Model, &requestBytes, &task.Status, &resultBytes, &billingsBytes, &task.Error, &task.CreatedAt, &task.UpdatedAt) + if err != nil { + return GatewayTask{}, err + } + task.Request = decodeObject(requestBytes) + task.Result = decodeObject(resultBytes) + task.Billings = decodeArray(billingsBytes) + return task, nil +} + +func IsNotFound(err error) bool { + return err == pgx.ErrNoRows +} + +func decodeObject(bytes []byte) map[string]any { + if len(bytes) == 0 { + return nil + } + var out map[string]any + if err := json.Unmarshal(bytes, &out); err != nil { + return nil + } + return out +} + +func decodeArray(bytes []byte) []any { + if len(bytes) == 0 { + return nil + } + var out []any + if err := json.Unmarshal(bytes, &out); err != nil { + return nil + } + return out +} diff --git a/apps/api/migrations/0001_init.sql b/apps/api/migrations/0001_init.sql new file mode 100644 index 0000000..5ca8e49 --- /dev/null +++ b/apps/api/migrations/0001_init.sql @@ -0,0 +1,333 @@ +CREATE EXTENSION IF NOT EXISTS pgcrypto; + +CREATE TABLE IF NOT EXISTS model_catalog_providers ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + provider_key text NOT NULL UNIQUE, + display_name text NOT NULL, + provider_type text NOT NULL DEFAULT 'openai_compatible', + capability_schema jsonb NOT NULL DEFAULT '{}'::jsonb, + default_rate_limit_policy jsonb NOT NULL DEFAULT '{}'::jsonb, + status text NOT NULL DEFAULT 'active', + metadata jsonb NOT NULL DEFAULT '{}'::jsonb, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now() +); + +CREATE INDEX IF NOT EXISTS idx_model_catalog_provider_status + ON model_catalog_providers(status); + +CREATE TABLE IF NOT EXISTS base_model_catalog ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + provider_id uuid REFERENCES model_catalog_providers(id) ON DELETE SET NULL, + provider_key text NOT NULL, + canonical_model_key text NOT NULL UNIQUE, + provider_model_name text NOT NULL, + model_type text NOT NULL, + display_name text NOT NULL, + capabilities jsonb NOT NULL DEFAULT '{}'::jsonb, + base_billing_config jsonb NOT NULL DEFAULT '{}'::jsonb, + default_rate_limit_policy jsonb NOT NULL DEFAULT '{}'::jsonb, + pricing_version integer NOT NULL DEFAULT 1, + status text NOT NULL DEFAULT 'active', + metadata jsonb NOT NULL DEFAULT '{}'::jsonb, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now() +); + +CREATE INDEX IF NOT EXISTS idx_base_model_catalog_provider + ON base_model_catalog(provider_key, model_type, status); + +CREATE INDEX IF NOT EXISTS idx_base_model_catalog_capabilities + ON base_model_catalog USING gin(capabilities); + +CREATE TABLE IF NOT EXISTS integration_platforms ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + provider text NOT NULL, + platform_key text NOT NULL UNIQUE DEFAULT ('platform_' || replace(gen_random_uuid()::text, '-', '')), + name text NOT NULL, + base_url text, + auth_type text NOT NULL DEFAULT 'bearer', + credentials jsonb NOT NULL DEFAULT '{}'::jsonb, + config jsonb NOT NULL DEFAULT '{}'::jsonb, + default_pricing_mode text NOT NULL DEFAULT 'inherit_discount', + default_discount_factor numeric NOT NULL DEFAULT 1, + retry_policy jsonb NOT NULL DEFAULT '{}'::jsonb, + rate_limit_policy jsonb NOT NULL DEFAULT '{}'::jsonb, + priority integer NOT NULL DEFAULT 100, + dynamic_priority integer, + status text NOT NULL DEFAULT 'enabled', + disabled_reason text, + cooldown_until timestamptz, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + deleted_at timestamptz +); + +CREATE INDEX IF NOT EXISTS idx_integration_platforms_provider_status + ON integration_platforms(provider, status); + +CREATE INDEX IF NOT EXISTS idx_integration_platforms_status_priority + ON integration_platforms(status, priority, dynamic_priority); + +CREATE INDEX IF NOT EXISTS idx_integration_platforms_cooldown + ON integration_platforms(cooldown_until); + +CREATE TABLE IF NOT EXISTS model_pricing_rules ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + scope_type text NOT NULL, + scope_id uuid, + resource_type text NOT NULL, + unit text NOT NULL, + base_price numeric NOT NULL, + currency text NOT NULL DEFAULT 'resource', + base_weight jsonb NOT NULL DEFAULT '{}'::jsonb, + dynamic_weight jsonb NOT NULL DEFAULT '{}'::jsonb, + effective_from timestamptz, + effective_to timestamptz, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now() +); + +CREATE INDEX IF NOT EXISTS idx_model_pricing_scope + ON model_pricing_rules(scope_type, scope_id, resource_type); + +CREATE INDEX IF NOT EXISTS idx_model_pricing_effective + ON model_pricing_rules(effective_from, effective_to); + +CREATE TABLE IF NOT EXISTS platform_models ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + platform_id uuid NOT NULL REFERENCES integration_platforms(id) ON DELETE CASCADE, + base_model_id uuid REFERENCES base_model_catalog(id) ON DELETE SET NULL, + model_name text NOT NULL, + model_alias text, + model_type text NOT NULL, + display_name text NOT NULL DEFAULT '', + capability_override jsonb NOT NULL DEFAULT '{}'::jsonb, + capabilities jsonb NOT NULL DEFAULT '{}'::jsonb, + pricing_mode text NOT NULL DEFAULT 'inherit_discount', + discount_factor numeric, + billing_config_override jsonb NOT NULL DEFAULT '{}'::jsonb, + billing_config jsonb NOT NULL DEFAULT '{}'::jsonb, + permission_config jsonb NOT NULL DEFAULT '{}'::jsonb, + retry_policy jsonb NOT NULL DEFAULT '{}'::jsonb, + rate_limit_policy jsonb NOT NULL DEFAULT '{}'::jsonb, + enabled boolean NOT NULL DEFAULT true, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + UNIQUE(platform_id, model_name, model_type) +); + +CREATE INDEX IF NOT EXISTS idx_platform_models_base + ON platform_models(base_model_id); + +CREATE INDEX IF NOT EXISTS idx_platform_models_lookup + ON platform_models(model_type, model_name, enabled); + +CREATE INDEX IF NOT EXISTS idx_platform_models_alias + ON platform_models(model_alias); + +CREATE INDEX IF NOT EXISTS idx_platform_models_capabilities + ON platform_models USING gin(capabilities); + +CREATE TABLE IF NOT EXISTS gateway_tasks ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + external_task_id text, + kind text NOT NULL, + run_mode text NOT NULL DEFAULT 'production', + user_id text NOT NULL, + tenant_id text, + api_key_id text, + model text NOT NULL, + model_type text, + request jsonb NOT NULL DEFAULT '{}'::jsonb, + normalized_request jsonb NOT NULL DEFAULT '{}'::jsonb, + status text NOT NULL DEFAULT 'queued', + queue_key text NOT NULL DEFAULT 'default', + priority integer NOT NULL DEFAULT 100, + idempotency_key text, + remote_task_id text, + remote_task_payload jsonb, + simulation_profile jsonb, + simulation_seed text, + locked_by text, + locked_at timestamptz, + heartbeat_at timestamptz, + next_run_at timestamptz NOT NULL DEFAULT now(), + attempt_count integer NOT NULL DEFAULT 0, + max_attempts integer NOT NULL DEFAULT 1, + result jsonb, + billings jsonb, + error text, + error_code text, + error_message text, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + finished_at timestamptz +); + +CREATE INDEX IF NOT EXISTS idx_gateway_tasks_queue + ON gateway_tasks(status, next_run_at, priority, created_at); + +CREATE INDEX IF NOT EXISTS idx_gateway_tasks_lease + ON gateway_tasks(status, heartbeat_at); + +CREATE INDEX IF NOT EXISTS idx_gateway_tasks_user_created + ON gateway_tasks(user_id, created_at DESC); + +CREATE INDEX IF NOT EXISTS idx_gateway_tasks_external + ON gateway_tasks(external_task_id); + +CREATE UNIQUE INDEX IF NOT EXISTS uniq_gateway_tasks_idempotency + ON gateway_tasks(user_id, idempotency_key) + WHERE idempotency_key IS NOT NULL; + +CREATE TABLE IF NOT EXISTS gateway_task_attempts ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + task_id uuid NOT NULL REFERENCES gateway_tasks(id) ON DELETE CASCADE, + attempt_no integer NOT NULL, + platform_id uuid REFERENCES integration_platforms(id) ON DELETE SET NULL, + platform_model_id uuid REFERENCES platform_models(id) ON DELETE SET NULL, + client_id text, + queue_key text NOT NULL, + status text NOT NULL, + retryable boolean NOT NULL DEFAULT false, + simulated boolean NOT NULL DEFAULT false, + remote_task_id text, + request_snapshot jsonb NOT NULL DEFAULT '{}'::jsonb, + response_snapshot jsonb, + error_code text, + error_message text, + started_at timestamptz NOT NULL DEFAULT now(), + finished_at timestamptz, + UNIQUE(task_id, attempt_no) +); + +CREATE INDEX IF NOT EXISTS idx_gateway_attempts_task + ON gateway_task_attempts(task_id); + +CREATE INDEX IF NOT EXISTS idx_gateway_attempts_client + ON gateway_task_attempts(client_id, started_at DESC); + +CREATE TABLE IF NOT EXISTS gateway_task_events ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + task_id uuid NOT NULL REFERENCES gateway_tasks(id) ON DELETE CASCADE, + seq bigint NOT NULL, + event_type text NOT NULL, + status text, + phase text, + progress numeric, + message text, + payload jsonb NOT NULL DEFAULT '{}'::jsonb, + simulated boolean NOT NULL DEFAULT false, + created_at timestamptz NOT NULL DEFAULT now(), + UNIQUE(task_id, seq) +); + +CREATE INDEX IF NOT EXISTS idx_gateway_events_task_created + ON gateway_task_events(task_id, created_at); + +CREATE TABLE IF NOT EXISTS runtime_client_states ( + client_id text PRIMARY KEY, + platform_id uuid REFERENCES integration_platforms(id) ON DELETE SET NULL, + provider text NOT NULL, + method_name text NOT NULL, + queue_key text NOT NULL, + running_count integer NOT NULL DEFAULT 0, + waiting_count integer NOT NULL DEFAULT 0, + limiter_ratio numeric NOT NULL DEFAULT 0, + cooldown_until timestamptz, + last_assigned_at timestamptz, + last_error text, + updated_at timestamptz NOT NULL DEFAULT now() +); + +CREATE INDEX IF NOT EXISTS idx_runtime_client_queue + ON runtime_client_states(queue_key, cooldown_until); + +CREATE INDEX IF NOT EXISTS idx_runtime_client_platform + ON runtime_client_states(platform_id); + +CREATE TABLE IF NOT EXISTS gateway_upload_assets ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + task_id uuid REFERENCES gateway_tasks(id) ON DELETE SET NULL, + source text NOT NULL, + server_main_file_id text, + url text NOT NULL, + object_key text, + content_type text, + size bigint, + checksum text, + metadata jsonb NOT NULL DEFAULT '{}'::jsonb, + created_at timestamptz NOT NULL DEFAULT now() +); + +CREATE INDEX IF NOT EXISTS idx_gateway_upload_task + ON gateway_upload_assets(task_id); + +CREATE INDEX IF NOT EXISTS idx_gateway_upload_file + ON gateway_upload_assets(server_main_file_id); + +CREATE TABLE IF NOT EXISTS gateway_retry_policies ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + scope_type text NOT NULL, + scope_key text NOT NULL, + enabled boolean NOT NULL DEFAULT true, + policy jsonb NOT NULL DEFAULT '{}'::jsonb, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + UNIQUE(scope_type, scope_key) +); + +CREATE TABLE IF NOT EXISTS gateway_rate_limit_policies ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + scope_type text NOT NULL, + scope_key text NOT NULL, + policy jsonb NOT NULL DEFAULT '{}'::jsonb, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + UNIQUE(scope_type, scope_key) +); + +CREATE TABLE IF NOT EXISTS gateway_rate_limit_counters ( + scope_type text NOT NULL, + scope_key text NOT NULL, + metric text NOT NULL, + window_start timestamptz NOT NULL, + limit_value numeric NOT NULL, + used_value numeric NOT NULL DEFAULT 0, + reserved_value numeric NOT NULL DEFAULT 0, + reset_at timestamptz NOT NULL, + updated_at timestamptz NOT NULL DEFAULT now(), + PRIMARY KEY(scope_type, scope_key, metric, window_start) +); + +CREATE TABLE IF NOT EXISTS gateway_concurrency_leases ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + task_id uuid NOT NULL REFERENCES gateway_tasks(id) ON DELETE CASCADE, + attempt_id uuid REFERENCES gateway_task_attempts(id) ON DELETE SET NULL, + scope_type text NOT NULL, + scope_key text NOT NULL, + lease_value numeric NOT NULL DEFAULT 1, + acquired_at timestamptz NOT NULL DEFAULT now(), + expires_at timestamptz NOT NULL, + released_at timestamptz +); + +CREATE INDEX IF NOT EXISTS idx_concurrency_leases_active + ON gateway_concurrency_leases(scope_type, scope_key, released_at, expires_at); + +CREATE INDEX IF NOT EXISTS idx_concurrency_leases_task + ON gateway_concurrency_leases(task_id); + +CREATE TABLE IF NOT EXISTS settlement_outbox ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + task_id uuid NOT NULL REFERENCES gateway_tasks(id) ON DELETE CASCADE, + event_type text NOT NULL DEFAULT 'task.settlement.requested', + payload jsonb NOT NULL, + status text NOT NULL DEFAULT 'pending', + attempts integer NOT NULL DEFAULT 0, + next_attempt_at timestamptz NOT NULL DEFAULT now(), + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + UNIQUE(task_id, event_type) +); diff --git a/apps/api/project.json b/apps/api/project.json new file mode 100644 index 0000000..daa0fb7 --- /dev/null +++ b/apps/api/project.json @@ -0,0 +1,39 @@ +{ + "name": "api", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "root": "apps/api", + "sourceRoot": "apps/api", + "projectType": "application", + "targets": { + "dev": { + "executor": "nx:run-commands", + "options": { + "cwd": "apps/api", + "command": "go run ./cmd/gateway" + } + }, + "migrate": { + "executor": "nx:run-commands", + "options": { + "cwd": "apps/api", + "command": "go run ./cmd/migrate" + } + }, + "test": { + "executor": "nx:run-commands", + "outputs": ["{workspaceRoot}/coverage/apps/api"], + "options": { + "cwd": "apps/api", + "command": "go test ./..." + } + }, + "build": { + "executor": "nx:run-commands", + "outputs": ["{workspaceRoot}/dist/apps/api"], + "options": { + "cwd": "apps/api", + "command": "go build -o ../../dist/apps/api/easyai-ai-gateway ./cmd/gateway" + } + } + } +} diff --git a/apps/web/index.html b/apps/web/index.html new file mode 100644 index 0000000..b3b00c0 --- /dev/null +++ b/apps/web/index.html @@ -0,0 +1,12 @@ + + + + + + EasyAI AI Gateway + + +
+ + + diff --git a/apps/web/package.json b/apps/web/package.json new file mode 100644 index 0000000..33de153 --- /dev/null +++ b/apps/web/package.json @@ -0,0 +1,24 @@ +{ + "name": "@easyai-ai-gateway/web", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --host 0.0.0.0 --port 5178", + "build": "tsc --noEmit && vite build", + "preview": "vite preview --host 0.0.0.0 --port 4178", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@easyai-ai-gateway/contracts": "workspace:*", + "@vitejs/plugin-react": "^5.0.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "vite": "^7.0.0" + }, + "devDependencies": { + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "typescript": "^5.8.0" + } +} diff --git a/apps/web/project.json b/apps/web/project.json new file mode 100644 index 0000000..fa9913d --- /dev/null +++ b/apps/web/project.json @@ -0,0 +1,38 @@ +{ + "name": "web", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "root": "apps/web", + "sourceRoot": "apps/web/src", + "projectType": "application", + "targets": { + "dev": { + "executor": "nx:run-commands", + "options": { + "cwd": "apps/web", + "command": "pnpm dev" + } + }, + "build": { + "executor": "nx:run-commands", + "outputs": ["{projectRoot}/dist"], + "options": { + "cwd": "apps/web", + "command": "pnpm build" + } + }, + "lint": { + "executor": "nx:run-commands", + "options": { + "cwd": "apps/web", + "command": "pnpm typecheck" + } + }, + "test": { + "executor": "nx:run-commands", + "options": { + "cwd": "apps/web", + "command": "pnpm typecheck" + } + } + } +} diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx new file mode 100644 index 0000000..6b400ff --- /dev/null +++ b/apps/web/src/App.tsx @@ -0,0 +1,227 @@ +import { useEffect, useMemo, useState } from 'react'; +import type { + BaseModelCatalogItem, + CatalogProvider, + IntegrationPlatform, + PlatformModel, + PricingRule, + RateLimitWindow, +} from '@easyai-ai-gateway/contracts'; +import { + getHealth, + listBaseModels, + listCatalogProviders, + listModels, + listPlatforms, + listPricingRules, + listRateLimitWindows, + type HealthResponse, +} from './api'; + +type LoadState = 'idle' | 'loading' | 'ready' | 'error'; + +export function App() { + const [token, setToken] = useState(''); + const [health, setHealth] = useState(null); + const [platforms, setPlatforms] = useState([]); + const [models, setModels] = useState([]); + const [providers, setProviders] = useState([]); + const [baseModels, setBaseModels] = useState([]); + const [pricingRules, setPricingRules] = useState([]); + const [rateLimitWindows, setRateLimitWindows] = useState([]); + const [state, setState] = useState('idle'); + const [error, setError] = useState(''); + + useEffect(() => { + getHealth() + .then(setHealth) + .catch((err: Error) => setError(err.message)); + }, []); + + const stats = useMemo(() => { + const enabledPlatforms = platforms.filter((item) => item.status === 'enabled').length; + const enabledModels = models.filter((item) => item.enabled).length; + const activeProviders = providers.filter((item) => item.status === 'active').length; + const activeRateWindows = rateLimitWindows.filter((item) => item.resetAt >= new Date().toISOString()).length; + return [ + { label: '平台', value: platforms.length, tone: 'blue' }, + { label: '启用平台', value: enabledPlatforms, tone: 'green' }, + { label: '基准模型', value: baseModels.length, tone: 'violet' }, + { label: 'Provider', value: activeProviders || providers.length || enabledModels, tone: 'amber' }, + { label: '定价规则', value: pricingRules.length, tone: 'cyan' }, + { label: '限流窗口', value: activeRateWindows, tone: 'rose' }, + ]; + }, [baseModels.length, models, platforms, pricingRules.length, providers, rateLimitWindows]); + + async function refresh() { + setState('loading'); + setError(''); + try { + const [ + platformResponse, + modelResponse, + providerResponse, + baseModelResponse, + pricingRuleResponse, + rateLimitWindowResponse, + ] = await Promise.all([ + listPlatforms(token), + listModels(token), + listCatalogProviders(token), + listBaseModels(token), + listPricingRules(token), + listRateLimitWindows(token), + ]); + setPlatforms(platformResponse.items); + setModels(modelResponse.items); + setProviders(providerResponse.items); + setBaseModels(baseModelResponse.items); + setPricingRules(pricingRuleResponse.items); + setRateLimitWindows(rateLimitWindowResponse.items); + setState('ready'); + } catch (err) { + setState('error'); + setError(err instanceof Error ? err.message : '加载失败'); + } + } + + return ( +
+
+
+

EasyAI

+

AI Gateway Console

+
+
+ + {health?.service ?? 'API 未连接'} +
+
+ +
+ + +
+ + {error &&
{error}
} + +
+ {stats.map((item) => ( +
+ {item.label} + {item.value} +
+ ))} +
+ +
+
+
+

平台

+ {platforms.length} +
+
+
+ Provider + 名称 + 状态 + 优先级 +
+ {platforms.map((item) => ( +
+ {item.provider} + {item.name} + {item.status} + {item.priority} +
+ ))} + {!platforms.length &&

暂无平台数据

} +
+
+ +
+
+

模型

+ {models.length} +
+
+
+ 模型 + 类型 + 平台 + 启用 +
+ {models.map((item) => ( +
+ {item.modelName} + {item.modelType} + {item.provider ?? item.platformName} + {item.enabled ? '是' : '否'} +
+ ))} + {!models.length &&

暂无模型数据

} +
+
+
+ +
+
+
+

基准模型库

+ {baseModels.length} +
+
+
+ Provider + 模型 + 类型 + 版本 +
+ {baseModels.map((item) => ( +
+ {item.providerKey} + {item.canonicalModelKey} + {item.modelType} + {item.pricingVersion} +
+ ))} + {!baseModels.length &&

暂无基准模型

} +
+
+ +
+
+

TPM/RPM 窗口

+ {rateLimitWindows.length} +
+
+
+ Scope + 指标 + 使用 + 预占 +
+ {rateLimitWindows.map((item) => ( +
+ {item.scopeKey} + {item.metric} + {item.usedValue}/{item.limitValue} + {item.reservedValue} +
+ ))} + {!rateLimitWindows.length &&

暂无限流窗口

} +
+
+
+
+ ); +} diff --git a/apps/web/src/api.ts b/apps/web/src/api.ts new file mode 100644 index 0000000..e4d661a --- /dev/null +++ b/apps/web/src/api.ts @@ -0,0 +1,60 @@ +import type { + BaseModelCatalogItem, + CatalogProvider, + IntegrationPlatform, + ListResponse, + PlatformModel, + PricingRule, + RateLimitWindow, +} from '@easyai-ai-gateway/contracts'; + +const API_BASE = import.meta.env.VITE_GATEWAY_API_BASE_URL ?? 'http://localhost:8088'; + +export interface HealthResponse { + ok: boolean; + service: string; + env: string; +} + +export async function getHealth(): Promise { + return request('/healthz', { auth: false }); +} + +export async function listPlatforms(token: string): Promise> { + return request>('/api/v1/platforms', { token }); +} + +export async function listModels(token: string): Promise> { + return request>('/api/v1/models', { token }); +} + +export async function listCatalogProviders(token: string): Promise> { + return request>('/api/v1/catalog/providers', { token }); +} + +export async function listBaseModels(token: string): Promise> { + return request>('/api/v1/catalog/base-models', { token }); +} + +export async function listPricingRules(token: string): Promise> { + return request>('/api/v1/pricing/rules', { token }); +} + +export async function listRateLimitWindows(token: string): Promise> { + return request>('/api/v1/runtime/rate-limit-windows', { token }); +} + +async function request(path: string, options: { token?: string; auth?: boolean } = {}): Promise { + const headers: Record = {}; + if (options.auth !== false && options.token) { + headers.Authorization = `Bearer ${options.token}`; + } + const response = await fetch(`${API_BASE}${path}`, { + headers, + }); + if (!response.ok) { + const body = await response.text(); + throw new Error(body || `Request failed: ${response.status}`); + } + return response.json() as Promise; +} diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx new file mode 100644 index 0000000..ed3b67f --- /dev/null +++ b/apps/web/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import { App } from './App'; +import './styles.css'; + +createRoot(document.getElementById('root')!).render( + + + , +); diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css new file mode 100644 index 0000000..ade662e --- /dev/null +++ b/apps/web/src/styles.css @@ -0,0 +1,286 @@ +:root { + color: #172033; + background: #f5f7fb; + font-family: + Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", + sans-serif; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + min-width: 320px; + min-height: 100vh; +} + +button, +input { + font: inherit; +} + +.page { + width: min(1180px, calc(100vw - 32px)); + margin: 0 auto; + padding: 28px 0 48px; +} + +.topbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 20px; + margin-bottom: 24px; +} + +.eyebrow { + margin: 0 0 4px; + color: #667085; + font-size: 13px; + font-weight: 700; + text-transform: uppercase; +} + +h1, +h2, +p { + margin: 0; +} + +h1 { + font-size: 30px; + line-height: 1.2; +} + +h2 { + font-size: 18px; +} + +.health { + display: inline-flex; + align-items: center; + gap: 8px; + color: #667085; + font-size: 14px; +} + +.health span { + width: 9px; + height: 9px; + border-radius: 50%; + background: #c43f3f; +} + +.health[data-ok="true"] span { + background: #1b8a5a; +} + +.toolbar { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 12px; + align-items: end; + padding: 16px; + margin-bottom: 18px; + border: 1px solid #dde3ee; + border-radius: 8px; + background: #ffffff; +} + +.tokenField { + display: grid; + gap: 8px; + color: #4a5568; + font-size: 13px; + font-weight: 700; +} + +.tokenField input { + width: 100%; + min-height: 42px; + padding: 0 12px; + border: 1px solid #cbd5e1; + border-radius: 6px; + color: #172033; + outline: none; +} + +.tokenField input:focus { + border-color: #2b6cb0; + box-shadow: 0 0 0 3px rgba(43, 108, 176, 0.14); +} + +button { + min-height: 42px; + padding: 0 18px; + border: 0; + border-radius: 6px; + background: #214e8a; + color: #ffffff; + font-weight: 700; + cursor: pointer; +} + +button:disabled { + cursor: not-allowed; + opacity: 0.5; +} + +.notice { + padding: 12px 14px; + margin-bottom: 18px; + border: 1px solid #f0b8b8; + border-radius: 8px; + background: #fff1f1; + color: #9b2c2c; + font-size: 14px; +} + +.metrics { + display: grid; + grid-template-columns: repeat(6, minmax(0, 1fr)); + gap: 12px; + margin-bottom: 18px; +} + +.metric { + min-height: 96px; + padding: 16px; + border: 1px solid #dde3ee; + border-radius: 8px; + background: #ffffff; +} + +.metric span { + color: #667085; + font-size: 13px; + font-weight: 700; +} + +.metric strong { + display: block; + margin-top: 10px; + font-size: 30px; +} + +.metric[data-tone="blue"] { + border-top: 3px solid #2b6cb0; +} + +.metric[data-tone="green"] { + border-top: 3px solid #1b8a5a; +} + +.metric[data-tone="violet"] { + border-top: 3px solid #6b46c1; +} + +.metric[data-tone="amber"] { + border-top: 3px solid #b7791f; +} + +.metric[data-tone="cyan"] { + border-top: 3px solid #087f8c; +} + +.metric[data-tone="rose"] { + border-top: 3px solid #b8325f; +} + +.split { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 18px; +} + +.split.secondary { + margin-top: 18px; +} + +.panel { + overflow: hidden; + border: 1px solid #dde3ee; + border-radius: 8px; + background: #ffffff; +} + +.panelHeader { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px; + border-bottom: 1px solid #e7ecf4; +} + +.panelHeader span { + color: #667085; + font-size: 13px; + font-weight: 700; +} + +.table { + display: grid; +} + +.row { + display: grid; + grid-template-columns: 1.2fr 1.2fr 0.8fr 0.6fr; + gap: 12px; + min-height: 46px; + align-items: center; + padding: 0 16px; + border-bottom: 1px solid #edf1f7; + color: #2d3748; + font-size: 14px; +} + +.row span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.row.head { + min-height: 38px; + background: #f8fafc; + color: #667085; + font-size: 12px; + font-weight: 800; + text-transform: uppercase; +} + +.empty { + padding: 22px 16px; + color: #667085; + font-size: 14px; +} + +@media (max-width: 860px) { + .topbar, + .toolbar { + grid-template-columns: 1fr; + } + + .topbar { + align-items: flex-start; + flex-direction: column; + } + + .metrics, + .split { + grid-template-columns: 1fr; + } + + .row { + grid-template-columns: 1fr 0.8fr; + padding: 10px 16px; + } +} + +@media (min-width: 861px) and (max-width: 1180px) { + .metrics { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } +} diff --git a/apps/web/src/vite-env.d.ts b/apps/web/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/apps/web/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json new file mode 100644 index 0000000..596336d --- /dev/null +++ b/apps/web/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": ["src"], + "references": [] +} diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts new file mode 100644 index 0000000..fcdac62 --- /dev/null +++ b/apps/web/vite.config.ts @@ -0,0 +1,9 @@ +import react from '@vitejs/plugin-react'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [react()], + server: { + port: 5178, + }, +}); diff --git a/docs/design.md b/docs/design.md new file mode 100644 index 0000000..93eca63 --- /dev/null +++ b/docs/design.md @@ -0,0 +1,1584 @@ +# EasyAI AI 网关中台设计文档 + +## 1. 建设目标 + +将现有 `easyai-server-main` / `easyai-integration-api` 中与 `integration-platform` 强相关的能力拆分为一个独立项目: + +- 独立运行:后端 Go、复用 Agent memory 的 PostgreSQL(`easyai-pgvector`)、前端 React + TSX。 +- 前端 UI:使用 `shadcn-ui` + Tailwind CSS + Radix UI + `lucide-react`,保持控制台形态与 EasyAI 运维后台一致。 +- 独立部署:HTTP API、任务执行器、未来 WebSocket / SSE 推送均由本服务承载。 +- 授权复用:用户授权沿用 `easyai-server-main` 的 JWT claim、角色权限和 OpenAPI `sk-*` 校验方式。 +- 能力对接:作为 Chat、生图、生视频等模型能力网关,供 `easyai-server-main` 和前端直接调用。 +- 渐进迁移:新服务稳定后,再让 `server-main` 的 `OpenaiService` / `IntegrationPlatformService` 切为远程调用门面。 + +## 2. 迁移边界 + +### 2.1 迁入 AI Gateway + +| 能力 | 新服务职责 | +| --- | --- | +| 平台管理 | 平台 CRUD、启停、优先级、凭证配置、可用性检测、默认折扣系数 | +| 基准模型库 | 维护全量 provider、基准模型、模型能力、图像/视频/文本能力标签、基准定价和默认限流模板 | +| 模型管理 | 平台模型、模型类型、能力标签、上下文窗口、计费配置;支持从基准模型继承、按折扣继承或自由覆盖 | +| 模型路由 | 复用现有 `assignClientsByModelName` 的平台过滤、并发负载、优先级、队列等待语义 | +| 生成任务 | Chat、图片生成、图片编辑、视频生成、语音、Embedding 等任务入口 | +| 队列执行 | 按平台/模型/方法维度做并发控制、等待队列、超时、重试;队列状态必须持久化,服务异常重启后可恢复 | +| 测试模式 | 支持 simulation / dry-run,不向真实平台提交任务,仍完整走路由、队列、限流、重试、进度、结果归一流程 | +| 计费预估 | 在任务创建前或前端配置面提供 estimated billing,且必须使用与真实路由一致的有效模型价格 | +| 任务结果 | 保存任务请求、状态、结果摘要、billings,向主服务发送结算事件 | +| 推送 | 承接任务级 SSE / WebSocket / OpenAI stream 进度,提供类似原 RxJS Observable 的中间过程返回能力,不再经 `server-main` 转发 | + +### 2.2 保留在 server-main + +| 能力 | 原因 | +| --- | --- | +| 用户、组织、租户、角色 | 仍是 EasyAI 主账号体系 | +| 余额、资源包、扣费流水 | 需要复用现有账单锁、组织扣费、消费记录 | +| API Key 创建与撤销 | `sk-*` 生命周期属于主服务用户体系 | +| 文件上传 | 复用 `server-main` 已开放的文件上传接口,OSS/COS/S3 密钥和上传实现继续只落在主服务 | +| 对话、绘图历史、工作流历史 | 与产品域模型绑定,Gateway 只返回任务结果和结算载荷 | + +## 3. 总体架构 + +```mermaid +flowchart LR + subgraph client [调用方] + FE[React / easyai-main-web] + MAIN[easyai-server-main] + OPENAPI[OpenAPI Client] + end + + subgraph gw [API Gateway / Ingress] + HTTP[HTTP Routes] + PUSH[SSE / WebSocket Routes] + end + + subgraph aigw [EasyAI AI Gateway] + API[Go HTTP API] + AUTH[Auth Middleware] + ROUTER[Model Router] + QUEUE[Queue + Runtime] + VENDOR[Vendor Clients] + ADMIN[React Admin Console] + end + + subgraph data [Data] + PG[(Agent memory PostgreSQL
easyai-pgvector)] + REDIS[(Redis later)] + end + + subgraph servermain [server-main] + USER[User / Org] + APIKEY[API Key Verify] + BILL[Billing + Ledger] + FILES[Open File Upload] + end + + FE --> HTTP --> API + FE --> PUSH + OPENAPI --> HTTP --> API + MAIN -->|internal HTTP| API + ADMIN --> API + API --> AUTH + AUTH -->|JWT local verify| API + AUTH -->|sk-* delegate| APIKEY + API --> ROUTER --> QUEUE --> VENDOR + API --> PG + QUEUE --> PG + QUEUE -->|settlement event| BILL + API -->|POST /v1/files/upload| FILES +``` + +## 4. Monorepo 方案 + +本脚手架采用 `Nx + pnpm + go.work`: + +- `Nx` 负责任务编排、缓存、前后端统一命令:`dev`、`build`、`test`、`migrate`。 +- `go.work` 管理 Go 模块,避免把 Go 项目硬塞进 Node 包管理。 +- `pnpm-workspace.yaml` 管理 React 前端与 TypeScript contracts。 +- 前端控制台使用 `shadcn-ui` 作为组件体系,配套 Tailwind CSS、Radix UI primitive、`lucide-react` 图标和 `class-variance-authority` 管理组件变体。 + +选择这个组合的原因: + +- 与 EasyAI 现有 Nx 使用习惯接近,便于团队统一命令。 +- Go 后端保持 Go 原生模块边界,后续可以拆 `apps/worker`、`libs/go/*`。 +- 前端仍可复用 React 生态和 TSX 风格,contracts 可给前端、SDK、BFF 共用。 +- `shadcn-ui` 代码是可拷贝进仓库的 TSX 组件,不绑定运行时 UI 框架,适合后续沉淀平台管理、队列监控、模型计费配置等复杂表单。 + +### 4.1 前端 UI 约束 + +- 基础组件统一从 `apps/web/src/components/ui/*` 引入,不直接散落自定义 button/input/table 样式。 +- 首期需要引入并固化:`Button`、`Input`、`Textarea`、`Select`、`Switch`、`Checkbox`、`Tabs`、`Dialog`、`Sheet`、`Table`、`Badge`、`Alert`、`Toast/Sonner`、`Tooltip`、`Progress`、`DropdownMenu`、`Form`。 +- 图标统一使用 `lucide-react`,按钮类动作优先用语义图标加 tooltip。 +- 控制台不是营销页,布局以高密度管理视图为主:平台列表、模型配置、队列监控、任务详情、客户端运行状态、限流配置、重试策略。 +- 主题 token 以 CSS variables 管理,后续可与 EasyAI 全局后台色板对齐。 + +## 5. 授权设计 + +### 5.1 JWT 用户授权 + +一期直接兼容 `server-main` 的 JWT: + +- secret:读取 `CONFIG_JWT_SECRET`,默认值与现有 `jwtConstants.secret` 保持一致。 +- token 有效期:由 `server-main` 签发控制,当前 access token 为 `600s`,refresh token 为 `7d`。 +- claim 兼容: + - `sub`:用户 ID + - `username` + - `role` + - `tenantId` + - `sso_id` + - API Key 场景扩展:`apiKeyId`、`apiKeySecret`、`apiKeyName` + +Gateway 只校验 access token,不签发 refresh token。刷新仍走 `server-main`。 + +### 5.2 权限等级 + +权限枚举与 `server-main` 保持一致: + +| Permission | 用途 | +| --- | --- | +| `public` | 健康检查、公开模型列表等 | +| `basic` | 普通用户任务创建、任务查询 | +| `creat` | 创作者级能力 | +| `power` | 平台配置、模型配置、计费配置 | +| `manager` | 超级管理与危险操作 | + +角色映射与 `server-main` 一致:`user`、`creator`、`operator`、`manager`、`admin`。 + +### 5.3 OpenAPI API Key + +对于 `Authorization: Bearer sk-*` 或 `x-comfy-api-key`: + +1. Gateway 不直接持有 API Key hash。 +2. Gateway 调用 `server-main` 的内部校验接口。 +3. `server-main` 返回标准用户 claim。 +4. Gateway 将 claim 放入请求上下文,后续任务、结算事件都带 `apiKeyId` / `apiKeyName`。 + +建议在 `server-main` 增加内部接口: + +```http +POST /internal/platform/auth/verify-api-key +Authorization: Bearer ${SERVER_MAIN_INTERNAL_TOKEN} +Content-Type: application/json + +{ "apiKey": "sk-..." } +``` + +### 5.4 内部服务授权 + +`server-main` 调 Gateway 内部接口时使用服务令牌: + +```http +Authorization: Bearer ${SERVER_MAIN_INTERNAL_TOKEN} +X-EasyAI-Actor: easyai-server-main +``` + +内部令牌只用于服务间调用,不替代用户上下文。用户发起的任务仍应透传用户 JWT 或 API Key claim。 + +## 6. 接口规范与兼容路由 + +### 6.1 兼容优先原则 + +AI Gateway 的外部接口必须以“兼容现有 EasyAI 路由、请求 DTO、响应结构、错误码、流式语义”为第一目标,不能要求前端或 `server-main` 因拆分服务而大面积改调用路径。 + +- **管理面兼容**:沿用原 `integration-platform`、`integration/platform-api`、`integration-platform/snapshots` 路由。 +- **站内生成接口兼容**:沿用原 `OpenaiController` 根路径接口,例如 `/chat/completions`、`/images/generations`。 +- **OpenAPI 兼容**:沿用 `/v1/*` 路由,例如 `/v1/chat/completions`、`/v1/images/generations`,API Key 语义与原来一致。 +- **新增接口隔离**:`/api/v1/*` 只用于 Gateway 自身控制台、调试、内部 SDK 或新能力,不作为迁移期替代原路由。 +- **响应包装**:对原来返回 `ResOp` 的管理接口继续返回同形态;对 OpenAI-compatible 接口保持原 JSON / stream chunk 结构;对任务类接口保持原 `taskId`、`status`、`url`、`billings` 等字段语义。 +- **路径匹配顺序**:迁移 `integration-platform` 时必须保留静态路径优先于 `:id` / `:action/:id` 的规则,避免 `models/list/enabled`、`dynamic-priority/:id`、`scene-script/:id/partial` 被动态路由误吞。 + +### 6.2 平台管理兼容路由 + +| Method | Path | Permission | 说明 | +| --- | --- | --- | --- | +| `POST` | `/integration-platform` | `power` | 创建平台 | +| `POST` | `/integration-platform/preset` | `power` | 创建预置平台 | +| `GET` | `/integration-platform` | `power` | 平台列表 | +| `GET` | `/integration-platform/list/ops-compact` | `power` | 运维精简列表 | +| `GET` | `/integration-platform/queue-stats` | `power` | 队列统计 | +| `POST` | `/integration-platform/clear-queue/:platformId` | `power` | 清理平台队列 | +| `POST` | `/integration-platform/find-by-ids` | `power` | 按 ID 批量查平台 | +| `POST` | `/integration-platform/find-by-model` | `basic` | 按模型查候选平台 | +| `GET` | `/integration-platform/models/list/enabled` | `basic` | 启用模型列表 | +| `GET` | `/integration-platform/models/list/all` | `power` | 全量模型列表 | +| `PATCH` | `/integration-platform/dynamic-priority/:id` | `power` | 动态优先级 | +| `DELETE` | `/integration-platform/dynamic-priority/:id` | `power` | 删除动态优先级 | +| `POST` | `/integration-platform/disable/:id` | `power` | 禁用平台 | +| `POST` | `/integration-platform/enable-model/:id` | `power` | 启用平台模型 | +| `PATCH` | `/integration-platform/scene-script/:id/partial` | `power` | 局部更新场景脚本 | +| `PATCH` | `/integration-platform/:action/:id` | `power` | 兼容启停等 action | +| `PATCH` | `/integration-platform/:id` | `power` | 更新平台 | +| `DELETE` | `/integration-platform/:id` | `manager` | 删除平台 | +| `GET` | `/integration-platform/models/:type` | `basic` | 按类型查模型 | +| `GET` | `/integration-platform/models/strict/:type` | `basic` | 按类型严格查模型 | +| `POST` | `/integration-platform/models/estimatedBilling` | `basic` | 预估扣费 | +| `POST` | `/integration-platform/models/estimatedBilling/:workflowId` | `basic` | 工作流预估扣费 | +| `POST` | `/integration-platform/resetException/:id` | `power` | 重置异常状态 | +| `POST` | `/integration-platform/:id/copy` | `power` | 复制平台 | +| `GET` | `/integration-platform/:id` | `power` | 平台详情 | + +### 6.3 平台 API 配置兼容路由 + +| Method | Path | Permission | 说明 | +| --- | --- | --- | --- | +| `POST` | `/integration/platform-api` | `power` | 创建 API 配置 | +| `GET` | `/integration/platform-api` | `power` | API 配置列表 | +| `GET` | `/integration/platform-api/:id` | `power` | API 配置详情 | +| `PATCH` | `/integration/platform-api/:id` | `power` | 更新 API 配置 | +| `DELETE` | `/integration/platform-api/:id` | `manager` | 删除 API 配置 | +| `POST` | `/integration/platform-api/execute` | `power` | 调试执行 | +| `GET` | `/integration-platform/snapshots/platform/:platformId` | `power` | 平台快照 | + +### 6.4 站内生成兼容路由 + +| Method | Path | 说明 | +| --- | --- | --- | +| `GET` | `/models/list` | 站内模型列表 | +| `POST` | `/chat/completions` | 站内 Chat | +| `POST` | `/chat/completions/cancel/:requestId` | 取消 Chat | +| `POST` | `/chat/structured-output` | 结构化输出 | +| `POST` | `/responses` | Responses | +| `POST` | `/responses/cancel/:requestId` | 取消 Responses | +| `POST` | `/images/generations` | 生图 | +| `POST` | `/images/edits` | 图片编辑 | +| `POST` | `/video/generations` | 生视频 | +| `POST` | `/embeddings` | Embeddings | +| `GET` | `/embeddings/models` | Embedding 模型 | +| `POST` | `/text2Model/generations` | 文生 3D / 模型 | +| `POST` | `/digital-human/create` | 数字人创建 | +| `POST` | `/digital-human/generations` | 数字人生成 | +| `POST` | `/music/generations` | 音乐生成 | +| `POST` | `/speech/generations` | 语音生成 | +| `GET` | `/ai/result/:taskId` | 任务结果 | + +### 6.5 OpenAPI 兼容路由 + +| Method | Path | 说明 | +| --- | --- | --- | +| `POST` | `/v1/chat/completions` | OpenAI-compatible Chat | +| `POST` | `/v1/chat/completions/cancel/:requestId` | 取消 Chat | +| `POST` | `/v1/responses` | Responses | +| `POST` | `/v1/responses/cancel/:requestId` | 取消 Responses | +| `POST` | `/v1/images/generations` | 生图 | +| `POST` | `/v1/images/edits` | 图片编辑 | +| `POST` | `/v1/video/generations` | 生视频 | +| `POST` | `/v1/song/generations` | 音乐生成 | +| `POST` | `/v1/speech/generations` | 语音生成 | +| `POST` | `/v1/digital-human/create` | 数字人创建 | +| `POST` | `/v1/digital-human/generations` | 数字人生成 | +| `POST` | `/v1/ai/generations` | 通用 AI 任务 | +| `GET` | `/v1/ai/result/:taskId` | 任务结果 | +| `GET` | `/v1/ai/cancel/:taskId` | 取消任务 | +| `POST` | `/v1/embeddings` | Embeddings | +| `GET` | `/v1/embeddings/models` | Embedding 模型 | +| `GET` | `/v1/models` | OpenAI-compatible 模型列表 | +| `GET` | `/v1/models/list` | EasyAI 模型列表 | + +`/v1/balance`、`/v1/files/upload`、`/v1/creatToken`、`/v1/removeToken` 与用户余额、文件、API Key 生命周期强绑定,默认仍由 `server-main` 保留;若网关暴露同路径,只能做兼容代理或调用 `server-main` 内部接口。 + +### 6.6 Gateway 新增接口 + +| Method | Path | Permission | 说明 | +| --- | --- | --- | --- | +| `GET` | `/api/v1/me` | `basic` | 网关当前身份调试 | +| `GET` | `/api/v1/catalog/providers` | `power` | 基准 provider 列表 | +| `GET` | `/api/v1/catalog/base-models` | `power` | 基准模型库列表 | +| `GET` | `/api/v1/catalog/base-models/:id` | `power` | 基准模型详情 | +| `POST` | `/api/v1/pricing/estimate` | `basic` | 使用 effective pricing resolver 做价格预估 | +| `GET` | `/api/v1/pricing/rules` | `power` | 定价规则列表 | +| `GET` | `/api/v1/tasks/:taskId` | `basic` | Gateway 任务详情 | +| `GET` | `/api/v1/tasks/:taskId/events` | `basic` | SSE 任务进度 | +| `GET` | `/api/v1/runtime/clients` | `power` | 客户端运行状态 | +| `GET` | `/api/v1/runtime/queues` | `power` | 队列与限流状态 | +| `GET` | `/api/v1/runtime/rate-limit-windows` | `power` | TPM/RPM 当前窗口与并发 lease 状态 | +| `POST` | `/api/v1/runtime/tasks/:taskId/replay` | `power` | 重放任务事件 | + +### 6.7 内部接口 + +| Method | Path | 调用方 | 说明 | +| --- | --- | --- | --- | +| `POST` | `/internal/v1/settlements` | Gateway worker | 回调主服务结算失败时补偿 | +| `POST` | `/internal/v1/task-callbacks` | server-main | 迁移期主服务回写历史或任务绑定 | + +## 7. 数据模型 + +数据模型按“基准模型库 -> 平台实例 -> 平台模型覆盖 -> 任务运行态”分层: + +- 基准模型库保存 provider、canonical model、能力 schema、默认能力、基准价格、默认限流模板,是所有平台模型的 fallback。 +- 创建平台时可以设置 `default_discount_factor`,平台模型默认按“基准价格 x 折扣系数”计算;没有任何平台侧配置时,能力和价格都 follow 基准模型。 +- 平台模型可以覆盖能力、定价和限流。覆盖只作用于该平台模型,不能反向污染基准模型库。 +- estimated billing、真实任务 billings、控制台价格预览必须走同一个 effective pricing resolver。 + +### 7.1 `integration_platforms` + +保存平台实例与凭证: + +- `provider`:平台类型,如 `openai`、`runninghub`、`jimeng`。 +- `name`:运营可识别名称。 +- `base_url` +- `auth_type` +- `credentials`:加密后的凭证 JSON,后续应接入 KMS。 +- `config`:限流、超时、重试、平台私有配置。 +- `default_discount_factor`:平台默认折扣系数,基于基准模型价计算平台有效价。 +- `priority` +- `status` + +### 7.2 `platform_models` + +保存平台模型配置: + +- `platform_id` +- `base_model_id`:关联基准模型库,未配置能力/价格时 follow 基准模型。 +- `model_name` +- `model_type`:`chat`、`image`、`video`、`audio` 等。 +- `capabilities`:平台模型有效能力,来自基准模型能力与平台覆盖合并。 +- `pricing_mode`:`inherit`、`inherit_discount`、`custom`。 +- `discount_factor`:模型级折扣,未设置时使用平台默认折扣。 +- `billing_config`:平台模型有效计费配置,来自基准价、折扣和自定义覆盖。 +- `enabled` + +### 7.3 `gateway_tasks` + +保存任务请求与状态: + +- `kind`:`chat.completions`、`images.generations`、`videos.generations` +- `user_id` +- `tenant_id` +- `model` +- `request` +- `status`:`queued`、`running`、`succeeded`、`failed`、`cancelled` +- `queue_key`:限流队列 key,例如 `${platformKey}-${model}` 或 `${provider}-${methodName}` +- `priority` +- `idempotency_key` +- `remote_task_id` +- `remote_task_payload` +- `run_mode`:`production`、`simulation` +- `simulation_profile` +- `simulation_seed` +- `locked_by` +- `locked_at` +- `heartbeat_at` +- `next_run_at` +- `result` +- `billings` +- `error` + +### 7.4 `gateway_task_attempts` + +保存每一次客户端尝试,支持“上一个客户端失败,下一个客户端重试”的完整审计: + +- `task_id` +- `attempt_no` +- `platform_id` +- `client_id` +- `model_id` +- `status`:`submitted`、`polling`、`succeeded`、`failed`、`skipped` +- `retryable` +- `error_code` +- `error_message` +- `remote_task_id` +- `simulated` +- `request_snapshot` +- `response_snapshot` +- `started_at` +- `finished_at` + +### 7.5 `gateway_task_events` + +保存任务执行过程中的事件,用于 SSE / WebSocket 重放与服务重启后的进度恢复: + +- `task_id` +- `seq`:单任务内递增序号 +- `event_type`:`queue_status`、`node_status`、`progress`、`partial_result`、`completed`、`failed` +- `status` +- `progress` +- `message` +- `payload` +- `created_at` + +客户端断线重连时可通过 `Last-Event-ID` 或 `afterSeq` 回放事件。 + +### 7.6 `runtime_client_states` + +保存平台客户端的运行时状态: + +- `client_id` +- `platform_id` +- `provider` +- `method_name` +- `queue_key` +- `running_count` +- `waiting_count` +- `limiter_ratio` +- `cooldown_until` +- `last_assigned_at` +- `last_error` +- `updated_at` + +该表用于恢复与观测;实时并发计数仍可通过事务锁、`SKIP LOCKED`、Redis token bucket 或内存计数加 PG 校验组合实现,但 PG 是最终事实源。 + +### 7.7 `gateway_upload_assets` + +保存网关任务引用的上传资产: + +- `task_id` +- `source`:`server-main-open-upload`、`remote-url`、`multipart`、`simulation` +- `server_main_file_id`:主服务返回的文件 ID。 +- `object_key` +- `url` +- `content_type` +- `size` +- `checksum` +- `metadata` + +### 7.8 `settlement_outbox` + +保存结算事件 outbox: + +- `task_id` +- `event_type` +- `payload` +- `status` +- `attempts` +- `next_attempt_at` + +结算事件必须按 `eventId` / `taskId` 幂等,不能因为 Gateway 重启、HTTP 回调失败或 MQ 抖动重复扣费。 + +### 7.9 表结构草案 + +以下为首期 PostgreSQL 表结构草案。字段类型以后续 migration 为准,但语义和索引边界应保持稳定。 + +#### 7.9.1 `model_catalog_providers` + +| 字段 | 类型 | 约束 | 说明 | +| --- | --- | --- | --- | +| `id` | `uuid` | PK | 基准 provider ID | +| `provider_key` | `text` | unique, not null | 稳定 provider key,如 `openai`、`runninghub`、`jimeng` | +| `display_name` | `text` | not null | 展示名称 | +| `provider_type` | `text` | not null | `openai_compatible`、`workflow_app`、`video_vendor`、`audio_vendor` | +| `capability_schema` | `jsonb` | not null, default `{}` | provider 支持的能力字段定义 | +| `default_rate_limit_policy` | `jsonb` | not null, default `{}` | provider 默认 TPM/RPM/并发模板 | +| `status` | `text` | not null | `active`、`deprecated`、`hidden` | +| `metadata` | `jsonb` | not null, default `{}` | 文档链接、logo、排序等 | +| `created_at` | `timestamptz` | not null | 创建时间 | +| `updated_at` | `timestamptz` | not null | 更新时间 | + +索引: + +- `uniq_model_catalog_provider_key(provider_key)` +- `idx_model_catalog_provider_status(status)` + +#### 7.9.2 `base_model_catalog` + +保存全量基准模型。这个表不代表某个平台已经开通该模型,而是标准模型与价格来源。 + +| 字段 | 类型 | 约束 | 说明 | +| --- | --- | --- | --- | +| `id` | `uuid` | PK | 基准模型 ID | +| `provider_id` | `uuid` | FK | 所属基准 provider | +| `provider_key` | `text` | not null | 冗余 provider key,方便查询 | +| `canonical_model_key` | `text` | unique, not null | Gateway 内部标准模型 key | +| `provider_model_name` | `text` | not null | 供应商原始模型名 | +| `model_type` | `text` | not null | `chat`、`image`、`video`、`audio`、`embedding`、`music`、`digital_human`、`model_3d` | +| `display_name` | `text` | not null | 展示名称 | +| `capabilities` | `jsonb` | not null, default `{}` | 上下文、多模态、参考图/视频/音频、尺寸、时长、质量等基准能力 | +| `base_billing_config` | `jsonb` | not null, default `{}` | 基准计费配置 | +| `default_rate_limit_policy` | `jsonb` | not null, default `{}` | 默认 TPM/RPM/并发限制模板 | +| `pricing_version` | `int` | not null, default `1` | 基准价格版本 | +| `status` | `text` | not null | `active`、`deprecated`、`hidden` | +| `metadata` | `jsonb` | not null, default `{}` | 供应商文档、排序、标签 | +| `created_at` | `timestamptz` | not null | 创建时间 | +| `updated_at` | `timestamptz` | not null | 更新时间 | + +索引: + +- `uniq_base_model_catalog_key(canonical_model_key)` +- `idx_base_model_catalog_provider(provider_key, model_type, status)` +- `idx_base_model_catalog_capabilities` 使用 `GIN(capabilities)` + +#### 7.9.3 `model_pricing_rules` + +保存基准模型和平台模型的可版本化价格规则。平台模型没有自定义规则时,使用 `base_model_catalog.base_billing_config` 并应用折扣。 + +| 字段 | 类型 | 约束 | 说明 | +| --- | --- | --- | --- | +| `id` | `uuid` | PK | 价格规则 ID | +| `scope_type` | `text` | not null | `base_model`、`platform`、`platform_model` | +| `scope_id` | `uuid` | nullable | 对应基准模型、平台或平台模型 ID | +| `resource_type` | `text` | not null | `text_input`、`text_output`、`image`、`video`、`audio`、`music`、`digital_human`、`model` | +| `unit` | `text` | not null | `1k_tokens`、`image`、`5s`、`second`、`character_1k`、`item` | +| `base_price` | `numeric` | not null | 基准单价,单位使用 EasyAI resource / credit | +| `currency` | `text` | not null, default `resource` | `resource`、`credit`、`cny`、`usd` | +| `base_weight` | `jsonb` | not null, default `{}` | 固定权重,如默认倍率 | +| `dynamic_weight` | `jsonb` | not null, default `{}` | 分辨率、质量、时长、有无音频等动态权重 | +| `effective_from` | `timestamptz` | nullable | 生效开始 | +| `effective_to` | `timestamptz` | nullable | 生效结束 | +| `created_at` | `timestamptz` | not null | 创建时间 | +| `updated_at` | `timestamptz` | not null | 更新时间 | + +索引: + +- `idx_model_pricing_scope(scope_type, scope_id, resource_type)` +- `idx_model_pricing_effective(effective_from, effective_to)` + +#### 7.9.4 `integration_platforms` + +| 字段 | 类型 | 约束 | 说明 | +| --- | --- | --- | --- | +| `id` | `uuid` | PK | 平台实例 ID | +| `provider` | `text` | not null | 平台类型,如 `openai`、`runninghub`、`jimeng` | +| `platform_key` | `text` | unique, not null | 稳定平台 key,用于队列和客户端注册 | +| `name` | `text` | not null | 展示名称 | +| `base_url` | `text` | nullable | 平台 API 地址 | +| `auth_type` | `text` | not null | `bearer`、`api_key`、`basic`、`custom` | +| `credentials` | `jsonb` | not null, default `{}` | 加密后的凭证 JSON | +| `config` | `jsonb` | not null, default `{}` | 超时、重试、限流、平台私有配置 | +| `default_pricing_mode` | `text` | not null, default `inherit_discount` | 平台默认价格模式:`inherit`、`inherit_discount`、`custom` | +| `default_discount_factor` | `numeric` | not null, default `1` | 平台默认折扣系数,创建平台时可统一基于基准模型打折 | +| `retry_policy` | `jsonb` | not null, default `{}` | 平台级重试策略 | +| `rate_limit_policy` | `jsonb` | not null, default `{}` | 平台级限流策略 | +| `priority` | `int` | not null, default `100` | 静态优先级,越小越优先 | +| `dynamic_priority` | `int` | nullable | 动态优先级覆盖 | +| `status` | `text` | not null | `enabled`、`disabled`、`exception` | +| `disabled_reason` | `text` | nullable | 禁用原因 | +| `cooldown_until` | `timestamptz` | nullable | 熔断/冷却到期时间 | +| `created_at` | `timestamptz` | not null | 创建时间 | +| `updated_at` | `timestamptz` | not null | 更新时间 | +| `deleted_at` | `timestamptz` | nullable | 软删除 | + +索引: + +- `idx_integration_platforms_provider_status(provider, status)` +- `idx_integration_platforms_status_priority(status, priority, dynamic_priority)` +- `idx_integration_platforms_cooldown(cooldown_until)` + +#### 7.9.5 `platform_models` + +| 字段 | 类型 | 约束 | 说明 | +| --- | --- | --- | --- | +| `id` | `uuid` | PK | 模型配置 ID | +| `platform_id` | `uuid` | FK | 所属平台 | +| `base_model_id` | `uuid` | FK nullable | 关联基准模型;为空时表示纯自定义模型 | +| `model_name` | `text` | not null | 请求使用的模型名 | +| `model_alias` | `text` | nullable | 对外展示/兼容别名 | +| `model_type` | `text` | not null | `chat`、`image`、`video`、`audio`、`embedding` | +| `display_name` | `text` | not null | 展示名称 | +| `capability_override` | `jsonb` | not null, default `{}` | 相对基准模型的能力覆盖 | +| `capabilities` | `jsonb` | not null, default `{}` | 解析后的有效能力,用于路由和前端展示 | +| `pricing_mode` | `text` | not null, default `inherit_discount` | `inherit`、`inherit_discount`、`custom` | +| `discount_factor` | `numeric` | nullable | 模型级折扣,空值时继承平台默认折扣 | +| `billing_config_override` | `jsonb` | not null, default `{}` | 自定义计费覆盖 | +| `billing_config` | `jsonb` | not null, default `{}` | 解析后的有效计费配置,用于 estimated billing 和真实 billings | +| `permission_config` | `jsonb` | not null, default `{}` | 平台维度权限过滤配置 | +| `retry_policy` | `jsonb` | not null, default `{}` | 模型级重试策略 | +| `rate_limit_policy` | `jsonb` | not null, default `{}` | 模型级限流策略 | +| `enabled` | `boolean` | not null, default `true` | 是否启用 | +| `created_at` | `timestamptz` | not null | 创建时间 | +| `updated_at` | `timestamptz` | not null | 更新时间 | + +索引与约束: + +- `uniq_platform_model_type(platform_id, model_name, model_type)` +- `idx_platform_models_base(base_model_id)` +- `idx_platform_models_lookup(model_type, model_name, enabled)` +- `idx_platform_models_alias(model_alias)` +- `idx_platform_models_capabilities` 使用 `GIN(capabilities)` + +#### 7.9.6 `gateway_tasks` + +| 字段 | 类型 | 约束 | 说明 | +| --- | --- | --- | --- | +| `id` | `uuid` | PK | Gateway 任务 ID | +| `external_task_id` | `text` | nullable | 与 `server-main` / 前端兼容的外部任务 ID | +| `kind` | `text` | not null | `chat.completions`、`images.generations` 等 | +| `run_mode` | `text` | not null | `production`、`simulation` | +| `user_id` | `text` | not null | 用户 ID | +| `tenant_id` | `text` | nullable | 租户 ID | +| `api_key_id` | `text` | nullable | OpenAPI Key ID | +| `model` | `text` | not null | 请求模型 | +| `model_type` | `text` | nullable | 模型类型 | +| `request` | `jsonb` | not null | 原始请求快照 | +| `normalized_request` | `jsonb` | not null, default `{}` | 参数处理后的请求 | +| `status` | `text` | not null | `queued`、`leased`、`running`、`polling`、`succeeded`、`failed_retryable`、`failed_final`、`cancelled` | +| `queue_key` | `text` | not null | 队列与限流 key | +| `priority` | `int` | not null, default `100` | 任务优先级 | +| `idempotency_key` | `text` | nullable | 幂等 key | +| `remote_task_id` | `text` | nullable | 供应商任务 ID | +| `remote_task_payload` | `jsonb` | nullable | 供应商任务载荷 | +| `simulation_profile` | `jsonb` | nullable | 模拟配置 | +| `simulation_seed` | `text` | nullable | 模拟种子 | +| `locked_by` | `text` | nullable | worker ID | +| `locked_at` | `timestamptz` | nullable | 租约时间 | +| `heartbeat_at` | `timestamptz` | nullable | worker 心跳 | +| `next_run_at` | `timestamptz` | not null | 下次可运行时间 | +| `attempt_count` | `int` | not null, default `0` | 已尝试次数 | +| `max_attempts` | `int` | not null, default `1` | 最大尝试次数 | +| `result` | `jsonb` | nullable | 归一化结果 | +| `billings` | `jsonb` | nullable | 计费结果 | +| `error_code` | `text` | nullable | 错误码 | +| `error_message` | `text` | nullable | 错误信息 | +| `created_at` | `timestamptz` | not null | 创建时间 | +| `updated_at` | `timestamptz` | not null | 更新时间 | +| `finished_at` | `timestamptz` | nullable | 完成时间 | + +索引: + +- `idx_gateway_tasks_queue(status, next_run_at, priority, created_at)` +- `idx_gateway_tasks_lease(status, heartbeat_at)` +- `idx_gateway_tasks_user_created(user_id, created_at desc)` +- `idx_gateway_tasks_external(external_task_id)` +- `uniq_gateway_tasks_idempotency(user_id, idempotency_key)` where `idempotency_key is not null` + +#### 7.9.7 `gateway_task_attempts` + +| 字段 | 类型 | 约束 | 说明 | +| --- | --- | --- | --- | +| `id` | `uuid` | PK | attempt ID | +| `task_id` | `uuid` | FK | 所属任务 | +| `attempt_no` | `int` | not null | 第几次尝试 | +| `platform_id` | `uuid` | nullable | 尝试平台 | +| `platform_model_id` | `uuid` | nullable | 尝试模型配置 | +| `client_id` | `text` | nullable | 客户端实例 ID | +| `queue_key` | `text` | not null | 限流 key | +| `status` | `text` | not null | `submitted`、`polling`、`succeeded`、`failed`、`skipped` | +| `retryable` | `boolean` | not null, default `false` | 是否可重试 | +| `simulated` | `boolean` | not null, default `false` | 是否模拟 | +| `remote_task_id` | `text` | nullable | 供应商任务 ID | +| `request_snapshot` | `jsonb` | not null, default `{}` | 本次请求快照 | +| `response_snapshot` | `jsonb` | nullable | 本次响应快照 | +| `error_code` | `text` | nullable | 错误码 | +| `error_message` | `text` | nullable | 错误信息 | +| `started_at` | `timestamptz` | not null | 开始时间 | +| `finished_at` | `timestamptz` | nullable | 结束时间 | + +索引: + +- `uniq_gateway_attempt_no(task_id, attempt_no)` +- `idx_gateway_attempts_task(task_id)` +- `idx_gateway_attempts_client(client_id, started_at desc)` + +#### 7.9.8 `gateway_task_events` + +| 字段 | 类型 | 约束 | 说明 | +| --- | --- | --- | --- | +| `id` | `uuid` | PK | 事件 ID | +| `task_id` | `uuid` | FK | 所属任务 | +| `seq` | `bigint` | not null | 单任务递增序号 | +| `event_type` | `text` | not null | `queue_status`、`progress`、`partial_result`、`completed`、`failed` | +| `status` | `text` | nullable | 任务状态 | +| `phase` | `text` | nullable | `routing`、`queued`、`submit`、`polling`、`upload` | +| `progress` | `numeric` | nullable | 0 到 1 | +| `message` | `text` | nullable | 展示消息 | +| `payload` | `jsonb` | not null, default `{}` | 事件载荷 | +| `simulated` | `boolean` | not null, default `false` | 是否模拟 | +| `created_at` | `timestamptz` | not null | 创建时间 | + +索引: + +- `uniq_gateway_events_seq(task_id, seq)` +- `idx_gateway_events_task_created(task_id, created_at)` + +#### 7.9.9 `runtime_client_states` + +| 字段 | 类型 | 约束 | 说明 | +| --- | --- | --- | --- | +| `client_id` | `text` | PK | 客户端实例 ID | +| `platform_id` | `uuid` | nullable | 所属平台 | +| `provider` | `text` | not null | provider | +| `method_name` | `text` | not null | 方法名 | +| `queue_key` | `text` | not null | 队列 key | +| `running_count` | `int` | not null, default `0` | 当前运行数 | +| `waiting_count` | `int` | not null, default `0` | 当前等待数 | +| `limiter_ratio` | `numeric` | not null, default `0` | 负载比 | +| `cooldown_until` | `timestamptz` | nullable | 冷却截止 | +| `last_assigned_at` | `timestamptz` | nullable | 上次分配时间 | +| `last_error` | `text` | nullable | 最近错误 | +| `updated_at` | `timestamptz` | not null | 更新时间 | + +索引: + +- `idx_runtime_client_queue(queue_key, cooldown_until)` +- `idx_runtime_client_platform(platform_id)` + +#### 7.9.10 `gateway_upload_assets` + +| 字段 | 类型 | 约束 | 说明 | +| --- | --- | --- | --- | +| `id` | `uuid` | PK | 资产 ID | +| `task_id` | `uuid` | FK nullable | 关联任务 | +| `source` | `text` | not null | 固定为 `server-main-open-upload` 或 `remote-url` / `simulation` | +| `server_main_file_id` | `text` | nullable | 主服务返回的 file id | +| `url` | `text` | not null | 主服务返回 URL | +| `object_key` | `text` | nullable | 主服务返回对象 key | +| `content_type` | `text` | nullable | MIME | +| `size` | `bigint` | nullable | 文件大小 | +| `checksum` | `text` | nullable | 校验和 | +| `metadata` | `jsonb` | not null, default `{}` | 上传响应与业务元数据 | +| `created_at` | `timestamptz` | not null | 创建时间 | + +索引: + +- `idx_gateway_upload_task(task_id)` +- `idx_gateway_upload_file(server_main_file_id)` + +#### 7.9.11 `gateway_retry_policies` + +| 字段 | 类型 | 约束 | 说明 | +| --- | --- | --- | --- | +| `id` | `uuid` | PK | 策略 ID | +| `scope_type` | `text` | not null | `global`、`provider`、`platform`、`model`、`method` | +| `scope_key` | `text` | not null | 作用域 key | +| `enabled` | `boolean` | not null | 是否启用重试 | +| `policy` | `jsonb` | not null | 重试策略 JSON | +| `created_at` | `timestamptz` | not null | 创建时间 | +| `updated_at` | `timestamptz` | not null | 更新时间 | + +约束: + +- `uniq_retry_policy_scope(scope_type, scope_key)` + +#### 7.9.12 `gateway_rate_limit_policies` + +| 字段 | 类型 | 约束 | 说明 | +| --- | --- | --- | --- | +| `id` | `uuid` | PK | 策略 ID | +| `scope_type` | `text` | not null | `global`、`provider`、`platform`、`client`、`base_model`、`platform_model`、`method`、`tenant`、`user`、`api_key` | +| `scope_key` | `text` | not null | 作用域 key | +| `policy` | `jsonb` | not null | TPM、RPM、并发、队列长度、冷却策略 | +| `created_at` | `timestamptz` | not null | 创建时间 | +| `updated_at` | `timestamptz` | not null | 更新时间 | + +约束: + +- `uniq_rate_limit_policy_scope(scope_type, scope_key)` + +示例: + +```json +{ + "rules": [ + { "metric": "tpm_total", "limit": 90000, "windowSeconds": 60, "consume": "reserve_then_reconcile" }, + { "metric": "rpm", "limit": 600, "windowSeconds": 60, "consume": "fixed_window" }, + { "metric": "concurrent", "limit": 20, "leaseTtlSeconds": 900 }, + { "metric": "queue_size", "limit": 1000 } + ] +} +``` + +#### 7.9.13 `gateway_rate_limit_counters` + +保存一分钟窗口内的 TPM/RPM 使用量。Redis 可做加速,但 PG counter 是重启恢复和审计的事实源。 + +| 字段 | 类型 | 约束 | 说明 | +| --- | --- | --- | --- | +| `scope_type` | `text` | not null | 限流作用域 | +| `scope_key` | `text` | not null | 作用域 key | +| `metric` | `text` | not null | `tpm_total`、`tpm_input`、`tpm_output`、`rpm` | +| `window_start` | `timestamptz` | not null | 分钟窗口开始时间 | +| `limit_value` | `numeric` | not null | 当前窗口限制 | +| `used_value` | `numeric` | not null, default `0` | 已确认消耗 | +| `reserved_value` | `numeric` | not null, default `0` | 已预占但待回填消耗 | +| `reset_at` | `timestamptz` | not null | 窗口结束时间 | +| `updated_at` | `timestamptz` | not null | 更新时间 | + +约束: + +- `pk_rate_limit_counter(scope_type, scope_key, metric, window_start)` + +#### 7.9.14 `gateway_concurrency_leases` + +保存并发请求租约,服务异常重启后可释放过期并发。 + +| 字段 | 类型 | 约束 | 说明 | +| --- | --- | --- | --- | +| `id` | `uuid` | PK | 并发租约 ID | +| `task_id` | `uuid` | FK | 任务 ID | +| `attempt_id` | `uuid` | FK nullable | attempt ID | +| `scope_type` | `text` | not null | 限流作用域 | +| `scope_key` | `text` | not null | 作用域 key | +| `lease_value` | `numeric` | not null, default `1` | 占用并发数 | +| `acquired_at` | `timestamptz` | not null | 获取时间 | +| `expires_at` | `timestamptz` | not null | 过期时间 | +| `released_at` | `timestamptz` | nullable | 释放时间 | + +索引: + +- `idx_concurrency_leases_active(scope_type, scope_key, released_at, expires_at)` +- `idx_concurrency_leases_task(task_id)` + +## 8. 模型路由、限流与失败切换 + +迁移时不要做简单的 first-match。路由器需要复用现有语义: + +1. 先按模型名、模型类型、平台启用状态筛候选。 +2. 保留平台维度权限过滤,同名模型不能跨平台混用。 +3. 按平台优先级排序。 +4. 结合并发是否已满、limiter ratio、等待队列、lastAssignedAt 做负载均衡。 +5. 对 app-style provider,例如 RunningHub 模板类能力,使用 `provider + methodName` 的队列 key。 + +一期脚手架只预留接口,迁移实现时把 `IntegrationModelFactory.assignClientsByModelName` 与 `assignClientsByProviderMethod` 的行为写成 Go 版单元测试再实现。 + +### 8.1 限流维度 + +限流策略需要支持以下维度组合: + +- 全局:`global` +- Provider 级:`provider_key` +- 平台级:`platform_id` +- 客户端级:`client_id` +- 基准模型级:`base_model_id` / `canonical_model_key` +- 平台模型级:`platform_model_id` / `model_name` / `model_type` +- 方法级:`method_name`,例如 `runAIApp$` +- 租户/用户级:`tenant_id` / `user_id` +- OpenAPI Key 级:`api_key_id` + +同一请求可能命中多个策略,默认以最严格策略为准;如后续需要不同优先级,可在 policy 中增加 `mergeStrategy`。 + +### 8.1.1 一分钟窗口核心指标 + +限流必须把 TPM、RPM、并发作为一等指标,而不是只看 running count: + +| 指标 | 含义 | 适用能力 | 执行方式 | +| --- | --- | --- | --- | +| `tpm_total` | 一分钟内输入 + 输出 token 总数 | Chat、Responses、Embedding、可 token 化文本任务 | 提交前按 prompt / max_tokens 预估并预占,完成后用实际 token 回填 | +| `tpm_input` | 一分钟内输入 token 数 | Chat、Responses、Embedding | 提交前按 tokenizer 或近似估算预占 | +| `tpm_output` | 一分钟内输出 token 数 | Chat、Responses | stream 场景可按 chunk 增量累计,完成后 reconcile | +| `rpm` | 一分钟内请求数 | 所有同步/异步模型提交 | 任务 attempt 提交前扣减;失败切换的新 attempt 也算一次 provider 请求 | +| `concurrent` | 同时运行或轮询中的请求数 | 所有 provider client | 通过 `gateway_concurrency_leases` 获取租约,完成、失败、取消或过期释放 | +| `queue_size` | 等待队列长度 | 所有队列 | 超过后按策略返回兼容错误或降级到 simulation / fallback | + +TPM 的预占策略: + +- 文本任务:优先使用模型 tokenizer;没有 tokenizer 时用字符数估算,并把估算方式写入 event payload。 +- Chat stream:提交前按 `input_tokens + max_tokens` 预占,stream 中可累计输出 token,完成后用 usage 精确回填。 +- 未提供 `max_tokens` 的请求:使用模型默认输出上限或策略中的 `defaultOutputReserveTokens`。 +- 失败且未提交到供应商:释放预占 token;已提交但失败的 attempt 按供应商是否计费决定保留或部分释放。 +- 生图/生视频通常不计 TPM,但如果 provider 对多模态 token 有限制,可在 `capabilities.tokenPolicy` 中启用对应估算器。 + +RPM 和并发的边界: + +- RPM 以“向 provider 发起一次 submit / sync request”为计数点;排队但未提交不消耗 RPM。 +- 失败切换到下一个客户端时,新客户端 attempt 会重新消耗该客户端、平台、provider 维度的 RPM。 +- 并发租约从 submit 前获取,到同步响应完成、异步任务进入可安全释放阶段或任务终态时释放;长轮询 provider 可拆成 `submit_concurrent` 与 `poll_concurrent` 两类 scope。 +- 服务重启后扫描过期 lease,结合 `gateway_tasks.heartbeat_at` 判断释放或续租,避免并发永久泄漏。 + +除核心指标外,还保留每日/月度配额、错误冷却、余额不足拦截等策略,但这些属于调度前置校验,不替代 TPM/RPM/并发。 + +### 8.2 候选客户端选择 + +候选客户端排序规则必须可测试、可复现: + +1. 过滤禁用平台、禁用模型、不可用凭证、权限不满足的客户端。 +2. 过滤与请求能力不匹配的客户端,例如不支持视频参考、音频参考、多图输入时不能作为候选。 +3. 根据指定平台 / 指定客户端优先收窄候选。 +4. 按 `priority`、是否满载、`limiter_ratio`、等待队列长度、`last_assigned_at`、轻微 jitter 排序。 +5. 选中客户端后在同一事务内写入 `last_assigned_at` 和任务锁,避免并发请求都抢到同一个客户端。 + +### 8.3 失败切换与重试策略 + +一个模型请求进来,如果多个客户端满足要求,必须具备“上一个客户端失败,下一个客户端重试”的能力,且可配置是否允许: + +```json +{ + "retry": { + "enabled": true, + "failoverClients": true, + "maxAttempts": 3, + "maxSameClientAttempts": 1, + "backoff": { "type": "exponential", "baseMs": 500, "maxMs": 5000 }, + "retryableStatusCodes": [408, 409, 429, 500, 502, 503, 504], + "retryableErrorCodes": ["timeout", "rate_limit", "temporary_unavailable"], + "nonRetryableErrorCodes": ["invalid_request", "insufficient_quota", "content_policy"], + "retryOnSubmitUnknown": false + } +} +``` + +策略可挂在全局、provider、platform、model、method 四层,优先级为:请求显式配置 > 模型配置 > 平台配置 > provider 默认 > 全局默认。 + +重试要求: + +- 每次尝试都写入 `gateway_task_attempts`,包括跳过原因和失败分类。 +- 已明确提交成功但轮询失败的任务,优先继续轮询原 `remote_task_id`,不能盲目重复提交造成供应商侧重复任务。 +- 提交状态未知时,根据 `retryOnSubmitUnknown` 决定是否切换客户端;默认保守处理,标记为 `unknown` 并进入人工/补偿检查。 +- 非重试错误直接终止任务,不再切换客户端。 +- 重试产生的新客户端必须仍满足同一请求的能力、权限和计费要求。 + +## 9. 基准模型库与定价体系 + +定价不直接散落在平台模型里。Gateway 先有一个基准模型库,基准模型库存所有 provider、模型、能力、默认限流模板和基准价格;平台创建和平台模型配置都基于这个库继承或覆盖。 + +### 9.1 基准模型库内容 + +每个基准模型至少包含: + +- provider:如 `openai`、`runninghub`、`jimeng`。 +- canonical model key:Gateway 内部稳定模型 key。 +- provider model name:供应商真实请求模型名。 +- model type:`chat`、`image`、`video`、`audio`、`embedding`、`music`、`digital_human`、`model_3d`。 +- capabilities:上下文窗口、stream、多模态输入、参考图、参考视频、参考音频、支持尺寸、质量、时长、有无音频、最大 batch 等。 +- base billing config:文本、图像、视频、音频等不同能力的基准价格。 +- default rate limit policy:provider 或模型默认 TPM/RPM/并发模板。 + +### 9.2 平台价格继承与覆盖 + +创建平台时有三种方式: + +| 模式 | 配置 | 说明 | +| --- | --- | --- | +| `inherit` | 不填价格 | 完全 follow 基准模型价格和能力 | +| `inherit_discount` | `default_discount_factor` 或模型级 `discount_factor` | 有效价 = 基准价 x 折扣系数;能力默认 follow 基准模型,可局部覆盖 | +| `custom` | `billing_config_override` / `capability_override` | 平台模型完全自定义价格或能力,未覆盖字段仍从基准模型补齐 | + +有效配置解析顺序: + +1. 平台模型 `capability_override` / `billing_config_override`。 +2. 平台模型 `discount_factor`。 +3. 平台 `default_discount_factor`。 +4. 基准模型 `capabilities` / `base_billing_config`。 + +如果没有配置平台模型价格,必须 follow 基准模型;不能出现空价格导致预估扣费为 0 的隐式行为。 + +### 9.3 各能力计价模型 + +文本类: + +- 按 `1k_tokens` 计价。 +- 输入和输出分开计价:`text_input`、`text_output`。 +- 如果模型只配置总价,可 fallback 到 `text_total`,但 resolver 对外仍展开成输入/输出明细。 +- 预估时用输入 token + 输出 token 预估值;最终结算用 provider usage 或 Gateway tokenizer 回填。 + +图像类: + +- 基础单位为 `image`。 +- 动态权重至少支持:分辨率、质量、张数、编辑/生成模式。 +- 分辨率示例:`512x512`、`1024x1024`、`1K`、`2K`、`4K`、`8K`。 +- 质量示例:`standard`、`hd`、`high`、`ultra`。 +- 公式示例:`count * basePrice * resolutionWeight * qualityWeight * modeWeight`。 + +视频类: + +- 基础单位建议为 `5s` 或 `second`,与原 provider 配置保持可映射。 +- 动态权重至少支持:时长、分辨率、是否包含音频、是否使用参考视频、是否指定声音/音色、生成数量。 +- 分辨率示例:`480p`、`720p`、`1080p`、`2160p`。 +- 公式示例:`count * ceil(durationSeconds / unitSeconds) * basePrice * resolutionWeight * audioWeight * referenceWeight`。 + +音频、音乐、数字人、3D 模型: + +- TTS / audio 可按 `character_1k` 或秒计费。 +- 音乐可按首数、时长、模型权重计费。 +- 数字人可按时长、分辨率、音频驱动方式计费。 +- 3D / model 类按任务数、模型复杂度或 provider 返回的计费单位映射。 + +### 9.4 estimated billing 一致性 + +`/integration-platform/models/estimatedBilling`、`/integration-platform/models/estimatedBilling/:workflowId` 和真实任务结算必须使用同一个 resolver: + +```text +request -> normalize params -> resolve effective platform model +-> resolve effective capabilities -> resolve effective pricing +-> estimate billing -> route / submit -> final billing reconcile +``` + +要求: + +- 预估扣费不能只按模型名查第一条,必须使用真实候选平台过滤、权限过滤和优先级排序后的有效平台模型。 +- 同名模型跨平台时,平台权限和平台模型价格都不能串。 +- 测试模式使用同样的预估逻辑,但 billings 标记 `simulated=true`。 +- 基准模型价格变更时,平台模型可选择自动 follow 最新版本或 pin 到指定 `pricing_version`,避免历史价格被静默改变。 + +## 10. Base Client 与 Provider 实现架构 + +Gateway 需要保留类似原 `BaseModelClient` 的整体架构:公共基类定义通用生命周期,不同 provider/client 只实现平台差异。这样才能把参数预处理、限流、日志、进度、重试、计费和结果归一放在同一层处理。 + +### 10.1 Client 接口 + +Go 侧建议抽象为: + +```go +type ModelClient interface { + Provider() string + MethodName() string + BuildParams(ctx context.Context, task TaskContext) (VendorRequest, error) + SubmitTask(ctx context.Context, request VendorRequest) (RemoteTask, error) + PollResult(ctx context.Context, remote RemoteTask) (PollResult, error) + NormalizeResult(ctx context.Context, result PollResult) (NormalizedResult, error) + EstimateBilling(ctx context.Context, task TaskContext) ([]BillingItem, error) + Cancel(ctx context.Context, remote RemoteTask) error +} +``` + +### 10.2 Base Client 公共职责 + +`BaseClient` / `BaseModelClient` 负责: + +- 调用参数处理链:尺寸、画幅、时长、多模态内容过滤、默认值填充。 +- 校验模型能力:图片参考、视频参考、音频参考、文件输入、stream 支持等。 +- 发出进度事件:构建参数、排队、提交、轮询、下载、上传、完成。 +- 执行通用 timeout、backoff、错误分类、重试和 failover 策略。 +- 统一记录 `gateway_task_attempts`、`gateway_task_events`、运行指标。 +- 统一提取 `billings` 与 `result`,交给 outbox 做结算事件。 +- 统一处理文件资产:已上传文件引用、远程 URL 转存、provider 临时 URL 拉取后调用 `server-main` `/v1/files/upload`。 + +### 10.3 Provider Client 实现职责 + +具体 client 只负责供应商协议: + +- OpenAI-compatible:同步/stream chat、responses、image generation。 +- RunningHub:模板类 app 提交、轮询、结果提取,队列 key 使用 `${provider}-${methodName}`。 +- Jimeng / Vidu / Kling / Hunyuan Video:视频任务提交与轮询。 +- Suno / Speech / Digital Human:各自的提交和结果归一。 +- SimulationClient:用于测试模式和 dry-run,完整模拟提交、轮询、进度、失败、结果,不参与真实生产候选。 +- MockTest:用于 provider contract 回归和 loopback,不能和生产路由混在同一个职责里;应由独立 registry / loopback service 管理。 + +每个 provider 至少需要: + +- contract test:固定输入输出 DTO。 +- retry classification test:哪些错误可重试、哪些不可重试。 +- progress event snapshot:确保前端进度面板兼容。 +- billing snapshot:确保预估扣费和最终 billings 语义一致。 + +## 11. 队列持久化、恢复与限流执行 + +### 11.1 持久化队列原则 + +队列必须以 PostgreSQL 为事实源,Redis / 内存只作为加速和通知层。服务异常重启后,任务状态必须可从 PG 恢复。 + +任务生命周期: + +```text +created -> queued -> leased -> running -> polling -> succeeded + | | + | -> failed_retryable -> queued + -> failed_final / cancelled +``` + +核心规则: + +- 入队时写 `gateway_tasks`,状态为 `queued`,并写初始 `gateway_task_events`。 +- worker 获取任务时使用事务、`FOR UPDATE SKIP LOCKED` 或 advisory lock,写入 `locked_by`、`locked_at`、`heartbeat_at`。 +- worker 运行中定期刷新 `heartbeat_at`。 +- 服务启动时扫描 `running/polling/leased` 且 heartbeat 超时的任务。 +- 有 `remote_task_id` 的任务优先进入 reconciliation poller,继续取回供应商结果。 +- 没有 `remote_task_id` 的任务按 retry 策略回到 `queued` 或进入 `failed_final`。 +- 结算 outbox 独立重试,任务成功不等于结算成功。 + +### 11.2 恢复策略 + +| 重启前状态 | 恢复动作 | +| --- | --- | +| `queued` | 保持队列,等待 worker 重新租约 | +| `leased` 但无 heartbeat | 释放锁,回到 `queued` | +| `running` 且无 `remote_task_id` | 视为提交前失败,按 retry 策略重试 | +| `running/polling` 且有 `remote_task_id` | 进入 poller 继续取回结果 | +| `succeeded` 但 settlement pending | outbox 继续补偿 | +| `failed_retryable` 且未超最大次数 | 按 `next_run_at` 回队列 | +| `failed_final/cancelled` | 不自动恢复 | + +### 11.3 限流执行 + +worker 租约任务前必须先拿到限流令牌: + +- TPM 令牌:按 `gateway_rate_limit_counters` 的一分钟窗口预占 token,完成后 reconcile 实际 token。 +- RPM 令牌:提交 provider 前扣减一分钟窗口请求数,失败切换的新 attempt 重新评估。 +- 并发令牌:按 `queue_key` / `client_id` / `method_name` / `platform_model_id` 维度写 `gateway_concurrency_leases`。 +- 排队令牌:超过 `max_waiting` 后直接返回兼容错误。 +- 冷却令牌:客户端错误达到阈值后设置 `cooldown_until`。 + +如果 worker 拿不到令牌,任务保持 `queued` 并更新 `next_run_at`,不能占用 worker 长时间空等。 + +## 12. 进度流与 RxJS-like 中间结果 + +原系统里很多 provider 使用 RxJS Observable 在中间过程返回进度。Go 侧需要提供等价语义: + +```go +type ProgressPublisher interface { + Publish(ctx context.Context, event ProgressEvent) error + Subscribe(ctx context.Context, taskID string, afterSeq int64) (<-chan ProgressEvent, error) +} +``` + +### 12.1 ProgressEvent 结构 + +```json +{ + "taskId": "uuid", + "seq": 12, + "event": "progress", + "status": "running", + "phase": "polling", + "progress": 0.42, + "message": "Generating video frames", + "payload": {}, + "createdAt": "2026-05-09T12:00:00Z" +} +``` + +### 12.2 推送通道 + +- SSE:`GET /api/v1/tasks/:taskId/events`,支持 `Last-Event-ID` 回放。 +- WebSocket:用于高频任务进度、队列监控、控制台实时状态。 +- OpenAI stream:`/chat/completions`、`/v1/chat/completions`、`/responses` 需要保持原 stream chunk 格式。 +- server-main bridge:迁移期如仍需老前端通道,可由 `server-main` 订阅 Gateway 事件后转发,但目标态应由网关直推。 + +### 12.3 进度持久化要求 + +- 所有对用户可见的状态变化都必须先写 `gateway_task_events`,再广播。 +- 客户端断线后能从 `seq` 或 `Last-Event-ID` 补齐。 +- 任务恢复后必须先重放最近状态,再继续发布新进度。 +- 完成态事件必须包含足够的结果摘要,前端无需额外轮询才能更新卡片状态。 + +## 13. 测试模式与全链路模拟 + +AI Gateway 需要内置测试模式,目标是在不触达真实供应商、不真实扣费、不调用主服务生产上传接口的前提下,把请求完整跑过路由、候选客户端选择、队列、限流、失败切换、进度流、结果归一、任务恢复等核心链路。 + +### 13.1 启用方式 + +支持三种启用粒度: + +| 粒度 | 配置 | 说明 | +| --- | --- | --- | +| 全局 | `AI_GATEWAY_RUN_MODE=production|simulation` | 整个服务进入生产或模拟模式 | +| 请求级 | `X-EasyAI-Test-Mode: simulation` 或 body `test_mode=true` | 仅对单次请求模拟,默认只允许 `power/manager` 或内部服务使用 | +| 平台/模型级 | `platform.config.testMode`、`model.config.testMode` | 指定平台、模型在测试环境永远走模拟 | + +安全要求: + +- 生产环境默认禁止普通用户请求级 test mode;需要显式开启 `ALLOW_REQUEST_TEST_MODE=true`。 +- OpenAPI `sk-*` 默认不能打开 test mode,除非 API Key 或租户配置允许。 +- 进入 simulation 后必须在 task、attempt、event、response 中标记 `simulated=true`,避免和真实任务混淆。 + +### 13.2 模拟链路 + +测试模式仍走完整执行链: + +```text +HTTP route -> auth -> route compatibility adapter -> model router +-> persistent queue -> rate limiter -> SimulationClient +-> progress events -> normalized result -> dry-run billing -> task completed +``` + +不会执行的动作: + +- 不调用真实 provider 的 submit / poll / cancel API。 +- 不生成真实供应商订单或任务。 +- 不向 `server-main` 发真实扣费结算事件。 +- 不调用 `server-main` 的生产文件上传,除非显式使用测试环境上传接口。 +- 不消耗真实平台并发额度。 + +仍会执行的动作: + +- 真实鉴权。 +- 真实路由和候选客户端排序。 +- 真实队列持久化、锁、heartbeat、恢复。 +- 真实限流令牌计算。 +- 真实进度事件持久化和推送。 +- 真实任务记录、attempt 记录、dry-run outbox 记录。 + +### 13.3 Simulation Profile + +SimulationClient 根据 `simulation_profile` 生成确定性行为: + +```json +{ + "simulation_profile": { + "latencyMs": [300, 800, 1200], + "progressSteps": [0.1, 0.35, 0.7, 1], + "resultKind": "image", + "resultCount": 2, + "failures": [ + { "attempt": 1, "errorCode": "timeout", "retryable": true }, + { "attempt": 2, "errorCode": "rate_limit", "retryable": true } + ], + "seed": "task-id-or-custom-seed" + } +} +``` + +用途: + +- 模拟成功:返回固定格式图片、视频、音频、文本或 embedding 结果。 +- 模拟失败:按 attempt 注入可重试 / 不可重试错误。 +- 模拟慢任务:验证进度、断线重连、任务恢复。 +- 模拟限流:验证排队、等待上限、cooldown。 +- 模拟未知提交态:验证 `retryOnSubmitUnknown` 和人工补偿路径。 + +### 13.4 失败切换测试 + +测试模式必须能验证“上一个客户端失败,下一个客户端重试”: + +1. router 仍按真实规则选出多个候选客户端。 +2. SimulationClient 为第一个候选返回 retryable error。 +3. runtime 写入 failed attempt。 +4. retry policy 判断可切换。 +5. router 排除已失败且不可复用的 client,选下一个候选。 +6. 第二个候选成功后,任务以 `succeeded` 完成,并记录完整 attempts。 + +禁用重试时,同样的失败应直接进入 `failed_final`,不能切换客户端。 + +### 13.5 Dry-run 计费与结算 + +- `EstimateBilling` 仍使用真实计费配置计算,结果标记 `simulated=true`。 +- `settlement_outbox` 可写入 `dry_run` 事件,但默认状态为 `skipped`,不触发主服务扣费。 +- 若需要联调主服务结算接口,只能调用 `server-main` 的 dry-run settlement endpoint,不能复用真实扣费 endpoint。 + +### 13.6 模拟结果资产 + +测试模式下的文件结果有三种来源: + +- 内置静态占位 URL,例如 `/static/simulation/image.png`。 +- data URL / inline metadata,仅用于接口契约测试。 +- 测试文件上传接口:只有显式启用 `AI_GATEWAY_SIMULATION_UPLOAD=true` 时才允许调用 `server-main` 测试环境上传接口;默认使用内置静态占位资产。 + +结果必须带: + +```json +{ + "simulated": true, + "assetSource": "simulation", + "provider": "simulation" +} +``` + +### 13.7 观测与审计 + +控制台需要能筛选 `run_mode=simulation` 的任务,展示: + +- 使用的 simulation profile。 +- 每次 attempt 的模拟错误和重试决策。 +- 生成的 progress events。 +- dry-run billing。 +- 是否触发了恢复流程。 + +## 14. server-main 对接方式 + +### 14.1 短期:server-main 仍保留入口 + +`server-main` 对外接口不变: + +- `/chat/completions` +- `/images/generations` +- `/video/generations` +- `/v1/chat/completions` +- `/v1/images/generations` +- `/v1/video/generations` + +内部 `OpenaiService` 变成薄门面: + +```mermaid +sequenceDiagram + participant FE as Frontend + participant MAIN as server-main + participant GW as AI Gateway + participant V as Vendor + + FE->>MAIN: POST /images/generations + MAIN->>MAIN: create history / task id + MAIN->>GW: POST /api/v1/images/generations + GW->>V: submit task + V-->>GW: result + GW-->>MAIN: settlement event with billings + MAIN->>MAIN: billing + history update + GW-->>MAIN: task result + MAIN-->>FE: response +``` + +### 14.2 中期:前端直接打 Gateway + +前端配置 `VITE_GATEWAY_API_BASE_URL`,生成类请求直接走网关路由到 AI Gateway。`server-main` 只处理登录、刷新、余额、历史、开放文件上传、扣费。 + +### 14.3 结算事件 + +Gateway 任务成功后向 `server-main` 发送结算事件: + +```json +{ + "eventId": "uuid", + "taskId": "uuid", + "userId": "user-id", + "tenantId": "tenant-id", + "apiKeyId": "optional", + "kind": "images.generations", + "model": "gpt-image-1", + "billings": [ + { + "resourceType": "image", + "amount": 1, + "unit": "item", + "cost": 100 + } + ], + "resultRef": { + "url": "https://..." + } +} +``` + +要求: + +- `server-main` 以 `eventId` / `taskId` 做幂等。 +- Gateway 保留 outbox 或重试表,避免结算事件丢失。 +- 扣费失败时任务结果仍可先保存,但前端展示需要明确异常状态。 + +## 15. 文件上传 + +文件上传策略收敛为:**AI Gateway 不维护自己的 OSS/COS/S3 配置,不做预签名,不直接写对象存储;所有需要上传或转存的文件,统一调用 `server-main` 已开放的文件上传接口**。这样可以复用主服务现有 OSS 配置、权限、文件记录、MIME 推断和后续审计。 + +### 15.1 ServerMainUploadClient 抽象 + +```go +type ServerMainUploadClient interface { + UploadMultipart(ctx context.Context, input UploadMultipartInput) (UploadedAsset, error) + UploadFromURL(ctx context.Context, input UploadFromURLInput) (UploadedAsset, error) + ResolveAsset(ctx context.Context, ref AssetRef) (ResolvedAsset, error) +} +``` + +### 15.2 调用接口 + +默认调用 `server-main` 的开放上传接口: + +| Method | Path | 说明 | +| --- | --- | --- | +| `POST` | `/v1/files/upload` | OpenAPI 兼容 multipart 文件上传 | + +调用要求: + +- 用户请求:透传用户 JWT 或 OpenAPI `sk-*`,保持与原主服务上传权限一致。 +- Gateway 内部上传:使用服务令牌加原始用户上下文头,例如 `X-EasyAI-User-Id`、`X-EasyAI-Tenant-Id`、`X-EasyAI-Task-Id`,具体以 `server-main` 内部约定为准。 +- 上传返回值以主服务响应为准,Gateway 只做字段归一并写入 `gateway_upload_assets`。 +- 测试模式默认不调用上传接口,除非显式启用测试上传。 + +### 15.3 上传场景 + +| 场景 | 处理方式 | +| --- | --- | +| 前端已经通过主服务上传 | 请求中携带主服务返回的 URL / file id,Gateway 只校验和记录 | +| Gateway 需要转存远程 URL | Gateway 下载远程内容,作为 multipart 调 `/v1/files/upload` 上传到主服务 | +| Provider 返回临时 URL | Gateway 拉取结果,再调 `/v1/files/upload` 转存,最终结果使用主服务 URL | +| OpenAPI multipart 上传 | 继续由 `server-main` 的 `/v1/files/upload` 承接;Gateway 不新增并行上传入口 | +| base64 小文件 | Gateway 解码为 multipart,再调用 `/v1/files/upload` | + +### 15.4 安全与可靠性 + +- 限制最大文件大小、content-type、扩展名。 +- 远程 URL 下载必须禁止内网地址、metadata 地址、超长重定向链。 +- 上传到 `server-main` 时带 `Idempotency-Key`,避免重启或 retry 造成重复文件记录。 +- `gateway_upload_assets` 记录主服务返回的 file id、URL、object key、checksum、响应快照。 +- 如果上传失败,任务按 provider retry 策略判断是否可重试;上传失败本身也要写 attempt/event。 +- Gateway 不保存长期文件密钥,不暴露 OSS 配置页面。 + +## 16. 前端控制台 + +前端控制台使用 React + TypeScript + TSX + `shadcn-ui`。首期包含: + +- 健康检查。 +- 基准 provider / 基准模型库。 +- 基准定价与平台折扣预览。 +- 平台列表。 +- 模型能力概览。 +- 任务入口占位。 +- 队列与 TPM/RPM/并发限流状态。 +- 客户端运行状态与 cooldown。 +- 重试策略配置。 +- 上传记录与主服务上传接口调用状态。 +- 后续补:凭证编辑、模型计费配置、任务详情、平台探活。 + +控制台走同一套 JWT,管理页面需要 `power` 权限。 + +### 16.1 页面路由模块 + +| 页面 | 路由 | 权限 | 说明 | +| --- | --- | --- | --- | +| 总览 | `/` 或 `/dashboard` | `power` | 服务健康、任务吞吐、成功率、队列积压、异常平台 | +| 基准 Provider | `/catalog/providers` | `power` | provider 列表、能力 schema、默认限流模板、启停状态 | +| 基准模型库 | `/catalog/base-models` | `power` | 全量模型、provider、模型类型、能力、基准价格、默认限流 | +| 基准模型详情 | `/catalog/base-models/:id` | `power` | 能力 schema、基准定价、价格版本、平台引用情况 | +| 定价规则 | `/pricing/rules` | `power` | 文本/图像/视频等资源价格、动态权重、版本和生效时间 | +| 平台管理 | `/platforms` | `power` | 平台列表、启停、优先级、凭证状态、复制平台、异常重置 | +| 平台详情 | `/platforms/:id` | `power` | 基础配置、凭证、默认折扣、模型、限流、重试、运行状态 | +| 模型管理 | `/models` | `power` | 平台模型列表、基准模型映射、类型、能力、有效计费、启用状态 | +| 模型详情 | `/models/:id` | `power` | 基准继承、能力覆盖、计费规则、权限过滤、测试请求 | +| API 配置 | `/platform-apis` | `power` | 兼容原 `integration/platform-api` 的 CRUD 与执行测试 | +| 任务列表 | `/tasks` | `basic` | 任务查询、状态过滤、用户/模型/平台筛选 | +| 任务详情 | `/tasks/:id` | `basic` | 请求、attempt、进度事件、结果、计费、上传资产 | +| 队列监控 | `/runtime/queues` | `power` | queue key、等待数、运行数、租约、恢复状态 | +| 客户端运行态 | `/runtime/clients` | `power` | running、waiting、limiter ratio、cooldown、last error | +| 重试策略 | `/policies/retry` | `power` | global/provider/platform/model/method 重试策略 | +| 限流策略 | `/policies/rate-limit` | `power` | TPM、RPM、并发、队列长度、冷却策略 | +| 测试模式 | `/simulation` | `power` | simulation profile、失败注入、慢任务、dry-run billing | +| 上传记录 | `/uploads` | `power` | 主服务上传记录映射、任务关联、失败重试 | +| 结算 outbox | `/settlements` | `manager` | 结算事件、重试、跳过、幂等 key | +| 系统设置 | `/settings` | `manager` | server-main 地址、内部令牌、运行模式、安全开关 | + +### 16.2 关键页面设计 + +**基准模型库** + +- 表格列:provider、模型名、类型、状态、能力标签、基准输入价、基准输出价、图像/视频基准价、默认 TPM/RPM/并发。 +- 详情页 tab:基础信息、能力 schema、基准定价、默认限流、价格版本、引用平台模型。 +- 支持导入/同步原项目基准配置,变更价格时创建新 `pricing_version`。 + +**定价规则** + +- 文本规则按输入/输出 token 分开配置,单位为 `1k_tokens`。 +- 图像规则提供分辨率、质量、数量、模式权重表。 +- 视频规则提供时长单位、分辨率、有无音频、参考视频、指定声音等权重表。 +- 提供“基准价 x 平台折扣 x 模型覆盖”的实时预览,避免运营保存前看不到最终价。 + +**平台管理** + +- 表格列:平台名、provider、状态、优先级、动态优先级、启用模型数、running/waiting、cooldown、最近错误、更新时间。 +- 操作:启用/禁用、复制、重置异常、清队列、进入详情。 +- 创建/编辑平台时可选择基准 provider,配置默认折扣系数。 +- 详情页 tab:基础信息、凭证、模型、基准模型映射、限流、重试、运行态、快照。 + +**模型管理** + +- 支持按 `model_type`、provider、平台、启用状态、能力标签筛选。 +- 能力配置用 JSON editor + 表单化快捷项结合:是否 stream、多模态输入、参考图/视频/音频、尺寸、时长、上下文窗口。 +- 模型详情展示“follow 基准模型 / 按折扣继承 / 自定义覆盖”三态,保存后写入 effective capabilities 和 effective billing config。 +- 计费配置保留原 `estimatedBilling` 语义,前端提供即时 dry-run 预估。 + +**任务详情** + +- 顶部状态条展示:状态、run mode、模型、平台、客户端、attempt 次数、耗时、是否模拟。 +- `Attempts` 表展示每次客户端尝试、错误码、是否可重试、失败切换决策。 +- `Events` 时间线展示 queue/routing/submit/poll/upload/completed 等进度,可重放。 +- `Request/Result` 使用 JSON viewer。 +- `Uploads` 展示调用主服务上传接口后的 file id、URL、content-type、size。 + +**队列监控** + +- 按 queue key 展示 waiting、running、leased timeout、failed retryable、next run。 +- 展示当前窗口的 TPM/RPM 使用量、预占量、并发 lease、重置时间。 +- 提供只读诊断为主;危险操作如清队列、重放任务需要二次确认 dialog。 + +**测试模式** + +- Profile 编辑器:成功、慢任务、可重试失败、不可重试失败、未知提交态。 +- 一键发起模拟 Chat / 生图 / 生视频任务。 +- 展示完整进度流、attempt 切换和 dry-run billing。 +- 页面显著标识 simulation,避免与真实生产任务混淆。 + +### 16.3 前端模块目录 + +```text +apps/web/src/ + app/ + routes.tsx + providers.tsx + components/ + ui/ # shadcn-ui generated components + layout/ + data-table/ + json-viewer/ + status-badge/ + features/ + dashboard/ + catalog/ + pricing/ + platforms/ + models/ + platform-apis/ + tasks/ + runtime/ + policies/ + simulation/ + uploads/ + settlements/ + settings/ + lib/ + api-client.ts + auth.ts + format.ts + query-keys.ts + contracts/ + dto.ts # 或从 packages/contracts 引入 +``` + +### 16.4 前端数据流 + +- 使用 `@tanstack/react-query` 管理列表、详情、轮询和 mutation。 +- 表格使用 `@tanstack/react-table`,配合 shadcn `Table`。 +- 表单使用 `react-hook-form` + `zod`,校验策略与后端 DTO 对齐。 +- SSE / WebSocket 事件统一封装为 `useTaskEvents(taskId)`。 +- 所有 destructive action 使用 shadcn `Dialog` 二次确认。 +- 页面级权限由路由 loader 或 wrapper 判断,不在每个按钮里重复散落判断。 + +## 17. 迁移计划 + +### Phase 0:脚手架与契约 + +- 建立 monorepo、Go API、React 控制台、PG migration。 +- 前端接入 `shadcn-ui`,建立基础组件目录和主题 token。 +- PG 复用 Agent memory 的 `easyai-pgvector` 实例,但使用独立数据库 `easyai_ai_gateway`,避免与 `easyai_memory` 的记忆表混库。 +- 容器网络内默认连接串为 `postgresql://easyai:easyai2025@easyai-pgvector:5432/easyai_ai_gateway?schema=public`;宿主机直跑时必须改成宿主机可访问的 host/port,例如 `localhost` 或实际映射端口。 +- 如果只提供 `MEMORY_DATABASE_URL`,Go 侧只借用其中的 host/user/password,并按 `AI_GATEWAY_DATABASE_NAME` 替换数据库名;同时会把 Prisma 风格的 `schema=public` 转成 PostgreSQL `search_path` 参数。 +- 建立 JWT / API Key 授权中间件骨架。 +- 固化 API、事件、数据库设计。 +- 建立 simulation / dry-run 模式配置与安全开关,默认禁止生产环境普通用户随意开启。 +- 建立基准 provider、基准模型库、价格规则和 TPM/RPM/并发限流表结构。 + +### Phase 1:平台管理迁移 + +- 导入原项目模型能力、基准计费配置、provider 默认配置,形成 `base_model_catalog`。 +- 迁移 `integration-platform` CRUD、模型配置、权限过滤。 +- 平台创建支持选择基准模型、设置默认折扣系数;平台模型支持 follow 基准模型、折扣继承和自定义覆盖。 +- 从 Mongo/Mongoose schema 映射到 PostgreSQL。 +- 提供与旧接口兼容的 DTO。 + +### Phase 2:模型路由与客户端迁移 + +- 迁移 `IntegrationModelFactory` 行为。 +- 建立 Base Client 生命周期:构建参数、提交任务、轮询结果、归一化结果、估算计费、取消任务。 +- 实现客户端失败切换策略:上一个客户端失败后按配置切到下一个候选客户端。 +- 迁移 OpenAI-compatible client、RunningHub、Jimeng、Vidu 等高频 provider。 +- 为每个 provider 增加 contract test 和 snapshot。 + +### Phase 3:生成任务迁移 + +- Chat、生图、生视频优先。 +- 迁移参数预处理链,尤其多模态能力过滤。 +- 补队列持久化、TPM/RPM/并发限流、重启恢复、重试、超时、任务事件。 +- 实现 SSE / WebSocket / OpenAI stream 进度流,替代原 RxJS Observable 的对外效果。 +- 实现调用 `server-main` 开放上传接口的 `ServerMainUploadClient`,覆盖远程 URL 转存、provider 临时 URL 转存、base64 小文件上传。 +- 实现 SimulationClient,支持成功、失败、慢任务、限流、未知提交态等 profile,验证全链路但不触达真实平台。 + +### Phase 4:server-main 切薄门面 + +- `OpenaiService` 内部切到 Gateway HTTP SDK。 +- 结算事件接入 `server-main` 扣费链路。 +- 前端逐步改 `Gateway API Base URL`。 + +### Phase 5:清理旧实现 + +- 删除或冻结 `server-main` 中重复的 runtime client。 +- 保留必要 BFF、历史、账单、文件上传能力。 + +## 18. 验收标准 + +- `pnpm nx run api:migrate` 可在 Agent memory PostgreSQL 中初始化 AI Gateway 表。 +- `pnpm nx run api:test` 通过。 +- `pnpm nx run web:build` 通过。 +- 控制台使用 `shadcn-ui` 组件,不出现散落的独立 UI 体系。 +- 使用 `server-main` 签发的 JWT 能访问 `/api/v1/me`。 +- OpenAPI `sk-*` 能委托 `server-main` 校验并获得用户 claim。 +- 原 `integration-platform`、站内生成、`/v1/*` 核心路由可用,DTO 与响应结构兼容。 +- 基准 provider、基准模型库、基准价格和默认限流模板可在控制台维护。 +- 平台创建可设置默认折扣系数;平台模型未配置时 follow 基准模型,配置折扣时按基准价折扣计算,自定义覆盖时覆盖价格和能力。 +- 文本定价支持输入/输出 token 分开计费;图像定价支持分辨率和质量权重;视频定价支持时长、分辨率、有无音频等权重。 +- estimated billing 与真实任务 billings 使用同一个 effective pricing resolver,不能出现预估和真实路由价格来源不一致。 +- 队列任务在服务异常重启后可恢复:已提交供应商的任务继续 poll,未提交任务重新排队。 +- 限流策略覆盖 TPM、RPM、并发,并支持平台、客户端、基准模型、平台模型、方法、租户、用户、API Key 维度。 +- TPM/RPM 一分钟窗口和并发 lease 在服务重启后可恢复或释放,不产生永久占用。 +- 多客户端候选下,一个客户端可重试失败后按策略切换下一个客户端;禁用重试时必须直接失败。 +- Base Client 架构覆盖构建参数、提交任务、取回结果、归一化结果、计费估算、取消任务。 +- 任务中间进度可通过 SSE / WebSocket / stream 返回,并支持断线重放。 +- 文件上传统一调用 `server-main` 开放上传接口,Gateway 不维护自己的 OSS 配置。 +- 测试模式下不会向真实平台提交任务、不会真实扣费、不会调用主服务生产上传接口,但会完整经过路由、队列、限流、重试、进度和结果归一。 +- 测试模式支持 profile 注入可重试错误,能验证客户端失败切换;禁用重试时能验证直接失败。 +- Chat / 生图 / 生视频至少各完成一个 provider 的端到端任务。 +- 结算事件在 `server-main` 幂等扣费。 +- 同名模型跨平台权限过滤与旧逻辑一致。 diff --git a/docs/migration-plan.md b/docs/migration-plan.md new file mode 100644 index 0000000..529671c --- /dev/null +++ b/docs/migration-plan.md @@ -0,0 +1,49 @@ +# integration-platform 迁移实施计划 + +## 第 1 周:基础设施 + +- 在 Agent memory 的 `easyai-pgvector` 实例上建立独立数据库 `easyai_ai_gateway` 和 AI Gateway 表,不直接使用 `easyai_memory` 记忆库。正式 EasyAI compose 默认账号为 `easyai` / `easyai2025`。 +- 完成 JWT / API Key 授权验证。 +- 完成基准 provider、基准模型库、平台与模型管理 API。 +- 完成基准定价、平台默认折扣、平台模型覆盖的 schema。 +- React 控制台接入平台、基准模型、TPM/RPM 限流窗口列表。 + +## 第 2 周:路由行为复刻 + +- 从旧代码抽取以下行为测试: + - 同名模型平台权限过滤。 + - `assignClientsByModelName` 候选排序。 + - `assignClientsByProviderMethod` provider-level 负载均衡。 + - estimated billing 使用真实候选集。 +- 建立 TPM/RPM/并发限流 fixtures,覆盖预占、释放、失败切换重新计数。 +- Go 侧实现 router,并用 fixtures 对齐旧行为。 + +## 第 3 周:核心 provider + +- 先迁 OpenAI-compatible / Universal。 +- 再迁生图、生视频主 provider。 +- 每个 provider 建 contract test。 + +## 第 4 周:任务链路 + +- 实现队列、任务状态、SSE 进度。 +- 实现 TPM/RPM 一分钟窗口计数和并发 lease 恢复。 +- 打通 Chat、生图、生视频端到端。 +- 生成结算事件,接入 server-main 幂等扣费。 + +## 第 5 周:切流 + +- server-main `OpenaiService` 加 Gateway client。 +- 开启 shadow / dry-run 比对。 +- 前端增加 `VITE_GATEWAY_API_BASE_URL`。 +- 灰度切流,观察任务成功率、平均排队、扣费一致性。 + +## 风险控制 + +- 不做 first-match 回退,所有候选选择都要有行为测试。 +- API Key 不在 Gateway 落库。 +- OSS 密钥不进入 Gateway;文件统一调用 server-main 开放上传接口。 +- 平台模型没有自定义价格时必须 follow 基准模型,不能隐式按 0 计费。 +- estimated billing 与真实结算必须使用同一个 effective pricing resolver。 +- 结算事件必须幂等和可重试。 +- 任务推送与余额/历史推送拆分,避免重新耦合回 server-main。 diff --git a/docs/server-main-integration.md b/docs/server-main-integration.md new file mode 100644 index 0000000..2acc3f8 --- /dev/null +++ b/docs/server-main-integration.md @@ -0,0 +1,87 @@ +# server-main 对接清单 + +## 1. 需要在 server-main 增加的内部接口 + +### 1.1 API Key 校验 + +```http +POST /internal/platform/auth/verify-api-key +Authorization: Bearer ${SERVER_MAIN_INTERNAL_TOKEN} +Content-Type: application/json + +{ "apiKey": "sk-..." } +``` + +返回: + +```json +{ + "sub": "user-id", + "username": "demo", + "role": ["user"], + "tenantId": null, + "apiKeyId": "key-id", + "apiKeySecret": "sk-...", + "apiKeyName": "production-key" +} +``` + +### 1.2 文件上传 + +```http +POST /v1/files/upload +Authorization: Bearer ${USER_JWT_OR_SK} +Content-Type: multipart/form-data + +file=@result.png +``` + +AI Gateway 不维护独立 OSS 配置,也不向 `server-main` 申请预签名。需要上传本地中间产物、provider 临时 URL 转存、base64 解码结果时,统一组装 multipart 请求调用主服务开放上传接口,并记录主服务返回的 file id / URL / object key。 + +### 1.3 结算事件 + +```http +POST /internal/platform/settlements +Authorization: Bearer ${SERVER_MAIN_INTERNAL_TOKEN} +Content-Type: application/json +Idempotency-Key: ${eventId} +``` + +结算事件中的 `billings` 由 AI Gateway 根据基准模型库、平台折扣、平台模型覆盖后的 effective pricing 计算。`server-main` 仍负责余额、资源包、账单锁和消费流水,不重新推导模型价格,只按幂等事件扣费。 + +## 2. server-main OpenaiService 薄门面 + +保留现有对内方法签名,内部新增 `AiGatewayClient`: + +- `createChatCompletion` +- `generateImage` +- `editImage` +- `generateVideo` +- `createEmbedding` +- `estimateBilling` + +切流开关: + +```env +AI_GATEWAY_ENABLED=true +AI_GATEWAY_BASE_URL=http://easyai-ai-gateway:8088 +AI_GATEWAY_INTERNAL_TOKEN=change-me +``` + +## 3. 迁移期双写与比对 + +高风险接口可短期 shadow: + +1. 主路径仍走旧实现。 +2. 异步把同一请求投递到 Gateway dry-run。 +3. 比对候选平台、TPM/RPM/并发限流决策、预估扣费、参数预处理结果。 +4. 结果稳定后切主路径。 + +## 4. 不迁移项 + +- `refresh_token` 签发和刷新。 +- 用户余额查询。 +- 用户 API Key 的创建、撤销、列表。 +- 账单锁、扣费流水。 +- OSS/COS/S3 上传配置和实际文件落库。 +- 对话与绘图历史最终落库。 diff --git a/go.work b/go.work new file mode 100644 index 0000000..35bdae7 --- /dev/null +++ b/go.work @@ -0,0 +1,5 @@ +go 1.23 + +use ( + ./apps/api +) diff --git a/go.work.sum b/go.work.sum new file mode 100644 index 0000000..7033131 --- /dev/null +++ b/go.work.sum @@ -0,0 +1,16 @@ +github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= +github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI= +github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= diff --git a/nx.json b/nx.json new file mode 100644 index 0000000..82e8041 --- /dev/null +++ b/nx.json @@ -0,0 +1,19 @@ +{ + "$schema": "./node_modules/nx/schemas/nx-schema.json", + "namedInputs": { + "default": ["{projectRoot}/**/*", "sharedGlobals"], + "production": ["default"], + "sharedGlobals": [] + }, + "targetDefaults": { + "build": { + "cache": true + }, + "test": { + "cache": true + }, + "lint": { + "cache": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..2dc929f --- /dev/null +++ b/package.json @@ -0,0 +1,21 @@ +{ + "name": "easyai-ai-gateway", + "version": "0.1.0", + "private": true, + "packageManager": "pnpm@10.18.1", + "scripts": { + "dev": "scripts/dev.sh", + "build": "nx run-many -t build -p api web", + "test": "nx run-many -t test -p api web", + "lint": "nx run-many -t lint -p web contracts", + "db:create": "scripts/create-database.sh", + "migrate": "nx run api:migrate" + }, + "devDependencies": { + "@nx/vite": "^21.0.0", + "@vitejs/plugin-react": "^5.0.0", + "nx": "^21.0.0", + "typescript": "^5.8.0", + "vite": "^7.0.0" + } +} diff --git a/packages/contracts/package.json b/packages/contracts/package.json new file mode 100644 index 0000000..d3f9b54 --- /dev/null +++ b/packages/contracts/package.json @@ -0,0 +1,17 @@ +{ + "name": "@easyai-ai-gateway/contracts", + "version": "0.1.0", + "private": true, + "type": "module", + "main": "./src/index.ts", + "types": "./src/index.ts", + "exports": { + ".": "./src/index.ts" + }, + "scripts": { + "typecheck": "tsc --noEmit" + }, + "devDependencies": { + "typescript": "^5.8.0" + } +} diff --git a/packages/contracts/project.json b/packages/contracts/project.json new file mode 100644 index 0000000..a4c4c02 --- /dev/null +++ b/packages/contracts/project.json @@ -0,0 +1,23 @@ +{ + "name": "contracts", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "root": "packages/contracts", + "sourceRoot": "packages/contracts/src", + "projectType": "library", + "targets": { + "lint": { + "executor": "nx:run-commands", + "options": { + "cwd": "packages/contracts", + "command": "tsc --noEmit" + } + }, + "build": { + "executor": "nx:run-commands", + "options": { + "cwd": "packages/contracts", + "command": "tsc --noEmit" + } + } + } +} diff --git a/packages/contracts/src/index.ts b/packages/contracts/src/index.ts new file mode 100644 index 0000000..956c854 --- /dev/null +++ b/packages/contracts/src/index.ts @@ -0,0 +1,163 @@ +export type Permission = 'public' | 'basic' | 'creat' | 'power' | 'manager'; + +export interface AuthUser { + sub: string; + username: string; + role?: string[]; + tenantId?: string | null; + sso_id?: string; + apiKeyId?: string; + apiKeySecret?: string; + apiKeyName?: string; +} + +export interface IntegrationPlatform { + id: string; + provider: string; + platformKey: string; + name: string; + baseUrl?: string; + authType: string; + status: 'enabled' | 'disabled' | string; + priority: number; + defaultPricingMode: PricingMode; + defaultDiscountFactor: number; + config?: Record; + createdAt: string; + updatedAt: string; +} + +export type PricingMode = 'inherit' | 'inherit_discount' | 'custom'; + +export type RateLimitMetric = + | 'tpm_total' + | 'tpm_input' + | 'tpm_output' + | 'rpm' + | 'concurrent' + | 'queue_size'; + +export interface CatalogProvider { + id: string; + providerKey: string; + displayName: string; + providerType: string; + capabilitySchema?: Record; + defaultRateLimitPolicy?: RateLimitPolicy; + status: 'active' | 'deprecated' | 'hidden' | string; + createdAt: string; + updatedAt: string; +} + +export interface BaseModelCatalogItem { + id: string; + providerKey: string; + canonicalModelKey: string; + providerModelName: string; + modelType: 'chat' | 'image' | 'video' | 'audio' | 'embedding' | 'music' | 'digital_human' | 'model_3d' | string; + displayName: string; + capabilities?: Record; + baseBillingConfig?: BillingConfig; + defaultRateLimitPolicy?: RateLimitPolicy; + pricingVersion: number; + status: 'active' | 'deprecated' | 'hidden' | string; + createdAt: string; + updatedAt: string; +} + +export interface PricingRule { + id: string; + scopeType: 'base_model' | 'platform' | 'platform_model' | string; + scopeId?: string; + resourceType: + | 'text_input' + | 'text_output' + | 'text_total' + | 'image' + | 'video' + | 'audio' + | 'music' + | 'digital_human' + | 'model' + | string; + unit: '1k_tokens' | 'image' | '5s' | 'second' | 'character_1k' | 'item' | string; + basePrice: number; + currency: 'resource' | 'credit' | 'cny' | 'usd' | string; + baseWeight?: Record; + dynamicWeight?: Record; + createdAt: string; + updatedAt: string; +} + +export interface RateLimitRule { + metric: RateLimitMetric; + limit: number; + windowSeconds?: number; + leaseTtlSeconds?: number; + consume?: 'fixed_window' | 'reserve_then_reconcile' | string; +} + +export interface RateLimitPolicy { + rules?: RateLimitRule[]; + [key: string]: unknown; +} + +export interface BillingConfig { + resourceType?: string; + basePrice?: number; + baseWeight?: Record; + dynamicWeight?: Record; + [key: string]: unknown; +} + +export interface PlatformModel { + id: string; + platformId: string; + baseModelId?: string; + provider?: string; + platformName?: string; + modelName: string; + modelAlias?: string; + modelType: 'chat' | 'image' | 'video' | 'audio' | 'embedding' | string; + displayName: string; + capabilityOverride?: Record; + capabilities?: Record; + pricingMode: PricingMode; + discountFactor?: number; + billingConfigOverride?: BillingConfig; + billingConfig?: BillingConfig; + enabled: boolean; + createdAt: string; + updatedAt: string; +} + +export interface RateLimitWindow { + scopeType: string; + scopeKey: string; + metric: RateLimitMetric | string; + windowStart: string; + limitValue: number; + usedValue: number; + reservedValue: number; + resetAt: string; + updatedAt: string; +} + +export interface GatewayTask { + id: string; + kind: string; + userId: string; + tenantId?: string; + model: string; + request?: Record; + status: 'queued' | 'running' | 'succeeded' | 'failed' | 'cancelled' | string; + result?: Record; + billings?: unknown[]; + error?: string; + createdAt: string; + updatedAt: string; +} + +export interface ListResponse { + items: T[]; +} diff --git a/packages/contracts/tsconfig.json b/packages/contracts/tsconfig.json new file mode 100644 index 0000000..e77b932 --- /dev/null +++ b/packages/contracts/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "skipLibCheck": true, + "noEmit": true + }, + "include": ["src/**/*.ts"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..4f73f4b --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,4172 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@nx/vite': + specifier: ^21.0.0 + version: 21.6.11(@babel/traverse@7.29.0)(nx@21.6.11)(typescript@5.9.3)(vite@7.3.3(yaml@2.8.4))(vitest@3.2.4(yaml@2.8.4)) + '@vitejs/plugin-react': + specifier: ^5.0.0 + version: 5.2.0(vite@7.3.3(yaml@2.8.4)) + nx: + specifier: ^21.0.0 + version: 21.6.11 + typescript: + specifier: ^5.8.0 + version: 5.9.3 + vite: + specifier: ^7.0.0 + version: 7.3.3(yaml@2.8.4) + + apps/web: + dependencies: + '@easyai-ai-gateway/contracts': + specifier: workspace:* + version: link:../../packages/contracts + '@vitejs/plugin-react': + specifier: ^5.0.0 + version: 5.2.0(vite@7.3.3(yaml@2.8.4)) + react: + specifier: ^19.0.0 + version: 19.2.6 + react-dom: + specifier: ^19.0.0 + version: 19.2.6(react@19.2.6) + vite: + specifier: ^7.0.0 + version: 7.3.3(yaml@2.8.4) + devDependencies: + '@types/react': + specifier: ^19.0.0 + version: 19.2.14 + '@types/react-dom': + specifier: ^19.0.0 + version: 19.2.3(@types/react@19.2.14) + typescript: + specifier: ^5.8.0 + version: 5.9.3 + + packages/contracts: + devDependencies: + typescript: + specifier: ^5.8.0 + version: 5.9.3 + +packages: + + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.3': + resolution: {integrity: sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-annotate-as-pure@7.27.3': + resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-create-class-features-plugin@7.29.3': + resolution: {integrity: sha512-RpLYy2sb51oNLjuu1iD3bwBqCBWUzjO0ocp+iaCP/lJtb2CPLcnC2Fftw+4sAzaMELGeWTgExSKADbdo0GFVzA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-create-regexp-features-plugin@7.28.5': + resolution: {integrity: sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-define-polyfill-provider@0.6.8': + resolution: {integrity: sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-member-expression-to-functions@7.28.5': + resolution: {integrity: sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-optimise-call-expression@7.27.1': + resolution: {integrity: sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} + engines: {node: '>=6.9.0'} + + '@babel/helper-remap-async-to-generator@7.27.1': + resolution: {integrity: sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-replace-supers@7.28.6': + resolution: {integrity: sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + resolution: {integrity: sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-wrap-function@7.28.6': + resolution: {integrity: sha512-z+PwLziMNBeSQJonizz2AGnndLsP2DeGHIxDAn+wdHOGuo4Fo1x1HBPPXeE9TAOPHNNWQKCSlA2VZyYyyibDnQ==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.2': + resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.3': + resolution: {integrity: sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.28.5': + resolution: {integrity: sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1': + resolution: {integrity: sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1': + resolution: {integrity: sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-bugfix-safari-rest-destructuring-rhs-array@7.29.3': + resolution: {integrity: sha512-SRS46DFR4HqzUzCVgi90/xMoL+zeBDBvWdKYXSEzh79kXswNFEglUpMKxR04//dPqwYXWUBJ3mpUd933ru9Kmg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1': + resolution: {integrity: sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.13.0 + + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.28.6': + resolution: {integrity: sha512-a0aBScVTlNaiUe35UtfxAN7A/tehvvG4/ByO6+46VPKTRSlfnAFsgKy0FUh+qAkQrDTmhDkT+IBOKlOoMUxQ0g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-proposal-decorators@7.29.0': + resolution: {integrity: sha512-CVBVv3VY/XRMxRYq5dwr2DS7/MvqPm23cOCjbwNnVrfOqcWlnefua1uUs0sjdKOGjvPUG633o07uWzJq4oI6dA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2': + resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-decorators@7.28.6': + resolution: {integrity: sha512-71EYI0ONURHJBL4rSFXnITXqXrrY8q4P0q006DPfN+Rk+ASM+++IBXem/ruokgBZR8YNEWZ8R6B+rCb8VcUTqA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-assertions@7.28.6': + resolution: {integrity: sha512-pSJUpFHdx9z5nqTSirOCMtYVP2wFgoWhP0p3g8ONK/4IHhLIBd0B9NYqAvIUAhq+OkhO4VM1tENCt0cjlsNShw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-attributes@7.28.6': + resolution: {integrity: sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-jsx@7.28.6': + resolution: {integrity: sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.28.6': + resolution: {integrity: sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-unicode-sets-regex@7.18.6': + resolution: {integrity: sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-arrow-functions@7.27.1': + resolution: {integrity: sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-async-generator-functions@7.29.0': + resolution: {integrity: sha512-va0VdWro4zlBr2JsXC+ofCPB2iG12wPtVGTWFx2WLDOM3nYQZZIGP82qku2eW/JR83sD+k2k+CsNtyEbUqhU6w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-async-to-generator@7.28.6': + resolution: {integrity: sha512-ilTRcmbuXjsMmcZ3HASTe4caH5Tpo93PkTxF9oG2VZsSWsahydmcEHhix9Ik122RcTnZnUzPbmux4wh1swfv7g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-block-scoped-functions@7.27.1': + resolution: {integrity: sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-block-scoping@7.28.6': + resolution: {integrity: sha512-tt/7wOtBmwHPNMPu7ax4pdPz6shjFrmHDghvNC+FG9Qvj7D6mJcoRQIF5dy4njmxR941l6rgtvfSB2zX3VlUIw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-class-properties@7.28.6': + resolution: {integrity: sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-class-static-block@7.28.6': + resolution: {integrity: sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.12.0 + + '@babel/plugin-transform-classes@7.28.6': + resolution: {integrity: sha512-EF5KONAqC5zAqT783iMGuM2ZtmEBy+mJMOKl2BCvPZ2lVrwvXnB6o+OBWCS+CoeCCpVRF2sA2RBKUxvT8tQT5Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-computed-properties@7.28.6': + resolution: {integrity: sha512-bcc3k0ijhHbc2lEfpFHgx7eYw9KNXqOerKWfzbxEHUGKnS3sz9C4CNL9OiFN1297bDNfUiSO7DaLzbvHQQQ1BQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-destructuring@7.28.5': + resolution: {integrity: sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-dotall-regex@7.28.6': + resolution: {integrity: sha512-SljjowuNKB7q5Oayv4FoPzeB74g3QgLt8IVJw9ADvWy3QnUb/01aw8I4AVv8wYnPvQz2GDDZ/g3GhcNyDBI4Bg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-duplicate-keys@7.27.1': + resolution: {integrity: sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.29.0': + resolution: {integrity: sha512-zBPcW2lFGxdiD8PUnPwJjag2J9otbcLQzvbiOzDxpYXyCuYX9agOwMPGn1prVH0a4qzhCKu24rlH4c1f7yA8rw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-dynamic-import@7.27.1': + resolution: {integrity: sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-explicit-resource-management@7.28.6': + resolution: {integrity: sha512-Iao5Konzx2b6g7EPqTy40UZbcdXE126tTxVFr/nAIj+WItNxjKSYTEw3RC+A2/ZetmdJsgueL1KhaMCQHkLPIg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-exponentiation-operator@7.28.6': + resolution: {integrity: sha512-WitabqiGjV/vJ0aPOLSFfNY1u9U3R7W36B03r5I2KoNix+a3sOhJ3pKFB3R5It9/UiK78NiO0KE9P21cMhlPkw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-export-namespace-from@7.27.1': + resolution: {integrity: sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-for-of@7.27.1': + resolution: {integrity: sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-function-name@7.27.1': + resolution: {integrity: sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-json-strings@7.28.6': + resolution: {integrity: sha512-Nr+hEN+0geQkzhbdgQVPoqr47lZbm+5fCUmO70722xJZd0Mvb59+33QLImGj6F+DkK3xgDi1YVysP8whD6FQAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-literals@7.27.1': + resolution: {integrity: sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-logical-assignment-operators@7.28.6': + resolution: {integrity: sha512-+anKKair6gpi8VsM/95kmomGNMD0eLz1NQ8+Pfw5sAwWH9fGYXT50E55ZpV0pHUHWf6IUTWPM+f/7AAff+wr9A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-member-expression-literals@7.27.1': + resolution: {integrity: sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-amd@7.27.1': + resolution: {integrity: sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-commonjs@7.28.6': + resolution: {integrity: sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-systemjs@7.29.4': + resolution: {integrity: sha512-N7QmZ0xRZfjHOfZeQLJjwgX2zS9pdGHSVl/cjSGlo4dXMqvurfxXDMKY4RqEKzPozV78VMcd0lxyG13mlbKc4w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-umd@7.27.1': + resolution: {integrity: sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-named-capturing-groups-regex@7.29.0': + resolution: {integrity: sha512-1CZQA5KNAD6ZYQLPw7oi5ewtDNxH/2vuCh+6SmvgDfhumForvs8a1o9n0UrEoBD8HU4djO2yWngTQlXl1NDVEQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-new-target@7.27.1': + resolution: {integrity: sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-nullish-coalescing-operator@7.28.6': + resolution: {integrity: sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-numeric-separator@7.28.6': + resolution: {integrity: sha512-SJR8hPynj8outz+SlStQSwvziMN4+Bq99it4tMIf5/Caq+3iOc0JtKyse8puvyXkk3eFRIA5ID/XfunGgO5i6w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-object-rest-spread@7.28.6': + resolution: {integrity: sha512-5rh+JR4JBC4pGkXLAcYdLHZjXudVxWMXbB6u6+E9lRL5TrGVbHt1TjxGbZ8CkmYw9zjkB7jutzOROArsqtncEA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-object-super@7.27.1': + resolution: {integrity: sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-optional-catch-binding@7.28.6': + resolution: {integrity: sha512-R8ja/Pyrv0OGAvAXQhSTmWyPJPml+0TMqXlO5w+AsMEiwb2fg3WkOvob7UxFSL3OIttFSGSRFKQsOhJ/X6HQdQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-optional-chaining@7.28.6': + resolution: {integrity: sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-parameters@7.27.7': + resolution: {integrity: sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-private-methods@7.28.6': + resolution: {integrity: sha512-piiuapX9CRv7+0st8lmuUlRSmX6mBcVeNQ1b4AYzJxfCMuBfB0vBXDiGSmm03pKJw1v6cZ8KSeM+oUnM6yAExg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-private-property-in-object@7.28.6': + resolution: {integrity: sha512-b97jvNSOb5+ehyQmBpmhOCiUC5oVK4PMnpRvO7+ymFBoqYjeDHIU9jnrNUuwHOiL9RpGDoKBpSViarV+BU+eVA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-property-literals@7.27.1': + resolution: {integrity: sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-self@7.27.1': + resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.27.1': + resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-regenerator@7.29.0': + resolution: {integrity: sha512-FijqlqMA7DmRdg/aINBSs04y8XNTYw/lr1gJ2WsmBnnaNw1iS43EPkJW+zK7z65auG3AWRFXWj+NcTQwYptUog==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-regexp-modifiers@7.28.6': + resolution: {integrity: sha512-QGWAepm9qxpaIs7UM9FvUSnCGlb8Ua1RhyM4/veAxLwt3gMat/LSGrZixyuj4I6+Kn9iwvqCyPTtbdxanYoWYg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-reserved-words@7.27.1': + resolution: {integrity: sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-runtime@7.29.0': + resolution: {integrity: sha512-jlaRT5dJtMaMCV6fAuLbsQMSwz/QkvaHOHOSXRitGGwSpR1blCY4KUKoyP2tYO8vJcqYe8cEj96cqSztv3uF9w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-shorthand-properties@7.27.1': + resolution: {integrity: sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-spread@7.28.6': + resolution: {integrity: sha512-9U4QObUC0FtJl05AsUcodau/RWDytrU6uKgkxu09mLR9HLDAtUMoPuuskm5huQsoktmsYpI+bGmq+iapDcriKA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-sticky-regex@7.27.1': + resolution: {integrity: sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-template-literals@7.27.1': + resolution: {integrity: sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typeof-symbol@7.27.1': + resolution: {integrity: sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typescript@7.28.6': + resolution: {integrity: sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-escapes@7.27.1': + resolution: {integrity: sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-property-regex@7.28.6': + resolution: {integrity: sha512-4Wlbdl/sIZjzi/8St0evF0gEZrgOswVO6aOzqxh1kDZOl9WmLrHq2HtGhnOJZmHZYKP8WZ1MDLCt5DAWwRo57A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-regex@7.27.1': + resolution: {integrity: sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-sets-regex@7.28.6': + resolution: {integrity: sha512-/wHc/paTUmsDYN7SZkpWxogTOBNnlx7nBQYfy6JJlCT7G3mVhltk3e++N7zV0XfgGsrqBxd4rJQt9H16I21Y1Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/preset-env@7.29.5': + resolution: {integrity: sha512-/69t2aEzGKHD76DyLbHysF/QH2LJOB8iFnYO37unDTKBTubzcMRv0f3H5EiN1Q6ajOd/eB7dAInF0qdFVS06kA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/preset-modules@0.1.6-no-external-plugins': + resolution: {integrity: sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==} + peerDependencies: + '@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0 + + '@babel/preset-typescript@7.28.5': + resolution: {integrity: sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/runtime@7.29.2': + resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + + '@esbuild/aix-ppc64@0.27.7': + resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.7': + resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.7': + resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.7': + resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.7': + resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.7': + resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.7': + resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.7': + resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.7': + resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.7': + resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.7': + resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.7': + resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.7': + resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.7': + resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.7': + resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.7': + resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.7': + resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.7': + resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.7': + resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.7': + resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.7': + resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.7': + resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.7': + resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.7': + resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.7': + resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.7': + resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@jest/diff-sequences@30.4.0': + resolution: {integrity: sha512-zOpzlfUs45l6u7jm39qr87JCHUDsaeCtvL+kQe/Vn9jSnRB4/5IPXISm0h9I1vZW/o00Kn4UTJ2MOlhnUGwv3g==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/get-type@30.1.0': + resolution: {integrity: sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/schemas@30.4.1': + resolution: {integrity: sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@napi-rs/wasm-runtime@0.2.4': + resolution: {integrity: sha512-9zESzOO5aDByvhIAsOy9TbpZ0Ur2AJbUI7UT73kcUTS2mxAMHOBaa1st/jAymNoCtvrit99kkzT1FZuXVcgfIQ==} + + '@nx/devkit@21.6.11': + resolution: {integrity: sha512-tjx0GMuJQSXBhmz4XZD3D5jVtXO9/bIc7fLe0tXJ5w6ohQMKhBdkpyyjFm+1nRdAnWRQARqM2HqV+WZLYr3axQ==} + peerDependencies: + nx: '>= 20 <= 22' + + '@nx/js@21.6.11': + resolution: {integrity: sha512-4o6+zcxa82FgUMYfC8a4UujvYIINqwEaBPV0wq64Kk7h6YVeTioCNCDqUVMHQdcv4NRiWsUpGhc19PMnGGHTBQ==} + peerDependencies: + verdaccio: ^6.0.5 + peerDependenciesMeta: + verdaccio: + optional: true + + '@nx/nx-darwin-arm64@21.6.11': + resolution: {integrity: sha512-4hXhV7ShXIlfPEjjm7dJY383xM2vTcnkKr5FUncAU08GKkkL67ib5CMlQADtdi32ewfCZntqiT8gUfFFSNvKtA==} + cpu: [arm64] + os: [darwin] + + '@nx/nx-darwin-x64@21.6.11': + resolution: {integrity: sha512-VxjKkzyhdO47X7d4JPx/f6HslERKetFmouDUBIoqbKDPVWpRegGMsRKcMYh4l61usxI1Qa2U4Ec6MgO5Fnm41g==} + cpu: [x64] + os: [darwin] + + '@nx/nx-freebsd-x64@21.6.11': + resolution: {integrity: sha512-3jDn7Tb3FMFfeFTM/XKAcI+J92kDLNmxUS/N/n/+kF/XYLPgMUZQqSeDpVifk4fgy0BCoH8DSMQIqTQau6dV/g==} + cpu: [x64] + os: [freebsd] + + '@nx/nx-linux-arm-gnueabihf@21.6.11': + resolution: {integrity: sha512-37tpiVod5FN/EAuCGh+uad/6nsfDFze02OjYReUPKeYPs8Q7Ac/V9j4kn/y5uZhKSle+9+sa2QR/K79B0+lNxw==} + cpu: [arm] + os: [linux] + + '@nx/nx-linux-arm64-gnu@21.6.11': + resolution: {integrity: sha512-r+czH0OtldQqFm2B6BBBUPW8aO+sBMvpZCgN855vko30WaCeXo8Fkuk71rQMxByz0jnoCxSnzLtEWVZm1rmJYA==} + cpu: [arm64] + os: [linux] + + '@nx/nx-linux-arm64-musl@21.6.11': + resolution: {integrity: sha512-0FAPyWEGPCukXxR1qowvFx6Q/cU906vwPlAQtcbrBU1e2H2aMUslk9EmL8iHb5MsHdmGcFyFemwr9oN8gxmz4g==} + cpu: [arm64] + os: [linux] + + '@nx/nx-linux-x64-gnu@21.6.11': + resolution: {integrity: sha512-+bWJWXJ8tdddl1L3bTKE0VmvTmdsk4zzUr6P9ts9hXQbwoWqMcuA6LNqOhUfzVOL3VioirRMtKMfFuUAUhi3Yg==} + cpu: [x64] + os: [linux] + + '@nx/nx-linux-x64-musl@21.6.11': + resolution: {integrity: sha512-Mh09mLc+yeJk7DKfx7x6XfB+bm2dP1/7gyUuRQj4WjkT+Il2ZponyTuH8LmX06Jpr7efAAFk+8lBXeleV7XvMw==} + cpu: [x64] + os: [linux] + + '@nx/nx-win32-arm64-msvc@21.6.11': + resolution: {integrity: sha512-d7ZeCCDwaeyKiWD2JLSSxMSuSHHwIdpbnMtZtzRE6tDdAgs6e9F36/OBNQB3IRXH8V6QKOy2OGDyWeOKr01X9A==} + cpu: [arm64] + os: [win32] + + '@nx/nx-win32-x64-msvc@21.6.11': + resolution: {integrity: sha512-otHSkhyoGilttV4RRkVmLLGb2W+Ia4b90lWxLnEm9jAAKmA9O2FUG2vAvKDHNcqTyDwwCcHfHxslgnR1u/3OIg==} + cpu: [x64] + os: [win32] + + '@nx/vite@21.6.11': + resolution: {integrity: sha512-T0yx+8R2N/srGOE1b2QJhaETS4UH690YmaM2hHUwWHWyXi83BzNBM8TPPj007wsoEAcbd6PYKp/vHaIVEker6g==} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 + vitest: ^1.3.1 || ^2.0.0 || ^3.0.0 + + '@nx/workspace@21.6.11': + resolution: {integrity: sha512-EMY3erQiP/VNqzsPyfxLhCrBzbTb1ug9+LWr7McDd30f4qM8VSgc2f/L00HcmNhRInkwBeEO+PqL11rIqz/lFw==} + + '@phenomnomnominal/tsquery@5.0.1': + resolution: {integrity: sha512-3nVv+e2FQwsW8Aw6qTU6f+1rfcJ3hrcnvH/mu9i8YhxO+9sqbOfpL8m6PbET5+xKOlz/VSbp0RoYWYCtIsnmuA==} + peerDependencies: + typescript: ^3 || ^4 || ^5 + + '@rolldown/pluginutils@1.0.0-rc.3': + resolution: {integrity: sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==} + + '@rollup/rollup-android-arm-eabi@4.60.3': + resolution: {integrity: sha512-x35CNW/ANXG3hE/EZpRU8MXX1JDN86hBb2wMGAtltkz7pc6cxgjpy1OMMfDosOQ+2hWqIkag/fGok1Yady9nGw==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.60.3': + resolution: {integrity: sha512-xw3xtkDApIOGayehp2+Rz4zimfkaX65r4t47iy+ymQB2G4iJCBBfj0ogVg5jpvjpn8UWn/+q9tprxleYeNp3Hw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.60.3': + resolution: {integrity: sha512-vo6Y5Qfpx7/5EaamIwi0WqW2+zfiusVihKatLvtN1VFVy3D13uERk/6gZLU1UiHRL6fDXqj/ELIeVRGnvcTE1g==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.60.3': + resolution: {integrity: sha512-D+0QGcZhBzTN82weOnsSlY7V7+RMmPuF1CkbxyMAGE8+ZHeUjyb76ZiWmBlCu//AQQONvxcqRbwZTajZKqjuOw==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.60.3': + resolution: {integrity: sha512-6HnvHCT7fDyj6R0Ph7A6x8dQS/S38MClRWeDLqc0MdfWkxjiu1HSDYrdPhqSILzjTIC/pnXbbJbo+ft+gy/9hQ==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.60.3': + resolution: {integrity: sha512-KHLgC3WKlUYW3ShFKnnosZDOJ0xjg9zp7au3sIm2bs/tGBeC2ipmvRh/N7JKi0t9Ue20C0dpEshi8WUubg+cnA==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.60.3': + resolution: {integrity: sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.60.3': + resolution: {integrity: sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.60.3': + resolution: {integrity: sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.60.3': + resolution: {integrity: sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.60.3': + resolution: {integrity: sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.60.3': + resolution: {integrity: sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.60.3': + resolution: {integrity: sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.60.3': + resolution: {integrity: sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.60.3': + resolution: {integrity: sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.60.3': + resolution: {integrity: sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.60.3': + resolution: {integrity: sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.60.3': + resolution: {integrity: sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.60.3': + resolution: {integrity: sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.60.3': + resolution: {integrity: sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.60.3': + resolution: {integrity: sha512-AaXwSvUi3QIPtroAUw1t5yHGIyqKEXwH54WUocFolZhpGDruJcs8c+xPNDRn4XiQsS7MEwnYsHW2l0MBLDMkWg==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.60.3': + resolution: {integrity: sha512-65LAKM/bAWDqKNEelHlcHvm2V+Vfb8C6INFxQXRHCvaVN1rJfwr4NvdP4FyzUaLqWfaCGaadf6UbTm8xJeYfEg==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.60.3': + resolution: {integrity: sha512-EEM2gyhBF5MFnI6vMKdX1LAosE627RGBzIoGMdLloPZkXrUN0Ckqgr2Qi8+J3zip/8NVVro3/FjB+tjhZUgUHA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.60.3': + resolution: {integrity: sha512-E5Eb5H/DpxaoXH++Qkv28RcUJboMopmdDUALBczvHMf7hNIxaDZqwY5lK12UK1BHacSmvupoEWGu+n993Z0y1A==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.60.3': + resolution: {integrity: sha512-hPt/bgL5cE+Qp+/TPHBqptcAgPzgj46mPcg/16zNUmbQk0j+mOEQV/+Lqu8QRtDV3Ek95Q6FeFITpuhl6OTsAA==} + cpu: [x64] + os: [win32] + + '@sinclair/typebox@0.34.49': + resolution: {integrity: sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==} + + '@tybys/wasm-util@0.9.0': + resolution: {integrity: sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/parse-json@4.0.2': + resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} + + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.14': + resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} + + '@vitejs/plugin-react@5.2.0': + resolution: {integrity: sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + + '@vitest/expect@3.2.4': + resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} + + '@vitest/mocker@3.2.4': + resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@3.2.4': + resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} + + '@vitest/runner@3.2.4': + resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==} + + '@vitest/snapshot@3.2.4': + resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==} + + '@vitest/spy@3.2.4': + resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} + + '@vitest/utils@3.2.4': + resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + + '@yarnpkg/lockfile@1.1.0': + resolution: {integrity: sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==} + + '@yarnpkg/parsers@3.0.2': + resolution: {integrity: sha512-/HcYgtUSiJiot/XWGLOlGxPYUG65+/31V8oqk17vZLW1xlCoR4PampyePljOxY2n8/3jz9+tIFzICsyGujJZoA==} + engines: {node: '>=18.12.0'} + + '@zkochan/js-yaml@0.0.7': + resolution: {integrity: sha512-nrUSn7hzt7J6JWgWGz78ZYI8wj+gdIJdk0Ynjpp8l+trkn58Uqsf6RYrYkEK+3X18EX+TNdtJI0WxAtc+L84SQ==} + hasBin: true + + address@1.2.2: + resolution: {integrity: sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==} + engines: {node: '>= 10.0.0'} + + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + axios@1.16.0: + resolution: {integrity: sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==} + + babel-plugin-const-enum@1.2.0: + resolution: {integrity: sha512-o1m/6iyyFnp9MRsK1dHF3bneqyf3AlM2q3A/YbgQr2pCat6B6XJVDv2TXqzfY2RYUi4mak6WAksSBPlyYGx9dg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + babel-plugin-macros@3.1.0: + resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} + engines: {node: '>=10', npm: '>=6'} + + babel-plugin-polyfill-corejs2@0.4.17: + resolution: {integrity: sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + babel-plugin-polyfill-corejs3@0.13.0: + resolution: {integrity: sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + babel-plugin-polyfill-corejs3@0.14.2: + resolution: {integrity: sha512-coWpDLJ410R781Npmn/SIBZEsAetR4xVi0SxLMXPaMO4lSf1MwnkGYMtkFxew0Dn8B3/CpbpYxN0JCgg8mn67g==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + babel-plugin-polyfill-regenerator@0.6.8: + resolution: {integrity: sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + babel-plugin-transform-typescript-metadata@0.3.2: + resolution: {integrity: sha512-mWEvCQTgXQf48yDqgN7CH50waTyYBeP2Lpqx4nNWab9sxEpdXVeKgfj1qYI2/TgUPQtNFZ85i3PemRtnXVYYJg==} + peerDependencies: + '@babel/core': ^7 + '@babel/traverse': ^7 + peerDependenciesMeta: + '@babel/traverse': + optional: true + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + baseline-browser-mapping@2.10.28: + resolution: {integrity: sha512-Ic44hnOtFIgravCunj1ifSoQPSUrkNiJuH9Mf6jr2jjoA74icqV8wU0KuadXeOR8zuIJMOoTv0GuQjZ9ZYNMeA==} + engines: {node: '>=6.0.0'} + hasBin: true + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + + brace-expansion@2.1.0: + resolution: {integrity: sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==} + + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001792: + resolution: {integrity: sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==} + + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + + cli-cursor@3.1.0: + resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} + engines: {node: '>=8'} + + cli-spinners@2.6.1: + resolution: {integrity: sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g==} + engines: {node: '>=6'} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + clone@1.0.4: + resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} + engines: {node: '>=0.8'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + columnify@1.6.0: + resolution: {integrity: sha512-lomjuFZKfM6MSAnV9aCZC9sc0qGbmZdfygNv+nCpqVkSKdCxCklLtd16O0EILGkImHw9ZpHkAnHaB+8Zxq5W6Q==} + engines: {node: '>=8.0.0'} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + core-js-compat@3.49.0: + resolution: {integrity: sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==} + + cosmiconfig@7.1.0: + resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} + engines: {node: '>=10'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + defaults@1.0.4: + resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + + define-lazy-prop@2.0.0: + resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} + engines: {node: '>=8'} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + detect-port@1.6.1: + resolution: {integrity: sha512-CmnVc+Hek2egPx1PeTFVta2W78xy2K/9Rkf6cC4T59S50tVnzKj+tnx5mmx5lwvCkujZ4uRrpRSuV+IVs3f90Q==} + engines: {node: '>= 4.0.0'} + hasBin: true + + dotenv-expand@11.0.7: + resolution: {integrity: sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==} + engines: {node: '>=12'} + + dotenv@16.4.7: + resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} + engines: {node: '>=12'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + ejs@3.1.10: + resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} + engines: {node: '>=0.10.0'} + hasBin: true + + electron-to-chromium@1.5.353: + resolution: {integrity: sha512-kOrWphBi8TOZyiJZqsgqIle0lw+tzmnQK83pV9dZUd01Nm2POECSyFQMAuarzZdYqQW7FH9RaYOuaRo3h+bQ3w==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + + enquirer@2.3.6: + resolution: {integrity: sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==} + engines: {node: '>=8.6'} + + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + esbuild@0.27.7: + resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-uri@3.1.2: + resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + figures@3.2.0: + resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} + engines: {node: '>=8'} + + filelist@1.0.6: + resolution: {integrity: sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==} + + flat@5.0.2: + resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} + hasBin: true + + follow-redirects@1.16.0: + resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + + front-matter@4.0.2: + resolution: {integrity: sha512-I8ZuJ/qG92NWX8i5x1Y8qyj3vizhXS31OxjKDu3LKP+7/qBgfIKValiZIEwoVoJKUHlhWtYrktkxV1XsX+pPlg==} + + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.3: + resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} + engines: {node: '>= 0.4'} + + hosted-git-info@7.0.2: + resolution: {integrity: sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==} + engines: {node: ^16.14.0 || >=18.0.0} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-core-module@2.16.2: + resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} + engines: {node: '>= 0.4'} + + is-docker@2.2.1: + resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} + engines: {node: '>=8'} + hasBin: true + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-interactive@1.0.0: + resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} + engines: {node: '>=8'} + + is-unicode-supported@0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + + is-wsl@2.2.0: + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} + + jake@10.9.4: + resolution: {integrity: sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==} + engines: {node: '>=10'} + hasBin: true + + jest-diff@30.4.1: + resolution: {integrity: sha512-CRpFK0RtLriVDGcPPAnR6HMVI8bSR2jnUIgralhauzYQZIb4RH9AtEInTuQr65LmmGggGcRT6HIASxwqsVsmlA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + + js-yaml@3.14.2: + resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonc-parser@3.2.0: + resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + lines-and-columns@2.0.3: + resolution: {integrity: sha512-cNOjgCnLB+FnvWWtyRTzmB3POJ+cXxTA81LoW7u8JdmhfXzriropYwpjShnz1QLLWsQwY7nIxoDmcPTwphDK9w==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + lodash.debounce@4.0.8: + resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + + log-symbols@4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + minimatch@5.1.9: + resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==} + engines: {node: '>=10'} + + minimatch@9.0.3: + resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.12: + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + node-machine-id@1.1.12: + resolution: {integrity: sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ==} + + node-releases@2.0.38: + resolution: {integrity: sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==} + + npm-package-arg@11.0.1: + resolution: {integrity: sha512-M7s1BD4NxdAvBKUPqqRW957Xwcl/4Zvo8Aj+ANrzvIPzGJZElrH7Z//rSaec2ORcND6FHHLnZeY8qgTpXDMFQQ==} + engines: {node: ^16.14.0 || >=18.0.0} + + npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + + nx@21.6.11: + resolution: {integrity: sha512-AAgJGhS+7xlsmZF6ArKX1vgONxf7IymUYZ1BxGXHVa5927rGfgKoMaPOgwwtvN0OL3o/QYaNGwlDfIzCvlpOLQ==} + hasBin: true + peerDependencies: + '@swc-node/register': ^1.8.0 + '@swc/core': ^1.3.85 + peerDependenciesMeta: + '@swc-node/register': + optional: true + '@swc/core': + optional: true + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + open@8.4.2: + resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} + engines: {node: '>=12'} + + ora@5.3.0: + resolution: {integrity: sha512-zAKMgGXUim0Jyd6CXK9lraBnD3H5yPGBPPOkC23a2BG6hsm4Zu6OQSjQuEtV0BHDf4aKHcUFvJiGRrFuW3MG8g==} + engines: {node: '>=10'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.2: + resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} + engines: {node: '>=12'} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + postcss@8.5.14: + resolution: {integrity: sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==} + engines: {node: ^10 || ^12 || >=14} + + pretty-format@30.4.1: + resolution: {integrity: sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + proc-log@3.0.0: + resolution: {integrity: sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + proxy-from-env@2.1.0: + resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} + engines: {node: '>=10'} + + react-dom@19.2.6: + resolution: {integrity: sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==} + peerDependencies: + react: ^19.2.6 + + react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + + react-is@19.2.6: + resolution: {integrity: sha512-XjBR15BhXuylgWGuslhDKqlSayuqvqBX91BP8pauG8kd1zY8kotkNWbXksTCNRarse4kuGbe2kIY05ARtwNIvw==} + + react-refresh@0.18.0: + resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} + engines: {node: '>=0.10.0'} + + react@19.2.6: + resolution: {integrity: sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==} + engines: {node: '>=0.10.0'} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + regenerate-unicode-properties@10.2.2: + resolution: {integrity: sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==} + engines: {node: '>=4'} + + regenerate@1.4.2: + resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} + + regexpu-core@6.4.0: + resolution: {integrity: sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==} + engines: {node: '>=4'} + + regjsgen@0.8.0: + resolution: {integrity: sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==} + + regjsparser@0.13.1: + resolution: {integrity: sha512-dLsljMd9sqwRkby8zhO1gSg3PnJIBFid8f4CQj/sXx+7cKx+E7u0PKhZ+U4wmhx7EfmtvnA318oVaIkAB1lRJw==} + hasBin: true + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve.exports@2.0.3: + resolution: {integrity: sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==} + engines: {node: '>=10'} + + resolve@1.22.12: + resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} + engines: {node: '>= 0.4'} + hasBin: true + + restore-cursor@3.1.0: + resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} + engines: {node: '>=8'} + + rollup@4.60.3: + resolution: {integrity: sha512-pAQK9HalE84QSm4Po3EmWIZPd3FnjkShVkiMlz1iligWYkWQ7wHYd1PF/T7QZ5TVSD6uSTon5gBVMSM4JfBV+A==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.8.0: + resolution: {integrity: sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==} + engines: {node: '>=10'} + hasBin: true + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map-support@0.5.19: + resolution: {integrity: sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-literal@3.1.0: + resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.0.0'} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyspy@4.0.4: + resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} + engines: {node: '>=14.0.0'} + + tmp@0.2.5: + resolution: {integrity: sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==} + engines: {node: '>=14.14'} + + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + tsconfig-paths@4.2.0: + resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} + engines: {node: '>=6'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + unicode-canonical-property-names-ecmascript@2.0.1: + resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==} + engines: {node: '>=4'} + + unicode-match-property-ecmascript@2.0.0: + resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==} + engines: {node: '>=4'} + + unicode-match-property-value-ecmascript@2.2.1: + resolution: {integrity: sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==} + engines: {node: '>=4'} + + unicode-property-aliases-ecmascript@2.2.0: + resolution: {integrity: sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==} + engines: {node: '>=4'} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + validate-npm-package-name@5.0.1: + resolution: {integrity: sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + vite-node@3.2.4: + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + + vite@7.3.3: + resolution: {integrity: sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + 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 + + vitest@3.2.4: + resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.2.4 + '@vitest/ui': 3.2.4 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + wcwidth@1.0.1: + resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yaml@1.10.3: + resolution: {integrity: sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==} + engines: {node: '>= 6'} + + yaml@2.8.4: + resolution: {integrity: sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==} + engines: {node: '>= 14.6'} + hasBin: true + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + +snapshots: + + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.3': {} + + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.29.2 + '@babel/parser': 7.29.3 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-annotate-as-pure@7.27.3': + dependencies: + '@babel/types': 7.29.0 + + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.29.3 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.2 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-create-class-features-plugin@7.29.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-member-expression-to-functions': 7.28.5 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0) + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/traverse': 7.29.0 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-create-regexp-features-plugin@7.28.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + regexpu-core: 6.4.0 + semver: 6.3.1 + + '@babel/helper-define-polyfill-provider@0.6.8(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + debug: 4.4.3 + lodash.debounce: 4.0.8 + resolve: 1.22.12 + transitivePeerDependencies: + - supports-color + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-member-expression-to-functions@7.28.5': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-optimise-call-expression@7.27.1': + dependencies: + '@babel/types': 7.29.0 + + '@babel/helper-plugin-utils@7.28.6': {} + + '@babel/helper-remap-async-to-generator@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-wrap-function': 7.28.6 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-replace-supers@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-member-expression-to-functions': 7.28.5 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helper-wrap-function@7.28.6': + dependencies: + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helpers@7.29.2': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + + '@babel/parser@7.29.3': + dependencies: + '@babel/types': 7.29.0 + + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.28.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-bugfix-safari-rest-destructuring-rhs-array@7.29.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/plugin-transform-optional-chaining': 7.28.6(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-proposal-decorators@7.29.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-class-features-plugin': 7.29.3(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-syntax-decorators': 7.28.6(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + + '@babel/plugin-syntax-decorators@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-import-assertions@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-import-attributes@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-typescript@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-arrow-functions@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-async-generator-functions@7.29.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.29.0) + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-async-to-generator@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-block-scoped-functions@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-block-scoping@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-class-properties@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-class-features-plugin': 7.29.3(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-class-static-block@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-class-features-plugin': 7.29.3(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-classes@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-globals': 7.28.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0) + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-computed-properties@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/template': 7.28.6 + + '@babel/plugin-transform-destructuring@7.28.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-dotall-regex@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-duplicate-keys@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.29.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-dynamic-import@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-explicit-resource-management@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-exponentiation-operator@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-export-namespace-from@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-for-of@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-function-name@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-json-strings@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-literals@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-logical-assignment-operators@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-member-expression-literals@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-modules-amd@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-commonjs@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-systemjs@7.29.4(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-umd@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-named-capturing-groups-regex@7.29.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-new-target@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-nullish-coalescing-operator@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-numeric-separator@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-object-rest-spread@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.29.0) + '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.29.0) + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-object-super@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-optional-catch-binding@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-optional-chaining@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-parameters@7.27.7(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-private-methods@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-class-features-plugin': 7.29.3(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-private-property-in-object@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-create-class-features-plugin': 7.29.3(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-property-literals@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-regenerator@7.29.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-regexp-modifiers@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-reserved-words@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-runtime@7.29.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + babel-plugin-polyfill-corejs2: 0.4.17(@babel/core@7.29.0) + babel-plugin-polyfill-corejs3: 0.13.0(@babel/core@7.29.0) + babel-plugin-polyfill-regenerator: 0.6.8(@babel/core@7.29.0) + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-shorthand-properties@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-spread@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-sticky-regex@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-template-literals@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-typeof-symbol@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-typescript@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-create-class-features-plugin': 7.29.3(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-unicode-escapes@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-unicode-property-regex@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-unicode-regex@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-unicode-sets-regex@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/preset-env@7.29.5(@babel/core@7.29.0)': + dependencies: + '@babel/compat-data': 7.29.3 + '@babel/core': 7.29.0 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-validator-option': 7.27.1 + '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.28.5(@babel/core@7.29.0) + '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-bugfix-safari-rest-destructuring-rhs-array': 7.29.3(@babel/core@7.29.0) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.29.0) + '@babel/plugin-syntax-import-assertions': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.29.0) + '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-async-generator-functions': 7.29.0(@babel/core@7.29.0) + '@babel/plugin-transform-async-to-generator': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-block-scoped-functions': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-block-scoping': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-class-properties': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-class-static-block': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-classes': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-computed-properties': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.29.0) + '@babel/plugin-transform-dotall-regex': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-duplicate-keys': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.29.0(@babel/core@7.29.0) + '@babel/plugin-transform-dynamic-import': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-explicit-resource-management': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-exponentiation-operator': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-export-namespace-from': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-for-of': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-function-name': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-json-strings': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-literals': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-logical-assignment-operators': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-member-expression-literals': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-modules-amd': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-modules-systemjs': 7.29.4(@babel/core@7.29.0) + '@babel/plugin-transform-modules-umd': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-named-capturing-groups-regex': 7.29.0(@babel/core@7.29.0) + '@babel/plugin-transform-new-target': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-nullish-coalescing-operator': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-numeric-separator': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-object-rest-spread': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-object-super': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-optional-catch-binding': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-optional-chaining': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.29.0) + '@babel/plugin-transform-private-methods': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-private-property-in-object': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-property-literals': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-regenerator': 7.29.0(@babel/core@7.29.0) + '@babel/plugin-transform-regexp-modifiers': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-reserved-words': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-spread': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-sticky-regex': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-template-literals': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-typeof-symbol': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-unicode-escapes': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-unicode-property-regex': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-unicode-sets-regex': 7.28.6(@babel/core@7.29.0) + '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.29.0) + babel-plugin-polyfill-corejs2: 0.4.17(@babel/core@7.29.0) + babel-plugin-polyfill-corejs3: 0.14.2(@babel/core@7.29.0) + babel-plugin-polyfill-regenerator: 0.6.8(@babel/core@7.29.0) + core-js-compat: 3.49.0 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/types': 7.29.0 + esutils: 2.0.3 + + '@babel/preset-typescript@7.28.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-validator-option': 7.27.1 + '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/runtime@7.29.2': {} + + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.3 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@emnapi/core@1.10.0': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + + '@esbuild/aix-ppc64@0.27.7': + optional: true + + '@esbuild/android-arm64@0.27.7': + optional: true + + '@esbuild/android-arm@0.27.7': + optional: true + + '@esbuild/android-x64@0.27.7': + optional: true + + '@esbuild/darwin-arm64@0.27.7': + optional: true + + '@esbuild/darwin-x64@0.27.7': + optional: true + + '@esbuild/freebsd-arm64@0.27.7': + optional: true + + '@esbuild/freebsd-x64@0.27.7': + optional: true + + '@esbuild/linux-arm64@0.27.7': + optional: true + + '@esbuild/linux-arm@0.27.7': + optional: true + + '@esbuild/linux-ia32@0.27.7': + optional: true + + '@esbuild/linux-loong64@0.27.7': + optional: true + + '@esbuild/linux-mips64el@0.27.7': + optional: true + + '@esbuild/linux-ppc64@0.27.7': + optional: true + + '@esbuild/linux-riscv64@0.27.7': + optional: true + + '@esbuild/linux-s390x@0.27.7': + optional: true + + '@esbuild/linux-x64@0.27.7': + optional: true + + '@esbuild/netbsd-arm64@0.27.7': + optional: true + + '@esbuild/netbsd-x64@0.27.7': + optional: true + + '@esbuild/openbsd-arm64@0.27.7': + optional: true + + '@esbuild/openbsd-x64@0.27.7': + optional: true + + '@esbuild/openharmony-arm64@0.27.7': + optional: true + + '@esbuild/sunos-x64@0.27.7': + optional: true + + '@esbuild/win32-arm64@0.27.7': + optional: true + + '@esbuild/win32-ia32@0.27.7': + optional: true + + '@esbuild/win32-x64@0.27.7': + optional: true + + '@jest/diff-sequences@30.4.0': {} + + '@jest/get-type@30.1.0': {} + + '@jest/schemas@30.4.1': + dependencies: + '@sinclair/typebox': 0.34.49 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@napi-rs/wasm-runtime@0.2.4': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.9.0 + + '@nx/devkit@21.6.11(nx@21.6.11)': + dependencies: + ejs: 3.1.10 + enquirer: 2.3.6 + ignore: 5.3.2 + minimatch: 9.0.3 + nx: 21.6.11 + semver: 7.8.0 + tslib: 2.8.1 + yargs-parser: 21.1.1 + + '@nx/js@21.6.11(@babel/traverse@7.29.0)(nx@21.6.11)': + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-proposal-decorators': 7.29.0(@babel/core@7.29.0) + '@babel/plugin-transform-class-properties': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-runtime': 7.29.0(@babel/core@7.29.0) + '@babel/preset-env': 7.29.5(@babel/core@7.29.0) + '@babel/preset-typescript': 7.28.5(@babel/core@7.29.0) + '@babel/runtime': 7.29.2 + '@nx/devkit': 21.6.11(nx@21.6.11) + '@nx/workspace': 21.6.11 + '@zkochan/js-yaml': 0.0.7 + babel-plugin-const-enum: 1.2.0(@babel/core@7.29.0) + babel-plugin-macros: 3.1.0 + babel-plugin-transform-typescript-metadata: 0.3.2(@babel/core@7.29.0)(@babel/traverse@7.29.0) + chalk: 4.1.2 + columnify: 1.6.0 + detect-port: 1.6.1 + enquirer: 2.3.6 + ignore: 5.3.2 + js-tokens: 4.0.0 + jsonc-parser: 3.2.0 + npm-package-arg: 11.0.1 + npm-run-path: 4.0.1 + ora: 5.3.0 + picocolors: 1.1.1 + picomatch: 4.0.2 + semver: 7.8.0 + source-map-support: 0.5.19 + tinyglobby: 0.2.16 + tslib: 2.8.1 + transitivePeerDependencies: + - '@babel/traverse' + - '@swc-node/register' + - '@swc/core' + - debug + - nx + - supports-color + + '@nx/nx-darwin-arm64@21.6.11': + optional: true + + '@nx/nx-darwin-x64@21.6.11': + optional: true + + '@nx/nx-freebsd-x64@21.6.11': + optional: true + + '@nx/nx-linux-arm-gnueabihf@21.6.11': + optional: true + + '@nx/nx-linux-arm64-gnu@21.6.11': + optional: true + + '@nx/nx-linux-arm64-musl@21.6.11': + optional: true + + '@nx/nx-linux-x64-gnu@21.6.11': + optional: true + + '@nx/nx-linux-x64-musl@21.6.11': + optional: true + + '@nx/nx-win32-arm64-msvc@21.6.11': + optional: true + + '@nx/nx-win32-x64-msvc@21.6.11': + optional: true + + '@nx/vite@21.6.11(@babel/traverse@7.29.0)(nx@21.6.11)(typescript@5.9.3)(vite@7.3.3(yaml@2.8.4))(vitest@3.2.4(yaml@2.8.4))': + dependencies: + '@nx/devkit': 21.6.11(nx@21.6.11) + '@nx/js': 21.6.11(@babel/traverse@7.29.0)(nx@21.6.11) + '@phenomnomnominal/tsquery': 5.0.1(typescript@5.9.3) + ajv: 8.20.0 + enquirer: 2.3.6 + picomatch: 4.0.2 + semver: 7.8.0 + tsconfig-paths: 4.2.0 + tslib: 2.8.1 + vite: 7.3.3(yaml@2.8.4) + vitest: 3.2.4(yaml@2.8.4) + transitivePeerDependencies: + - '@babel/traverse' + - '@swc-node/register' + - '@swc/core' + - debug + - nx + - supports-color + - typescript + - verdaccio + + '@nx/workspace@21.6.11': + dependencies: + '@nx/devkit': 21.6.11(nx@21.6.11) + '@zkochan/js-yaml': 0.0.7 + chalk: 4.1.2 + enquirer: 2.3.6 + nx: 21.6.11 + picomatch: 4.0.2 + semver: 7.8.0 + tslib: 2.8.1 + yargs-parser: 21.1.1 + transitivePeerDependencies: + - '@swc-node/register' + - '@swc/core' + - debug + + '@phenomnomnominal/tsquery@5.0.1(typescript@5.9.3)': + dependencies: + esquery: 1.7.0 + typescript: 5.9.3 + + '@rolldown/pluginutils@1.0.0-rc.3': {} + + '@rollup/rollup-android-arm-eabi@4.60.3': + optional: true + + '@rollup/rollup-android-arm64@4.60.3': + optional: true + + '@rollup/rollup-darwin-arm64@4.60.3': + optional: true + + '@rollup/rollup-darwin-x64@4.60.3': + optional: true + + '@rollup/rollup-freebsd-arm64@4.60.3': + optional: true + + '@rollup/rollup-freebsd-x64@4.60.3': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.60.3': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.60.3': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.60.3': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.60.3': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.60.3': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.60.3': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.60.3': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.60.3': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.60.3': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.60.3': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.60.3': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.60.3': + optional: true + + '@rollup/rollup-linux-x64-musl@4.60.3': + optional: true + + '@rollup/rollup-openbsd-x64@4.60.3': + optional: true + + '@rollup/rollup-openharmony-arm64@4.60.3': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.60.3': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.60.3': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.60.3': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.60.3': + optional: true + + '@sinclair/typebox@0.34.49': {} + + '@tybys/wasm-util@0.9.0': + dependencies: + tslib: 2.8.1 + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.0 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.0 + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.8': {} + + '@types/estree@1.0.9': {} + + '@types/parse-json@4.0.2': {} + + '@types/react-dom@19.2.3(@types/react@19.2.14)': + dependencies: + '@types/react': 19.2.14 + + '@types/react@19.2.14': + dependencies: + csstype: 3.2.3 + + '@vitejs/plugin-react@5.2.0(vite@7.3.3(yaml@2.8.4))': + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0) + '@rolldown/pluginutils': 1.0.0-rc.3 + '@types/babel__core': 7.20.5 + react-refresh: 0.18.0 + vite: 7.3.3(yaml@2.8.4) + transitivePeerDependencies: + - supports-color + + '@vitest/expect@3.2.4': + dependencies: + '@types/chai': 5.2.3 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + tinyrainbow: 2.0.0 + + '@vitest/mocker@3.2.4(vite@7.3.3(yaml@2.8.4))': + dependencies: + '@vitest/spy': 3.2.4 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.3.3(yaml@2.8.4) + + '@vitest/pretty-format@3.2.4': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/runner@3.2.4': + dependencies: + '@vitest/utils': 3.2.4 + pathe: 2.0.3 + strip-literal: 3.1.0 + + '@vitest/snapshot@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@3.2.4': + dependencies: + tinyspy: 4.0.4 + + '@vitest/utils@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + loupe: 3.2.1 + tinyrainbow: 2.0.0 + + '@yarnpkg/lockfile@1.1.0': {} + + '@yarnpkg/parsers@3.0.2': + dependencies: + js-yaml: 3.14.2 + tslib: 2.8.1 + + '@zkochan/js-yaml@0.0.7': + dependencies: + argparse: 2.0.1 + + address@1.2.2: {} + + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.2 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-colors@4.1.3: {} + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@5.2.0: {} + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + argparse@2.0.1: {} + + assertion-error@2.0.1: {} + + async@3.2.6: {} + + asynckit@0.4.0: {} + + axios@1.16.0: + dependencies: + follow-redirects: 1.16.0 + form-data: 4.0.5 + proxy-from-env: 2.1.0 + transitivePeerDependencies: + - debug + + babel-plugin-const-enum@1.2.0(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-macros@3.1.0: + dependencies: + '@babel/runtime': 7.29.2 + cosmiconfig: 7.1.0 + resolve: 1.22.12 + + babel-plugin-polyfill-corejs2@0.4.17(@babel/core@7.29.0): + dependencies: + '@babel/compat-data': 7.29.3 + '@babel/core': 7.29.0 + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.0) + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + babel-plugin-polyfill-corejs3@0.13.0(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.0) + core-js-compat: 3.49.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-polyfill-corejs3@0.14.2(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.0) + core-js-compat: 3.49.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-polyfill-regenerator@0.6.8(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + babel-plugin-transform-typescript-metadata@0.3.2(@babel/core@7.29.0)(@babel/traverse@7.29.0): + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + optionalDependencies: + '@babel/traverse': 7.29.0 + + balanced-match@1.0.2: {} + + base64-js@1.5.1: {} + + baseline-browser-mapping@2.10.28: {} + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + + brace-expansion@2.1.0: + dependencies: + balanced-match: 1.0.2 + + browserslist@4.28.2: + dependencies: + baseline-browser-mapping: 2.10.28 + caniuse-lite: 1.0.30001792 + electron-to-chromium: 1.5.353 + node-releases: 2.0.38 + update-browserslist-db: 1.2.3(browserslist@4.28.2) + + buffer-from@1.1.2: {} + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + cac@6.7.14: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + callsites@3.1.0: {} + + caniuse-lite@1.0.30001792: {} + + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + check-error@2.1.3: {} + + cli-cursor@3.1.0: + dependencies: + restore-cursor: 3.1.0 + + cli-spinners@2.6.1: {} + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + clone@1.0.4: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + columnify@1.6.0: + dependencies: + strip-ansi: 6.0.1 + wcwidth: 1.0.1 + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + convert-source-map@2.0.0: {} + + core-js-compat@3.49.0: + dependencies: + browserslist: 4.28.2 + + cosmiconfig@7.1.0: + dependencies: + '@types/parse-json': 4.0.2 + import-fresh: 3.3.1 + parse-json: 5.2.0 + path-type: 4.0.0 + yaml: 1.10.3 + + csstype@3.2.3: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-eql@5.0.2: {} + + defaults@1.0.4: + dependencies: + clone: 1.0.4 + + define-lazy-prop@2.0.0: {} + + delayed-stream@1.0.0: {} + + detect-port@1.6.1: + dependencies: + address: 1.2.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + dotenv-expand@11.0.7: + dependencies: + dotenv: 16.4.7 + + dotenv@16.4.7: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + ejs@3.1.10: + dependencies: + jake: 10.9.4 + + electron-to-chromium@1.5.353: {} + + emoji-regex@8.0.0: {} + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + + enquirer@2.3.6: + dependencies: + ansi-colors: 4.1.3 + + error-ex@1.3.4: + dependencies: + is-arrayish: 0.2.1 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-module-lexer@1.7.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.3 + + esbuild@0.27.7: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.7 + '@esbuild/android-arm': 0.27.7 + '@esbuild/android-arm64': 0.27.7 + '@esbuild/android-x64': 0.27.7 + '@esbuild/darwin-arm64': 0.27.7 + '@esbuild/darwin-x64': 0.27.7 + '@esbuild/freebsd-arm64': 0.27.7 + '@esbuild/freebsd-x64': 0.27.7 + '@esbuild/linux-arm': 0.27.7 + '@esbuild/linux-arm64': 0.27.7 + '@esbuild/linux-ia32': 0.27.7 + '@esbuild/linux-loong64': 0.27.7 + '@esbuild/linux-mips64el': 0.27.7 + '@esbuild/linux-ppc64': 0.27.7 + '@esbuild/linux-riscv64': 0.27.7 + '@esbuild/linux-s390x': 0.27.7 + '@esbuild/linux-x64': 0.27.7 + '@esbuild/netbsd-arm64': 0.27.7 + '@esbuild/netbsd-x64': 0.27.7 + '@esbuild/openbsd-arm64': 0.27.7 + '@esbuild/openbsd-x64': 0.27.7 + '@esbuild/openharmony-arm64': 0.27.7 + '@esbuild/sunos-x64': 0.27.7 + '@esbuild/win32-arm64': 0.27.7 + '@esbuild/win32-ia32': 0.27.7 + '@esbuild/win32-x64': 0.27.7 + + escalade@3.2.0: {} + + escape-string-regexp@1.0.5: {} + + esprima@4.0.1: {} + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + esutils@2.0.3: {} + + expect-type@1.3.0: {} + + fast-deep-equal@3.1.3: {} + + fast-uri@3.1.2: {} + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + figures@3.2.0: + dependencies: + escape-string-regexp: 1.0.5 + + filelist@1.0.6: + dependencies: + minimatch: 5.1.9 + + flat@5.0.2: {} + + follow-redirects@1.16.0: {} + + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.3 + mime-types: 2.1.35 + + front-matter@4.0.2: + dependencies: + js-yaml: 3.14.2 + + fs-constants@1.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.3 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + gopd@1.2.0: {} + + has-flag@4.0.0: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.3: + dependencies: + function-bind: 1.1.2 + + hosted-git-info@7.0.2: + dependencies: + lru-cache: 10.4.3 + + ieee754@1.2.1: {} + + ignore@5.3.2: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + inherits@2.0.4: {} + + is-arrayish@0.2.1: {} + + is-core-module@2.16.2: + dependencies: + hasown: 2.0.3 + + is-docker@2.2.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-interactive@1.0.0: {} + + is-unicode-supported@0.1.0: {} + + is-wsl@2.2.0: + dependencies: + is-docker: 2.2.1 + + jake@10.9.4: + dependencies: + async: 3.2.6 + filelist: 1.0.6 + picocolors: 1.1.1 + + jest-diff@30.4.1: + dependencies: + '@jest/diff-sequences': 30.4.0 + '@jest/get-type': 30.1.0 + chalk: 4.1.2 + pretty-format: 30.4.1 + + js-tokens@4.0.0: {} + + js-tokens@9.0.1: {} + + js-yaml@3.14.2: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + + jsesc@3.1.0: {} + + json-parse-even-better-errors@2.3.1: {} + + json-schema-traverse@1.0.0: {} + + json5@2.2.3: {} + + jsonc-parser@3.2.0: {} + + lines-and-columns@1.2.4: {} + + lines-and-columns@2.0.3: {} + + lodash.debounce@4.0.8: {} + + log-symbols@4.1.0: + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + + loupe@3.2.1: {} + + lru-cache@10.4.3: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + math-intrinsics@1.1.0: {} + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mimic-fn@2.1.0: {} + + minimatch@5.1.9: + dependencies: + brace-expansion: 2.1.0 + + minimatch@9.0.3: + dependencies: + brace-expansion: 2.1.0 + + minimist@1.2.8: {} + + ms@2.1.3: {} + + nanoid@3.3.12: {} + + node-machine-id@1.1.12: {} + + node-releases@2.0.38: {} + + npm-package-arg@11.0.1: + dependencies: + hosted-git-info: 7.0.2 + proc-log: 3.0.0 + semver: 7.8.0 + validate-npm-package-name: 5.0.1 + + npm-run-path@4.0.1: + dependencies: + path-key: 3.1.1 + + nx@21.6.11: + dependencies: + '@napi-rs/wasm-runtime': 0.2.4 + '@yarnpkg/lockfile': 1.1.0 + '@yarnpkg/parsers': 3.0.2 + '@zkochan/js-yaml': 0.0.7 + axios: 1.16.0 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-spinners: 2.6.1 + cliui: 8.0.1 + dotenv: 16.4.7 + dotenv-expand: 11.0.7 + enquirer: 2.3.6 + figures: 3.2.0 + flat: 5.0.2 + front-matter: 4.0.2 + ignore: 5.3.2 + jest-diff: 30.4.1 + jsonc-parser: 3.2.0 + lines-and-columns: 2.0.3 + minimatch: 9.0.3 + node-machine-id: 1.1.12 + npm-run-path: 4.0.1 + open: 8.4.2 + ora: 5.3.0 + resolve.exports: 2.0.3 + semver: 7.8.0 + string-width: 4.2.3 + tar-stream: 2.2.0 + tmp: 0.2.5 + tree-kill: 1.2.2 + tsconfig-paths: 4.2.0 + tslib: 2.8.1 + yaml: 2.8.4 + yargs: 17.7.2 + yargs-parser: 21.1.1 + optionalDependencies: + '@nx/nx-darwin-arm64': 21.6.11 + '@nx/nx-darwin-x64': 21.6.11 + '@nx/nx-freebsd-x64': 21.6.11 + '@nx/nx-linux-arm-gnueabihf': 21.6.11 + '@nx/nx-linux-arm64-gnu': 21.6.11 + '@nx/nx-linux-arm64-musl': 21.6.11 + '@nx/nx-linux-x64-gnu': 21.6.11 + '@nx/nx-linux-x64-musl': 21.6.11 + '@nx/nx-win32-arm64-msvc': 21.6.11 + '@nx/nx-win32-x64-msvc': 21.6.11 + transitivePeerDependencies: + - debug + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + open@8.4.2: + dependencies: + define-lazy-prop: 2.0.0 + is-docker: 2.2.1 + is-wsl: 2.2.0 + + ora@5.3.0: + dependencies: + bl: 4.1.0 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-spinners: 2.6.1 + is-interactive: 1.0.0 + log-symbols: 4.1.0 + strip-ansi: 6.0.1 + wcwidth: 1.0.1 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.29.0 + error-ex: 1.3.4 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + path-type@4.0.0: {} + + pathe@2.0.3: {} + + pathval@2.0.1: {} + + picocolors@1.1.1: {} + + picomatch@4.0.2: {} + + picomatch@4.0.4: {} + + postcss@8.5.14: + dependencies: + nanoid: 3.3.12 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + pretty-format@30.4.1: + dependencies: + '@jest/schemas': 30.4.1 + ansi-styles: 5.2.0 + react-is-18: react-is@18.3.1 + react-is-19: react-is@19.2.6 + + proc-log@3.0.0: {} + + proxy-from-env@2.1.0: {} + + react-dom@19.2.6(react@19.2.6): + dependencies: + react: 19.2.6 + scheduler: 0.27.0 + + react-is@18.3.1: {} + + react-is@19.2.6: {} + + react-refresh@0.18.0: {} + + react@19.2.6: {} + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + regenerate-unicode-properties@10.2.2: + dependencies: + regenerate: 1.4.2 + + regenerate@1.4.2: {} + + regexpu-core@6.4.0: + dependencies: + regenerate: 1.4.2 + regenerate-unicode-properties: 10.2.2 + regjsgen: 0.8.0 + regjsparser: 0.13.1 + unicode-match-property-ecmascript: 2.0.0 + unicode-match-property-value-ecmascript: 2.2.1 + + regjsgen@0.8.0: {} + + regjsparser@0.13.1: + dependencies: + jsesc: 3.1.0 + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + resolve-from@4.0.0: {} + + resolve.exports@2.0.3: {} + + resolve@1.22.12: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.16.2 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + restore-cursor@3.1.0: + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + + rollup@4.60.3: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.60.3 + '@rollup/rollup-android-arm64': 4.60.3 + '@rollup/rollup-darwin-arm64': 4.60.3 + '@rollup/rollup-darwin-x64': 4.60.3 + '@rollup/rollup-freebsd-arm64': 4.60.3 + '@rollup/rollup-freebsd-x64': 4.60.3 + '@rollup/rollup-linux-arm-gnueabihf': 4.60.3 + '@rollup/rollup-linux-arm-musleabihf': 4.60.3 + '@rollup/rollup-linux-arm64-gnu': 4.60.3 + '@rollup/rollup-linux-arm64-musl': 4.60.3 + '@rollup/rollup-linux-loong64-gnu': 4.60.3 + '@rollup/rollup-linux-loong64-musl': 4.60.3 + '@rollup/rollup-linux-ppc64-gnu': 4.60.3 + '@rollup/rollup-linux-ppc64-musl': 4.60.3 + '@rollup/rollup-linux-riscv64-gnu': 4.60.3 + '@rollup/rollup-linux-riscv64-musl': 4.60.3 + '@rollup/rollup-linux-s390x-gnu': 4.60.3 + '@rollup/rollup-linux-x64-gnu': 4.60.3 + '@rollup/rollup-linux-x64-musl': 4.60.3 + '@rollup/rollup-openbsd-x64': 4.60.3 + '@rollup/rollup-openharmony-arm64': 4.60.3 + '@rollup/rollup-win32-arm64-msvc': 4.60.3 + '@rollup/rollup-win32-ia32-msvc': 4.60.3 + '@rollup/rollup-win32-x64-gnu': 4.60.3 + '@rollup/rollup-win32-x64-msvc': 4.60.3 + fsevents: 2.3.3 + + safe-buffer@5.2.1: {} + + scheduler@0.27.0: {} + + semver@6.3.1: {} + + semver@7.8.0: {} + + siginfo@2.0.0: {} + + signal-exit@3.0.7: {} + + source-map-js@1.2.1: {} + + source-map-support@0.5.19: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + sprintf-js@1.0.3: {} + + stackback@0.0.2: {} + + std-env@3.10.0: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-bom@3.0.0: {} + + strip-literal@3.1.0: + dependencies: + js-tokens: 9.0.1 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.5 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinyglobby@0.2.16: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + tinypool@1.1.1: {} + + tinyrainbow@2.0.0: {} + + tinyspy@4.0.4: {} + + tmp@0.2.5: {} + + tree-kill@1.2.2: {} + + tsconfig-paths@4.2.0: + dependencies: + json5: 2.2.3 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tslib@2.8.1: {} + + typescript@5.9.3: {} + + unicode-canonical-property-names-ecmascript@2.0.1: {} + + unicode-match-property-ecmascript@2.0.0: + dependencies: + unicode-canonical-property-names-ecmascript: 2.0.1 + unicode-property-aliases-ecmascript: 2.2.0 + + unicode-match-property-value-ecmascript@2.2.1: {} + + unicode-property-aliases-ecmascript@2.2.0: {} + + update-browserslist-db@1.2.3(browserslist@4.28.2): + dependencies: + browserslist: 4.28.2 + escalade: 3.2.0 + picocolors: 1.1.1 + + util-deprecate@1.0.2: {} + + validate-npm-package-name@5.0.1: {} + + vite-node@3.2.4(yaml@2.8.4): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 7.3.3(yaml@2.8.4) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vite@7.3.3(yaml@2.8.4): + dependencies: + esbuild: 0.27.7 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + postcss: 8.5.14 + rollup: 4.60.3 + tinyglobby: 0.2.16 + optionalDependencies: + fsevents: 2.3.3 + yaml: 2.8.4 + + vitest@3.2.4(yaml@2.8.4): + dependencies: + '@types/chai': 5.2.3 + '@vitest/expect': 3.2.4 + '@vitest/mocker': 3.2.4(vite@7.3.3(yaml@2.8.4)) + '@vitest/pretty-format': 3.2.4 + '@vitest/runner': 3.2.4 + '@vitest/snapshot': 3.2.4 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.3.0 + magic-string: 0.30.21 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.16 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 7.3.3(yaml@2.8.4) + vite-node: 3.2.4(yaml@2.8.4) + why-is-node-running: 2.3.0 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + wcwidth@1.0.1: + dependencies: + defaults: 1.0.4 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrappy@1.0.2: {} + + y18n@5.0.8: {} + + yallist@3.1.1: {} + + yaml@1.10.3: {} + + yaml@2.8.4: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..213202c --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +packages: + - apps/web + - packages/* diff --git a/scripts/create-database.sh b/scripts/create-database.sh new file mode 100755 index 0000000..3a33ba6 --- /dev/null +++ b/scripts/create-database.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +set -euo pipefail + +CONTAINER="${AI_GATEWAY_PG_CONTAINER:-easyai-pgvector}" +PGUSER="${AI_GATEWAY_PG_USER:-easyai}" +DB_NAME="${AI_GATEWAY_DATABASE_NAME:-easyai_ai_gateway}" + +exists="$( + docker exec "$CONTAINER" \ + psql -U "$PGUSER" -d postgres -tAc "SELECT 1 FROM pg_database WHERE datname='${DB_NAME}'" \ + | tr -d '[:space:]' +)" + +if [[ "$exists" == "1" ]]; then + echo "[ai-gateway] database already exists: ${DB_NAME}" + exit 0 +fi + +docker exec "$CONTAINER" \ + psql -U "$PGUSER" -d postgres -c "CREATE DATABASE \"${DB_NAME}\"" + +echo "[ai-gateway] database created: ${DB_NAME}" diff --git a/scripts/dev.sh b/scripts/dev.sh new file mode 100755 index 0000000..5cddc77 --- /dev/null +++ b/scripts/dev.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +set -euo pipefail + +export AI_GATEWAY_PG_CONTAINER="${AI_GATEWAY_PG_CONTAINER:-easyai-pgvector}" +export AI_GATEWAY_PG_USER="${AI_GATEWAY_PG_USER:-easyai}" +export AI_GATEWAY_DATABASE_NAME="${AI_GATEWAY_DATABASE_NAME:-easyai_ai_gateway}" +export AI_GATEWAY_DATABASE_URL="${AI_GATEWAY_DATABASE_URL:-postgresql://easyai:easyai2025@localhost:5432/easyai_ai_gateway?sslmode=disable}" + +echo "[ai-gateway] using database: ${AI_GATEWAY_DATABASE_URL}" + +scripts/create-database.sh +pnpm nx run api:migrate +exec pnpm nx run-many -t dev -p api web --parallel=2