diff --git a/.gitignore b/.gitignore index d031038..53fdeed 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ frontend/out/ # Go backend/bin/ +/node_modules/ diff --git a/README.md b/README.md index 10e0341..d22cb1e 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Go 实现的 OpenAI 兼容 API 网关(多租户 API Key、额度、兑换/邀请、渠道路由)。本仓库为 **monorepo**:**`backend/`** 为网关服务,**`frontend/`** 为管理控制台(Next.js + Ant Design)。 -设计说明见 `docs/` 与 OpenSpec:**后端** `openspec/changes/gateway-foundation-invite-billing/`;**管理控制台前端提案** `openspec/changes/gateway-admin-console-frontend/`。 +设计说明见 `docs/` 与 OpenSpec:**后端** `openspec/changes/gateway-foundation-invite-billing/`;**管理控制台前端** `openspec/changes/gateway-admin-console-frontend/`;**租户门户(蓝移类产品)** 已归档至 `openspec/changes/archive/2026-05-27-gateway-tenant-portal-platform/`,现行规格见 `openspec/specs/portal-*/`(需求来源:`zhencai/lanyiapi-site-audit/IMPLEMENTATION_PROMPT.md`)。 ## 目录结构 @@ -10,17 +10,23 @@ Go 实现的 OpenAI 兼容 API 网关(多租户 API Key、额度、兑换/邀 |------|------| | `backend/` | Go 网关:`go.mod`、`cmd/server`、`internal/`、`migrations/` | | `frontend/` | 管理控制台(`npm run dev`,详见 `frontend/README.md`) | +| `portal/` | 租户门户 Semi UI(`npm run dev`,默认端口 3001,详见 `portal/README.md`) | | `docker-compose.yml` | 本地 Postgres + Redis(仓库根,与后端 `.env` 配合) | | `openspec/` | 变更提案与规格 | ## 运行(后端) 1. 在仓库根执行:`docker compose up -d`(Postgres + Redis,若已自备可跳过) -2. 应用迁移:`backend/migrations/001_init.sql` +2. 应用迁移:`backend/migrations/001_init.sql`;门户另需 `003`–`006` 迁移脚本(见 `portal/README.md`) 3. 若启用消费返利:追加 `backend/migrations/002_rebate_tasks.sql`,并设置 `REBATE_ENABLED=true`、`REBATE_BPS`(万分比,如 100=1%) 4. 复制 `backend/.env.example` 为 `backend/.env` 并按需填写 5. `cd backend && go run ./cmd/server` +## 运行(租户门户) + +1. `cd portal && cp .env.example .env.local`,设置 `NEXT_PUBLIC_GATEWAY_API_URL` +2. `npm install && npm run dev` → [http://localhost:3001](http://localhost:3001) + ## 运行(管理控制台) 1. `cd frontend && cp .env.example .env.local`,设置 `NEXT_PUBLIC_GATEWAY_API_URL`(与网关监听地址一致,无尾斜杠) @@ -46,3 +52,5 @@ Go 实现的 OpenAI 兼容 API 网关(多租户 API Key、额度、兑换/邀 归档前在仓库根目录执行: `npx @fission-ai/openspec@latest status --change gateway-foundation-invite-billing` + +租户门户 change 已于 2026-05-27 归档(`openspec/changes/archive/2026-05-27-gateway-tenant-portal-platform/`)。现行能力规格:`openspec/specs/portal-public-site/` 等 10 个 `portal-*` spec。 diff --git a/backend/.env.example b/backend/.env.example index 189a6f0..b0fa58c 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -21,3 +21,10 @@ QUOTA_RECONCILE_INTERVAL_SEC=120 # REBATE_ENABLED=false # REBATE_BPS=0 # REBATE_POLL_SEC=10 + +# Portal wallet / invite +PORTAL_ORIGIN=http://localhost:3001 +# JSON: [{"name":"主站","url":"http://localhost:8080","region":"local"},...] +PORTAL_API_NODES_JSON= +AFFILIATE_RECHARGE_BPS=1000 +# RECHARGE_ENABLED=true diff --git a/backend/go.mod b/backend/go.mod index 4403e78..79f1a14 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -30,6 +30,7 @@ require ( github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.20.0 // indirect github.com/goccy/go-json v0.10.2 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect diff --git a/backend/go.sum b/backend/go.sum index 529fc33..8739884 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -45,6 +45,8 @@ github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVI github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go index 8c8d8fb..159a87f 100644 --- a/backend/internal/config/config.go +++ b/backend/internal/config/config.go @@ -26,6 +26,14 @@ type Config struct { RebateBPS int // RebatePollSec is worker poll interval when RebateEnabled; 0 uses 10. RebatePollSec int + // PortalAPINodesJSON is JSON array of {name,url,region} for dashboard node card. + PortalAPINodesJSON string + // RechargeEnabled allows mock online recharge API (dev only by default). + RechargeEnabled bool + // PortalOrigin is used to build invite URLs in wallet API. + PortalOrigin string + // AffiliateRechargeBPS is inviter share on invitee recharge (e.g. 1000 = 10%). + AffiliateRechargeBPS int } func Load() (*Config, error) { @@ -43,6 +51,10 @@ func Load() (*Config, error) { viper.SetDefault("REBATE_ENABLED", false) viper.SetDefault("REBATE_BPS", 0) viper.SetDefault("REBATE_POLL_SEC", 10) + viper.SetDefault("PORTAL_API_NODES_JSON", "") + viper.SetDefault("RECHARGE_ENABLED", false) + viper.SetDefault("PORTAL_ORIGIN", "http://localhost:3001") + viper.SetDefault("AFFILIATE_RECHARGE_BPS", 1000) _ = viper.ReadInConfig() return &Config{ @@ -59,5 +71,9 @@ func Load() (*Config, error) { RebateEnabled: viper.GetBool("REBATE_ENABLED"), RebateBPS: viper.GetInt("REBATE_BPS"), RebatePollSec: viper.GetInt("REBATE_POLL_SEC"), + PortalAPINodesJSON: viper.GetString("PORTAL_API_NODES_JSON"), + RechargeEnabled: viper.GetBool("RECHARGE_ENABLED"), + PortalOrigin: strings.TrimRight(viper.GetString("PORTAL_ORIGIN"), "/"), + AffiliateRechargeBPS: viper.GetInt("AFFILIATE_RECHARGE_BPS"), }, nil } diff --git a/backend/internal/handler/announcement.go b/backend/internal/handler/announcement.go new file mode 100644 index 0000000..f531c89 --- /dev/null +++ b/backend/internal/handler/announcement.go @@ -0,0 +1,40 @@ +package handler + +import ( + "net/http" + "time" + + "github.com/gin-gonic/gin" + "gorm.io/gorm" + + "github.com/leno23/ai-api-gateway/internal/model" +) + +type AnnouncementDeps struct { + DB *gorm.DB +} + +func ListAnnouncements(deps AnnouncementDeps) gin.HandlerFunc { + return func(c *gin.Context) { + placement := c.DefaultQuery("placement", "home") + now := time.Now() + var rows []model.Announcement + q := deps.DB.Where("status = ? AND placement = ?", 1, placement) + q = q.Where("(starts_at IS NULL OR starts_at <= ?)", now). + Where("(ends_at IS NULL OR ends_at >= ?)", now) + if err := q.Order("id desc").Limit(20).Find(&rows).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "announcements"}) + return + } + items := make([]gin.H, 0, len(rows)) + for _, a := range rows { + items = append(items, gin.H{ + "id": a.ID, + "title": a.Title, + "content": a.Content, + "level": a.Level, + }) + } + c.JSON(http.StatusOK, gin.H{"success": true, "data": items}) + } +} diff --git a/backend/internal/handler/auth.go b/backend/internal/handler/auth.go index bcdd5df..2041763 100644 --- a/backend/internal/handler/auth.go +++ b/backend/internal/handler/auth.go @@ -3,6 +3,7 @@ package handler import ( "errors" "net/http" + "strings" "time" "github.com/gin-gonic/gin" @@ -136,8 +137,16 @@ func Register(deps AuthDeps) gin.HandlerFunc { } type loginReq struct { - Email string `json:"email" binding:"required,email"` - Password string `json:"password" binding:"required"` + Email string `json:"email"` + Username string `json:"username"` + Password string `json:"password" binding:"required"` +} + +func loginIdentifier(req loginReq) string { + if req.Email != "" { + return req.Email + } + return req.Username } func Login(deps AuthDeps) gin.HandlerFunc { @@ -147,8 +156,17 @@ func Login(deps AuthDeps) gin.HandlerFunc { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } + id := loginIdentifier(req) + if id == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "email or username required"}) + return + } var u model.User - if err := deps.DB.Where("email = ?", req.Email).First(&u).Error; err != nil { + q := deps.DB.Where("email = ?", id) + if !strings.Contains(id, "@") { + q = deps.DB.Where("username = ?", id) + } + if err := q.First(&u).Error; err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid credentials"}) return } @@ -165,8 +183,69 @@ func Login(deps AuthDeps) gin.HandlerFunc { c.JSON(http.StatusInternalServerError, gin.H{"error": "token"}) return } - c.JSON(http.StatusOK, gin.H{"access_token": tok, "token_type": "Bearer", "expires_in": int(jwtTTL.Seconds())}) + groupSlug := userTokenGroupSlug(deps.DB, &u) + c.JSON(http.StatusOK, gin.H{ + "access_token": tok, + "token_type": "Bearer", + "expires_in": int(jwtTTL.Seconds()), + "success": true, + "data": gin.H{ + "id": u.ID, + "username": u.Username, + "display_name": u.Username, + "email": u.Email, + "role": u.Role, + "group": groupSlug, + "status": u.Status, + }, + }) + } +} + +func UserSelf(deps AuthDeps) gin.HandlerFunc { + return func(c *gin.Context) { + uid, ok := c.Get(middleware.CtxUserID) + if !ok { + c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) + return + } + var u model.User + if err := deps.DB.First(&u, uid.(int64)).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "user not found"}) + return + } + groupSlug := userTokenGroupSlug(deps.DB, &u) + c.JSON(http.StatusOK, gin.H{ + "success": true, + "data": gin.H{ + "id": u.ID, + "username": u.Username, + "display_name": u.Username, + "email": u.Email, + "role": u.Role, + "group": groupSlug, + "status": u.Status, + "quota": u.Quota, + "used_quota": u.UsedQuota, + "invite_code": u.InviteCode, + "affiliate_pending": u.AffiliatePending, + }, + }) + } +} + +func userTokenGroupSlug(db *gorm.DB, u *model.User) string { + if u.TokenGroupID != nil { + var g model.TokenGroup + if err := db.First(&g, *u.TokenGroupID).Error; err == nil { + return g.Slug + } + } + var g model.TokenGroup + if err := db.Where("slug = ?", "default").First(&g).Error; err == nil { + return g.Slug } + return "default" } type createKeyReq struct { diff --git a/backend/internal/handler/catalog.go b/backend/internal/handler/catalog.go new file mode 100644 index 0000000..e72b7ab --- /dev/null +++ b/backend/internal/handler/catalog.go @@ -0,0 +1,147 @@ +package handler + +import ( + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "gorm.io/gorm" + + "github.com/leno23/ai-api-gateway/internal/repository" + "github.com/leno23/ai-api-gateway/internal/service" +) + +type CatalogDeps struct { + Repos *repository.Repos +} + +func ListTokenGroups(deps CatalogDeps) gin.HandlerFunc { + return func(c *gin.Context) { + rows, err := deps.Repos.ListTokenGroups() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "token groups"}) + return + } + items := make([]gin.H, 0, len(rows)) + for _, g := range rows { + items = append(items, gin.H{ + "slug": g.Slug, + "name": g.Name, + "multiplier": g.Multiplier, + }) + } + c.JSON(http.StatusOK, gin.H{"success": true, "data": items}) + } +} + +func ListCatalogModels(deps CatalogDeps) gin.HandlerFunc { + return func(c *gin.Context) { + page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) + pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) + var billingType *int16 + if bt := c.Query("billing_type"); bt != "" { + if v, err := strconv.ParseInt(bt, 10, 16); err == nil { + v16 := int16(v) + billingType = &v16 + } + } + filter := repository.CatalogFilter{ + Provider: c.Query("provider"), + EndpointType: c.Query("endpoint_type"), + BillingType: billingType, + Tag: c.Query("tag"), + Query: c.Query("q"), + Page: page, + PageSize: pageSize, + } + rows, total, err := deps.Repos.ListCatalogModels(filter) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "models"}) + return + } + groupSlug := c.DefaultQuery("token_group", "default") + multiplier := 1.0 + if g, err := deps.Repos.TokenGroupBySlug(groupSlug); err == nil { + multiplier = g.Multiplier + } + items := make([]gin.H, 0, len(rows)) + for _, r := range rows { + name := r.DisplayName + if name == "" { + name = r.Model + } + prices := service.ModelPriceBreakdownFrom(r, 1) + pricesWithMult := service.ModelPriceBreakdownFrom(r, multiplier) + items = append(items, gin.H{ + "model": r.Model, + "display_name": name, + "provider": r.Provider, + "endpoint_type": r.EndpointType, + "billing_type": r.BillingType, + "billing_label": service.BillingLabel(r.BillingType), + "tags": []string(r.Tags), + "prices": prices, + "prices_applied": pricesWithMult, + }) + } + providers, _ := deps.Repos.DistinctProviders() + groups, _ := deps.Repos.ListTokenGroups() + groupItems := make([]gin.H, 0, len(groups)) + for _, g := range groups { + groupItems = append(groupItems, gin.H{ + "slug": g.Slug, + "name": g.Name, + "multiplier": g.Multiplier, + }) + } + c.JSON(http.StatusOK, gin.H{ + "success": true, + "data": gin.H{ + "items": items, + "total": total, + "page": page, + "page_size": pageSize, + "multiplier": multiplier, + "token_group": groupSlug, + }, + "meta": gin.H{ + "providers": providers, + "token_groups": groupItems, + }, + }) + } +} + +func GetModelPrice(deps CatalogDeps) gin.HandlerFunc { + return func(c *gin.Context) { + modelName := c.Param("model") + if modelName == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "model required"}) + return + } + mp, err := deps.Repos.ModelPriceByName(modelName) + if err != nil { + if err == gorm.ErrRecordNotFound { + c.JSON(http.StatusNotFound, gin.H{"error": "model not found"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": "lookup"}) + return + } + groupSlug := c.DefaultQuery("token_group", "default") + multiplier := 1.0 + if g, err := deps.Repos.TokenGroupBySlug(groupSlug); err == nil { + multiplier = g.Multiplier + } + c.JSON(http.StatusOK, gin.H{ + "success": true, + "data": gin.H{ + "model": mp.Model, + "display_name": mp.DisplayName, + "base": service.ModelPriceBreakdownFrom(*mp, 1), + "with_multiplier": service.ModelPriceBreakdownFrom(*mp, multiplier), + "token_group": groupSlug, + }, + }) + } +} diff --git a/backend/internal/handler/dashboard.go b/backend/internal/handler/dashboard.go new file mode 100644 index 0000000..6df0910 --- /dev/null +++ b/backend/internal/handler/dashboard.go @@ -0,0 +1,233 @@ +package handler + +import ( + "encoding/json" + "net/http" + "strconv" + "time" + + "github.com/gin-gonic/gin" + + "github.com/leno23/ai-api-gateway/internal/model" + "github.com/leno23/ai-api-gateway/internal/repository" +) + +type DashboardDeps struct { + Repos *repository.Repos + Nodes []PortalNode +} + +type PortalNode struct { + Name string `json:"name"` + URL string `json:"url"` + Region string `json:"region"` +} + +func DashboardStats(deps DashboardDeps) gin.HandlerFunc { + return func(c *gin.Context) { + userID, ok := ctxUserID(c) + if !ok { + c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) + return + } + data, err := deps.Repos.DashboardStats(userID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "stats"}) + return + } + c.JSON(http.StatusOK, gin.H{"success": true, "data": data}) + } +} + +func DashboardCharts(deps DashboardDeps) gin.HandlerFunc { + return func(c *gin.Context) { + userID, ok := ctxUserID(c) + if !ok { + c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) + return + } + rangeKey := c.DefaultQuery("range", "7d") + data, err := deps.Repos.DashboardCharts(userID, rangeKey) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "charts"}) + return + } + c.JSON(http.StatusOK, gin.H{"success": true, "data": data}) + } +} + +func DashboardNodes(deps DashboardDeps) gin.HandlerFunc { + return func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"success": true, "data": deps.Nodes}) + } +} + +func ListUsageLogs(deps DashboardDeps) gin.HandlerFunc { + return func(c *gin.Context) { + userID, ok := ctxUserID(c) + if !ok { + c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) + return + } + f := usageLogFilterFromQuery(c, userID) + rows, total, err := deps.Repos.ListUsageLogs(f) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "logs"}) + return + } + items := make([]gin.H, 0, len(rows)) + for _, row := range rows { + items = append(items, serializeUsageLog(row)) + } + c.JSON(http.StatusOK, gin.H{ + "success": true, + "data": gin.H{ + "items": items, + "total": total, + "page": f.Page, + "page_size": f.PageSize, + }, + }) + } +} + +func ListTaskLogs(deps DashboardDeps) gin.HandlerFunc { + return func(c *gin.Context) { + userID, ok := ctxUserID(c) + if !ok { + c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) + return + } + f := taskLogFilterFromQuery(c, userID) + rows, total, err := deps.Repos.ListTaskLogs(f) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "tasks"}) + return + } + items := make([]gin.H, 0, len(rows)) + for _, row := range rows { + items = append(items, serializeTaskLog(row)) + } + c.JSON(http.StatusOK, gin.H{ + "success": true, + "data": gin.H{ + "items": items, + "total": total, + "page": f.Page, + "page_size": f.PageSize, + }, + }) + } +} + +func serializeUsageLog(row model.RequestLog) gin.H { + var billing any + if len(row.BillingDetail) > 0 { + _ = json.Unmarshal(row.BillingDetail, &billing) + } + ttft := any(nil) + if row.TimeToFirstMs != nil { + ttft = *row.TimeToFirstMs + } + latency := any(nil) + if row.LatencyMs != nil { + latency = *row.LatencyMs + } + status := any(nil) + if row.StatusCode != nil { + status = *row.StatusCode + } + return gin.H{ + "id": row.ID, + "request_id": row.RequestID, + "token_name": row.TokenName, + "token_group": row.TokenGroup, + "model": row.Model, + "request_path": row.RequestPath, + "prompt_tokens": row.PromptTokens, + "completion_tokens": row.CompletionTokens, + "total_tokens": row.TotalTokens, + "cost_quota": row.CostQuota, + "latency_ms": latency, + "time_to_first_ms": ttft, + "billing_detail": billing, + "status_code": status, + "error_message": row.ErrorMessage, + "created_at": row.CreatedAt, + } +} + +func serializeTaskLog(row model.TaskLog) gin.H { + durationMs := any(nil) + if row.FinishedAt != nil { + d := row.FinishedAt.Sub(row.SubmittedAt).Milliseconds() + durationMs = d + } + return gin.H{ + "id": row.ID, + "task_id": row.TaskID, + "platform": row.Platform, + "type": row.TaskType, + "status": row.Status, + "progress": row.Progress, + "detail": row.Detail, + "submitted_at": row.SubmittedAt, + "finished_at": row.FinishedAt, + "duration_ms": durationMs, + } +} + +func usageLogFilterFromQuery(c *gin.Context, userID int64) repository.UsageLogFilter { + page, pageSize := pageFromQuery(c) + return repository.UsageLogFilter{ + UserID: userID, + Start: parseTimeQuery(c.Query("start")), + End: parseTimeQuery(c.Query("end")), + TokenName: c.Query("token_name"), + Model: c.Query("model"), + RequestID: c.Query("request_id"), + TokenGroup: c.Query("token_group"), + Page: page, + PageSize: pageSize, + } +} + +func taskLogFilterFromQuery(c *gin.Context, userID int64) repository.TaskLogFilter { + page, pageSize := pageFromQuery(c) + return repository.TaskLogFilter{ + UserID: userID, + Start: parseTimeQuery(c.Query("start")), + End: parseTimeQuery(c.Query("end")), + TaskID: c.Query("task_id"), + Page: page, + PageSize: pageSize, + } +} + +func parseTimeQuery(v string) *time.Time { + if v == "" { + return nil + } + if t, err := time.Parse(time.RFC3339, v); err == nil { + return &t + } + if t, err := time.Parse("2006-01-02", v); err == nil { + return &t + } + return nil +} + +func pageFromQuery(c *gin.Context) (int, int) { + page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) + pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10")) + if page < 1 { + page = 1 + } + if pageSize < 1 { + pageSize = 10 + } + if pageSize > 100 { + pageSize = 100 + } + return page, pageSize +} diff --git a/backend/internal/handler/gateway.go b/backend/internal/handler/gateway.go index 98d970b..e90c541 100644 --- a/backend/internal/handler/gateway.go +++ b/backend/internal/handler/gateway.go @@ -9,6 +9,7 @@ import ( "time" "github.com/gin-gonic/gin" + "github.com/google/uuid" "github.com/redis/go-redis/v9" "go.uber.org/zap" "gorm.io/gorm" @@ -106,8 +107,11 @@ func ChatCompletions(deps GatewayDeps) gin.HandlerFunc { } uid, _ := c.Get(middleware.CtxUserID) userID := uid.(int64) - apiKeyIDVal, _ := c.Get(middleware.CtxAPIKeyID) - apiKeyID := apiKeyIDVal.(int64) + apiKeyID := int64(0) + if apiKeyIDVal, ok := c.Get(middleware.CtxAPIKeyID); ok { + apiKeyID, _ = apiKeyIDVal.(int64) + } + tokenNameOverride := logTokenNameOverride(c) targets, err := routing.BuildUpstreamTargets(deps.Repos, deps.UpstreamBase, deps.UpstreamKey, req.Model) if err != nil { @@ -235,7 +239,7 @@ func ChatCompletions(deps GatewayDeps) gin.HandlerFunc { return } metrics.ChatCompletionsLatency.WithLabelValues(streamLabel).Observe(time.Since(start).Seconds()) - if err := settleStream(ctx, deps, userID, apiKeyID, picked.ChannelID, req.Model, estimate, maxOut, promptTok, lat, resp.StatusCode); err != nil { + if err := settleStream(ctx, deps, userID, apiKeyID, picked.ChannelID, req.Model, estimate, maxOut, promptTok, lat, resp.StatusCode, tokenNameOverride); err != nil { deps.Log.Warn("settle stream", zap.Error(err)) } return @@ -275,7 +279,7 @@ func ChatCompletions(deps GatewayDeps) gin.HandlerFunc { _ = service.RefundQuota(ctx, deps.RDB, userID, diff) } - if err := persistUsage(ctx, deps, userID, apiKeyID, picked.ChannelID, req.Model, actual, int(inTok), int(outTok), lat, resp.StatusCode, ""); err != nil { + if err := persistUsage(ctx, deps, userID, apiKeyID, picked.ChannelID, req.Model, actual, int(inTok), int(outTok), lat, resp.StatusCode, "", tokenNameOverride); err != nil { deps.Log.Warn("persist", zap.Error(err)) } c.Status(resp.StatusCode) @@ -326,6 +330,7 @@ func settleStream( modelName string, estimate, maxOut, promptTok int64, latencyMs, status int, + tokenNameOverride string, ) error { actual := estimate if err := deps.DB.Transaction(func(tx *gorm.DB) error { @@ -336,6 +341,12 @@ func settleStream( }).Error; err != nil { return err } + if apiKeyID > 0 { + if err := tx.Model(&model.APIKey{}).Where("id = ?", apiKeyID). + UpdateColumn("used_quota", gorm.Expr("used_quota + ?", actual)).Error; err != nil { + return err + } + } var q int64 if err := tx.Model(&model.User{}).Select("quota").Where("id = ?", userID).Scan(&q).Error; err != nil { return err @@ -363,7 +374,7 @@ func settleStream( tt := pt + co rec := &model.RequestLog{ UserID: userID, - APIKeyID: &apiKeyID, + APIKeyID: apiKeyIDPtr(apiKeyID), ChannelID: channelID, Model: modelName, RequestMethod: http.MethodPost, @@ -375,6 +386,7 @@ func settleStream( LatencyMs: &l, StatusCode: &sc, } + enrichRequestLogMeta(deps.DB, rec, apiKeyID, modelName, actual, tokenNameOverride) audit.SubmitRequestLog(deps.DB, deps.Log, rec) rebate.EnqueueConsumeRebate(deps.DB, deps.Log, userID, actual, rebate.RefChat(modelName), deps.RebateEnabled, deps.RebateBPS) return nil @@ -389,6 +401,7 @@ func persistUsage( actual int64, inTok, outTok, latencyMs, status int, errMsg string, + tokenNameOverride string, ) error { err := deps.DB.Transaction(func(tx *gorm.DB) error { if err := tx.Model(&model.User{}).Where("id = ? AND quota >= ?", userID, actual). @@ -398,6 +411,12 @@ func persistUsage( }).Error; err != nil { return err } + if apiKeyID > 0 { + if err := tx.Model(&model.APIKey{}).Where("id = ?", apiKeyID). + UpdateColumn("used_quota", gorm.Expr("used_quota + ?", actual)).Error; err != nil { + return err + } + } var q int64 if err := tx.Model(&model.User{}).Select("quota").Where("id = ?", userID).Scan(&q).Error; err != nil { return err @@ -424,7 +443,7 @@ func persistUsage( tt := inTok + outTok rec := &model.RequestLog{ UserID: userID, - APIKeyID: &apiKeyID, + APIKeyID: apiKeyIDPtr(apiKeyID), ChannelID: channelID, Model: modelName, RequestMethod: http.MethodPost, @@ -437,11 +456,46 @@ func persistUsage( StatusCode: &sc, ErrorMessage: errMsg, } + enrichRequestLogMeta(deps.DB, rec, apiKeyID, modelName, actual, tokenNameOverride) audit.SubmitRequestLog(deps.DB, deps.Log, rec) rebate.EnqueueConsumeRebate(deps.DB, deps.Log, userID, actual, rebate.RefChat(modelName), deps.RebateEnabled, deps.RebateBPS) return nil } +func enrichRequestLogMeta(db *gorm.DB, rec *model.RequestLog, apiKeyID int64, modelName string, cost int64, tokenNameOverride string) { + rec.RequestID = uuid.NewString() + if tokenNameOverride != "" { + rec.TokenName = tokenNameOverride + rec.TokenGroup = "default" + } else if apiKeyID > 0 { + var key model.APIKey + if err := db.First(&key, apiKeyID).Error; err == nil { + rec.TokenName = key.Name + rec.TokenGroup = apiKeyGroupSlug(db, key.TokenGroupID) + } + } + detail, _ := json.Marshal(gin.H{ + "model": modelName, + "cost_quota": cost, + "type": "usage", + }) + rec.BillingDetail = detail + if rec.LatencyMs != nil && rec.TimeToFirstMs == nil { + rec.TimeToFirstMs = rec.LatencyMs + } +} + +func apiKeyGroupSlug(db *gorm.DB, groupID *int64) string { + if groupID == nil { + return "default" + } + var g model.TokenGroup + if err := db.First(&g, *groupID).Error; err != nil { + return "default" + } + return g.Slug +} + func Placeholder(name string) gin.HandlerFunc { return func(c *gin.Context) { c.JSON(http.StatusNotImplemented, gin.H{"error": name + " not implemented"}) diff --git a/backend/internal/handler/nodes_ping.go b/backend/internal/handler/nodes_ping.go new file mode 100644 index 0000000..c4e6a9b --- /dev/null +++ b/backend/internal/handler/nodes_ping.go @@ -0,0 +1,62 @@ +package handler + +import ( + "net/http" + "strconv" + "strings" + "time" + + "github.com/gin-gonic/gin" +) + +type NodePingResult struct { + Name string `json:"name"` + URL string `json:"url"` + Region string `json:"region"` + OK bool `json:"ok"` + LatencyMs int64 `json:"latency_ms"` + Status string `json:"status,omitempty"` +} + +func NodesPing(deps DashboardDeps) gin.HandlerFunc { + client := &http.Client{Timeout: 8 * time.Second} + + return func(c *gin.Context) { + ctx := c.Request.Context() + results := make([]NodePingResult, 0, len(deps.Nodes)) + + for _, node := range deps.Nodes { + target := strings.TrimRight(node.URL, "/") + "/health" + start := time.Now() + req, err := http.NewRequestWithContext(ctx, http.MethodGet, target, nil) + if err != nil { + results = append(results, NodePingResult{ + Name: node.Name, URL: node.URL, Region: node.Region, + OK: false, LatencyMs: 0, Status: "bad url", + }) + continue + } + + resp, err := client.Do(req) + latency := time.Since(start).Milliseconds() + row := NodePingResult{ + Name: node.Name, + URL: node.URL, + Region: node.Region, + LatencyMs: latency, + } + if err != nil { + row.OK = false + row.Status = err.Error() + results = append(results, row) + continue + } + _ = resp.Body.Close() + row.OK = resp.StatusCode >= 200 && resp.StatusCode < 300 + row.Status = strconv.Itoa(resp.StatusCode) + results = append(results, row) + } + + c.JSON(http.StatusOK, gin.H{"success": true, "data": results}) + } +} diff --git a/backend/internal/handler/playground.go b/backend/internal/handler/playground.go new file mode 100644 index 0000000..362912f --- /dev/null +++ b/backend/internal/handler/playground.go @@ -0,0 +1,153 @@ +package handler + +import ( + "bytes" + "encoding/json" + "errors" + "io" + "net/http" + + "github.com/gin-gonic/gin" + "gorm.io/gorm" + + "github.com/leno23/ai-api-gateway/internal/middleware" + "github.com/leno23/ai-api-gateway/internal/model" +) + +const ctxLogTokenNameOverride = "log_token_name_override" + +type playgroundChatReq struct { + Model string `json:"model" binding:"required"` + Messages json.RawMessage `json:"messages" binding:"required"` + Stream *bool `json:"stream"` + MaxTokens *int `json:"max_tokens"` + Temperature *float64 `json:"temperature"` + TopP *float64 `json:"top_p"` + FrequencyPenalty *float64 `json:"frequency_penalty"` + PresencePenalty *float64 `json:"presence_penalty"` + APIKeyID *int64 `json:"api_key_id"` + TokenGroup string `json:"token_group"` + CustomBody bool `json:"custom_body"` + CustomBodyRaw json.RawMessage `json:"custom_body_raw"` +} + +func PlaygroundChat(deps GatewayDeps) gin.HandlerFunc { + return func(c *gin.Context) { + userID, ok := ctxUserID(c) + if !ok { + c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) + return + } + + var pg playgroundChatReq + if err := c.ShouldBindJSON(&pg); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"}) + return + } + + apiKeyID, tokenOverride, allowedModels, err := resolvePlaygroundAPIKey(deps.DB, userID, pg.APIKeyID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + c.JSON(http.StatusNotFound, gin.H{"error": "token not found"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": "token"}) + return + } + + upstreamBody, err := buildPlaygroundUpstreamBody(pg) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + c.Set(middleware.CtxUserID, userID) + c.Set(middleware.CtxAPIKeyID, apiKeyID) + if tokenOverride != "" { + c.Set(ctxLogTokenNameOverride, tokenOverride) + } + if len(allowedModels) > 0 { + c.Set(middleware.CtxAPIModels, allowedModels) + } + + c.Request.Body = io.NopCloser(bytes.NewReader(upstreamBody)) + c.Request.ContentLength = int64(len(upstreamBody)) + c.Request.Header.Set("Content-Type", "application/json") + + ChatCompletions(deps)(c) + } +} + +func resolvePlaygroundAPIKey(db *gorm.DB, userID int64, requestedID *int64) (apiKeyID int64, tokenOverride string, allowedModels []string, err error) { + base := db.Where("user_id = ? AND status = ?", userID, model.APIKeyStatusActive) + if requestedID != nil && *requestedID > 0 { + var key model.APIKey + if err := base.Where("id = ?", *requestedID).First(&key).Error; err != nil { + return 0, "", nil, err + } + return key.ID, "", []string(key.Models), nil + } + var key model.APIKey + if err := base.Order("id ASC").First(&key).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return 0, "playground-default", nil, nil + } + return 0, "", nil, err + } + return key.ID, "", []string(key.Models), nil +} + +func buildPlaygroundUpstreamBody(pg playgroundChatReq) ([]byte, error) { + if pg.CustomBody && len(pg.CustomBodyRaw) > 0 { + var probe struct { + Model string `json:"model"` + } + if err := json.Unmarshal(pg.CustomBodyRaw, &probe); err != nil || probe.Model == "" { + return nil, errors.New("custom body must include model") + } + return pg.CustomBodyRaw, nil + } + + stream := true + if pg.Stream != nil { + stream = *pg.Stream + } + payload := map[string]any{ + "model": pg.Model, + "messages": json.RawMessage(pg.Messages), + "stream": stream, + } + if pg.MaxTokens != nil { + payload["max_tokens"] = *pg.MaxTokens + } + if pg.Temperature != nil { + payload["temperature"] = *pg.Temperature + } + if pg.TopP != nil { + payload["top_p"] = *pg.TopP + } + if pg.FrequencyPenalty != nil { + payload["frequency_penalty"] = *pg.FrequencyPenalty + } + if pg.PresencePenalty != nil { + payload["presence_penalty"] = *pg.PresencePenalty + } + return json.Marshal(payload) +} + +func logTokenNameOverride(c *gin.Context) string { + v, ok := c.Get(ctxLogTokenNameOverride) + if !ok { + return "" + } + s, _ := v.(string) + return s +} + +func apiKeyIDPtr(id int64) *int64 { + if id <= 0 { + return nil + } + v := id + return &v +} diff --git a/backend/internal/handler/portal_nodes.go b/backend/internal/handler/portal_nodes.go new file mode 100644 index 0000000..c069a84 --- /dev/null +++ b/backend/internal/handler/portal_nodes.go @@ -0,0 +1,22 @@ +package handler + +import "encoding/json" + +func DefaultPortalNodes() []PortalNode { + return []PortalNode{ + {Name: "主站", URL: "http://localhost:8080", Region: "local"}, + {Name: "香港", URL: "https://hk.example.com", Region: "hk"}, + {Name: "美区", URL: "https://us.example.com", Region: "us"}, + } +} + +func ParsePortalNodes(raw string) []PortalNode { + if raw == "" { + return DefaultPortalNodes() + } + var nodes []PortalNode + if err := json.Unmarshal([]byte(raw), &nodes); err != nil || len(nodes) == 0 { + return DefaultPortalNodes() + } + return nodes +} diff --git a/backend/internal/handler/tokens.go b/backend/internal/handler/tokens.go new file mode 100644 index 0000000..abb4735 --- /dev/null +++ b/backend/internal/handler/tokens.go @@ -0,0 +1,300 @@ +package handler + +import ( + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/lib/pq" + "golang.org/x/crypto/bcrypt" + "gorm.io/gorm" + + "github.com/leno23/ai-api-gateway/internal/middleware" + "github.com/leno23/ai-api-gateway/internal/model" + "github.com/leno23/ai-api-gateway/internal/repository" + "github.com/leno23/ai-api-gateway/internal/service" +) + +type TokenDeps struct { + DB *gorm.DB + Repos *repository.Repos +} + +type tokenCreateReq struct { + Name string `json:"name" binding:"required,min=1,max=128"` + TokenGroup string `json:"token_group"` + QuotaLimit *int64 `json:"quota_limit"` + Models []string `json:"models"` + IPWhitelist []string `json:"ip_whitelist"` +} + +type tokenUpdateReq struct { + Name *string `json:"name"` + Status *int16 `json:"status"` + TokenGroup *string `json:"token_group"` + QuotaLimit *int64 `json:"quota_limit"` + ClearQuotaLimit bool `json:"clear_quota_limit"` + Models []string `json:"models"` + IPWhitelist []string `json:"ip_whitelist"` +} + +type batchDeleteReq struct { + IDs []int64 `json:"ids" binding:"required,min=1"` +} + +func maskAPIKey(prefix string) string { + if len(prefix) <= 10 { + return prefix + "******" + } + return prefix[:10] + "******" +} + +func tokenGroupSlug(db *gorm.DB, groupID *int64) string { + if groupID == nil { + return "default" + } + var g model.TokenGroup + if err := db.First(&g, *groupID).Error; err != nil { + return "default" + } + return g.Slug +} + +func resolveTokenGroupID(db *gorm.DB, slug string) (*int64, error) { + if slug == "" { + slug = "default" + } + var g model.TokenGroup + if err := db.Where("slug = ?", slug).First(&g).Error; err != nil { + return nil, err + } + id := g.ID + return &id, nil +} + +func serializeToken(db *gorm.DB, k model.APIKey) gin.H { + return gin.H{ + "id": k.ID, + "name": k.Name, + "status": k.Status, + "enabled": k.Status == model.APIKeyStatusActive, + "key_prefix": k.KeyPrefix, + "key_masked": maskAPIKey(k.KeyPrefix), + "token_group": tokenGroupSlug(db, k.TokenGroupID), + "quota_limit": k.QuotaLimit, + "used_quota": k.UsedQuota, + "models": []string(k.Models), + "ip_whitelist": []string(k.IPWhitelist), + "rate_limit": k.RateLimit, + "created_at": k.CreatedAt, + } +} + +func ctxUserID(c *gin.Context) (int64, bool) { + uid, ok := c.Get(middleware.CtxUserID) + if !ok { + return 0, false + } + id, ok := uid.(int64) + return id, ok +} + +func ListTokens(deps TokenDeps) gin.HandlerFunc { + return func(c *gin.Context) { + userID, ok := ctxUserID(c) + if !ok { + c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) + return + } + page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) + pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10")) + rows, total, err := deps.Repos.ListAPIKeysByUser(userID, page, pageSize) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "list"}) + return + } + items := make([]gin.H, 0, len(rows)) + for _, k := range rows { + items = append(items, serializeToken(deps.DB, k)) + } + c.JSON(http.StatusOK, gin.H{ + "success": true, + "data": gin.H{ + "items": items, + "total": total, + "page": page, + "page_size": pageSize, + }, + }) + } +} + +func CreateToken(deps TokenDeps) gin.HandlerFunc { + return func(c *gin.Context) { + userID, ok := ctxUserID(c) + if !ok { + c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) + return + } + var req tokenCreateReq + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + groupID, err := resolveTokenGroupID(deps.DB, req.TokenGroup) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid token group"}) + return + } + raw, err := service.RandomRedeemCode() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "rng"}) + return + } + plain := "sk-" + raw + hash, err := bcrypt.GenerateFromPassword([]byte(plain), bcrypt.DefaultCost) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "hash"}) + return + } + prefix := plain + if len(prefix) > 16 { + prefix = prefix[:16] + } + k := model.APIKey{ + UserID: userID, + Name: req.Name, + KeyHash: string(hash), + KeyPrefix: prefix, + Status: model.APIKeyStatusActive, + TokenGroupID: groupID, + QuotaLimit: req.QuotaLimit, + Models: pq.StringArray(req.Models), + IPWhitelist: pq.StringArray(req.IPWhitelist), + } + if err := deps.DB.Create(&k).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "persist"}) + return + } + c.JSON(http.StatusCreated, gin.H{ + "success": true, + "data": gin.H{ + "token": serializeToken(deps.DB, k), + "api_key": plain, + }, + }) + } +} + +func UpdateToken(deps TokenDeps) gin.HandlerFunc { + return func(c *gin.Context) { + userID, ok := ctxUserID(c) + if !ok { + c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) + return + } + keyID, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"}) + return + } + k, err := deps.Repos.GetAPIKeyForUser(userID, keyID) + if err != nil { + if err == repository.ErrAPIKeyNotFound { + c.JSON(http.StatusNotFound, gin.H{"error": "not found"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": "lookup"}) + return + } + var req tokenUpdateReq + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + updates := map[string]any{} + if req.Name != nil { + updates["name"] = *req.Name + } + if req.Status != nil { + updates["status"] = *req.Status + } + if req.TokenGroup != nil { + gid, err := resolveTokenGroupID(deps.DB, *req.TokenGroup) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid token group"}) + return + } + updates["token_group_id"] = gid + } + if req.ClearQuotaLimit { + updates["quota_limit"] = nil + } else if req.QuotaLimit != nil { + updates["quota_limit"] = *req.QuotaLimit + } + if req.Models != nil { + updates["models"] = pq.StringArray(req.Models) + } + if req.IPWhitelist != nil { + updates["ip_whitelist"] = pq.StringArray(req.IPWhitelist) + } + if len(updates) > 0 { + if err := deps.DB.Model(k).Updates(updates).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "update"}) + return + } + } + var fresh model.APIKey + _ = deps.DB.First(&fresh, k.ID) + c.JSON(http.StatusOK, gin.H{"success": true, "data": serializeToken(deps.DB, fresh)}) + } +} + +func DeleteToken(deps TokenDeps) gin.HandlerFunc { + return func(c *gin.Context) { + userID, ok := ctxUserID(c) + if !ok { + c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) + return + } + keyID, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"}) + return + } + if _, err := deps.Repos.GetAPIKeyForUser(userID, keyID); err != nil { + if err == repository.ErrAPIKeyNotFound { + c.JSON(http.StatusNotFound, gin.H{"error": "not found"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": "lookup"}) + return + } + if err := deps.DB.Delete(&model.APIKey{}, keyID).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "delete"}) + return + } + c.JSON(http.StatusOK, gin.H{"success": true}) + } +} + +func BatchDeleteTokens(deps TokenDeps) gin.HandlerFunc { + return func(c *gin.Context) { + userID, ok := ctxUserID(c) + if !ok { + c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) + return + } + var req batchDeleteReq + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + n, err := deps.Repos.DeleteAPIKeysForUser(userID, req.IDs) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "delete"}) + return + } + c.JSON(http.StatusOK, gin.H{"success": true, "deleted": n}) + } +} diff --git a/backend/internal/handler/wallet.go b/backend/internal/handler/wallet.go new file mode 100644 index 0000000..c21648a --- /dev/null +++ b/backend/internal/handler/wallet.go @@ -0,0 +1,241 @@ +package handler + +import ( + "fmt" + "net/http" + "time" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "gorm.io/gorm" + "gorm.io/gorm/clause" + + "github.com/leno23/ai-api-gateway/internal/model" + "github.com/leno23/ai-api-gateway/internal/repository" + "github.com/leno23/ai-api-gateway/internal/service" +) + +type WalletDeps struct { + DB *gorm.DB + Repos *repository.Repos + RechargeEnabled bool + AffiliateBPS int + PortalOrigin string +} + +func WalletSummary(deps WalletDeps) gin.HandlerFunc { + return func(c *gin.Context) { + userID, ok := ctxUserID(c) + if !ok { + c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) + return + } + var u model.User + if err := deps.DB.First(&u, userID).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "user not found"}) + return + } + var requestCount int64 + _ = deps.DB.Model(&model.RequestLog{}).Where("user_id = ?", userID).Count(&requestCount).Error + + origin := deps.PortalOrigin + if origin == "" { + origin = "http://localhost:3001" + } + inviteURL := fmt.Sprintf("%s/register?aff=%s", origin, u.InviteCode) + + c.JSON(http.StatusOK, gin.H{ + "success": true, + "data": gin.H{ + "quota": u.Quota, + "used_quota": u.UsedQuota, + "request_count": requestCount, + "invite_code": u.InviteCode, + "invite_url": inviteURL, + "affiliate_pending": u.AffiliatePending, + "recharge_enabled": deps.RechargeEnabled, + }, + }) + } +} + +func RedeemWithAffiliate(deps WalletDeps) gin.HandlerFunc { + return func(c *gin.Context) { + userID, ok := ctxUserID(c) + if !ok { + c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) + return + } + var req redeemReq + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + var outQuota int64 + var redeemedQuota int64 + err := deps.DB.Transaction(func(tx *gorm.DB) error { + var rc model.RedeemCode + if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). + Where("code = ?", req.Code).First(&rc).Error; err != nil { + return err + } + if rc.Status != model.RedeemStatusAvailable { + return gorm.ErrRecordNotFound + } + if rc.ExpiresAt != nil && time.Now().After(*rc.ExpiresAt) { + return gorm.ErrRecordNotFound + } + now := time.Now() + if err := tx.Model(&model.RedeemCode{}).Where("id = ?", rc.ID).Updates(map[string]any{ + "status": model.RedeemStatusUsed, + "used_by": userID, + "used_at": now, + }).Error; err != nil { + return err + } + if err := tx.Model(&model.User{}).Where("id = ?", userID). + UpdateColumn("quota", gorm.Expr("quota + ?", rc.Quota)).Error; err != nil { + return err + } + redeemedQuota = rc.Quota + if err := service.CreditAffiliateOnRecharge(tx, userID, rc.Quota, deps.AffiliateBPS); err != nil { + return err + } + if err := tx.Model(&model.User{}).Select("quota").Where("id = ?", userID).Scan(&outQuota).Error; err != nil { + return err + } + return tx.Create(&model.QuotaLog{ + UserID: userID, + Delta: rc.Quota, + Balance: outQuota, + Type: model.QuotaLogTypeRecharge, + Reference: rc.Code, + Remark: "redeem", + }).Error + }) + if err != nil { + if err == gorm.ErrRecordNotFound { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid or used code"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": "redeem failed"}) + return + } + _ = redeemedQuota + c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"quota": outQuota}}) + } +} + +func TransferAffiliate(deps WalletDeps) gin.HandlerFunc { + return func(c *gin.Context) { + userID, ok := ctxUserID(c) + if !ok { + c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) + return + } + var outQuota int64 + err := deps.DB.Transaction(func(tx *gorm.DB) error { + var u model.User + if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&u, userID).Error; err != nil { + return err + } + if u.AffiliatePending <= 0 { + return gorm.ErrRecordNotFound + } + pending := u.AffiliatePending + if err := tx.Model(&model.User{}).Where("id = ?", userID).Updates(map[string]any{ + "affiliate_pending": 0, + "quota": gorm.Expr("quota + ?", pending), + }).Error; err != nil { + return err + } + if err := tx.Model(&model.User{}).Select("quota").Where("id = ?", userID).Scan(&outQuota).Error; err != nil { + return err + } + return tx.Create(&model.QuotaLog{ + UserID: userID, + Delta: pending, + Balance: outQuota, + Type: model.QuotaLogTypeRecharge, + Reference: "affiliate", + Remark: "affiliate transfer", + }).Error + }) + if err != nil { + if err == gorm.ErrRecordNotFound { + c.JSON(http.StatusBadRequest, gin.H{"error": "no pending affiliate earnings"}) + return + } + c.JSON(http.StatusInternalServerError, gin.H{"error": "transfer failed"}) + return + } + c.JSON(http.StatusOK, gin.H{ + "success": true, + "data": gin.H{"quota": outQuota, "transferred": true}, + }) + } +} + +type mockRechargeReq struct { + Amount int64 `json:"amount" binding:"required,min=1"` +} + +func MockRecharge(deps WalletDeps) gin.HandlerFunc { + return func(c *gin.Context) { + if !deps.RechargeEnabled { + c.JSON(http.StatusForbidden, gin.H{"error": "online recharge disabled"}) + return + } + userID, ok := ctxUserID(c) + if !ok { + c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) + return + } + var req mockRechargeReq + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + var outQuota int64 + tradeNo := "mock-" + uuid.NewString() + err := deps.DB.Transaction(func(tx *gorm.DB) error { + if err := tx.Create(&model.RechargeRecord{ + UserID: userID, + Amount: float64(req.Amount) / 10000, + PaymentMethod: "mock", + TradeNo: tradeNo, + Status: 1, + CompletedAt: ptrTime(time.Now()), + }).Error; err != nil { + return err + } + if err := tx.Model(&model.User{}).Where("id = ?", userID). + UpdateColumn("quota", gorm.Expr("quota + ?", req.Amount)).Error; err != nil { + return err + } + if err := service.CreditAffiliateOnRecharge(tx, userID, req.Amount, deps.AffiliateBPS); err != nil { + return err + } + if err := tx.Model(&model.User{}).Select("quota").Where("id = ?", userID).Scan(&outQuota).Error; err != nil { + return err + } + return tx.Create(&model.QuotaLog{ + UserID: userID, + Delta: req.Amount, + Balance: outQuota, + Type: model.QuotaLogTypeRecharge, + Reference: tradeNo, + Remark: "mock recharge", + }).Error + }) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "recharge failed"}) + return + } + c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"quota": outQuota, "trade_no": tradeNo}}) + } +} + +func ptrTime(t time.Time) *time.Time { + return &t +} diff --git a/backend/internal/integration/api_test.go b/backend/internal/integration/api_test.go index 4060505..dec6935 100644 --- a/backend/internal/integration/api_test.go +++ b/backend/internal/integration/api_test.go @@ -12,6 +12,7 @@ import ( "time" "github.com/gin-gonic/gin" + "gorm.io/gorm" "github.com/leno23/ai-api-gateway/internal/config" applog "github.com/leno23/ai-api-gateway/internal/log" @@ -26,7 +27,12 @@ func skipWithoutIntegrationDSN(t *testing.T) { } } -func newIntegrationRouter(t *testing.T) *gin.Engine { +type integrationEnv struct { + Router *gin.Engine + DB *gorm.DB +} + +func newIntegrationEnv(t *testing.T) integrationEnv { t.Helper() skipWithoutIntegrationDSN(t) t.Setenv("DATABASE_DSN", os.Getenv("INTEGRATION_DATABASE_DSN")) @@ -63,7 +69,12 @@ func newIntegrationRouter(t *testing.T) *gin.Engine { t.Cleanup(func() { _ = rdb.Close() }) gin.SetMode(gin.TestMode) - return router.New(cfg, db, rdb, log) + return integrationEnv{Router: router.New(cfg, db, rdb, log), DB: db} +} + +func newIntegrationRouter(t *testing.T) *gin.Engine { + t.Helper() + return newIntegrationEnv(t).Router } func TestIntegration_Health(t *testing.T) { diff --git a/backend/internal/integration/portal_flow_test.go b/backend/internal/integration/portal_flow_test.go new file mode 100644 index 0000000..c6d76ea --- /dev/null +++ b/backend/internal/integration/portal_flow_test.go @@ -0,0 +1,138 @@ +package integration + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/leno23/ai-api-gateway/internal/model" +) + +// TestIntegration_PortalCoreFlow covers register → login → portal token → playground chat. +func TestIntegration_PortalCoreFlow(t *testing.T) { + env := newIntegrationEnv(t) + r := env.Router + + suffix := fmt.Sprintf("%d", time.Now().UnixNano()) + email := "p" + suffix + "@example.com" + username := "pu" + suffix + if len(username) > 64 { + username = username[:64] + } + + regBody := map[string]any{ + "email": email, "username": username, "password": "testpass12", + } + b, _ := json.Marshal(regBody) + w := httptest.NewRecorder() + r.ServeHTTP(w, httptest.NewRequest(http.MethodPost, "/auth/register", bytes.NewReader(b))) + if w.Code != http.StatusCreated { + t.Fatalf("register: %d %s", w.Code, w.Body.String()) + } + + loginBody := map[string]any{"username": username, "password": "testpass12"} + lb, _ := json.Marshal(loginBody) + w = httptest.NewRecorder() + r.ServeHTTP(w, httptest.NewRequest(http.MethodPost, "/auth/login", bytes.NewReader(lb))) + if w.Code != http.StatusOK { + t.Fatalf("login: %d %s", w.Code, w.Body.String()) + } + var loginResp struct { + AccessToken string `json:"access_token"` + } + if err := json.Unmarshal(w.Body.Bytes(), &loginResp); err != nil { + t.Fatal(err) + } + if loginResp.AccessToken == "" { + t.Fatal("empty access_token") + } + jwt := loginResp.AccessToken + + if err := env.DB.Model(&model.User{}).Where("email = ?", email). + Update("quota", int64(50_000_000)).Error; err != nil { + t.Fatalf("seed quota: %v", err) + } + + req := httptest.NewRequest(http.MethodGet, "/api/user/self", nil) + req.Header.Set("Authorization", "Bearer "+jwt) + w = httptest.NewRecorder() + r.ServeHTTP(w, req) + if w.Code != http.StatusOK { + t.Fatalf("user self: %d %s", w.Code, w.Body.String()) + } + + tokenBody := map[string]any{"name": "portal-e2e", "token_group": "default"} + tb, _ := json.Marshal(tokenBody) + req = httptest.NewRequest(http.MethodPost, "/api/tokens", bytes.NewReader(tb)) + req.Header.Set("Authorization", "Bearer "+jwt) + req.Header.Set("Content-Type", "application/json") + w = httptest.NewRecorder() + r.ServeHTTP(w, req) + if w.Code != http.StatusCreated { + t.Fatalf("create token: %d %s", w.Code, w.Body.String()) + } + var tokenResp struct { + Success bool `json:"success"` + Data struct { + Token struct { + ID int64 `json:"id"` + } `json:"token"` + APIKey string `json:"api_key"` + } `json:"data"` + } + if err := json.Unmarshal(w.Body.Bytes(), &tokenResp); err != nil { + t.Fatal(err) + } + if tokenResp.Data.APIKey == "" || tokenResp.Data.Token.ID == 0 { + t.Fatalf("unexpected token response: %s", w.Body.String()) + } + + playBody := map[string]any{ + "model": "gpt-4o-mini", + "stream": true, + "max_tokens": 16, + "api_key_id": tokenResp.Data.Token.ID, + "token_group": "default", + "messages": []map[string]string{ + {"role": "user", "content": "Say hi in one word."}, + }, + } + pb, _ := json.Marshal(playBody) + req = httptest.NewRequest(http.MethodPost, "/api/playground/chat", bytes.NewReader(pb)) + req.Header.Set("Authorization", "Bearer "+jwt) + req.Header.Set("Content-Type", "application/json") + w = httptest.NewRecorder() + r.ServeHTTP(w, req) + + switch w.Code { + case http.StatusOK: + ct := w.Header().Get("Content-Type") + if ct == "" { + t.Fatalf("playground: missing content-type") + } + case http.StatusPaymentRequired, http.StatusBadGateway, http.StatusServiceUnavailable: + t.Logf("playground upstream/quota path: %d %s", w.Code, w.Body.String()) + default: + t.Fatalf("playground: %d %s", w.Code, w.Body.String()) + } + + req = httptest.NewRequest(http.MethodGet, "/api/dashboard/stats", nil) + req.Header.Set("Authorization", "Bearer "+jwt) + w = httptest.NewRecorder() + r.ServeHTTP(w, req) + if w.Code != http.StatusOK { + t.Fatalf("dashboard stats: %d %s", w.Code, w.Body.String()) + } + + req = httptest.NewRequest(http.MethodGet, "/api/nodes/ping", nil) + req.Header.Set("Authorization", "Bearer "+jwt) + w = httptest.NewRecorder() + r.ServeHTTP(w, req) + if w.Code != http.StatusOK { + t.Fatalf("nodes ping: %d %s", w.Code, w.Body.String()) + } +} diff --git a/backend/internal/middleware/cors.go b/backend/internal/middleware/cors.go new file mode 100644 index 0000000..3dc0b72 --- /dev/null +++ b/backend/internal/middleware/cors.go @@ -0,0 +1,27 @@ +package middleware + +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + +// CORS allows browser admin console to call the gateway API in local development. +func CORS() gin.HandlerFunc { + return func(c *gin.Context) { + origin := c.GetHeader("Origin") + if origin != "" { + c.Header("Access-Control-Allow-Origin", origin) + c.Header("Vary", "Origin") + c.Header("Access-Control-Allow-Credentials", "true") + c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization") + c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS") + c.Header("Access-Control-Max-Age", "86400") + } + if c.Request.Method == http.MethodOptions { + c.AbortWithStatus(http.StatusNoContent) + return + } + c.Next() + } +} diff --git a/backend/internal/middleware/gateway.go b/backend/internal/middleware/gateway.go index 009a7af..4373848 100644 --- a/backend/internal/middleware/gateway.go +++ b/backend/internal/middleware/gateway.go @@ -31,21 +31,28 @@ func GatewayAPIKey(repos *repository.Repos) gin.HandlerFunc { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid api key format"}) return } - userID, keyID, models, err := repos.AuthenticateGatewayAPIKey(raw, prefixLen) + clientIP := c.ClientIP() + auth, err := repos.AuthenticateGatewayAPIKey(raw, prefixLen, clientIP) if err != nil { switch { case errors.Is(err, repository.ErrInvalidAPIKey): c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid api key"}) + case errors.Is(err, repository.ErrAPIKeyDisabled): + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "api key disabled"}) case errors.Is(err, repository.ErrUserInactive): c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "user inactive"}) + case errors.Is(err, repository.ErrAPIKeyIPDenied): + c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "ip not allowed"}) + case errors.Is(err, repository.ErrAPIKeyQuota): + c.AbortWithStatusJSON(http.StatusPaymentRequired, gin.H{"error": "api key quota exceeded", "code": "insufficient_quota"}) default: c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "key lookup failed"}) } return } - c.Set(CtxUserID, userID) - c.Set(CtxAPIKeyID, keyID) - c.Set(CtxAPIModels, models) + c.Set(CtxUserID, auth.UserID) + c.Set(CtxAPIKeyID, auth.KeyID) + c.Set(CtxAPIModels, auth.Models) c.Next() } } diff --git a/backend/internal/model/model.go b/backend/internal/model/model.go index ce92880..19e9d3f 100644 --- a/backend/internal/model/model.go +++ b/backend/internal/model/model.go @@ -36,34 +36,52 @@ const ( ) type User struct { - ID int64 `gorm:"primaryKey"` - Username string `gorm:"uniqueIndex;size:64;not null"` - Email string `gorm:"uniqueIndex;size:255;not null"` - PasswordHash string `gorm:"column:password_hash;size:255;not null"` - Role int16 `gorm:"default:1;not null"` - Status int16 `gorm:"default:1;not null"` - Balance float64 `gorm:"type:decimal(16,6);default:0"` - Quota int64 `gorm:"default:0;not null"` - UsedQuota int64 `gorm:"column:used_quota;default:0;not null"` - InviteCode string `gorm:"column:invite_code;uniqueIndex;size:32"` - InvitedBy *int64 `gorm:"column:invited_by"` - CreatedAt time.Time `gorm:"autoCreateTime"` - UpdatedAt time.Time `gorm:"autoUpdateTime"` + ID int64 `gorm:"primaryKey"` + Username string `gorm:"uniqueIndex;size:64;not null"` + Email string `gorm:"uniqueIndex;size:255;not null"` + PasswordHash string `gorm:"column:password_hash;size:255;not null"` + Role int16 `gorm:"default:1;not null"` + Status int16 `gorm:"default:1;not null"` + Balance float64 `gorm:"type:decimal(16,6);default:0"` + Quota int64 `gorm:"default:0;not null"` + UsedQuota int64 `gorm:"column:used_quota;default:0;not null"` + InviteCode string `gorm:"column:invite_code;uniqueIndex;size:32"` + InvitedBy *int64 `gorm:"column:invited_by"` + TokenGroupID *int64 `gorm:"column:token_group_id"` + AffiliatePending int64 `gorm:"column:affiliate_pending;default:0;not null"` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` } +type TokenGroup struct { + ID int64 `gorm:"primaryKey"` + Slug string `gorm:"uniqueIndex;size:64;not null"` + Name string `gorm:"size:128;not null"` + Multiplier float64 `gorm:"type:decimal(6,2);default:1;not null"` + Status int16 `gorm:"default:1;not null"` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` +} + +func (TokenGroup) TableName() string { return "token_groups" } + func (User) TableName() string { return "users" } type APIKey struct { - ID int64 `gorm:"primaryKey"` - UserID int64 `gorm:"index:idx_api_keys_user_id;not null"` - Name string `gorm:"size:128;not null"` - KeyHash string `gorm:"column:key_hash;uniqueIndex:idx_api_keys_key_hash;size:255;not null"` - KeyPrefix string `gorm:"column:key_prefix;size:16;not null"` - Status int16 `gorm:"default:1;not null"` - Models pq.StringArray `gorm:"type:text[]"` - RateLimit int `gorm:"column:rate_limit;default:60"` - ExpiresAt *time.Time `gorm:"column:expires_at"` - CreatedAt time.Time `gorm:"autoCreateTime"` + ID int64 `gorm:"primaryKey"` + UserID int64 `gorm:"index:idx_api_keys_user_id;not null"` + Name string `gorm:"size:128;not null"` + KeyHash string `gorm:"column:key_hash;uniqueIndex:idx_api_keys_key_hash;size:255;not null"` + KeyPrefix string `gorm:"column:key_prefix;size:16;not null"` + Status int16 `gorm:"default:1;not null"` + TokenGroupID *int64 `gorm:"column:token_group_id"` + QuotaLimit *int64 `gorm:"column:quota_limit"` + UsedQuota int64 `gorm:"column:used_quota;default:0;not null"` + Models pq.StringArray `gorm:"type:text[]"` + IPWhitelist pq.StringArray `gorm:"column:ip_whitelist;type:text[]"` + RateLimit int `gorm:"column:rate_limit;default:60"` + ExpiresAt *time.Time `gorm:"column:expires_at"` + CreatedAt time.Time `gorm:"autoCreateTime"` } func (APIKey) TableName() string { return "api_keys" } @@ -88,15 +106,21 @@ type Channel struct { func (Channel) TableName() string { return "channels" } type ModelPrice struct { - ID int64 `gorm:"primaryKey"` - Model string `gorm:"column:model;uniqueIndex;size:128;not null"` - PromptPrice int64 `gorm:"column:prompt_price;not null"` - CompletionPrice int64 `gorm:"column:completion_price;not null"` - UnitPrice int64 `gorm:"column:unit_price;not null"` - BillingType int16 `gorm:"column:billing_type;default:1;not null"` - Currency string `gorm:"size:8;default:CNY;not null"` - CreatedAt time.Time `gorm:"autoCreateTime"` - UpdatedAt time.Time `gorm:"autoUpdateTime"` + ID int64 `gorm:"primaryKey"` + Model string `gorm:"column:model;uniqueIndex;size:128;not null"` + DisplayName string `gorm:"column:display_name;size:128"` + Provider string `gorm:"size:32;default:openai;not null"` + EndpointType string `gorm:"column:endpoint_type;size:32;default:openai;not null"` + PromptPrice int64 `gorm:"column:prompt_price;not null"` + CompletionPrice int64 `gorm:"column:completion_price;not null"` + CacheReadPrice int64 `gorm:"column:cache_read_price;not null"` + CacheWritePrice int64 `gorm:"column:cache_write_price;not null"` + UnitPrice int64 `gorm:"column:unit_price;not null"` + BillingType int16 `gorm:"column:billing_type;default:1;not null"` + Tags pq.StringArray `gorm:"type:text[]"` + Currency string `gorm:"size:8;default:CNY;not null"` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` } func (ModelPrice) TableName() string { return "model_prices" } @@ -106,6 +130,9 @@ type RequestLog struct { UserID int64 `gorm:"index:idx_request_logs_user_id;not null"` APIKeyID *int64 `gorm:"column:api_key_id"` ChannelID *int64 `gorm:"column:channel_id"` + RequestID string `gorm:"column:request_id;size:64"` + TokenName string `gorm:"column:token_name;size:128"` + TokenGroup string `gorm:"column:token_group;size:64"` Model string `gorm:"size:128"` RequestMethod string `gorm:"column:request_method;size:16"` RequestPath string `gorm:"column:request_path;size:256"` @@ -114,6 +141,8 @@ type RequestLog struct { TotalTokens int `gorm:"column:total_tokens"` CostQuota int64 `gorm:"column:cost_quota"` LatencyMs *int `gorm:"column:latency_ms"` + TimeToFirstMs *int `gorm:"column:time_to_first_ms"` + BillingDetail []byte `gorm:"column:billing_detail;type:jsonb"` StatusCode *int `gorm:"column:status_code"` ErrorMessage string `gorm:"column:error_message;type:text"` CreatedAt time.Time `gorm:"autoCreateTime;index:idx_request_logs_user_id,priority:2"` @@ -121,6 +150,37 @@ type RequestLog struct { func (RequestLog) TableName() string { return "request_logs" } +type TaskLog struct { + ID int64 `gorm:"primaryKey"` + UserID int64 `gorm:"index:idx_task_logs_user_submitted;not null"` + TaskID string `gorm:"column:task_id;size:128;not null"` + Platform string `gorm:"size:64"` + TaskType string `gorm:"column:task_type;size:64"` + Status string `gorm:"size:32;not null"` + Progress int `gorm:"default:0;not null"` + Detail string `gorm:"type:text"` + SubmittedAt time.Time `gorm:"column:submitted_at;not null"` + FinishedAt *time.Time `gorm:"column:finished_at"` + CreatedAt time.Time `gorm:"autoCreateTime"` +} + +func (TaskLog) TableName() string { return "task_logs" } + +type Announcement struct { + ID int64 `gorm:"primaryKey"` + Title string `gorm:"size:256;not null"` + Content string `gorm:"type:text;not null"` + Level string `gorm:"size:32;default:info;not null"` + Placement string `gorm:"size:64;default:home;not null"` + Status int16 `gorm:"default:1;not null"` + StartsAt *time.Time `gorm:"column:starts_at"` + EndsAt *time.Time `gorm:"column:ends_at"` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` +} + +func (Announcement) TableName() string { return "announcements" } + type RechargeRecord struct { ID int64 `gorm:"primaryKey"` UserID int64 `gorm:"index;not null"` diff --git a/backend/internal/openapi/contract_test.go b/backend/internal/openapi/contract_test.go new file mode 100644 index 0000000..3351072 --- /dev/null +++ b/backend/internal/openapi/contract_test.go @@ -0,0 +1,43 @@ +package openapi + +import ( + "strings" + "testing" +) + +// portalPaths are tenant-portal REST routes that must stay documented in spec.yaml. +var portalPaths = []string{ + "/api/announcements", + "/api/dashboard/stats", + "/api/dashboard/charts", + "/api/dashboard/nodes", + "/api/nodes/ping", + "/api/logs/usage", + "/api/logs/tasks", + "/api/wallet/summary", + "/api/wallet/redeem", + "/api/wallet/affiliate/transfer", + "/api/wallet/recharge/mock", + "/api/playground/chat", + "/api/tokens", + "/api/tokens/batch-delete", + "/api/user/self", + "/api/models", + "/api/token-groups", +} + +func TestSpecDocumentsPortalRoutes(t *testing.T) { + body := string(Spec) + for _, path := range portalPaths { + needle := "\n " + path + ":" + if !strings.Contains(body, needle) { + t.Errorf("spec.yaml missing path definition: %s", path) + } + } +} + +func TestSpecHasPortalTag(t *testing.T) { + if !strings.Contains(string(Spec), "name: Portal") { + t.Fatal("spec.yaml missing Portal tag") + } +} diff --git a/backend/internal/openapi/spec.yaml b/backend/internal/openapi/spec.yaml index ac4597b..bc962f6 100644 --- a/backend/internal/openapi/spec.yaml +++ b/backend/internal/openapi/spec.yaml @@ -13,6 +13,7 @@ tags: - name: Health - name: Auth - name: User + - name: Portal - name: Admin - name: Gateway - name: Observability @@ -84,7 +85,7 @@ paths: $ref: "#/components/schemas/LoginRequest" responses: "200": - description: Bearer access token + description: Bearer access token and optional user profile content: application/json: schema: @@ -92,6 +93,300 @@ paths: "401": description: Invalid credentials + /api/models: + get: + tags: [User] + summary: Model catalog for pricing page (public) + parameters: + - in: query + name: page + schema: { type: integer, default: 1 } + - in: query + name: page_size + schema: { type: integer, default: 20 } + - in: query + name: provider + schema: { type: string } + - in: query + name: endpoint_type + schema: { type: string } + - in: query + name: billing_type + schema: { type: integer } + - in: query + name: tag + schema: { type: string } + - in: query + name: token_group + schema: { type: string, default: default } + - in: query + name: q + schema: { type: string } + responses: + "200": + description: Paginated catalog with prices and multipliers + + /api/models/{model}/price: + get: + tags: [User] + summary: Single model price breakdown + parameters: + - in: path + name: model + required: true + schema: { type: string } + - in: query + name: token_group + schema: { type: string } + responses: + "200": + description: Price breakdown + "404": + description: Unknown model + + /api/token-groups: + get: + tags: [User] + summary: List token groups with multipliers + responses: + "200": + description: Token groups + + /api/announcements: + get: + tags: [Portal] + summary: Active system announcements (public) + responses: + "200": + description: Announcement list + + /api/dashboard/stats: + get: + tags: [Portal] + security: [{ bearerJWT: [] }] + summary: Dashboard metric cards (balance, usage, RPM/TPM) + responses: + "200": + description: Stats payload + "401": + description: Unauthorized + + /api/dashboard/charts: + get: + tags: [Portal] + security: [{ bearerJWT: [] }] + summary: Model consumption charts + parameters: + - in: query + name: range + schema: { type: string, default: 7d, enum: [24h, 7d, 30d] } + responses: + "200": + description: Chart series + + /api/dashboard/nodes: + get: + tags: [Portal] + security: [{ bearerJWT: [] }] + summary: Configured API node list (from PORTAL_API_NODES_JSON) + responses: + "200": + description: Nodes + + /api/nodes/ping: + get: + tags: [Portal] + security: [{ bearerJWT: [] }] + summary: Probe each node /health and return latency + responses: + "200": + description: Per-node ping results + + /api/logs/usage: + get: + tags: [Portal] + security: [{ bearerJWT: [] }] + summary: Paginated request usage logs + parameters: + - in: query + name: page + schema: { type: integer, default: 1 } + - in: query + name: page_size + schema: { type: integer, default: 10 } + - in: query + name: token_name + schema: { type: string } + - in: query + name: model + schema: { type: string } + - in: query + name: request_id + schema: { type: string } + - in: query + name: token_group + schema: { type: string } + responses: + "200": + description: Usage log page + + /api/logs/tasks: + get: + tags: [Portal] + security: [{ bearerJWT: [] }] + summary: Paginated async task logs + responses: + "200": + description: Task log page + + /api/wallet/summary: + get: + tags: [Portal] + security: [{ bearerJWT: [] }] + summary: Wallet balance and affiliate summary + responses: + "200": + description: Wallet summary + + /api/wallet/redeem: + post: + tags: [Portal] + security: [{ bearerJWT: [] }] + summary: Redeem code (wallet flow) + responses: + "200": + description: Updated balance + + /api/wallet/affiliate/transfer: + post: + tags: [Portal] + security: [{ bearerJWT: [] }] + summary: Transfer pending affiliate earnings to balance + responses: + "200": + description: Transferred + + /api/wallet/recharge/mock: + post: + tags: [Portal] + security: [{ bearerJWT: [] }] + summary: Mock online recharge when RECHARGE_ENABLED=true + responses: + "200": + description: Credited + "403": + description: Recharge disabled + + /api/playground/chat: + post: + tags: [Portal] + security: [{ bearerJWT: [] }] + summary: Playground chat (SSE stream); bills user quota like /v1/chat/completions + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [model, messages] + properties: + model: { type: string } + messages: { type: array, items: { type: object } } + stream: { type: boolean, default: true } + max_tokens: { type: integer } + temperature: { type: number } + top_p: { type: number } + api_key_id: { type: integer, format: int64 } + token_group: { type: string } + custom_body: { type: boolean } + responses: + "200": + description: text/event-stream or JSON completion + "402": + description: Insufficient quota + "502": + description: Upstream unavailable + + /api/tokens: + get: + tags: [User] + security: [{ bearerJWT: [] }] + summary: List API tokens for current user + parameters: + - in: query + name: page + schema: { type: integer, default: 1 } + - in: query + name: page_size + schema: { type: integer, default: 10 } + responses: + "200": + description: Paginated tokens (masked) + post: + tags: [User] + security: [{ bearerJWT: [] }] + summary: Create API token (plaintext returned once) + responses: + "201": + description: Created with api_key + + /api/tokens/batch-delete: + post: + tags: [User] + security: [{ bearerJWT: [] }] + summary: Batch delete tokens by id + responses: + "200": + description: Deleted count + + /api/tokens/{id}: + put: + tags: [User] + security: [{ bearerJWT: [] }] + summary: Update token name, status, group, quota, models, IP whitelist + parameters: + - in: path + name: id + required: true + schema: { type: integer, format: int64 } + responses: + "200": + description: Updated + delete: + tags: [User] + security: [{ bearerJWT: [] }] + summary: Delete token + parameters: + - in: path + name: id + required: true + schema: { type: integer, format: int64 } + responses: + "200": + description: OK + + /user/self: + get: + tags: [User] + security: [{ bearerJWT: [] }] + summary: Current user profile (portal) + responses: + "200": + description: Profile + "401": + description: Missing or invalid JWT + + /api/user/self: + get: + tags: [User] + security: [{ bearerJWT: [] }] + summary: Alias of GET /user/self for portal clients + responses: + "200": + description: Profile + "401": + description: Missing or invalid JWT + /user/api-keys: post: tags: [User] @@ -342,9 +637,10 @@ components: LoginRequest: type: object - required: [email, password] + required: [password] properties: email: { type: string, format: email } + username: { type: string, description: Login by username when email omitted } password: { type: string } LoginResponse: diff --git a/backend/internal/repository/apikey.go b/backend/internal/repository/apikey.go index a24345f..6e662f4 100644 --- a/backend/internal/repository/apikey.go +++ b/backend/internal/repository/apikey.go @@ -2,6 +2,8 @@ package repository import ( "errors" + "net" + "strings" "golang.org/x/crypto/bcrypt" @@ -9,20 +11,33 @@ import ( ) var ( - ErrInvalidAPIKey = errors.New("invalid api key") - ErrAPIKeyLookup = errors.New("api key lookup failed") - ErrUserInactive = errors.New("user inactive") + ErrInvalidAPIKey = errors.New("invalid api key") + ErrAPIKeyLookup = errors.New("api key lookup failed") + ErrUserInactive = errors.New("user inactive") + ErrAPIKeyDisabled = errors.New("api key disabled") + ErrAPIKeyIPDenied = errors.New("ip not allowed") + ErrAPIKeyQuota = errors.New("api key quota exceeded") ) -// AuthenticateGatewayAPIKey resolves `sk-...` to an active user and returns allowed models (empty = all). -func (r *Repos) AuthenticateGatewayAPIKey(raw string, prefixLen int) (userID, keyID int64, models []string, err error) { +// GatewayKeyAuth is the resolved gateway API key context. +type GatewayKeyAuth struct { + UserID int64 + KeyID int64 + Models []string + QuotaLimit *int64 + UsedQuota int64 + IPWhitelist []string +} + +// AuthenticateGatewayAPIKey resolves `sk-...` to an active user and key policy. +func (r *Repos) AuthenticateGatewayAPIKey(raw string, prefixLen int, clientIP string) (*GatewayKeyAuth, error) { if len(raw) < prefixLen { - return 0, 0, nil, ErrInvalidAPIKey + return nil, ErrInvalidAPIKey } prefix := raw[:prefixLen] var keys []model.APIKey - if err := r.DB.Where("key_prefix = ? AND status = ?", prefix, model.APIKeyStatusActive).Find(&keys).Error; err != nil { - return 0, 0, nil, ErrAPIKeyLookup + if err := r.DB.Where("key_prefix = ?", prefix).Find(&keys).Error; err != nil { + return nil, ErrAPIKeyLookup } var matched *model.APIKey for i := range keys { @@ -32,11 +47,54 @@ func (r *Repos) AuthenticateGatewayAPIKey(raw string, prefixLen int) (userID, ke } } if matched == nil { - return 0, 0, nil, ErrInvalidAPIKey + return nil, ErrInvalidAPIKey + } + if matched.Status != model.APIKeyStatusActive { + return nil, ErrAPIKeyDisabled } var u model.User if err := r.DB.First(&u, matched.UserID).Error; err != nil || u.Status != model.UserStatusActive { - return 0, 0, nil, ErrUserInactive + return nil, ErrUserInactive + } + whitelist := []string(matched.IPWhitelist) + if len(whitelist) > 0 && !ipAllowed(clientIP, whitelist) { + return nil, ErrAPIKeyIPDenied + } + if matched.QuotaLimit != nil && matched.UsedQuota >= *matched.QuotaLimit { + return nil, ErrAPIKeyQuota + } + return &GatewayKeyAuth{ + UserID: u.ID, + KeyID: matched.ID, + Models: []string(matched.Models), + QuotaLimit: matched.QuotaLimit, + UsedQuota: matched.UsedQuota, + IPWhitelist: whitelist, + }, nil +} + +func ipAllowed(clientIP string, whitelist []string) bool { + if clientIP == "" { + return false + } + ip := net.ParseIP(clientIP) + if ip == nil { + return false + } + for _, entry := range whitelist { + entry = strings.TrimSpace(entry) + if entry == "" { + continue + } + if _, cidr, err := net.ParseCIDR(entry); err == nil { + if cidr.Contains(ip) { + return true + } + continue + } + if host := net.ParseIP(entry); host != nil && host.Equal(ip) { + return true + } } - return u.ID, matched.ID, []string(matched.Models), nil + return false } diff --git a/backend/internal/repository/apikey_admin.go b/backend/internal/repository/apikey_admin.go new file mode 100644 index 0000000..fa2a622 --- /dev/null +++ b/backend/internal/repository/apikey_admin.go @@ -0,0 +1,54 @@ +package repository + +import ( + "errors" + + "github.com/leno23/ai-api-gateway/internal/model" + "gorm.io/gorm" +) + +var ErrAPIKeyNotFound = errors.New("api key not found") + +func (r *Repos) ListAPIKeysByUser(userID int64, page, pageSize int) ([]model.APIKey, int64, error) { + q := r.DB.Model(&model.APIKey{}).Where("user_id = ?", userID) + var total int64 + if err := q.Count(&total).Error; err != nil { + return nil, 0, err + } + if page < 1 { + page = 1 + } + if pageSize < 1 { + pageSize = 10 + } + if pageSize > 100 { + pageSize = 100 + } + var rows []model.APIKey + if err := q.Order("id desc"). + Offset((page - 1) * pageSize). + Limit(pageSize). + Find(&rows).Error; err != nil { + return nil, 0, err + } + return rows, total, nil +} + +func (r *Repos) GetAPIKeyForUser(userID, keyID int64) (*model.APIKey, error) { + var k model.APIKey + if err := r.DB.Where("id = ? AND user_id = ?", keyID, userID).First(&k).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrAPIKeyNotFound + } + return nil, err + } + return &k, nil +} + +func (r *Repos) DeleteAPIKeysForUser(userID int64, ids []int64) (int64, error) { + if len(ids) == 0 { + return 0, nil + } + res := r.DB.Where("user_id = ? AND id IN ?", userID, ids).Delete(&model.APIKey{}) + return res.RowsAffected, res.Error +} diff --git a/backend/internal/repository/catalog.go b/backend/internal/repository/catalog.go new file mode 100644 index 0000000..6ce7a93 --- /dev/null +++ b/backend/internal/repository/catalog.go @@ -0,0 +1,80 @@ +package repository + +import ( + "strings" + + "github.com/leno23/ai-api-gateway/internal/model" +) + +type CatalogFilter struct { + Provider string + EndpointType string + BillingType *int16 + Tag string + Query string + Page int + PageSize int +} + +func (r *Repos) ListCatalogModels(f CatalogFilter) ([]model.ModelPrice, int64, error) { + q := r.DB.Model(&model.ModelPrice{}) + if f.Provider != "" { + q = q.Where("provider = ?", f.Provider) + } + if f.EndpointType != "" { + q = q.Where("endpoint_type = ?", f.EndpointType) + } + if f.BillingType != nil { + q = q.Where("billing_type = ?", *f.BillingType) + } + if f.Tag != "" { + q = q.Where("? = ANY(tags)", f.Tag) + } + if f.Query != "" { + like := "%" + strings.ToLower(f.Query) + "%" + q = q.Where( + "LOWER(model) LIKE ? OR LOWER(COALESCE(display_name, '')) LIKE ?", + like, like, + ) + } + var total int64 + if err := q.Count(&total).Error; err != nil { + return nil, 0, err + } + page := f.Page + if page < 1 { + page = 1 + } + size := f.PageSize + if size < 1 { + size = 20 + } + if size > 200 { + size = 200 + } + var rows []model.ModelPrice + if err := q.Order("provider asc, model asc"). + Offset((page - 1) * size). + Limit(size). + Find(&rows).Error; err != nil { + return nil, 0, err + } + return rows, total, nil +} + +func (r *Repos) ModelPriceByName(modelName string) (*model.ModelPrice, error) { + var mp model.ModelPrice + if err := r.DB.Where("model = ?", modelName).First(&mp).Error; err != nil { + return nil, err + } + return &mp, nil +} + +func (r *Repos) DistinctProviders() ([]string, error) { + var providers []string + err := r.DB.Model(&model.ModelPrice{}). + Distinct("provider"). + Order("provider asc"). + Pluck("provider", &providers).Error + return providers, err +} diff --git a/backend/internal/repository/dashboard.go b/backend/internal/repository/dashboard.go new file mode 100644 index 0000000..98d1000 --- /dev/null +++ b/backend/internal/repository/dashboard.go @@ -0,0 +1,145 @@ +package repository + +import ( + "time" + + "github.com/leno23/ai-api-gateway/internal/model" +) + +type TimePoint struct { + T string `json:"t"` + V int64 `json:"v"` +} + +type ModelAgg struct { + Model string `json:"model"` + Cost int64 `json:"cost"` + Count int64 `json:"count"` +} + +type HourlyAgg struct { + Hour string `json:"hour"` + Count int64 `json:"count"` + Cost int64 `json:"cost"` + Tokens int64 `json:"tokens"` +} + +func parseRange(rangeKey string) time.Duration { + switch rangeKey { + case "24h", "1d": + return 24 * time.Hour + case "30d": + return 30 * 24 * time.Hour + default: + return 7 * 24 * time.Hour + } +} + +func (r *Repos) DashboardStats(userID int64) (map[string]any, error) { + var u model.User + if err := r.DB.First(&u, userID).Error; err != nil { + return nil, err + } + since24h := time.Now().Add(-24 * time.Hour) + since1m := time.Now().Add(-time.Minute) + + type agg struct { + Cnt int64 + Tokens int64 + Cost int64 + AvgLatency float64 + } + var a24 agg + _ = r.DB.Model(&model.RequestLog{}). + Select(`COUNT(*) AS cnt, COALESCE(SUM(total_tokens),0) AS tokens, COALESCE(SUM(cost_quota),0) AS cost, COALESCE(AVG(latency_ms),0) AS avg_latency`). + Where("user_id = ? AND created_at >= ?", userID, since24h). + Scan(&a24).Error + + var rpm int64 + _ = r.DB.Model(&model.RequestLog{}).Where("user_id = ? AND created_at >= ?", userID, since1m).Count(&rpm).Error + + var tpm int64 + _ = r.DB.Model(&model.RequestLog{}). + Where("user_id = ? AND created_at >= ?", userID, since1m). + Select("COALESCE(SUM(total_tokens),0)").Scan(&tpm).Error + + var sparkReq []TimePoint + _ = r.DB.Raw(` + SELECT to_char(date_trunc('hour', created_at), 'YYYY-MM-DD"T"HH24:00:00Z') AS t, + COUNT(*)::bigint AS v + FROM request_logs + WHERE user_id = ? AND created_at >= ? + GROUP BY 1 ORDER BY 1 + `, userID, since24h).Scan(&sparkReq).Error + + var sparkCost []TimePoint + _ = r.DB.Raw(` + SELECT to_char(date_trunc('hour', created_at), 'YYYY-MM-DD"T"HH24:00:00Z') AS t, + COALESCE(SUM(cost_quota),0)::bigint AS v + FROM request_logs + WHERE user_id = ? AND created_at >= ? + GROUP BY 1 ORDER BY 1 + `, userID, since24h).Scan(&sparkCost).Error + + groupSlug := "default" + if u.TokenGroupID != nil { + var g model.TokenGroup + if err := r.DB.First(&g, *u.TokenGroupID).Error; err == nil { + groupSlug = g.Slug + } + } + + return map[string]any{ + "account": map[string]any{ + "quota": u.Quota, + "used_quota": u.UsedQuota, + "group": groupSlug, + }, + "usage": map[string]any{ + "request_count_24h": a24.Cnt, + "total_tokens_24h": a24.Tokens, + }, + "consumption": map[string]any{ + "cost_quota_24h": a24.Cost, + }, + "performance": map[string]any{ + "avg_latency_ms": int64(a24.AvgLatency), + "rpm": rpm, + "tpm": tpm, + }, + "sparklines": map[string]any{ + "requests": sparkReq, + "cost": sparkCost, + }, + }, nil +} + +func (r *Repos) DashboardCharts(userID int64, rangeKey string) (map[string]any, error) { + since := time.Now().Add(-parseRange(rangeKey)) + + var byModel []ModelAgg + _ = r.DB.Raw(` + SELECT model, COALESCE(SUM(cost_quota),0)::bigint AS cost, COUNT(*)::bigint AS count + FROM request_logs + WHERE user_id = ? AND created_at >= ? AND model <> '' + GROUP BY model ORDER BY cost DESC LIMIT 20 + `, userID, since).Scan(&byModel).Error + + var trend []HourlyAgg + _ = r.DB.Raw(` + SELECT to_char(date_trunc('hour', created_at), 'YYYY-MM-DD"T"HH24:00:00Z') AS hour, + COUNT(*)::bigint AS count, + COALESCE(SUM(cost_quota),0)::bigint AS cost, + COALESCE(SUM(total_tokens),0)::bigint AS tokens + FROM request_logs + WHERE user_id = ? AND created_at >= ? + GROUP BY 1 ORDER BY 1 + `, userID, since).Scan(&trend).Error + + return map[string]any{ + "consumption_by_model": byModel, + "call_trend": trend, + "call_distribution": byModel, + "ranking": byModel, + }, nil +} diff --git a/backend/internal/repository/logs.go b/backend/internal/repository/logs.go new file mode 100644 index 0000000..a2cd2d0 --- /dev/null +++ b/backend/internal/repository/logs.go @@ -0,0 +1,110 @@ +package repository + +import ( + "strings" + "time" + + "github.com/leno23/ai-api-gateway/internal/model" +) + +type UsageLogFilter struct { + UserID int64 + Start *time.Time + End *time.Time + TokenName string + Model string + RequestID string + TokenGroup string + Page int + PageSize int +} + +func (r *Repos) ListUsageLogs(f UsageLogFilter) ([]model.RequestLog, int64, error) { + q := r.DB.Model(&model.RequestLog{}).Where("user_id = ?", f.UserID) + if f.Start != nil { + q = q.Where("created_at >= ?", *f.Start) + } + if f.End != nil { + q = q.Where("created_at <= ?", *f.End) + } + if f.TokenName != "" { + q = q.Where("token_name ILIKE ?", "%"+f.TokenName+"%") + } + if f.Model != "" { + q = q.Where("model ILIKE ?", "%"+f.Model+"%") + } + if f.RequestID != "" { + q = q.Where("request_id = ?", f.RequestID) + } + if f.TokenGroup != "" { + q = q.Where("token_group = ?", f.TokenGroup) + } + var total int64 + if err := q.Count(&total).Error; err != nil { + return nil, 0, err + } + page := f.Page + if page < 1 { + page = 1 + } + size := f.PageSize + if size < 1 { + size = 10 + } + if size > 100 { + size = 100 + } + var rows []model.RequestLog + if err := q.Order("created_at desc"). + Offset((page - 1) * size). + Limit(size). + Find(&rows).Error; err != nil { + return nil, 0, err + } + return rows, total, nil +} + +type TaskLogFilter struct { + UserID int64 + Start *time.Time + End *time.Time + TaskID string + Page int + PageSize int +} + +func (r *Repos) ListTaskLogs(f TaskLogFilter) ([]model.TaskLog, int64, error) { + q := r.DB.Model(&model.TaskLog{}).Where("user_id = ?", f.UserID) + if f.Start != nil { + q = q.Where("submitted_at >= ?", *f.Start) + } + if f.End != nil { + q = q.Where("submitted_at <= ?", *f.End) + } + if f.TaskID != "" { + q = q.Where("task_id ILIKE ?", "%"+strings.TrimSpace(f.TaskID)+"%") + } + var total int64 + if err := q.Count(&total).Error; err != nil { + return nil, 0, err + } + page := f.Page + if page < 1 { + page = 1 + } + size := f.PageSize + if size < 1 { + size = 10 + } + if size > 100 { + size = 100 + } + var rows []model.TaskLog + if err := q.Order("submitted_at desc"). + Offset((page - 1) * size). + Limit(size). + Find(&rows).Error; err != nil { + return nil, 0, err + } + return rows, total, nil +} diff --git a/backend/internal/repository/tokengroup.go b/backend/internal/repository/tokengroup.go new file mode 100644 index 0000000..1cfca1e --- /dev/null +++ b/backend/internal/repository/tokengroup.go @@ -0,0 +1,33 @@ +package repository + +import "github.com/leno23/ai-api-gateway/internal/model" + +func (r *Repos) ListTokenGroups() ([]model.TokenGroup, error) { + var rows []model.TokenGroup + if err := r.DB.Where("status = ?", 1).Order("id asc").Find(&rows).Error; err != nil { + return nil, err + } + return rows, nil +} + +func (r *Repos) TokenGroupBySlug(slug string) (*model.TokenGroup, error) { + var g model.TokenGroup + if err := r.DB.Where("slug = ? AND status = ?", slug, 1).First(&g).Error; err != nil { + return nil, err + } + return &g, nil +} + +func (r *Repos) TokenGroupForUser(userID int64) (*model.TokenGroup, error) { + var u model.User + if err := r.DB.Select("token_group_id").First(&u, userID).Error; err != nil { + return nil, err + } + if u.TokenGroupID != nil { + var g model.TokenGroup + if err := r.DB.First(&g, *u.TokenGroupID).Error; err == nil { + return &g, nil + } + } + return r.TokenGroupBySlug("default") +} diff --git a/backend/internal/router/router.go b/backend/internal/router/router.go index 5c5356b..2bc5760 100644 --- a/backend/internal/router/router.go +++ b/backend/internal/router/router.go @@ -19,6 +19,9 @@ import ( func New(cfg *config.Config, db *gorm.DB, rdb *redis.Client, log *zap.Logger) *gin.Engine { r := gin.New() r.Use(gin.Logger(), gin.Recovery()) + if cfg.AppEnv != "production" { + r.Use(middleware.CORS()) + } repos := repository.New(db) @@ -49,11 +52,70 @@ func New(cfg *config.Config, db *gorm.DB, rdb *redis.Client, log *zap.Logger) *g r.POST("/auth/register", handler.Register(authDeps)) r.POST("/auth/login", handler.Login(authDeps)) + annDeps := handler.AnnouncementDeps{DB: db} + r.GET("/api/announcements", handler.ListAnnouncements(annDeps)) + + catalogDeps := handler.CatalogDeps{Repos: repos} + r.GET("/api/models", handler.ListCatalogModels(catalogDeps)) + r.GET("/api/models/:model/price", handler.GetModelPrice(catalogDeps)) + r.GET("/api/token-groups", handler.ListTokenGroups(catalogDeps)) + u := r.Group("/user") u.Use(middleware.JWTAuth([]byte(cfg.JWTSecret))) + u.GET("/self", handler.UserSelf(authDeps)) u.POST("/api-keys", handler.CreateAPIKey(keyDeps)) u.POST("/redeem", handler.RedeemUser(redeemDeps)) + apiUser := r.Group("/api/user") + apiUser.Use(middleware.JWTAuth([]byte(cfg.JWTSecret))) + apiUser.GET("/self", handler.UserSelf(authDeps)) + + dashDeps := handler.DashboardDeps{ + Repos: repos, + Nodes: handler.ParsePortalNodes(cfg.PortalAPINodesJSON), + } + apiDash := r.Group("/api/dashboard") + apiDash.Use(middleware.JWTAuth([]byte(cfg.JWTSecret))) + apiDash.GET("/stats", handler.DashboardStats(dashDeps)) + apiDash.GET("/charts", handler.DashboardCharts(dashDeps)) + apiDash.GET("/nodes", handler.DashboardNodes(dashDeps)) + + apiNodes := r.Group("/api/nodes") + apiNodes.Use(middleware.JWTAuth([]byte(cfg.JWTSecret))) + apiNodes.GET("/ping", handler.NodesPing(dashDeps)) + + apiLogs := r.Group("/api/logs") + apiLogs.Use(middleware.JWTAuth([]byte(cfg.JWTSecret))) + apiLogs.GET("/usage", handler.ListUsageLogs(dashDeps)) + apiLogs.GET("/tasks", handler.ListTaskLogs(dashDeps)) + + walletDeps := handler.WalletDeps{ + DB: db, + Repos: repos, + RechargeEnabled: cfg.RechargeEnabled, + AffiliateBPS: cfg.AffiliateRechargeBPS, + PortalOrigin: cfg.PortalOrigin, + } + apiWallet := r.Group("/api/wallet") + apiWallet.Use(middleware.JWTAuth([]byte(cfg.JWTSecret))) + apiWallet.GET("/summary", handler.WalletSummary(walletDeps)) + apiWallet.POST("/redeem", handler.RedeemWithAffiliate(walletDeps)) + apiWallet.POST("/affiliate/transfer", handler.TransferAffiliate(walletDeps)) + apiWallet.POST("/recharge/mock", handler.MockRecharge(walletDeps)) + + tokenDeps := handler.TokenDeps{DB: db, Repos: repos} + apiTokens := r.Group("/api/tokens") + apiTokens.Use(middleware.JWTAuth([]byte(cfg.JWTSecret))) + apiTokens.GET("", handler.ListTokens(tokenDeps)) + apiTokens.POST("", handler.CreateToken(tokenDeps)) + apiTokens.POST("/batch-delete", handler.BatchDeleteTokens(tokenDeps)) + apiTokens.PUT("/:id", handler.UpdateToken(tokenDeps)) + apiTokens.DELETE("/:id", handler.DeleteToken(tokenDeps)) + + apiPlayground := r.Group("/api/playground") + apiPlayground.Use(middleware.JWTAuth([]byte(cfg.JWTSecret))) + apiPlayground.POST("/chat", handler.PlaygroundChat(gwDeps)) + a := r.Group("/admin") a.Use(middleware.JWTAuth([]byte(cfg.JWTSecret)), middleware.RequireAdmin()) a.POST("/redeem/batch", handler.BatchRedeem(adminDeps)) diff --git a/backend/internal/service/affiliate.go b/backend/internal/service/affiliate.go new file mode 100644 index 0000000..96aeecd --- /dev/null +++ b/backend/internal/service/affiliate.go @@ -0,0 +1,27 @@ +package service + +import ( + "gorm.io/gorm" + + "github.com/leno23/ai-api-gateway/internal/model" +) + +// CreditAffiliateOnRecharge adds a share of recharge quota to the inviter's pending balance. +func CreditAffiliateOnRecharge(tx *gorm.DB, inviteeID int64, rechargeQuota int64, bps int) error { + if bps <= 0 || rechargeQuota <= 0 { + return nil + } + var invitee model.User + if err := tx.Select("invited_by").First(&invitee, inviteeID).Error; err != nil { + return nil + } + if invitee.InvitedBy == nil { + return nil + } + bonus := rechargeQuota * int64(bps) / 10000 + if bonus <= 0 { + return nil + } + return tx.Model(&model.User{}).Where("id = ?", *invitee.InvitedBy). + UpdateColumn("affiliate_pending", gorm.Expr("affiliate_pending + ?", bonus)).Error +} diff --git a/backend/internal/service/catalog.go b/backend/internal/service/catalog.go new file mode 100644 index 0000000..6269c13 --- /dev/null +++ b/backend/internal/service/catalog.go @@ -0,0 +1,61 @@ +package service + +import ( + "math" + + "github.com/leno23/ai-api-gateway/internal/model" +) + +// Quota per 1M tokens = per-1K price * 1000 (internal integer quota units). +func quotaPerMillion(per1K int64) int64 { + return per1K * 1000 +} + +func applyMultiplier(v int64, multiplier float64) int64 { + if multiplier <= 0 { + multiplier = 1 + } + return int64(math.Round(float64(v) * multiplier)) +} + +type ModelPriceBreakdown struct { + InputPerMillion int64 `json:"input_per_million"` + OutputPerMillion int64 `json:"output_per_million"` + CacheReadPerMillion int64 `json:"cache_read_per_million"` + CacheWritePerMillion int64 `json:"cache_write_per_million"` + UnitPrice int64 `json:"unit_price"` + Multiplier float64 `json:"multiplier"` +} + +func ModelPriceBreakdownFrom(mp model.ModelPrice, multiplier float64) ModelPriceBreakdown { + if multiplier <= 0 { + multiplier = 1 + } + in := quotaPerMillion(mp.PromptPrice) + out := quotaPerMillion(mp.CompletionPrice) + cr := quotaPerMillion(mp.CacheReadPrice) + cw := quotaPerMillion(mp.CacheWritePrice) + unit := mp.UnitPrice + if multiplier != 1 { + in = applyMultiplier(in, multiplier) + out = applyMultiplier(out, multiplier) + cr = applyMultiplier(cr, multiplier) + cw = applyMultiplier(cw, multiplier) + unit = applyMultiplier(unit, multiplier) + } + return ModelPriceBreakdown{ + InputPerMillion: in, + OutputPerMillion: out, + CacheReadPerMillion: cr, + CacheWritePerMillion: cw, + UnitPrice: unit, + Multiplier: multiplier, + } +} + +func BillingLabel(t int16) string { + if t == 2 { + return "按次计费" + } + return "按量计费" +} diff --git a/backend/internal/service/catalog_test.go b/backend/internal/service/catalog_test.go new file mode 100644 index 0000000..366863a --- /dev/null +++ b/backend/internal/service/catalog_test.go @@ -0,0 +1,23 @@ +package service + +import ( + "testing" + + "github.com/leno23/ai-api-gateway/internal/model" +) + +func TestModelPriceBreakdownFrom_multiplier(t *testing.T) { + mp := model.ModelPrice{ + PromptPrice: 9000, + CompletionPrice: 27000, + CacheReadPrice: 4500, + } + base := ModelPriceBreakdownFrom(mp, 1) + if base.InputPerMillion != 9_000_000 { + t.Fatalf("input base: got %d", base.InputPerMillion) + } + applied := ModelPriceBreakdownFrom(mp, 1.5) + if applied.InputPerMillion != 13_500_000 { + t.Fatalf("input 1.5x: got %d", applied.InputPerMillion) + } +} diff --git a/backend/migrations/003_token_groups_catalog.sql b/backend/migrations/003_token_groups_catalog.sql new file mode 100644 index 0000000..6378577 --- /dev/null +++ b/backend/migrations/003_token_groups_catalog.sql @@ -0,0 +1,65 @@ +-- Token groups + model catalog fields for portal /pricing + +CREATE TABLE IF NOT EXISTS token_groups ( + id BIGSERIAL PRIMARY KEY, + slug VARCHAR(64) UNIQUE NOT NULL, + name VARCHAR(128) NOT NULL, + multiplier NUMERIC(6, 2) NOT NULL DEFAULT 1.00, + status SMALLINT NOT NULL DEFAULT 1, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +ALTER TABLE users + ADD COLUMN IF NOT EXISTS token_group_id BIGINT REFERENCES token_groups (id); + +CREATE INDEX IF NOT EXISTS idx_users_token_group_id ON users (token_group_id); + +ALTER TABLE model_prices + ADD COLUMN IF NOT EXISTS provider VARCHAR(32) NOT NULL DEFAULT 'openai', + ADD COLUMN IF NOT EXISTS endpoint_type VARCHAR(32) NOT NULL DEFAULT 'openai', + ADD COLUMN IF NOT EXISTS cache_read_price BIGINT NOT NULL DEFAULT 0, + ADD COLUMN IF NOT EXISTS cache_write_price BIGINT NOT NULL DEFAULT 0, + ADD COLUMN IF NOT EXISTS tags TEXT[] NOT NULL DEFAULT '{}', + ADD COLUMN IF NOT EXISTS display_name VARCHAR(128); + +INSERT INTO token_groups (slug, name, multiplier, status) +VALUES + ('default', 'default', 1.00, 1), + ('vip', 'vip', 1.00, 1), + ('claude_code', 'claude_code', 1.50, 1), + ('aws', 'AWS分组', 3.00, 1), + ('guanzhuan_max', '官转max', 2.50, 1) +ON CONFLICT (slug) DO NOTHING; + +UPDATE users +SET token_group_id = (SELECT id FROM token_groups WHERE slug = 'default' LIMIT 1) +WHERE token_group_id IS NULL; + +-- Seed catalog rows when table is empty (dev/demo) +INSERT INTO model_prices ( + model, display_name, provider, endpoint_type, + prompt_price, completion_price, cache_read_price, cache_write_price, + unit_price, billing_type, tags +) +SELECT v.model, v.display_name, v.provider, v.endpoint_type, + v.prompt_price, v.completion_price, v.cache_read_price, v.cache_write_price, + v.unit_price, v.billing_type, v.tags +FROM (VALUES + ('gpt-4o', 'GPT-4o', 'openai', 'openai', 9000::bigint, 27000::bigint, 4500::bigint, 0::bigint, 0::bigint, 1::smallint, ARRAY['chat']::text[]), + ('gpt-4o-mini', 'GPT-4o mini', 'openai', 'openai', 750::bigint, 3000::bigint, 0::bigint, 0::bigint, 0::bigint, 1::smallint, ARRAY['chat']::text[]), + ('claude-sonnet-4-20250514', 'Claude Sonnet 4', 'anthropic', 'anthropic', 15000::bigint, 75000::bigint, 7500::bigint, 0::bigint, 0::bigint, 1::smallint, ARRAY['chat']::text[]), + ('claude-4-opus', 'Claude 4 Opus', 'anthropic', 'anthropic', 75000::bigint, 375000::bigint, 0::bigint, 0::bigint, 0::bigint, 1::smallint, ARRAY['chat']::text[]), + ('deepseek-v3', 'DeepSeek V3', 'deepseek', 'openai', 1350::bigint, 5500::bigint, 0::bigint, 0::bigint, 0::bigint, 1::smallint, ARRAY['chat']::text[]), + ('dall-e-3', 'DALL·E 3', 'openai', 'openai', 0::bigint, 0::bigint, 0::bigint, 0::bigint, 20000::bigint, 2::smallint, ARRAY['image']::text[]) +) AS v(model, display_name, provider, endpoint_type, prompt_price, completion_price, cache_read_price, cache_write_price, unit_price, billing_type, tags) +WHERE NOT EXISTS (SELECT 1 FROM model_prices LIMIT 1); + +-- Backfill provider/endpoint for existing rows +UPDATE model_prices SET provider = 'anthropic', endpoint_type = 'anthropic' +WHERE provider = 'openai' AND (model ILIKE 'claude%' OR model ILIKE '%anthropic%'); + +UPDATE model_prices SET provider = 'deepseek', endpoint_type = 'openai' +WHERE provider = 'openai' AND model ILIKE 'deepseek%'; + +UPDATE model_prices SET display_name = model WHERE display_name IS NULL OR display_name = ''; diff --git a/backend/migrations/004_api_tokens_extend.sql b/backend/migrations/004_api_tokens_extend.sql new file mode 100644 index 0000000..b85c802 --- /dev/null +++ b/backend/migrations/004_api_tokens_extend.sql @@ -0,0 +1,11 @@ +-- Extend api_keys for portal token management + +ALTER TABLE api_keys + ADD COLUMN IF NOT EXISTS token_group_id BIGINT REFERENCES token_groups (id), + ADD COLUMN IF NOT EXISTS quota_limit BIGINT, + ADD COLUMN IF NOT EXISTS used_quota BIGINT NOT NULL DEFAULT 0, + ADD COLUMN IF NOT EXISTS ip_whitelist TEXT[] NOT NULL DEFAULT '{}'; + +UPDATE api_keys +SET token_group_id = (SELECT id FROM token_groups WHERE slug = 'default' LIMIT 1) +WHERE token_group_id IS NULL; diff --git a/backend/migrations/005_dashboard_logs.sql b/backend/migrations/005_dashboard_logs.sql new file mode 100644 index 0000000..79c1a25 --- /dev/null +++ b/backend/migrations/005_dashboard_logs.sql @@ -0,0 +1,28 @@ +-- Dashboard / usage logs / async task logs + +ALTER TABLE request_logs + ADD COLUMN IF NOT EXISTS request_id VARCHAR(64), + ADD COLUMN IF NOT EXISTS token_name VARCHAR(128), + ADD COLUMN IF NOT EXISTS token_group VARCHAR(64), + ADD COLUMN IF NOT EXISTS time_to_first_ms INT, + ADD COLUMN IF NOT EXISTS billing_detail JSONB; + +CREATE INDEX IF NOT EXISTS idx_request_logs_request_id ON request_logs (request_id); +CREATE INDEX IF NOT EXISTS idx_request_logs_token_name ON request_logs (user_id, token_name); + +CREATE TABLE IF NOT EXISTS task_logs ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT NOT NULL REFERENCES users (id) ON DELETE CASCADE, + task_id VARCHAR(128) NOT NULL, + platform VARCHAR(64) NOT NULL DEFAULT '', + task_type VARCHAR(64) NOT NULL DEFAULT '', + status VARCHAR(32) NOT NULL DEFAULT 'pending', + progress INT NOT NULL DEFAULT 0, + detail TEXT, + submitted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + finished_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_task_logs_user_submitted ON task_logs (user_id, submitted_at DESC); +CREATE INDEX IF NOT EXISTS idx_task_logs_task_id ON task_logs (user_id, task_id); diff --git a/backend/migrations/006_wallet_announcements.sql b/backend/migrations/006_wallet_announcements.sql new file mode 100644 index 0000000..238a682 --- /dev/null +++ b/backend/migrations/006_wallet_announcements.sql @@ -0,0 +1,26 @@ +-- Wallet affiliate pending + system announcements + +ALTER TABLE users + ADD COLUMN IF NOT EXISTS affiliate_pending BIGINT NOT NULL DEFAULT 0; + +CREATE TABLE IF NOT EXISTS announcements ( + id BIGSERIAL PRIMARY KEY, + title VARCHAR(256) NOT NULL, + content TEXT NOT NULL, + level VARCHAR(32) NOT NULL DEFAULT 'info', + placement VARCHAR(64) NOT NULL DEFAULT 'home', + status SMALLINT NOT NULL DEFAULT 1, + starts_at TIMESTAMPTZ, + ends_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +INSERT INTO announcements (title, content, level, placement, status) +SELECT + '欢迎使用 AI API Gateway', + '企业级多模型接入网关已上线。注册后即可创建 API 令牌,在模型广场查看价格,于控制台查看用量。', + 'info', + 'home', + 1 +WHERE NOT EXISTS (SELECT 1 FROM announcements LIMIT 1); diff --git a/frontend/.env.example b/frontend/.env.example index d50c1a2..37c0da5 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -1,2 +1,2 @@ # 网关 HTTP 根地址(无尾斜杠)。开发可复制为 .env.local -NEXT_PUBLIC_GATEWAY_API_URL=http://127.0.0.1:8080 +NEXT_PUBLIC_GATEWAY_API_URL=http://127.0.0.1:8081 diff --git a/frontend/.gitignore b/frontend/.gitignore index 7b8da95..cb0e6cb 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -12,6 +12,10 @@ # testing /coverage +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ # next.js /.next/ diff --git a/frontend/README.md b/frontend/README.md index 98e6188..47fad6e 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -30,6 +30,18 @@ Next.js(App Router)+ Ant Design + Tailwind。契约对齐 `../backend/intern | `npm run dev` | 开发 | | `npm run build` / `npm start` | 生产构建与启动 | | `npm run lint` | ESLint | +| `npm run test:e2e` | Playwright 冒烟测试(需网关与 `npm run dev` 已启动) | + +### E2E 冒烟测试 + +先启动 Postgres/Redis、网关(默认 `http://127.0.0.1:8081`)与本前端 dev,再执行: + +```bash +npx playwright install chromium # 首次 +GATEWAY_BASE_URL=http://127.0.0.1:8081 FRONTEND_BASE_URL=http://localhost:3000 npm run test:e2e +``` + +可选环境变量:`ADMIN_EMAIL`、`ADMIN_PASSWORD`(默认 `admin@example.com` / `Admin@12345`)。 ## 规格 diff --git a/frontend/app/admin/layout.tsx b/frontend/app/admin/layout.tsx index d693e18..1dfda1c 100644 --- a/frontend/app/admin/layout.tsx +++ b/frontend/app/admin/layout.tsx @@ -23,7 +23,7 @@ export default function AdminLayout({ if (!ready || !token) { return ( -
+
加载中…
); diff --git a/frontend/app/admin/page.tsx b/frontend/app/admin/page.tsx index b86e866..fb3a039 100644 --- a/frontend/app/admin/page.tsx +++ b/frontend/app/admin/page.tsx @@ -1,3 +1,5 @@ +"use client"; + import { Typography } from "antd"; import Link from "next/link"; diff --git a/frontend/app/globals.css b/frontend/app/globals.css index a2dc41e..5fc5435 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -19,8 +19,28 @@ } } +html, body { - background: var(--background); + height: 100%; +} + +body { + background: #f5f5f5; color: var(--foreground); font-family: Arial, Helvetica, sans-serif; } + +/* Ant Design Layout:侧栏 + 主区占满视口 */ +.ant-layout.ant-layout-has-sider { + height: 100vh; + min-height: 100vh; +} + +.ant-layout.ant-layout-has-sider > .ant-layout-sider { + height: 100%; +} + +.ant-layout.ant-layout-has-sider > .ant-layout { + flex: 1; + min-height: 0; +} diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx index 21559b8..8f4f6cc 100644 --- a/frontend/app/layout.tsx +++ b/frontend/app/layout.tsx @@ -30,7 +30,7 @@ export default function RootLayout({ lang="zh-CN" className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`} > - + {children} diff --git a/frontend/components/admin-shell.tsx b/frontend/components/admin-shell.tsx index 4fe5b68..26ef463 100644 --- a/frontend/components/admin-shell.tsx +++ b/frontend/components/admin-shell.tsx @@ -55,8 +55,14 @@ export function AdminShell({ : "/admin"; return ( - - + +
Gateway 控制台 @@ -64,13 +70,13 @@ export function AdminShell({
- -
+ +
- + {children}
diff --git a/frontend/e2e/api.smoke.spec.ts b/frontend/e2e/api.smoke.spec.ts new file mode 100644 index 0000000..6538466 --- /dev/null +++ b/frontend/e2e/api.smoke.spec.ts @@ -0,0 +1,50 @@ +import { expect, test } from "@playwright/test"; + +const adminEmail = process.env.ADMIN_EMAIL ?? "admin@example.com"; +const adminPassword = process.env.ADMIN_PASSWORD ?? "Admin@12345"; + +test.describe("Gateway API smoke", () => { + test("health returns ok", async ({ request }) => { + const res = await request.get("/health"); + expect(res.ok()).toBeTruthy(); + await expect(res.json()).resolves.toEqual({ status: "ok" }); + }); + + test("openapi spec is served", async ({ request }) => { + const res = await request.get("/openapi.yaml"); + expect(res.ok()).toBeTruthy(); + const body = await res.text(); + expect(body).toContain("openapi: 3.0.3"); + }); + + test("admin login and protected admin routes", async ({ request }) => { + const loginRes = await request.post("/auth/login", { + data: { email: adminEmail, password: adminPassword }, + }); + expect(loginRes.ok()).toBeTruthy(); + const { access_token: token } = (await loginRes.json()) as { + access_token: string; + }; + expect(token.length).toBeGreaterThan(10); + + const headers = { Authorization: `Bearer ${token}` }; + + const channelsRes = await request.get("/admin/channels", { headers }); + expect(channelsRes.ok()).toBeTruthy(); + const channels = (await channelsRes.json()) as { items: unknown[] }; + expect(Array.isArray(channels.items)).toBeTruthy(); + + const redeemStatsRes = await request.get("/admin/redeem/stats", { headers }); + expect(redeemStatsRes.ok()).toBeTruthy(); + + const redeemListRes = await request.get("/admin/redeem/codes?page=1&page_size=10", { + headers, + }); + expect(redeemListRes.ok()).toBeTruthy(); + }); + + test("unauthenticated admin route returns 401", async ({ request }) => { + const res = await request.get("/admin/channels"); + expect(res.status()).toBe(401); + }); +}); diff --git a/frontend/e2e/ui.smoke.spec.ts b/frontend/e2e/ui.smoke.spec.ts new file mode 100644 index 0000000..cca674c --- /dev/null +++ b/frontend/e2e/ui.smoke.spec.ts @@ -0,0 +1,60 @@ +import { expect, test } from "@playwright/test"; + +const adminEmail = process.env.ADMIN_EMAIL ?? "admin@example.com"; +const adminPassword = process.env.ADMIN_PASSWORD ?? "Admin@12345"; + +/** Ant Design 中文按钮文案可能带空格,如「登 录」 */ +const loginButton = (page: import("@playwright/test").Page) => + page.getByRole("button", { name: /登\s*录/ }); + +async function loginAsAdmin(page: import("@playwright/test").Page) { + await page.goto("/login"); + await expect(page.getByRole("heading", { name: "管理员登录" })).toBeVisible(); + await page.getByLabel("邮箱").fill(adminEmail); + await page.getByLabel("密码").fill(adminPassword); + await Promise.all([ + page.waitForURL(/\/admin\/?$/, { timeout: 15_000 }), + loginButton(page).click(), + ]); + await expect(page.getByRole("heading", { name: "概览" })).toBeVisible(); +} + +test.describe("Admin console UI smoke", () => { + test("login page renders", async ({ page }) => { + await page.goto("/login"); + await expect(page.getByRole("heading", { name: "管理员登录" })).toBeVisible(); + await expect(loginButton(page)).toBeEnabled(); + }); + + test("home redirects unauthenticated user to login", async ({ page }) => { + await page.goto("/"); + await expect(page).toHaveURL(/\/login/); + }); + + test("admin login and navigate core pages", async ({ page }) => { + await loginAsAdmin(page); + + await expect(page.getByRole("heading", { name: "概览" })).toBeVisible(); + await expect(page.getByText("健康检查")).toBeVisible(); + + await page.getByRole("link", { name: "渠道管理" }).click(); + await expect(page).toHaveURL(/\/admin\/channels/); + await expect(page.getByRole("heading", { name: "渠道管理" })).toBeVisible(); + await expect(page.getByRole("button", { name: "新建渠道" })).toBeVisible(); + + await page.getByRole("link", { name: "兑换运营" }).click(); + await expect(page).toHaveURL(/\/admin\/redeem/); + await expect(page.getByRole("heading", { name: "兑换运营" })).toBeVisible(); + + await page.getByRole("link", { name: "用户治理" }).click(); + await expect(page).toHaveURL(/\/admin\/users/); + await expect(page.getByRole("heading", { name: "用户治理" })).toBeVisible(); + }); + + test("logout returns to login", async ({ page }) => { + await loginAsAdmin(page); + await page.getByRole("button", { name: /退\s*出/ }).click(); + await expect(page).toHaveURL(/\/login/); + await expect(page.getByRole("heading", { name: "管理员登录" })).toBeVisible(); + }); +}); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index bac5a2c..dfc305b 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -16,6 +16,7 @@ "react-dom": "19.2.4" }, "devDependencies": { + "@playwright/test": "^1.51.1", "@tailwindcss/postcss": "^4", "@types/node": "^20", "@types/react": "^19", @@ -1370,6 +1371,22 @@ "node": ">=12.4.0" } }, + "node_modules/@playwright/test": { + "version": "1.60.0", + "resolved": "https://registry.npmmirror.com/@playwright/test/-/test-1.60.0.tgz", + "integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@rc-component/async-validator": { "version": "5.1.0", "resolved": "https://registry.npmmirror.com/@rc-component/async-validator/-/async-validator-5.1.0.tgz", @@ -4543,6 +4560,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", @@ -6278,6 +6310,38 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/playwright": { + "version": "1.60.0", + "resolved": "https://registry.npmmirror.com/playwright/-/playwright-1.60.0.tgz", + "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.60.0", + "resolved": "https://registry.npmmirror.com/playwright-core/-/playwright-core-1.60.0.tgz", + "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmmirror.com/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 65e1a76..6a9e62a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,7 +6,9 @@ "dev": "next dev", "build": "next build", "start": "next start", - "lint": "eslint" + "lint": "eslint", + "test:e2e": "playwright test", + "test:e2e:ui": "playwright test --ui" }, "dependencies": { "@ant-design/icons": "^6.2.2", @@ -17,6 +19,7 @@ "react-dom": "19.2.4" }, "devDependencies": { + "@playwright/test": "^1.51.1", "@tailwindcss/postcss": "^4", "@types/node": "^20", "@types/react": "^19", diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts new file mode 100644 index 0000000..ed43aa4 --- /dev/null +++ b/frontend/playwright.config.ts @@ -0,0 +1,35 @@ +import { defineConfig, devices } from "@playwright/test"; + +const gatewayBase = process.env.GATEWAY_BASE_URL ?? "http://127.0.0.1:8080"; +const frontendBase = process.env.FRONTEND_BASE_URL ?? "http://localhost:3000"; + +export default defineConfig({ + testDir: "./e2e", + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 1 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: [["list"], ["html", { open: "never" }]], + timeout: 30_000, + use: { + baseURL: frontendBase, + trace: "on-first-retry", + screenshot: "only-on-failure", + }, + projects: [ + { + name: "api", + testMatch: /api\.smoke\.spec\.ts/, + use: { + baseURL: gatewayBase, + }, + }, + { + name: "ui", + testMatch: /ui\.smoke\.spec\.ts/, + use: { + ...devices["Desktop Chrome"], + }, + }, + ], +}); diff --git a/openspec/changes/archive/2026-05-27-gateway-tenant-portal-platform/.openspec.yaml b/openspec/changes/archive/2026-05-27-gateway-tenant-portal-platform/.openspec.yaml new file mode 100644 index 0000000..e7c42ac --- /dev/null +++ b/openspec/changes/archive/2026-05-27-gateway-tenant-portal-platform/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-05-27 diff --git a/openspec/changes/archive/2026-05-27-gateway-tenant-portal-platform/design.md b/openspec/changes/archive/2026-05-27-gateway-tenant-portal-platform/design.md new file mode 100644 index 0000000..111117d --- /dev/null +++ b/openspec/changes/archive/2026-05-27-gateway-tenant-portal-platform/design.md @@ -0,0 +1,149 @@ +## Context + +- **参考产品**:蓝移 API(lanyiapi.com)— 企业级多模型接入网关,含营销站、模型广场、租户控制台、按 Token/按次计费、多区域节点、操练场与钱包/邀请体系(详见 `zhencai/lanyiapi-site-audit/IMPLEMENTATION_PROMPT.md`)。 +- **本仓库现状**: + - 后端:`gateway-foundation-invite-billing` 已实现 `sk-` 网关、`/v1/chat/completions`、额度预扣/结算、兑换/邀请、基础 `POST /user/api-keys`。 + - 前端:`gateway-admin-console-frontend` 已实现 Ant Design **`/admin/*`** 运营台。 +- **缺口**:无 Semi 租户门户、无 `/pricing` 模型广场、无看板/日志/钱包/操练场页面;OpenAPI 无 `/api/dashboard/*`、`/api/logs/*`、令牌完整 CRUD。 + +## Goals / Non-Goals + +**Goals** + +- 交付 **业务等价** 的租户自助体验:开发者注册 → 创建 `sk-` → 模型广场查价 → 看板查用量 → 操练场调试 → 钱包充值/兑换/邀请。 +- **契约驱动**:租户面 API 写入 `backend/internal/openapi/spec.yaml`;路径优先对齐参考站 `/api/*`,与现有 `/auth/*`、`/user/*` 通过 BFF 或别名兼容。 +- **UI 验收**:间距/文案/分区对照 `lanyiapi-site-audit/screenshots/`;登录按钮 **「继续」**;侧栏菜单项与参考站一致。 +- **分 Phase 可合并**:每 Phase 可独立演示(见 `tasks.md`)。 + +**Non-Goals** + +- 蓝移商标、域名、企业微信二维码等运营素材硬编码(用配置/占位)。 +- 生产级 Turnstile、支付(本期 mock + 环境变量开关)。 +- 合并 Admin(Ant Design)与 Portal(Semi)为同一 SPA 路由树(避免双设计系统冲突)。 + +## 仓库布局(建议) + +``` +ai-api-gateway/ + backend/ # 扩展 handler + migrations + frontend/ # 现有 Ant Design 管理端(不变) + portal/ # 新建:Next.js 14 + Semi 租户门户 + app/ + (marketing)/ # /, /pricing, /about, /login, /register + console/ # /console/* + ... + openspec/changes/gateway-tenant-portal-platform/ +``` + +`NEXT_PUBLIC_GATEWAY_API_URL` 指向同一 Go 网关;门户与管理端可同域不同路径或子域部署。 + +## 信息架构(IA) + +### 公开站 + +| 路由 | 页面 | +|------|------| +| `/` | 首页营销 + 公告 Modal | +| `/pricing` | 模型广场 | +| `/about` | 关于 | +| `/register` | 注册(`?aff=`) | +| `/login` | 登录(主按钮「继续」) | + +### 控制台(需登录) + +| 分组 | 菜单 | 路由 | +|------|------|------| +| 聊天 | 操练场 | `/console/playground` | +| 控制台 | 数据看板 | `/console` 或 `/console/dashboard` | +| 控制台 | 令牌管理 | `/console/token` | +| 控制台 | 使用日志 | `/console/log` | +| 控制台 | 任务日志 | `/console/task` | +| 个人中心 | 钱包管理 | `/console/wallet` | +| 个人中心 | 个人设置 | `/console/setting` | + +顶栏(全局):`首页` | `控制台` | `模型广场` | `文档` | `关于` + 通知/主题/语言/用户菜单。 + +## 领域模型(增量) + +在 `gateway-foundation-invite-billing` 已有 `users`、`api_keys`、`model_prices`、`request_logs` 等基础上扩展: + +| 实体 | 说明 | +|------|------| +| `TokenGroup` | 分组名、`multiplier`(如 default 1x、vip、claude_code 1.5x) | +| `ApiToken` | 扩展:分组 ID、quota 上限、enabled、allowed_models[]、ip_whitelist[] | +| `Model` | 展示用元数据:provider、endpoint_type(openai/anthropic)、分项单价 | +| `UsageLog` | 对齐 `request_logs` 或视图:首字耗时、流式标记、分组倍率、计费明细 JSON | +| `TaskLog` | 异步任务:platform、type、status、progress | +| `Announcement` | 公告:级别、正文、生效时间 | +| `Wallet` | 余额字段复用 `users.quota`;充值订单、待结算邀请收益 | + +ER 与迁移在 Phase 1–3 后端任务中落库。 + +## API 分层 + +### 已有(复用) + +- `POST /auth/register`、`POST /auth/login`(JWT) +- `POST /user/api-keys`、`POST /user/redeem` +- `GET /v1/models`、`POST /v1/chat/completions`(`sk-`) + +### 待新增(租户 JWT) + +| 方法 | 路径 | 用途 | +|------|------|------| +| GET | `/api/user/self` | 当前用户 profile、余额、分组 | +| GET/POST/PUT/DELETE | `/api/tokens` | 令牌 CRUD | +| GET | `/api/models` | 模型广场(筛选/分页) | +| GET | `/api/dashboard/stats` | 四卡指标 | +| GET | `/api/dashboard/charts` | 时序图表 | +| GET | `/api/logs/usage` | 使用日志 | +| GET | `/api/logs/tasks` | 任务日志 | +| POST | `/api/playground/chat` | 操练场 SSE | +| GET/POST | `/api/wallet/*` | 充值 mock、兑换、邀请划转 | +| GET | `/api/announcements` | 公告列表 | + +网关面:`POST /v1/messages`(Anthropic)为 P1。 + +### 鉴权约定 + +- 租户 REST:`Authorization: Bearer `(来自 `/auth/login`)。 +- 网关调用:`Authorization: Bearer sk-...`。 +- 未登录访问 `/console/*` → Toast「未登录或登录已过期」→ `/login?expired=true`。 + +## UI/UX(对齐参考站) + +- **Semi Design** 浅色主题;主色 `#007AFF`;卡片圆角 8–12px。 +- 表格:紧凑模式、列设置、默认每页 10 条。 +- 图表:ECharts/Recharts;看板 sparkline。 +- 图标:Lucide React。 + +## 与现有 change 的关系 + +```mermaid +flowchart LR + subgraph done [已实现] + F[gateway-foundation-invite-billing] + A[gateway-admin-console-frontend] + end + subgraph new [本 change] + P[gateway-tenant-portal-platform] + end + F --> P + A -.->|运营配置渠道/兑换| P + P -->|sk- 调用| F +``` + +## Risks + +| 风险 | 缓解 | +|------|------| +| Semi 与 Ant Design 双前端维护成本 | 目录分离 `portal/` vs `frontend/`;共享 OpenAPI 类型生成 | +| 参考站 `/api/*` 与现有 `/auth/*` 不一致 | OpenAPI 同时文档化;实现层 handler 复用 service | +| 日志/看板查询性能 | 分页 + 索引 + 异步写 `request_logs`(已有) | +| 操练场滥用额度 | 独立 playground 令牌或速率限制 | + +## References + +- `openspec/changes/gateway-tenant-portal-platform/proposal.md` +- `zhencai/lanyiapi-site-audit/IMPLEMENTATION_PROMPT.md` +- `openspec/changes/gateway-foundation-invite-billing/design.md` diff --git a/openspec/changes/archive/2026-05-27-gateway-tenant-portal-platform/proposal.md b/openspec/changes/archive/2026-05-27-gateway-tenant-portal-platform/proposal.md new file mode 100644 index 0000000..296fa87 --- /dev/null +++ b/openspec/changes/archive/2026-05-27-gateway-tenant-portal-platform/proposal.md @@ -0,0 +1,102 @@ +## Why + +[蓝移 API(lanyiapi.com)](https://lanyiapi.com/) 类产品的完整形态是 **公开营销站 + 租户控制台 + OpenAI/Anthropic 兼容网关 + 按量计费与运营体系**,而非仅管理员后台。本仓库已完成 **`gateway-foundation-invite-billing`**(Go 网关、额度、`sk-` 鉴权、兑换/邀请)与 **`gateway-admin-console-frontend`**(Ant Design 管理端 `/admin/*`),但 **终端开发者仍无** 模型广场、数据看板、令牌管理、操练场、钱包/邀请等自助能力。 + +依据 `zhencai/lanyiapi-site-audit/IMPLEMENTATION_PROMPT.md` 及同目录 `screenshots/` 巡站结果,需要将 **业务等价的多模型 API 聚合平台(用户门户 + 控制台)** 固化为可评审、可拆任务的 OpenSpec change,并与现有后端能力对齐、显式列出 API 缺口。 + +## What Changes + +### 公开站点(未登录) + +- 路由:`/`(首页营销)、`/pricing`(模型广场)、`/about`、`/register?aff=`、`/login`。 +- 顶栏全局导航:首页 | 控制台 | 模型广场 | 文档 | 关于;通知、主题/语言、登录/注册。 +- 首页:卖点三列、模型 Logo 墙、系统公告 Modal(支持「今日不再提示」`localStorage`)。 +- 登录主按钮文案为 **「继续」**(非「登录」);支持用户名或邮箱(与后端契约协商后实现)。 + +### 租户控制台(登录后 `/console`) + +- 布局:顶栏 + 左侧 Semi Navigation(~240px,可收起)+ 主内容区(背景 `#f6f7f9`)。 +- **操练场** `/console/playground`:分组/模型/采样参数、多轮对话、流式 SSE、调试信息、配置导入导出。 +- **数据看板** `/console`:四指标卡(余额/消耗/请求/RPM·TPM)、模型消耗图表、API 多区域节点(主站/香港/美区,复制与测速)、公告/FAQ/服务可用性。 +- **令牌管理** `/console/token`:`sk-` CRUD、分组倍率、额度、模型白名单、IP 限制、掩码/一次性展示、批量操作。 +- **使用日志** `/console/log`、**任务日志** `/console/task`:分页筛选、列设置、流式首字耗时与计费明细。 +- **钱包管理** `/console/wallet`:余额/历史消耗、在线充值(可关)、兑换码、邀请链接与收益划转。 +- **个人设置** `/console/setting`:账户绑定、通知阈值、语言与显示偏好。 + +### 后端/API 增量(相对现有 OpenAPI) + +- 用户面 REST 风格对齐参考站:`/api/user/*`(或映射到现有 `/auth/*`、`/user/*` 并扩展)。 +- 模型目录:`GET /api/models`(供应商/分组/端点类型筛选、分页)。 +- 令牌:`/api/tokens` CRUD(扩展现有 `POST /user/api-keys` 为完整生命周期)。 +- 看板:`GET /api/dashboard/stats`、`GET /api/dashboard/charts?range=`。 +- 日志:`GET /api/logs/usage`、`GET /api/logs/tasks`。 +- 操练场:`POST /api/playground/chat`(SSE,专用或默认令牌)。 +- 运营:公告 CRUD、通知配置、钱包充值/划转(支付可 mock)。 +- 网关:补充 Anthropic 兼容 `POST /v1/messages`(P1);多区域节点配置与测速接口。 + +### 技术栈(本 change 前端约定) + +- **Next.js 14 App Router** + **Semi Design**(`@douyinfe/semi-ui`)+ TypeScript + Tailwind 辅助 + ECharts/Recharts + Lucide。 +- 与现有 `frontend/`(Ant Design 管理端)**共存**:建议子应用 `portal/` 或 `frontend-portal/` 目录,避免与 `/admin` 设计系统混用;详见 `design.md`。 + +## Capabilities + +### New Capabilities + +- `portal-public-site`: 营销首页、关于、顶栏 IA、公告弹窗、注册/登录(含 `aff` 邀请参数)。 +- `portal-console-shell`: 控制台壳层、侧栏菜单、路由守卫、401 跳转 `/login?expired=true`。 +- `portal-model-pricing`: 模型广场筛选/搜索/卡片与表格视图、价格与倍率展示开关。 +- `portal-token-management`: 令牌全生命周期 UI 与 `sk-` 安全展示策略。 +- `portal-dashboard-analytics`: 数据看板指标卡、图表、API 节点与可用性探测展示。 +- `portal-usage-task-logs`: 使用日志与异步任务日志列表与筛选。 +- `portal-wallet-affiliate`: 钱包、兑换码、邀请链接与收益划转。 +- `portal-playground`: 操练场三栏 UI 与流式对话调试。 +- `portal-user-settings`: 个人设置、通知阈值、第三方绑定占位(按阶段 mock)。 +- `portal-backend-extensions`: 租户面 HTTP API 与领域模型增量(令牌分组、定价展示、日志字段等)。 + +### Modified Capabilities + +- `user-identity-apikeys`(`gateway-foundation-invite-billing`):扩展令牌字段(分组、额度上限、模型白名单、IP 白名单、启用状态)及管理 API。 +- `billing-quota`:对外暴露按模型分项价格(input/output/cache)与分组倍率,供广场与日志展示。 +- `redeem-invite-growth`:钱包页邀请链接格式 `register?aff={code}`、待结算收益与划转到余额(若尚未实现则本 change 定义)。 + +### Out of Scope(另开 change 或 P2) + +- 完整支付渠道(支付宝/微信)生产对接;本期允许环境变量 mock 充值回调。 +- Cloudflare Turnstile 生产接入;开发环境 mock `?turnstile=`。 +- 独立 VitePress 文档站(可先外链或静态 MD)。 +- 管理后台渠道/兑换能力(已由 `gateway-admin-console-frontend` 覆盖)。 + +## Impact + +- **前端**:新增 Semi 租户门户工程(与 Ant Design 管理端分离);UI 对照 `lanyiapi-site-audit/screenshots/` 验收。 +- **后端**:Go 网关新增/扩展 handler、迁移表(`token_groups`、`announcements`、`task_logs` 等);OpenAPI 与 `GET /openapi.yaml` 同步更新。 +- **运维**:多区域 base URL 配置、CORS 增加门户 Origin;可选独立域名(主站/香港/美区)。 +- **依赖**:`gateway-foundation-invite-billing` 必须先可用;`gateway-admin-console-frontend` 无冲突。 + +## Phased Delivery(对齐 IMPLEMENTATION_PROMPT) + +| Phase | 范围 | 本 change 对应 | +|-------|------|----------------| +| 1 | 用户注册登录、路由守卫、公开认证页 | `portal-public-site` + `portal-console-shell`(壳层) | +| 2 | 模型与定价、模型广场 | `portal-model-pricing` + `portal-backend-extensions` | +| 3 | 令牌 CRUD、网关鉴权增强 | `portal-token-management` + 后端扩展 | +| 4 | 看板、使用/任务日志 | `portal-dashboard-analytics` + `portal-usage-task-logs` | +| 5 | 钱包、公告、通知 | `portal-wallet-affiliate` + 后端扩展 | +| 6 | 操练场 SSE | `portal-playground` | +| 7 | 多节点、文档、i18n | 看板节点卡 + 顶栏语言(P1) | +| 8 | 管理后台增强 | 不纳入;沿用现有 admin change | + +## References + +- **需求来源**:`zhencai/lanyiapi-site-audit/IMPLEMENTATION_PROMPT.md`(Phase 1–8、域模型、API 路径风格、UI 规范)。 +- **截图索引**:同目录 `screenshots/`(`01_console_dashboard.png` … `14_playground_url.png`)。 +- **已知抓包**:`POST /api/user/login?turnstile=`;分组示例 `default`、`vip`、`claude_code`(1.5x)、`AWS分组`(3x)。 +- **本仓库后端**:`openspec/changes/gateway-foundation-invite-billing/`、`GET /openapi.yaml`。 +- **本仓库管理端**:`openspec/changes/gateway-admin-console-frontend/`(Ant Design `/admin`,与本 change 租户门户分离)。 + +## Non-goals + +- 1:1 复刻蓝移品牌与域名;实现 **业务等价** 与可替换白标。 +- 替换现有 Go 技术栈为 Node/Prisma(IMPLEMENTATION_PROMPT 中的备选栈不采用;后端延续 Go + PostgreSQL + Redis)。 +- 在单页内合并 Semi 门户与 Ant Design 管理端为一套 UI 库。 diff --git a/openspec/changes/archive/2026-05-27-gateway-tenant-portal-platform/specs/portal-backend-extensions/spec.md b/openspec/changes/archive/2026-05-27-gateway-tenant-portal-platform/specs/portal-backend-extensions/spec.md new file mode 100644 index 0000000..54acb95 --- /dev/null +++ b/openspec/changes/archive/2026-05-27-gateway-tenant-portal-platform/specs/portal-backend-extensions/spec.md @@ -0,0 +1,46 @@ +## ADDED Requirements + +### Requirement: Tenant REST API surface + +The gateway SHALL expose tenant-authenticated JSON APIs documented in OpenAPI under paths aligned with `/api/user/*`, `/api/tokens`, `/api/models`, `/api/dashboard/*`, `/api/logs/*`, `/api/playground/chat`, and wallet/announcement endpoints defined in `design.md`. + +#### Scenario: OpenAPI includes new paths + +- **WHEN** a client fetches `GET /openapi.yaml` +- **THEN** new tenant routes appear with JWT security scheme + +### Requirement: Token group multiplier in billing + +The billing service SHALL apply a per-token or per-user group multiplier when calculating final quota debit for a request. + +#### Scenario: VIP group 1.5x + +- **WHEN** a request uses a token in group `claude_code` with multiplier 1.5 +- **THEN** the settled cost equals base cost × 1.5 rounded per integer quota rules + +### Requirement: Gateway key policy enforcement + +Before forwarding upstream, the gateway SHALL enforce per-key quota limit, enabled flag, allowed model list, and IP whitelist when configured. + +#### Scenario: Disabled key rejected + +- **WHEN** a disabled key calls `POST /v1/chat/completions` +- **THEN** the gateway returns 401 with a non-leaky error + +### Requirement: Usage log fields for console + +Usage log API responses SHALL include fields required by the console: timestamp, token name, group, type, model, time-to-first-token, input tokens, and billing detail breakdown. + +#### Scenario: Paginated usage logs + +- **WHEN** the client calls `GET /api/logs/usage` with page and filters +- **THEN** the response includes total count and rows with billing detail JSON + +### Requirement: Playground chat logging + +Playground requests SHALL write usage logs identifiable by a playground token name (e.g. `playground-default`) when using the shared playground key strategy. + +#### Scenario: Playground completion billed + +- **WHEN** a playground SSE session completes successfully +- **THEN** a usage log row is persisted and quota is debited diff --git a/openspec/changes/archive/2026-05-27-gateway-tenant-portal-platform/specs/portal-console-shell/spec.md b/openspec/changes/archive/2026-05-27-gateway-tenant-portal-platform/specs/portal-console-shell/spec.md new file mode 100644 index 0000000..82b3ca9 --- /dev/null +++ b/openspec/changes/archive/2026-05-27-gateway-tenant-portal-platform/specs/portal-console-shell/spec.md @@ -0,0 +1,29 @@ +## ADDED Requirements + +### Requirement: Console layout with Semi sidebar + +Authenticated console routes SHALL use a fixed top bar, left Semi Navigation sidebar (~240px, collapsible), and main content on background `#f6f7f9`. + +#### Scenario: User opens data dashboard + +- **WHEN** the user navigates to `/console` with a valid session +- **THEN** the sidebar shows groups 聊天 / 控制台 / 个人中心 with items matching the reference IA (操练场, 数据看板, 令牌管理, 使用日志, 任务日志, 钱包管理, 个人设置) + +### Requirement: Session guard for console routes + +All routes under `/console` SHALL require authentication. + +#### Scenario: Session expired + +- **WHEN** the backend returns 401 on a console API call +- **THEN** the UI shows a toast equivalent to 「未登录或登录已过期」 +- **AND** redirects to `/login?expired=true` + +### Requirement: Configurable gateway API base URL + +The portal SHALL read the gateway base URL from `NEXT_PUBLIC_GATEWAY_API_URL` (no trailing slash). + +#### Scenario: Missing configuration in development + +- **WHEN** the base URL is unset in development +- **THEN** the app surfaces a clear configuration error diff --git a/openspec/changes/archive/2026-05-27-gateway-tenant-portal-platform/specs/portal-dashboard-analytics/spec.md b/openspec/changes/archive/2026-05-27-gateway-tenant-portal-platform/specs/portal-dashboard-analytics/spec.md new file mode 100644 index 0000000..ec5f96f --- /dev/null +++ b/openspec/changes/archive/2026-05-27-gateway-tenant-portal-platform/specs/portal-dashboard-analytics/spec.md @@ -0,0 +1,37 @@ +## ADDED Requirements + +### Requirement: Dashboard summary cards + +The data dashboard SHALL show four summary cards: account balance/consumption, usage statistics, resource consumption, and performance metrics (including sparklines), backed by `GET /api/dashboard/stats`. + +#### Scenario: Load dashboard on entry + +- **WHEN** the user opens `/console` +- **THEN** the four cards load with loading states and render numeric metrics from the API + +### Requirement: Model analytics charts + +The dashboard SHALL provide tabs for consumption distribution, call trend, call count distribution, and ranking charts aggregated by hour via `GET /api/dashboard/charts`. + +#### Scenario: Switch chart tab + +- **WHEN** the user selects 「调用趋势」 +- **THEN** a time-series chart renders for the selected range query parameter + +### Requirement: Multi-region API node card + +The dashboard SHALL list configured API nodes (primary, Hong Kong, US) with copy URL, latency test, and external link actions. + +#### Scenario: Copy primary base URL + +- **WHEN** the user clicks copy on the primary node row +- **THEN** the configured base URL is copied to the clipboard + +### Requirement: Announcements FAQ and availability + +The dashboard side column SHALL list system announcements, collapsible FAQ, and service availability indicators (e.g. Claude reachability). + +#### Scenario: FAQ expand + +- **WHEN** the user expands an FAQ item +- **THEN** the answer content is shown without leaving the page diff --git a/openspec/changes/archive/2026-05-27-gateway-tenant-portal-platform/specs/portal-model-pricing/spec.md b/openspec/changes/archive/2026-05-27-gateway-tenant-portal-platform/specs/portal-model-pricing/spec.md new file mode 100644 index 0000000..c2f484e --- /dev/null +++ b/openspec/changes/archive/2026-05-27-gateway-tenant-portal-platform/specs/portal-model-pricing/spec.md @@ -0,0 +1,24 @@ +## ADDED Requirements + +### Requirement: Model catalog page + +The portal SHALL provide `/pricing` listing models with provider filter, token group multiplier filter, billing type, tags, endpoint type (openai/anthropic), fuzzy search, copy-list action, price visibility toggle, multiplier visibility toggle, and grid/table view with size M/L. + +#### Scenario: Filter by provider + +- **WHEN** the user selects provider Anthropic in the sidebar filter +- **THEN** the model list refreshes to matching models from `GET /api/models` + +#### Scenario: Toggle price display + +- **WHEN** the user turns off price display +- **THEN** per-token prices are hidden in cards/table while model names remain visible + +### Requirement: Model card pricing fields + +Each model entry SHALL display input, completion, and cache read/write prices per 1M tokens when prices are visible, plus billing mode label (usage-based). + +#### Scenario: Card view with prices on + +- **WHEN** prices are visible in card view +- **THEN** input and output unit prices are shown per reference screenshot layout diff --git a/openspec/changes/archive/2026-05-27-gateway-tenant-portal-platform/specs/portal-playground/spec.md b/openspec/changes/archive/2026-05-27-gateway-tenant-portal-platform/specs/portal-playground/spec.md new file mode 100644 index 0000000..2c8cc2f --- /dev/null +++ b/openspec/changes/archive/2026-05-27-gateway-tenant-portal-platform/specs/portal-playground/spec.md @@ -0,0 +1,29 @@ +## ADDED Requirements + +### Requirement: Playground layout and parameters + +The playground at `/console/playground` SHALL provide a left configuration panel (custom body toggle, group, model, image URL for multimodal, Temperature/TopP/Frequency/Presence penalties with enable switches) and a right conversation panel (user/assistant bubbles). + +#### Scenario: Send a chat message + +- **WHEN** the user sends a message with valid group and model selected +- **THEN** the client calls `POST /api/playground/chat` with SSE streaming +- **AND** assistant content appends incrementally in the thread + +### Requirement: Playground message actions + +The conversation UI SHALL support regenerate, copy, edit, delete, and show-debug for messages. + +#### Scenario: Regenerate last assistant reply + +- **WHEN** the user triggers regenerate on the latest assistant message +- **THEN** a new completion replaces or appends per product rules while logging usage + +### Requirement: Playground config import export + +The playground SHALL export and import session parameters as JSON. + +#### Scenario: Export config + +- **WHEN** the user exports configuration +- **THEN** a JSON file or clipboard payload contains group, model, and slider values diff --git a/openspec/changes/archive/2026-05-27-gateway-tenant-portal-platform/specs/portal-public-site/spec.md b/openspec/changes/archive/2026-05-27-gateway-tenant-portal-platform/specs/portal-public-site/spec.md new file mode 100644 index 0000000..ea33d28 --- /dev/null +++ b/openspec/changes/archive/2026-05-27-gateway-tenant-portal-platform/specs/portal-public-site/spec.md @@ -0,0 +1,47 @@ +## ADDED Requirements + +### Requirement: Marketing home page + +The portal SHALL serve a public home page at `/` with hero copy equivalent to the reference product (high-concurrency gateway, cost efficiency, multi-model), feature cards, partner/logo strip, and CTAs to register and login. + +#### Scenario: Guest views home + +- **WHEN** an unauthenticated user opens `/` +- **THEN** the page renders without requiring login +- **AND** primary CTAs link to `/register` and `/login` + +### Requirement: Global top navigation + +The portal SHALL show a persistent top bar on public and console layouts with links: Home, Console, Model pricing (`/pricing`), Docs, About; plus notification affordance, theme/display control, language switch, and auth actions (login/register or user menu). + +#### Scenario: Logged-in user sees console link + +- **WHEN** a valid JWT session exists +- **THEN** the Console link navigates to `/console` without forcing re-login + +### Requirement: Login page primary action label + +The sign-in form primary submit control SHALL display the label **继续** (not 「登录」). + +#### Scenario: User submits credentials + +- **WHEN** the user clicks the primary button on `/login` +- **THEN** the visible label is 「继续」 + +### Requirement: Registration with affiliate query param + +The registration flow SHALL accept `?aff=` on `/register` and pass the invite code to the backend register API. + +#### Scenario: Invite link registration + +- **WHEN** the user opens `/register?aff=ABC123` and completes registration +- **THEN** the invite code is included in the register request per backend contract + +### Requirement: System announcement modal on home + +The home page SHALL support a dismissible announcement modal (e.g. enterprise contact QR) with optional «do not show again today» stored in `localStorage`. + +#### Scenario: User dismisses for today + +- **WHEN** the user chooses today’s dismiss option +- **THEN** the modal does not reappear until the next calendar day (per implementation key) diff --git a/openspec/changes/archive/2026-05-27-gateway-tenant-portal-platform/specs/portal-token-management/spec.md b/openspec/changes/archive/2026-05-27-gateway-tenant-portal-platform/specs/portal-token-management/spec.md new file mode 100644 index 0000000..17bb38c --- /dev/null +++ b/openspec/changes/archive/2026-05-27-gateway-tenant-portal-platform/specs/portal-token-management/spec.md @@ -0,0 +1,25 @@ +## ADDED Requirements + +### Requirement: API token CRUD UI + +The portal SHALL provide `/console/token` to create, list, edit, enable/disable, and delete API keys with fields: name, status, quota (unlimited or fixed), token group, masked `sk-` secret, allowed models, IP whitelist. + +#### Scenario: Create token shows secret once + +- **WHEN** the user creates a new token +- **THEN** the full `sk-` plaintext is shown in a one-time reveal pattern (copy/QR supported) +- **AND** subsequent list views show masked values only + +#### Scenario: Batch delete selected tokens + +- **WHEN** the user selects multiple rows and confirms batch delete +- **THEN** the client calls the backend delete API for each selected id + +### Requirement: Quick open playground from token row + +The token table SHALL offer a 「聊天」 action that opens the playground with the token prefilled. + +#### Scenario: Open chat from token + +- **WHEN** the user chooses 聊天 on a token row +- **THEN** the app navigates to `/console/playground` with that token context diff --git a/openspec/changes/archive/2026-05-27-gateway-tenant-portal-platform/specs/portal-usage-task-logs/spec.md b/openspec/changes/archive/2026-05-27-gateway-tenant-portal-platform/specs/portal-usage-task-logs/spec.md new file mode 100644 index 0000000..fb40007 --- /dev/null +++ b/openspec/changes/archive/2026-05-27-gateway-tenant-portal-platform/specs/portal-usage-task-logs/spec.md @@ -0,0 +1,19 @@ +## ADDED Requirements + +### Requirement: Usage log list page + +The portal SHALL provide `/console/log` with filters for time range, token name, model, request ID, and group; table columns for time, token, group, type, model, time-to-first-token, input tokens, and billing details; plus compact mode, column settings, and pagination defaulting to 10 rows per page. + +#### Scenario: Filter by model name + +- **WHEN** the user enters a model filter and applies search +- **THEN** `GET /api/logs/usage` is called with the model query parameter + +### Requirement: Task log list page + +The portal SHALL provide `/console/task` listing async tasks with columns: submit time, end time, duration, platform, type, task ID, status, progress, and detail action. + +#### Scenario: Empty task log state + +- **WHEN** the user has no task records +- **THEN** the page shows an empty state matching reference screenshot behavior diff --git a/openspec/changes/archive/2026-05-27-gateway-tenant-portal-platform/specs/portal-user-settings/spec.md b/openspec/changes/archive/2026-05-27-gateway-tenant-portal-platform/specs/portal-user-settings/spec.md new file mode 100644 index 0000000..28080a5 --- /dev/null +++ b/openspec/changes/archive/2026-05-27-gateway-tenant-portal-platform/specs/portal-user-settings/spec.md @@ -0,0 +1,19 @@ +## ADDED Requirements + +### Requirement: Personal settings tabs + +The settings page at `/console/setting` SHALL provide account management (third-party binding placeholders: email, WeChat, GitHub, Discord, OIDC, Telegram, LinuxDO) and other settings (notification channels, balance alert threshold, notification email, price/privacy/sidebar sub-tabs). + +#### Scenario: Save notification threshold + +- **WHEN** the user updates balance alert threshold and saves +- **THEN** the client persists settings via the backend notification config API + +### Requirement: Language preference + +The portal SHALL default to Simplified Chinese and allow switching language from the top bar, persisting preference per user or local storage until backend i18n is available. + +#### Scenario: Switch to English + +- **WHEN** the user selects English from the language control +- **THEN** UI strings switch to English catalog where translated diff --git a/openspec/changes/archive/2026-05-27-gateway-tenant-portal-platform/specs/portal-wallet-affiliate/spec.md b/openspec/changes/archive/2026-05-27-gateway-tenant-portal-platform/specs/portal-wallet-affiliate/spec.md new file mode 100644 index 0000000..5c6a12a --- /dev/null +++ b/openspec/changes/archive/2026-05-27-gateway-tenant-portal-platform/specs/portal-wallet-affiliate/spec.md @@ -0,0 +1,37 @@ +## ADDED Requirements + +### Requirement: Wallet overview + +The wallet page at `/console/wallet` SHALL display current balance, historical consumption, and request count. + +#### Scenario: View wallet after login + +- **WHEN** the user opens `/console/wallet` +- **THEN** balance metrics load from the wallet or user self API + +### Requirement: Redeem code recharge + +The wallet page SHALL provide a redeem code input that calls the existing redeem API (`POST /user/redeem` or aliased `/api/wallet/redeem`). + +#### Scenario: Successful redeem + +- **WHEN** the user submits a valid unused code +- **THEN** balance increases and a success toast is shown + +### Requirement: Affiliate invite link and transfer + +The wallet page SHALL display an invite URL of the form `{portalOrigin}/register?aff={invite_code}`, pending affiliate earnings, and an action to transfer earnings to balance. + +#### Scenario: Copy invite link + +- **WHEN** the user copies the invite link +- **THEN** the clipboard contains the full URL with the user’s invite code + +### Requirement: Online recharge gate + +When online recharge is disabled by configuration, the UI SHALL show guidance to contact admin or use redeem codes instead of a payment form. + +#### Scenario: Recharge disabled + +- **WHEN** `RECHARGE_ENABLED` is false +- **THEN** no payment checkout UI is rendered diff --git a/openspec/changes/archive/2026-05-27-gateway-tenant-portal-platform/tasks.md b/openspec/changes/archive/2026-05-27-gateway-tenant-portal-platform/tasks.md new file mode 100644 index 0000000..311f91a --- /dev/null +++ b/openspec/changes/archive/2026-05-27-gateway-tenant-portal-platform/tasks.md @@ -0,0 +1,59 @@ +## Phase 1 — 基础与用户(portal-public-site + shell) + +- [x] 1.1 初始化 `portal/` Next.js 14 + Semi Design + Tailwind + TypeScript +- [x] 1.2 实现顶栏全局导航与营销布局壳层 +- [x] 1.3 登录页 `/login`:主按钮文案「继续」;对接 `POST /auth/login`;支持 expired 查询参数提示 +- [x] 1.4 注册页 `/register`:用户名/邮箱/密码;`?aff=` 写入 `invite_code`;对接 `POST /auth/register` +- [x] 1.5 JWT 存储与 `/console/*` 路由守卫;401 → `/login?expired=true` +- [x] 1.6 控制台侧栏 Semi Navigation(菜单项与参考站一致) +- [x] 1.7 (后端)`GET /api/user/self` 或扩展 login 响应含 balance、group、display_name + +## Phase 2 — 模型与定价(portal-model-pricing) + +- [x] 2.1 (后端)`token_groups` 表与种子数据;用户/令牌关联分组倍率 +- [x] 2.2 (后端)`GET /api/models`:供应商、分组、计费类型、端点类型筛选与分页 +- [x] 2.3 模型广场 `/pricing`:左侧筛选 + 顶栏搜索/视图切换(卡片/表格 M/L)/价格与倍率开关 +- [x] 2.4 (后端)价格计算服务对外暴露分项单价(input/output/cache × multiplier) + +## Phase 3 — 令牌与网关(portal-token-management) + +- [x] 3.1 (后端)`ApiToken` 扩展字段迁移;`GET/POST/PUT/DELETE /api/tokens` +- [x] 3.2 令牌管理页:表格 CRUD、掩码、复制/二维码、批量操作、启用/禁用 +- [x] 3.3 网关鉴权:校验额度/IP/模型白名单(扩展现有 middleware) +- [x] 3.4 「聊天」快捷入口:跳转操练场并预填令牌 + +## Phase 4 — 控制台数据(dashboard + logs) + +- [x] 4.1 (后端)`GET /api/dashboard/stats`、`GET /api/dashboard/charts?range=` +- [x] 4.2 数据看板:四指标卡 + 模型分析 Tab 图表 + API 节点卡(主站/香港/美区,复制/测速) +- [x] 4.3 (后端)`GET /api/logs/usage` 分页筛选 +- [x] 4.4 使用日志页:时间/令牌/模型/Request ID/分组筛选;列设置 +- [x] 4.5 (后端)`task_logs` 表与 `GET /api/logs/tasks` +- [x] 4.6 任务日志页:空状态与表格列 + +## Phase 5 — 钱包与运营(wallet + announcements) + +- [x] 5.1 钱包页:余额/历史消耗/请求数;兑换码表单对接 `POST /user/redeem` +- [x] 5.2 邀请链接 `register?aff={code}`、待结算收益与划转到余额(后端事务) +- [x] 5.3 在线充值 UI(`RECHARGE_ENABLED` mock 或关闭时展示引导文案) +- [x] 5.4 (后端)公告 CRUD + `GET /api/announcements`;首页/看板 Modal + localStorage 今日关闭 + +## Phase 6 — 操练场(portal-playground) + +- [x] 6.1 (后端)`POST /api/playground/chat` SSE;日志令牌名 `playground-default` 策略 +- [x] 6.2 操练场三栏 UI:参数滑条、多轮对话、重新生成/复制/编辑/删除、调试信息 +- [x] 6.3 配置导入导出 JSON + +## Phase 7 — 多节点、文档、i18n + +- [x] 7.1 节点配置环境变量;看板测速 `GET /api/nodes/ping`(可选) +- [x] 7.2 关于页 `/about`;首页营销区块对照截图 +- [x] 7.3 顶栏语言切换(简体中文默认,英文预留) +- [x] 7.4 文档入口(外链或占位 `/docs`) + +## Phase 8 — 质量与 OpenSpec + +- [x] 8.1 更新 `backend/internal/openapi/spec.yaml` 与租户面契约测试 +- [x] 8.2 核心路径 E2E:注册 → 登录 → 创建令牌 → 操练场一条消息 +- [x] 8.3 `portal/README.md` 本地联调说明 +- [x] 8.4 归档前 `npx @fission-ai/openspec@latest status --change gateway-tenant-portal-platform` diff --git a/openspec/specs/portal-backend-extensions/spec.md b/openspec/specs/portal-backend-extensions/spec.md new file mode 100644 index 0000000..e6111a0 --- /dev/null +++ b/openspec/specs/portal-backend-extensions/spec.md @@ -0,0 +1,50 @@ +# portal-backend-extensions Specification + +## Purpose +TBD - created by archiving change gateway-tenant-portal-platform. Update Purpose after archive. +## Requirements +### Requirement: Tenant REST API surface + +The gateway SHALL expose tenant-authenticated JSON APIs documented in OpenAPI under paths aligned with `/api/user/*`, `/api/tokens`, `/api/models`, `/api/dashboard/*`, `/api/logs/*`, `/api/playground/chat`, and wallet/announcement endpoints defined in `design.md`. + +#### Scenario: OpenAPI includes new paths + +- **WHEN** a client fetches `GET /openapi.yaml` +- **THEN** new tenant routes appear with JWT security scheme + +### Requirement: Token group multiplier in billing + +The billing service SHALL apply a per-token or per-user group multiplier when calculating final quota debit for a request. + +#### Scenario: VIP group 1.5x + +- **WHEN** a request uses a token in group `claude_code` with multiplier 1.5 +- **THEN** the settled cost equals base cost × 1.5 rounded per integer quota rules + +### Requirement: Gateway key policy enforcement + +Before forwarding upstream, the gateway SHALL enforce per-key quota limit, enabled flag, allowed model list, and IP whitelist when configured. + +#### Scenario: Disabled key rejected + +- **WHEN** a disabled key calls `POST /v1/chat/completions` +- **THEN** the gateway returns 401 with a non-leaky error + +### Requirement: Usage log fields for console + +Usage log API responses SHALL include fields required by the console: timestamp, token name, group, type, model, time-to-first-token, input tokens, and billing detail breakdown. + +#### Scenario: Paginated usage logs + +- **WHEN** the client calls `GET /api/logs/usage` with page and filters +- **THEN** the response includes total count and rows with billing detail JSON + +### Requirement: Playground chat logging + +Playground requests SHALL write usage logs identifiable by a playground token name (e.g. `playground-default`) when using the shared playground key strategy. + +#### Scenario: Playground completion billed + +- **WHEN** a playground SSE session completes successfully +- **THEN** a usage log row is persisted and quota is debited + diff --git a/openspec/specs/portal-console-shell/spec.md b/openspec/specs/portal-console-shell/spec.md new file mode 100644 index 0000000..6a3b6f9 --- /dev/null +++ b/openspec/specs/portal-console-shell/spec.md @@ -0,0 +1,33 @@ +# portal-console-shell Specification + +## Purpose +TBD - created by archiving change gateway-tenant-portal-platform. Update Purpose after archive. +## Requirements +### Requirement: Console layout with Semi sidebar + +Authenticated console routes SHALL use a fixed top bar, left Semi Navigation sidebar (~240px, collapsible), and main content on background `#f6f7f9`. + +#### Scenario: User opens data dashboard + +- **WHEN** the user navigates to `/console` with a valid session +- **THEN** the sidebar shows groups 聊天 / 控制台 / 个人中心 with items matching the reference IA (操练场, 数据看板, 令牌管理, 使用日志, 任务日志, 钱包管理, 个人设置) + +### Requirement: Session guard for console routes + +All routes under `/console` SHALL require authentication. + +#### Scenario: Session expired + +- **WHEN** the backend returns 401 on a console API call +- **THEN** the UI shows a toast equivalent to 「未登录或登录已过期」 +- **AND** redirects to `/login?expired=true` + +### Requirement: Configurable gateway API base URL + +The portal SHALL read the gateway base URL from `NEXT_PUBLIC_GATEWAY_API_URL` (no trailing slash). + +#### Scenario: Missing configuration in development + +- **WHEN** the base URL is unset in development +- **THEN** the app surfaces a clear configuration error + diff --git a/openspec/specs/portal-dashboard-analytics/spec.md b/openspec/specs/portal-dashboard-analytics/spec.md new file mode 100644 index 0000000..a20b21d --- /dev/null +++ b/openspec/specs/portal-dashboard-analytics/spec.md @@ -0,0 +1,41 @@ +# portal-dashboard-analytics Specification + +## Purpose +TBD - created by archiving change gateway-tenant-portal-platform. Update Purpose after archive. +## Requirements +### Requirement: Dashboard summary cards + +The data dashboard SHALL show four summary cards: account balance/consumption, usage statistics, resource consumption, and performance metrics (including sparklines), backed by `GET /api/dashboard/stats`. + +#### Scenario: Load dashboard on entry + +- **WHEN** the user opens `/console` +- **THEN** the four cards load with loading states and render numeric metrics from the API + +### Requirement: Model analytics charts + +The dashboard SHALL provide tabs for consumption distribution, call trend, call count distribution, and ranking charts aggregated by hour via `GET /api/dashboard/charts`. + +#### Scenario: Switch chart tab + +- **WHEN** the user selects 「调用趋势」 +- **THEN** a time-series chart renders for the selected range query parameter + +### Requirement: Multi-region API node card + +The dashboard SHALL list configured API nodes (primary, Hong Kong, US) with copy URL, latency test, and external link actions. + +#### Scenario: Copy primary base URL + +- **WHEN** the user clicks copy on the primary node row +- **THEN** the configured base URL is copied to the clipboard + +### Requirement: Announcements FAQ and availability + +The dashboard side column SHALL list system announcements, collapsible FAQ, and service availability indicators (e.g. Claude reachability). + +#### Scenario: FAQ expand + +- **WHEN** the user expands an FAQ item +- **THEN** the answer content is shown without leaving the page + diff --git a/openspec/specs/portal-model-pricing/spec.md b/openspec/specs/portal-model-pricing/spec.md new file mode 100644 index 0000000..022c6a3 --- /dev/null +++ b/openspec/specs/portal-model-pricing/spec.md @@ -0,0 +1,28 @@ +# portal-model-pricing Specification + +## Purpose +TBD - created by archiving change gateway-tenant-portal-platform. Update Purpose after archive. +## Requirements +### Requirement: Model catalog page + +The portal SHALL provide `/pricing` listing models with provider filter, token group multiplier filter, billing type, tags, endpoint type (openai/anthropic), fuzzy search, copy-list action, price visibility toggle, multiplier visibility toggle, and grid/table view with size M/L. + +#### Scenario: Filter by provider + +- **WHEN** the user selects provider Anthropic in the sidebar filter +- **THEN** the model list refreshes to matching models from `GET /api/models` + +#### Scenario: Toggle price display + +- **WHEN** the user turns off price display +- **THEN** per-token prices are hidden in cards/table while model names remain visible + +### Requirement: Model card pricing fields + +Each model entry SHALL display input, completion, and cache read/write prices per 1M tokens when prices are visible, plus billing mode label (usage-based). + +#### Scenario: Card view with prices on + +- **WHEN** prices are visible in card view +- **THEN** input and output unit prices are shown per reference screenshot layout + diff --git a/openspec/specs/portal-playground/spec.md b/openspec/specs/portal-playground/spec.md new file mode 100644 index 0000000..f1c823b --- /dev/null +++ b/openspec/specs/portal-playground/spec.md @@ -0,0 +1,33 @@ +# portal-playground Specification + +## Purpose +TBD - created by archiving change gateway-tenant-portal-platform. Update Purpose after archive. +## Requirements +### Requirement: Playground layout and parameters + +The playground at `/console/playground` SHALL provide a left configuration panel (custom body toggle, group, model, image URL for multimodal, Temperature/TopP/Frequency/Presence penalties with enable switches) and a right conversation panel (user/assistant bubbles). + +#### Scenario: Send a chat message + +- **WHEN** the user sends a message with valid group and model selected +- **THEN** the client calls `POST /api/playground/chat` with SSE streaming +- **AND** assistant content appends incrementally in the thread + +### Requirement: Playground message actions + +The conversation UI SHALL support regenerate, copy, edit, delete, and show-debug for messages. + +#### Scenario: Regenerate last assistant reply + +- **WHEN** the user triggers regenerate on the latest assistant message +- **THEN** a new completion replaces or appends per product rules while logging usage + +### Requirement: Playground config import export + +The playground SHALL export and import session parameters as JSON. + +#### Scenario: Export config + +- **WHEN** the user exports configuration +- **THEN** a JSON file or clipboard payload contains group, model, and slider values + diff --git a/openspec/specs/portal-public-site/spec.md b/openspec/specs/portal-public-site/spec.md new file mode 100644 index 0000000..8fd4586 --- /dev/null +++ b/openspec/specs/portal-public-site/spec.md @@ -0,0 +1,51 @@ +# portal-public-site Specification + +## Purpose +TBD - created by archiving change gateway-tenant-portal-platform. Update Purpose after archive. +## Requirements +### Requirement: Marketing home page + +The portal SHALL serve a public home page at `/` with hero copy equivalent to the reference product (high-concurrency gateway, cost efficiency, multi-model), feature cards, partner/logo strip, and CTAs to register and login. + +#### Scenario: Guest views home + +- **WHEN** an unauthenticated user opens `/` +- **THEN** the page renders without requiring login +- **AND** primary CTAs link to `/register` and `/login` + +### Requirement: Global top navigation + +The portal SHALL show a persistent top bar on public and console layouts with links: Home, Console, Model pricing (`/pricing`), Docs, About; plus notification affordance, theme/display control, language switch, and auth actions (login/register or user menu). + +#### Scenario: Logged-in user sees console link + +- **WHEN** a valid JWT session exists +- **THEN** the Console link navigates to `/console` without forcing re-login + +### Requirement: Login page primary action label + +The sign-in form primary submit control SHALL display the label **继续** (not 「登录」). + +#### Scenario: User submits credentials + +- **WHEN** the user clicks the primary button on `/login` +- **THEN** the visible label is 「继续」 + +### Requirement: Registration with affiliate query param + +The registration flow SHALL accept `?aff=` on `/register` and pass the invite code to the backend register API. + +#### Scenario: Invite link registration + +- **WHEN** the user opens `/register?aff=ABC123` and completes registration +- **THEN** the invite code is included in the register request per backend contract + +### Requirement: System announcement modal on home + +The home page SHALL support a dismissible announcement modal (e.g. enterprise contact QR) with optional «do not show again today» stored in `localStorage`. + +#### Scenario: User dismisses for today + +- **WHEN** the user chooses today’s dismiss option +- **THEN** the modal does not reappear until the next calendar day (per implementation key) + diff --git a/openspec/specs/portal-token-management/spec.md b/openspec/specs/portal-token-management/spec.md new file mode 100644 index 0000000..313743f --- /dev/null +++ b/openspec/specs/portal-token-management/spec.md @@ -0,0 +1,29 @@ +# portal-token-management Specification + +## Purpose +TBD - created by archiving change gateway-tenant-portal-platform. Update Purpose after archive. +## Requirements +### Requirement: API token CRUD UI + +The portal SHALL provide `/console/token` to create, list, edit, enable/disable, and delete API keys with fields: name, status, quota (unlimited or fixed), token group, masked `sk-` secret, allowed models, IP whitelist. + +#### Scenario: Create token shows secret once + +- **WHEN** the user creates a new token +- **THEN** the full `sk-` plaintext is shown in a one-time reveal pattern (copy/QR supported) +- **AND** subsequent list views show masked values only + +#### Scenario: Batch delete selected tokens + +- **WHEN** the user selects multiple rows and confirms batch delete +- **THEN** the client calls the backend delete API for each selected id + +### Requirement: Quick open playground from token row + +The token table SHALL offer a 「聊天」 action that opens the playground with the token prefilled. + +#### Scenario: Open chat from token + +- **WHEN** the user chooses 聊天 on a token row +- **THEN** the app navigates to `/console/playground` with that token context + diff --git a/openspec/specs/portal-usage-task-logs/spec.md b/openspec/specs/portal-usage-task-logs/spec.md new file mode 100644 index 0000000..cec4ce7 --- /dev/null +++ b/openspec/specs/portal-usage-task-logs/spec.md @@ -0,0 +1,23 @@ +# portal-usage-task-logs Specification + +## Purpose +TBD - created by archiving change gateway-tenant-portal-platform. Update Purpose after archive. +## Requirements +### Requirement: Usage log list page + +The portal SHALL provide `/console/log` with filters for time range, token name, model, request ID, and group; table columns for time, token, group, type, model, time-to-first-token, input tokens, and billing details; plus compact mode, column settings, and pagination defaulting to 10 rows per page. + +#### Scenario: Filter by model name + +- **WHEN** the user enters a model filter and applies search +- **THEN** `GET /api/logs/usage` is called with the model query parameter + +### Requirement: Task log list page + +The portal SHALL provide `/console/task` listing async tasks with columns: submit time, end time, duration, platform, type, task ID, status, progress, and detail action. + +#### Scenario: Empty task log state + +- **WHEN** the user has no task records +- **THEN** the page shows an empty state matching reference screenshot behavior + diff --git a/openspec/specs/portal-user-settings/spec.md b/openspec/specs/portal-user-settings/spec.md new file mode 100644 index 0000000..92bbd3f --- /dev/null +++ b/openspec/specs/portal-user-settings/spec.md @@ -0,0 +1,23 @@ +# portal-user-settings Specification + +## Purpose +TBD - created by archiving change gateway-tenant-portal-platform. Update Purpose after archive. +## Requirements +### Requirement: Personal settings tabs + +The settings page at `/console/setting` SHALL provide account management (third-party binding placeholders: email, WeChat, GitHub, Discord, OIDC, Telegram, LinuxDO) and other settings (notification channels, balance alert threshold, notification email, price/privacy/sidebar sub-tabs). + +#### Scenario: Save notification threshold + +- **WHEN** the user updates balance alert threshold and saves +- **THEN** the client persists settings via the backend notification config API + +### Requirement: Language preference + +The portal SHALL default to Simplified Chinese and allow switching language from the top bar, persisting preference per user or local storage until backend i18n is available. + +#### Scenario: Switch to English + +- **WHEN** the user selects English from the language control +- **THEN** UI strings switch to English catalog where translated + diff --git a/openspec/specs/portal-wallet-affiliate/spec.md b/openspec/specs/portal-wallet-affiliate/spec.md new file mode 100644 index 0000000..5ba637b --- /dev/null +++ b/openspec/specs/portal-wallet-affiliate/spec.md @@ -0,0 +1,41 @@ +# portal-wallet-affiliate Specification + +## Purpose +TBD - created by archiving change gateway-tenant-portal-platform. Update Purpose after archive. +## Requirements +### Requirement: Wallet overview + +The wallet page at `/console/wallet` SHALL display current balance, historical consumption, and request count. + +#### Scenario: View wallet after login + +- **WHEN** the user opens `/console/wallet` +- **THEN** balance metrics load from the wallet or user self API + +### Requirement: Redeem code recharge + +The wallet page SHALL provide a redeem code input that calls the existing redeem API (`POST /user/redeem` or aliased `/api/wallet/redeem`). + +#### Scenario: Successful redeem + +- **WHEN** the user submits a valid unused code +- **THEN** balance increases and a success toast is shown + +### Requirement: Affiliate invite link and transfer + +The wallet page SHALL display an invite URL of the form `{portalOrigin}/register?aff={invite_code}`, pending affiliate earnings, and an action to transfer earnings to balance. + +#### Scenario: Copy invite link + +- **WHEN** the user copies the invite link +- **THEN** the clipboard contains the full URL with the user’s invite code + +### Requirement: Online recharge gate + +When online recharge is disabled by configuration, the UI SHALL show guidance to contact admin or use redeem codes instead of a payment form. + +#### Scenario: Recharge disabled + +- **WHEN** `RECHARGE_ENABLED` is false +- **THEN** no payment checkout UI is rendered + diff --git a/portal/.env.example b/portal/.env.example new file mode 100644 index 0000000..25d7ea0 --- /dev/null +++ b/portal/.env.example @@ -0,0 +1,11 @@ +# Gateway API base URL (no trailing slash) +NEXT_PUBLIC_GATEWAY_API_URL=http://localhost:8080 + +# Backend PORTAL_ORIGIN should match this for invite links +NEXT_PUBLIC_PORTAL_ORIGIN=http://localhost:3001 + +# Optional external docs (VitePress / GitBook) +# NEXT_PUBLIC_DOCS_URL=https://docs.example.com + +# Optional external docs (VitePress, etc.) +# NEXT_PUBLIC_DOCS_URL=https://docs.example.com diff --git a/portal/.gitignore b/portal/.gitignore new file mode 100644 index 0000000..3fb1673 --- /dev/null +++ b/portal/.gitignore @@ -0,0 +1,34 @@ +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage +.ai-verification/ + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem +.env*.local + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/portal/README.md b/portal/README.md new file mode 100644 index 0000000..790ab8b --- /dev/null +++ b/portal/README.md @@ -0,0 +1,96 @@ +# 租户门户(Portal) + +蓝移类产品形态的 **用户门户 + 控制台**,技术栈:Next.js + Semi Design + Tailwind。 + +与仓库内 `frontend/`(Ant Design 管理端 `/admin`)分离运行,默认端口 **3001**。 + +## 本地联调 + +### 1. 数据库 + +在 `001_init.sql` 之后按顺序执行迁移: + +```bash +export DATABASE_URL='postgres://gateway:gateway@localhost:5432/ai_gateway?sslmode=disable' +psql "$DATABASE_URL" -f backend/migrations/003_token_groups_catalog.sql +psql "$DATABASE_URL" -f backend/migrations/004_api_tokens_extend.sql +psql "$DATABASE_URL" -f backend/migrations/005_dashboard_logs.sql +psql "$DATABASE_URL" -f backend/migrations/006_wallet_announcements.sql +``` + +### 2. 后端网关 + +```bash +cd backend +cp .env.example .env # 按需修改 DATABASE_DSN、JWT_SECRET、UPSTREAM_* +go run ./cmd/server # 默认 http://localhost:8080 +``` + +常用环境变量见 `backend/.env.example`(`PORTAL_API_NODES_JSON`、`RECHARGE_ENABLED`、`PORTAL_ORIGIN` 等)。 + +### 3. 门户前端 + +```bash +cd portal +cp .env.example .env.local +# NEXT_PUBLIC_GATEWAY_API_URL=http://localhost:8080 +# NEXT_PUBLIC_PORTAL_ORIGIN=http://localhost:3001 +# NEXT_PUBLIC_DOCS_URL=https://docs.example.com # 可选外链文档 +npm install +npm run dev +``` + +浏览器打开 [http://localhost:3001](http://localhost:3001)。 + +### 4. 管理端(可选) + +```bash +cd frontend +npm install +npm run dev # 默认 http://localhost:3000 +``` + +## API 契约 + +租户面 REST 与 OpenAI 网关路径见 `backend/internal/openapi/spec.yaml`,运行时可通过 `GET http://localhost:8080/openapi.yaml` 获取。 + +## 集成测试(后端 E2E) + +需本机 Postgres(及 Redis,默认 `localhost:6379`): + +```bash +export INTEGRATION_DATABASE_DSN='host=localhost user=gateway password=gateway dbname=ai_gateway port=5432 sslmode=disable' +export INTEGRATION_REDIS_ADDR=localhost:6379 # 可选,默认与 REDIS_ADDR 一致 + +cd backend +go test ./internal/openapi/... -count=1 +go test ./internal/integration/... -count=1 -v +``` + +`TestIntegration_PortalCoreFlow` 覆盖:**注册 → 登录 → 创建令牌 → 操练场请求 → 看板/节点测速**(操练场在无上游时可能返回 502,属预期)。 + +## 功能概览 + +| 模块 | 路由 / API | +|------|------------| +| 营销站 | `/`、`/about`、`/docs`、`/pricing` | +| 认证 | `/login`、`/register?aff=` | +| 控制台 | `/console/*`(看板、令牌、操练场、日志、钱包、设置) | +| 网关调用 | `Authorization: Bearer sk-...` → `/v1/chat/completions` | + +OpenSpec:现行规格 `openspec/specs/portal-*/`;归档提案 `openspec/changes/archive/2026-05-27-gateway-tenant-portal-platform/` + +## UI 还原验证(ai-ui-verification) + +```bash +# 参考截图 baseline(来自 lanyiapi-site-audit) +# portal/.ai-verification/baselines/{home,pricing,playground}.png + +bun run ai:verify:static # lint / tsc +bun run ai:verify:ui -- --baseline ./.ai-verification/baselines/home.png --url http://localhost:3001/ + +# 本地截图对比(需 dev :3001) +bun -e "import { chromium } from 'playwright'; ..." +``` + +设计对齐要点:品牌「蓝移 API」、首页渐变 Hero、模型广场顶栏渐变、控制台侧栏 240px + 收起、操练场「模型配置 | AI 对话」双栏。 diff --git a/portal/app/about/page.tsx b/portal/app/about/page.tsx new file mode 100644 index 0000000..9fbea87 --- /dev/null +++ b/portal/app/about/page.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { AboutPageContent } from "@/components/marketing/about-page"; + +export default function AboutPage() { + return ; +} diff --git a/portal/app/console/layout.tsx b/portal/app/console/layout.tsx new file mode 100644 index 0000000..ef98862 --- /dev/null +++ b/portal/app/console/layout.tsx @@ -0,0 +1,23 @@ +"use client"; + +import { Layout } from "@douyinfe/semi-ui"; + +import { ConsoleGuard } from "@/components/console-guard"; +import { ConsoleSidebar } from "@/components/console-sidebar"; + +const { Content } = Layout; + +export default function ConsoleLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + + {children} + + + ); +} diff --git a/portal/app/console/log/page.tsx b/portal/app/console/log/page.tsx new file mode 100644 index 0000000..dcf629a --- /dev/null +++ b/portal/app/console/log/page.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { UsageLogPage } from "@/components/logs/usage-log-page"; + +export default function UsageLogRoute() { + return ; +} diff --git a/portal/app/console/page.tsx b/portal/app/console/page.tsx new file mode 100644 index 0000000..87d4b55 --- /dev/null +++ b/portal/app/console/page.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { ConsoleDashboardPage } from "@/components/dashboard/console-dashboard-page"; + +export default function ConsoleDashboardRoute() { + return ; +} diff --git a/portal/app/console/playground/page.tsx b/portal/app/console/playground/page.tsx new file mode 100644 index 0000000..ff4650d --- /dev/null +++ b/portal/app/console/playground/page.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { Suspense } from "react"; + +import { PlaygroundPage } from "@/components/playground/playground-page"; + +export default function ConsolePlaygroundRoute() { + return ( + 加载中…
}> + + + ); +} diff --git a/portal/app/console/setting/page.tsx b/portal/app/console/setting/page.tsx new file mode 100644 index 0000000..5bf5aae --- /dev/null +++ b/portal/app/console/setting/page.tsx @@ -0,0 +1,27 @@ +"use client"; + +import { Card, Typography } from "@douyinfe/semi-ui"; + +import { useAuth } from "@/contexts/auth-context"; + +const { Title, Text } = Typography; + +export default function SettingPage() { + const { user } = useAuth(); + + return ( +
+ 个人设置 + + 用户名:{user?.username ?? "—"} +
+ 邮箱:{user?.email ?? "—"} +
+ 邀请码:{user?.invite_code ?? "—"} +
+ + 通知方式、余额预警等将在 Phase 5 接入。 + +
+ ); +} diff --git a/portal/app/console/task/page.tsx b/portal/app/console/task/page.tsx new file mode 100644 index 0000000..5e5ff03 --- /dev/null +++ b/portal/app/console/task/page.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { TaskLogPage } from "@/components/logs/task-log-page"; + +export default function TaskLogRoute() { + return ; +} diff --git a/portal/app/console/token/page.tsx b/portal/app/console/token/page.tsx new file mode 100644 index 0000000..049439f --- /dev/null +++ b/portal/app/console/token/page.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { TokenManagementPage } from "@/components/tokens/token-management-page"; + +export default function TokenPage() { + return ; +} diff --git a/portal/app/console/wallet/page.tsx b/portal/app/console/wallet/page.tsx new file mode 100644 index 0000000..f4a04f1 --- /dev/null +++ b/portal/app/console/wallet/page.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { WalletManagementPage } from "@/components/wallet/wallet-management-page"; + +export default function WalletPage() { + return ; +} diff --git a/portal/app/docs/page.tsx b/portal/app/docs/page.tsx new file mode 100644 index 0000000..4b67df7 --- /dev/null +++ b/portal/app/docs/page.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { DocsPageContent } from "@/components/marketing/docs-page"; + +export default function DocsPage() { + return ; +} diff --git a/portal/app/globals.css b/portal/app/globals.css new file mode 100644 index 0000000..38a5040 --- /dev/null +++ b/portal/app/globals.css @@ -0,0 +1,51 @@ +@import "tailwindcss"; +@import "../node_modules/@douyinfe/semi-ui/dist/css/semi.min.css"; + +:root { + --portal-bg: #f6f7f9; + --portal-primary: #007aff; + --portal-primary-dark: #0066d6; + --portal-gradient-start: #007aff; + --portal-gradient-end: #00c2ff; + --portal-sidebar-width: 240px; + --portal-header-height: 56px; + --portal-card-radius: 12px; +} + +body { + margin: 0; + min-height: 100vh; + background: var(--portal-bg); +} + +.portal-hero-gradient { + background: linear-gradient( + 135deg, + var(--portal-gradient-start) 0%, + var(--portal-gradient-end) 100% + ); + -webkit-background-clip: text; + background-clip: text; + color: transparent; +} + +.portal-pricing-banner { + background: linear-gradient(120deg, #1677ff 0%, #722ed1 55%, #9254de 100%); +} + +.portal-card { + border-radius: var(--portal-card-radius); +} + +@keyframes partner-marquee { + from { + transform: translateX(0); + } + to { + transform: translateX(-50%); + } +} + +.animate-partner-marquee { + animation: partner-marquee 28s linear infinite; +} diff --git a/portal/app/layout.tsx b/portal/app/layout.tsx new file mode 100644 index 0000000..9129b37 --- /dev/null +++ b/portal/app/layout.tsx @@ -0,0 +1,31 @@ +import type { Metadata } from "next"; + +import { AppProviders } from "@/app/providers"; +import { ConfigGate } from "@/components/config-gate"; +import { SiteHeader } from "@/components/site-header"; + +import "./globals.css"; + +export const metadata: Metadata = { + title: "AI API Gateway", + description: "企业级多模型接入网关", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + + + +
{children}
+
+
+ + + ); +} diff --git a/portal/app/login/page.tsx b/portal/app/login/page.tsx new file mode 100644 index 0000000..b56a795 --- /dev/null +++ b/portal/app/login/page.tsx @@ -0,0 +1,121 @@ +"use client"; + +import { Button, Card, Form, Toast, Typography } from "@douyinfe/semi-ui"; +import Link from "next/link"; +import { useRouter, useSearchParams } from "next/navigation"; +import { Suspense, useEffect, useState } from "react"; + +import { BrandLogo } from "@/components/brand-logo"; +import { useAuth } from "@/contexts/auth-context"; +import { ApiError, gatewayFetch } from "@/lib/api/client"; +import type { LoginResponse } from "@/lib/api/types"; + +const { Title, Text } = Typography; + +function LoginForm() { + const { setToken, setUser } = useAuth(); + const router = useRouter(); + const searchParams = useSearchParams(); + const [loading, setLoading] = useState(false); + + useEffect(() => { + if (searchParams.get("expired") === "true") { + Toast.warning("未登录或登录已过期"); + } + }, [searchParams]); + + return ( +
+ +
+ +
+ + 登录 + +
{ + const v = values as { login: string; password: string }; + setLoading(true); + try { + const loginField = v.login.includes("@") + ? { email: v.login } + : { username: v.login }; + const data = await gatewayFetch("/auth/login", { + method: "POST", + body: JSON.stringify({ + ...loginField, + password: v.password, + }), + }); + setToken(data.access_token); + if (data.data) { + setUser(data.data); + } + Toast.success("登录成功"); + router.replace("/console"); + } catch (e) { + if (e instanceof ApiError && e.status === 401) { + Toast.error("用户名或密码不正确"); + } else if (e instanceof ApiError) { + Toast.error(e.message || "登录失败"); + } else { + Toast.error("网络异常,请稍后重试"); + } + } finally { + setLoading(false); + } + }} + > + + +
+ + 忘记密码 + +
+ + + + 还没有账号?{" "} + + 注册 + + +
+
+ ); +} + +export default function LoginPage() { + return ( + 加载中… + } + > + + + ); +} diff --git a/portal/app/page.tsx b/portal/app/page.tsx new file mode 100644 index 0000000..59b8a1f --- /dev/null +++ b/portal/app/page.tsx @@ -0,0 +1,107 @@ +"use client"; + +import { Button, Card, Tag, Typography } from "@douyinfe/semi-ui"; +import { Play, FileText, Zap, Shield } from "lucide-react"; +import Link from "next/link"; + +import { AnnouncementModal } from "@/components/announcement/announcement-modal"; +import { PartnerMarquee } from "@/components/marketing/partner-marquee"; +import { usePortalLocale } from "@/contexts/portal-locale-context"; + +const { Title, Paragraph, Text } = Typography; + +export default function HomePage() { + const { messages: m } = usePortalLocale(); + + const features = [ + { + icon: Zap, + title: m.home.feature1Title, + desc: "基于协程池与连接复用,从容应对高并发 Token 请求,毫秒级负载均衡,为 AI 生产力提供稳定心跳。", + }, + { + icon: Shield, + title: m.home.feature2Title, + desc: "打破算力垄断,提供透明分组倍率与按量计费,让每一次模型调用都物超所值。", + }, + { + icon: FileText, + title: m.home.feature3Title, + desc: "OpenAI / Anthropic 兼容端点,一套密钥接入 Claude、GPT 等主流模型,降低集成成本。", + }, + ]; + + return ( +
+ +
+ + Claude / Codex 核心接口已接入 + + + {m.home.heroTitle} + + + {m.home.heroSubtitle} + + + 专为 Claude、OpenAI Codex 等头部大模型提供高并发、高可用 API 接入。极致的稳定性保证,全网更具性价比的算力调用引擎。 + +
+ + + + + + +
+
+ +
+
+ {features.map((f) => ( + + + + + + {f.title} + + + {f.desc} + + + ))} +
+
+ + +
+ ); +} diff --git a/portal/app/pricing/page.tsx b/portal/app/pricing/page.tsx new file mode 100644 index 0000000..f5599ae --- /dev/null +++ b/portal/app/pricing/page.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { ModelPricingPage } from "@/components/pricing/model-pricing-page"; + +export default function PricingPage() { + return ; +} diff --git a/portal/app/providers.tsx b/portal/app/providers.tsx new file mode 100644 index 0000000..af9b7f9 --- /dev/null +++ b/portal/app/providers.tsx @@ -0,0 +1,12 @@ +"use client"; + +import { AuthProvider } from "@/contexts/auth-context"; +import { PortalLocaleProvider } from "@/contexts/portal-locale-context"; + +export function AppProviders({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} diff --git a/portal/app/register/page.tsx b/portal/app/register/page.tsx new file mode 100644 index 0000000..c322a23 --- /dev/null +++ b/portal/app/register/page.tsx @@ -0,0 +1,126 @@ +"use client"; + +import { Button, Card, Form, Toast, Typography } from "@douyinfe/semi-ui"; +import Link from "next/link"; +import { useRouter, useSearchParams } from "next/navigation"; +import { Suspense, useMemo, useState } from "react"; + +import { BrandLogo } from "@/components/brand-logo"; +import { ApiError, gatewayFetch } from "@/lib/api/client"; + +const { Title, Text } = Typography; + +function RegisterForm() { + const router = useRouter(); + const searchParams = useSearchParams(); + const aff = useMemo(() => searchParams.get("aff") ?? "", [searchParams]); + const [loading, setLoading] = useState(false); + + return ( +
+ +
+ +
+ + 注册 + +
{ + const v = values as { + username: string; + email: string; + password: string; + invite_code?: string; + }; + setLoading(true); + try { + await gatewayFetch("/auth/register", { + method: "POST", + body: JSON.stringify({ + username: v.username, + email: v.email, + password: v.password, + invite_code: v.invite_code || undefined, + }), + }); + Toast.success("注册成功,请登录"); + router.push("/login"); + } catch (e) { + if (e instanceof ApiError) { + Toast.error(e.message || "注册失败"); + } else { + Toast.error("网络异常,请稍后重试"); + } + } finally { + setLoading(false); + } + }} + > + + + + {aff ? ( + + ) : ( + + )} + + + + 已有账号?{" "} + + 继续登录 + + +
+
+ ); +} + +export default function RegisterPage() { + return ( + 加载中… + } + > + + + ); +} diff --git a/portal/bun.lock b/portal/bun.lock new file mode 100644 index 0000000..7d4562a --- /dev/null +++ b/portal/bun.lock @@ -0,0 +1,1399 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "portal", + "dependencies": { + "@douyinfe/semi-icons": "^2.90.13", + "@douyinfe/semi-ui": "^2.90.13", + "lucide-react": "^0.511.0", + "next": "16.2.6", + "qrcode.react": "^4.2.0", + "react": "19.2.4", + "react-dom": "19.2.4", + "recharts": "^2.15.4", + }, + "devDependencies": { + "@tailwindcss/postcss": "^4", + "@types/node": "^20", + "@types/pngjs": "^6.0.5", + "@types/react": "^19", + "@types/react-dom": "^19", + "eslint": "^9", + "eslint-config-next": "16.2.6", + "pixelmatch": "^7.2.0", + "playwright": "^1.60.0", + "pngjs": "^7.0.0", + "tailwindcss": "^4", + "typescript": "^5", + }, + }, + }, + "packages": { + "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "https://registry.npmmirror.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], + + "@babel/code-frame": ["@babel/code-frame@7.29.7", "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.29.7.tgz", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], + + "@babel/compat-data": ["@babel/compat-data@7.29.7", "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.29.7.tgz", {}, "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg=="], + + "@babel/core": ["@babel/core@7.29.7", "https://registry.npmmirror.com/@babel/core/-/core-7.29.7.tgz", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-module-transforms": "^7.29.7", "@babel/helpers": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA=="], + + "@babel/generator": ["@babel/generator@7.29.7", "https://registry.npmmirror.com/@babel/generator/-/generator-7.29.7.tgz", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="], + + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.29.7", "https://registry.npmmirror.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", { "dependencies": { "@babel/compat-data": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g=="], + + "@babel/helper-globals": ["@babel/helper-globals@7.29.7", "https://registry.npmmirror.com/@babel/helper-globals/-/helper-globals-7.29.7.tgz", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="], + + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.29.7", "https://registry.npmmirror.com/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g=="], + + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.29.7", "https://registry.npmmirror.com/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg=="], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.29.7", "https://registry.npmmirror.com/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", {}, "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw=="], + + "@babel/helpers": ["@babel/helpers@7.29.7", "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.29.7.tgz", { "dependencies": { "@babel/template": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg=="], + + "@babel/parser": ["@babel/parser@7.29.7", "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.7.tgz", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": { "parser": "bin/babel-parser.js" } }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], + + "@babel/runtime": ["@babel/runtime@7.29.7", "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.29.7.tgz", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="], + + "@babel/template": ["@babel/template@7.29.7", "https://registry.npmmirror.com/@babel/template/-/template-7.29.7.tgz", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], + + "@babel/traverse": ["@babel/traverse@7.29.7", "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.29.7.tgz", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="], + + "@babel/types": ["@babel/types@7.29.7", "https://registry.npmmirror.com/@babel/types/-/types-7.29.7.tgz", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], + + "@dnd-kit/accessibility": ["@dnd-kit/accessibility@3.1.1", "https://registry.npmmirror.com/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw=="], + + "@dnd-kit/core": ["@dnd-kit/core@6.3.1", "https://registry.npmmirror.com/@dnd-kit/core/-/core-6.3.1.tgz", { "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ=="], + + "@dnd-kit/sortable": ["@dnd-kit/sortable@7.0.2", "https://registry.npmmirror.com/@dnd-kit/sortable/-/sortable-7.0.2.tgz", { "dependencies": { "@dnd-kit/utilities": "^3.2.0", "tslib": "^2.0.0" }, "peerDependencies": { "@dnd-kit/core": "^6.0.7", "react": ">=16.8.0" } }, "sha512-wDkBHHf9iCi1veM834Gbk1429bd4lHX4RpAwT0y2cHLf246GAvU2sVw/oxWNpPKQNQRQaeGXhAVgrOl1IT+iyA=="], + + "@dnd-kit/utilities": ["@dnd-kit/utilities@3.2.2", "https://registry.npmmirror.com/@dnd-kit/utilities/-/utilities-3.2.2.tgz", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg=="], + + "@douyinfe/semi-animation": ["@douyinfe/semi-animation@2.99.2", "https://registry.npmmirror.com/@douyinfe/semi-animation/-/semi-animation-2.99.2.tgz", { "dependencies": { "bezier-easing": "^2.1.0" } }, "sha512-MxAcD2PwHbpUSq7CFltBXST//qcyHzR5yaGbCTVUUQwLPnEctkunC52i/CLPTGqX6JMoivXpeKPHPSKNL2wcyw=="], + + "@douyinfe/semi-animation-react": ["@douyinfe/semi-animation-react@2.99.2", "https://registry.npmmirror.com/@douyinfe/semi-animation-react/-/semi-animation-react-2.99.2.tgz", { "dependencies": { "@douyinfe/semi-animation": "2.99.2", "@douyinfe/semi-animation-styled": "2.99.2", "classnames": "^2.2.6" } }, "sha512-7aiU529XoNL6jpV5Tdj0K0o+Pp9dRfypTPcHZY53YMLOQ7b8n4rh+G8C7iRgmgExphu8iifqylwVIOMU1ZirUw=="], + + "@douyinfe/semi-animation-styled": ["@douyinfe/semi-animation-styled@2.99.2", "https://registry.npmmirror.com/@douyinfe/semi-animation-styled/-/semi-animation-styled-2.99.2.tgz", {}, "sha512-4PvNW02ytNxoyEPpi9emYITrchf9Md/wSDfuvQWJfdFjK0JKvuOITRK10HCVp02GrCtbFK+Hf6pPe9lSRVOlVQ=="], + + "@douyinfe/semi-foundation": ["@douyinfe/semi-foundation@2.99.2", "https://registry.npmmirror.com/@douyinfe/semi-foundation/-/semi-foundation-2.99.2.tgz", { "dependencies": { "@douyinfe/semi-animation": "2.99.2", "@douyinfe/semi-json-viewer-core": "2.99.2", "@mdx-js/mdx": "^3.0.1", "async-validator": "^3.5.0", "classnames": "^2.2.6", "date-fns": "^2.29.3", "date-fns-tz": "^1.3.8", "fast-copy": "^3.0.1 ", "lodash": "^4.17.21", "lottie-web": "^5.13.0", "memoize-one": "^5.2.1", "prismjs": "^1.29.0", "remark-gfm": "^4.0.0", "scroll-into-view-if-needed": "^2.2.24" } }, "sha512-N5ZdrDdMEXSHe5vI+3CIuVwjd4m0OHlLRGfkJjgUctL1W0zOhlg9DRTfJn0tgefIn94ILJOKTOS3zC07Z9ZQ1g=="], + + "@douyinfe/semi-icons": ["@douyinfe/semi-icons@2.99.2", "https://registry.npmmirror.com/@douyinfe/semi-icons/-/semi-icons-2.99.2.tgz", { "dependencies": { "classnames": "^2.2.6" }, "peerDependencies": { "react": ">=16.0.0" } }, "sha512-S4TwmY7oPpPj3lg6OI9l2HNbK6KExP8tUO+wrOcPLXSe9XkQr5CCA7T+B3qNb1CPZ6PGsj7v2v51xlvOsD1Hpg=="], + + "@douyinfe/semi-illustrations": ["@douyinfe/semi-illustrations@2.99.2", "https://registry.npmmirror.com/@douyinfe/semi-illustrations/-/semi-illustrations-2.99.2.tgz", { "peerDependencies": { "react": ">=16.0.0" } }, "sha512-Ozla5vM9+tS5YQVEwO1IogBcxD+GE+NajZB2DIC3dEfxpPb2pyYg12sECbx2hq1jtLQ61CjZGOwWcohCm86Cig=="], + + "@douyinfe/semi-json-viewer-core": ["@douyinfe/semi-json-viewer-core@2.99.2", "https://registry.npmmirror.com/@douyinfe/semi-json-viewer-core/-/semi-json-viewer-core-2.99.2.tgz", { "dependencies": { "jsonc-parser": "^3.3.1" } }, "sha512-UYv5nfa2kajy7J7du5yDDcuuMkRivS6lYABxbrcHhwwDIg29QIZOFfKrs6JjHwEjZji6pl6BnFIj+tDXeA5R6A=="], + + "@douyinfe/semi-theme-default": ["@douyinfe/semi-theme-default@2.99.2", "https://registry.npmmirror.com/@douyinfe/semi-theme-default/-/semi-theme-default-2.99.2.tgz", {}, "sha512-1vh9IaYgZbIzoP7gknaltmk69vx6udfBYQCKIbHWNtg5gVJbwiUZCocmd2LNihdiZLuevBrWzmYOKbIaPHWh5w=="], + + "@douyinfe/semi-ui": ["@douyinfe/semi-ui@2.99.2", "https://registry.npmmirror.com/@douyinfe/semi-ui/-/semi-ui-2.99.2.tgz", { "dependencies": { "@dnd-kit/core": "^6.0.8", "@dnd-kit/sortable": "^7.0.2", "@dnd-kit/utilities": "^3.2.1", "@douyinfe/semi-animation": "2.99.2", "@douyinfe/semi-animation-react": "2.99.2", "@douyinfe/semi-foundation": "2.99.2", "@douyinfe/semi-icons": "2.99.2", "@douyinfe/semi-illustrations": "2.99.2", "@douyinfe/semi-theme-default": "2.99.2", "@tiptap/core": "^3.10.7", "@tiptap/extension-document": "^3.10.7", "@tiptap/extension-hard-break": "^3.10.7", "@tiptap/extension-image": "^3.10.7", "@tiptap/extension-mention": "^3.10.7", "@tiptap/extension-paragraph": "^3.10.7", "@tiptap/extension-text": "^3.10.7", "@tiptap/extension-text-align": "^3.10.7", "@tiptap/extension-text-style": "^3.10.7", "@tiptap/extensions": "^3.10.7", "@tiptap/pm": "^3.10.7", "@tiptap/react": "^3.10.7", "@tiptap/starter-kit": "^3.10.7", "async-validator": "^3.5.0", "classnames": "^2.2.6", "copy-text-to-clipboard": "^2.1.1", "date-fns": "^2.29.3", "date-fns-tz": "^1.3.8", "fast-copy": "^3.0.1 ", "jsonc-parser": "^3.3.1", "lodash": "^4.17.21", "prop-types": "^15.7.2", "prosemirror-state": "^1.4.3", "react-resizable": "^3.0.5", "react-window": "^1.8.2", "scroll-into-view-if-needed": "^2.2.24", "utility-types": "^3.10.0" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-0UKksaHD7HRXyjhEdb/Ak8F70Cz7E2oS9yEu8WInhDN6CFytW5Jx7tD8SB2wb2MkosyxHgl94mzkTg8X2e8zlw=="], + + "@emnapi/core": ["@emnapi/core@1.10.0", "https://registry.npmmirror.com/@emnapi/core/-/core-1.10.0.tgz", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], + + "@emnapi/runtime": ["@emnapi/runtime@1.10.0", "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.10.0.tgz", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], + + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "https://registry.npmmirror.com/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], + + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "https://registry.npmmirror.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], + + "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "https://registry.npmmirror.com/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], + + "@eslint/config-array": ["@eslint/config-array@0.21.2", "https://registry.npmmirror.com/@eslint/config-array/-/config-array-0.21.2.tgz", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.5" } }, "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw=="], + + "@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "https://registry.npmmirror.com/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="], + + "@eslint/core": ["@eslint/core@0.17.0", "https://registry.npmmirror.com/@eslint/core/-/core-0.17.0.tgz", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="], + + "@eslint/eslintrc": ["@eslint/eslintrc@3.3.5", "https://registry.npmmirror.com/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", { "dependencies": { "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" } }, "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg=="], + + "@eslint/js": ["@eslint/js@9.39.4", "https://registry.npmmirror.com/@eslint/js/-/js-9.39.4.tgz", {}, "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw=="], + + "@eslint/object-schema": ["@eslint/object-schema@2.1.7", "https://registry.npmmirror.com/@eslint/object-schema/-/object-schema-2.1.7.tgz", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="], + + "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "https://registry.npmmirror.com/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="], + + "@floating-ui/core": ["@floating-ui/core@1.7.5", "https://registry.npmmirror.com/@floating-ui/core/-/core-1.7.5.tgz", { "dependencies": { "@floating-ui/utils": "^0.2.11" } }, "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ=="], + + "@floating-ui/dom": ["@floating-ui/dom@1.7.6", "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.7.6.tgz", { "dependencies": { "@floating-ui/core": "^1.7.5", "@floating-ui/utils": "^0.2.11" } }, "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ=="], + + "@floating-ui/utils": ["@floating-ui/utils@0.2.11", "https://registry.npmmirror.com/@floating-ui/utils/-/utils-0.2.11.tgz", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="], + + "@humanfs/core": ["@humanfs/core@0.19.2", "https://registry.npmmirror.com/@humanfs/core/-/core-0.19.2.tgz", { "dependencies": { "@humanfs/types": "^0.15.0" } }, "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA=="], + + "@humanfs/node": ["@humanfs/node@0.16.8", "https://registry.npmmirror.com/@humanfs/node/-/node-0.16.8.tgz", { "dependencies": { "@humanfs/core": "^0.19.2", "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ=="], + + "@humanfs/types": ["@humanfs/types@0.15.0", "https://registry.npmmirror.com/@humanfs/types/-/types-0.15.0.tgz", {}, "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q=="], + + "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "https://registry.npmmirror.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], + + "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "https://registry.npmmirror.com/@humanwhocodes/retry/-/retry-0.4.3.tgz", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], + + "@img/colour": ["@img/colour@1.1.0", "https://registry.npmmirror.com/@img/colour/-/colour-1.1.0.tgz", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="], + + "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "https://registry.npmmirror.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], + + "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "https://registry.npmmirror.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="], + + "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "https://registry.npmmirror.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="], + + "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "https://registry.npmmirror.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="], + + "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "https://registry.npmmirror.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="], + + "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "https://registry.npmmirror.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="], + + "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "https://registry.npmmirror.com/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="], + + "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "https://registry.npmmirror.com/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="], + + "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "https://registry.npmmirror.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="], + + "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "https://registry.npmmirror.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="], + + "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "https://registry.npmmirror.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="], + + "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "https://registry.npmmirror.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="], + + "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "https://registry.npmmirror.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="], + + "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "https://registry.npmmirror.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="], + + "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "https://registry.npmmirror.com/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="], + + "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "https://registry.npmmirror.com/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="], + + "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "https://registry.npmmirror.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="], + + "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "https://registry.npmmirror.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="], + + "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "https://registry.npmmirror.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="], + + "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "https://registry.npmmirror.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="], + + "@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "https://registry.npmmirror.com/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="], + + "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "https://registry.npmmirror.com/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="], + + "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "https://registry.npmmirror.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="], + + "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "https://registry.npmmirror.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "https://registry.npmmirror.com/@jridgewell/remapping/-/remapping-2.3.5.tgz", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "https://registry.npmmirror.com/@mdx-js/mdx/-/mdx-3.1.1.tgz", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="], + + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "https://registry.npmmirror.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="], + + "@next/env": ["@next/env@16.2.6", "https://registry.npmmirror.com/@next/env/-/env-16.2.6.tgz", {}, "sha512-gd8HoHN4ufj73WmR3JmVolrpJR47ILK6LouP5xElPglaVxir6e1a7VzvTvDWkOoPXT9rkkTzyCxBu4yeZfZwcw=="], + + "@next/eslint-plugin-next": ["@next/eslint-plugin-next@16.2.6", "https://registry.npmmirror.com/@next/eslint-plugin-next/-/eslint-plugin-next-16.2.6.tgz", { "dependencies": { "fast-glob": "3.3.1" } }, "sha512-Z8l6o4JWKUl755x4R+wogD86KPeU+Ckw4K+SYG4kHeOJtRenDeK+OSbGcqZpDtbwn9DsJVdir2UxmwXuinUbUw=="], + + "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.2.6", "https://registry.npmmirror.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.6.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-ZJGkkcNfYgrrMkqOdZ7zoLa1TOy0qpcMfk/z4Mh/FKUz40gVO+HNQWqmLxf67Z5WB64DRp0dhEbyHfel+6sJUg=="], + + "@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.2.6", "https://registry.npmmirror.com/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.6.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-v/YLBHIY132Ced3puBJ7YJKw1lqsCrgcNo2aRJlCEyQrrCeRJlvGlnmxhPxNQI3KE3N1DN5r9TPNPvka3nq5RQ=="], + + "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.2.6", "https://registry.npmmirror.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.6.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-RPOvqlYBbcQjkz9VQQDZ2T2bARIjXZV1KFlt+V2Mr6SW/e4I9fcKsaA0hdyf2FHoTlsV2xnBd5Y912rP/1Ce6w=="], + + "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.2.6", "https://registry.npmmirror.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.6.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA=="], + + "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.2.6", "https://registry.npmmirror.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.6.tgz", { "os": "linux", "cpu": "x64" }, "sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw=="], + + "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.2.6", "https://registry.npmmirror.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.6.tgz", { "os": "linux", "cpu": "x64" }, "sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g=="], + + "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.2.6", "https://registry.npmmirror.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.6.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg=="], + + "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.2.6", "https://registry.npmmirror.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.6.tgz", { "os": "win32", "cpu": "x64" }, "sha512-F0+4i0h9J6C4eE3EAPWsoCk7UW/dbzOjyzxY0qnDUOYFu6FFmdZ6l97/XdV3/Nz3VYyO7UWjyEJUXkGqcoXfMA=="], + + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "https://registry.npmmirror.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "https://registry.npmmirror.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + + "@nolyfill/is-core-module": ["@nolyfill/is-core-module@1.0.39", "https://registry.npmmirror.com/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", {}, "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA=="], + + "@rtsao/scc": ["@rtsao/scc@1.1.0", "https://registry.npmmirror.com/@rtsao/scc/-/scc-1.1.0.tgz", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="], + + "@swc/helpers": ["@swc/helpers@0.5.15", "https://registry.npmmirror.com/@swc/helpers/-/helpers-0.5.15.tgz", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="], + + "@tailwindcss/node": ["@tailwindcss/node@4.3.0", "https://registry.npmmirror.com/@tailwindcss/node/-/node-4.3.0.tgz", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.21.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.0" } }, "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g=="], + + "@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.0", "https://registry.npmmirror.com/@tailwindcss/oxide/-/oxide-4.3.0.tgz", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.0", "@tailwindcss/oxide-darwin-arm64": "4.3.0", "@tailwindcss/oxide-darwin-x64": "4.3.0", "@tailwindcss/oxide-freebsd-x64": "4.3.0", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0", "@tailwindcss/oxide-linux-arm64-musl": "4.3.0", "@tailwindcss/oxide-linux-x64-gnu": "4.3.0", "@tailwindcss/oxide-linux-x64-musl": "4.3.0", "@tailwindcss/oxide-wasm32-wasi": "4.3.0", "@tailwindcss/oxide-win32-arm64-msvc": "4.3.0", "@tailwindcss/oxide-win32-x64-msvc": "4.3.0" } }, "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg=="], + + "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.3.0", "https://registry.npmmirror.com/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.0.tgz", { "os": "android", "cpu": "arm64" }, "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng=="], + + "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.3.0", "https://registry.npmmirror.com/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.0.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ=="], + + "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.3.0", "https://registry.npmmirror.com/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.0.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA=="], + + "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.3.0", "https://registry.npmmirror.com/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.0.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ=="], + + "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0", "https://registry.npmmirror.com/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.0.tgz", { "os": "linux", "cpu": "arm" }, "sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA=="], + + "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.3.0", "https://registry.npmmirror.com/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.0.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg=="], + + "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.3.0", "https://registry.npmmirror.com/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.0.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ=="], + + "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.3.0", "https://registry.npmmirror.com/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.0.tgz", { "os": "linux", "cpu": "x64" }, "sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ=="], + + "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.3.0", "https://registry.npmmirror.com/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.0.tgz", { "os": "linux", "cpu": "x64" }, "sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg=="], + + "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.3.0", "https://registry.npmmirror.com/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.0.tgz", { "cpu": "none" }, "sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA=="], + + "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.3.0", "https://registry.npmmirror.com/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.0.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ=="], + + "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.0", "https://registry.npmmirror.com/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.0.tgz", { "os": "win32", "cpu": "x64" }, "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA=="], + + "@tailwindcss/postcss": ["@tailwindcss/postcss@4.3.0", "https://registry.npmmirror.com/@tailwindcss/postcss/-/postcss-4.3.0.tgz", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.3.0", "@tailwindcss/oxide": "4.3.0", "postcss": "^8.5.10", "tailwindcss": "4.3.0" } }, "sha512-Jm05Tjx+9yCLGv5qw1c+84Psds8MnyrEQYCB+FFk2lgGiUjlRqdxke4mVTuYrj2xnVZqKim2Apr5ySuQRYAw/w=="], + + "@tiptap/core": ["@tiptap/core@3.23.6", "https://registry.npmmirror.com/@tiptap/core/-/core-3.23.6.tgz", { "peerDependencies": { "@tiptap/pm": "3.23.6" } }, "sha512-MRB3pHz4Oxqmcawh0cQ5iOGdY5xtNYp/1CoK7hdTLzw5K0C6/gTC2VvanB1R4INaB6EpBkxG/GiWkVirDRnuXw=="], + + "@tiptap/extension-blockquote": ["@tiptap/extension-blockquote@3.23.6", "https://registry.npmmirror.com/@tiptap/extension-blockquote/-/extension-blockquote-3.23.6.tgz", { "peerDependencies": { "@tiptap/core": "3.23.6" } }, "sha512-2RmnqNqTltZ2k1F7IfjoDNs935Uq4rRDR7d98mqkg3OlDktcQIyBpv0t9dTay6H5bkQeZUuS8ogK2S1E8Edjug=="], + + "@tiptap/extension-bold": ["@tiptap/extension-bold@3.23.6", "https://registry.npmmirror.com/@tiptap/extension-bold/-/extension-bold-3.23.6.tgz", { "peerDependencies": { "@tiptap/core": "3.23.6" } }, "sha512-1LMhjnytdbbhWHSoOwnLxZAOQZWPkKyXVCNmaIk0Mhi4tLPUXptG4qKS5sVYTCveE5H6IBPFrbgBFi5dMI6krA=="], + + "@tiptap/extension-bubble-menu": ["@tiptap/extension-bubble-menu@3.23.6", "https://registry.npmmirror.com/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.23.6.tgz", { "dependencies": { "@floating-ui/dom": "^1.0.0" }, "peerDependencies": { "@tiptap/core": "3.23.6", "@tiptap/pm": "3.23.6" } }, "sha512-Mwkyp9LkDHFbqmWRIkp63FinRxFu3ajC4qSb9t4mnHsb4kAdbNLLsGtbFg+le0SWk4CxGwAOwM7SzeJ+6UGqCA=="], + + "@tiptap/extension-bullet-list": ["@tiptap/extension-bullet-list@3.23.6", "https://registry.npmmirror.com/@tiptap/extension-bullet-list/-/extension-bullet-list-3.23.6.tgz", { "peerDependencies": { "@tiptap/extension-list": "3.23.6" } }, "sha512-RMRgfXZykr/13X8UBOwvpgysVOo9KchwqMoEbvqQSj4YFfU56iIn59C8sbxiQ1sKfeltUf0wH4fPc0I4iwKqAA=="], + + "@tiptap/extension-code": ["@tiptap/extension-code@3.23.6", "https://registry.npmmirror.com/@tiptap/extension-code/-/extension-code-3.23.6.tgz", { "peerDependencies": { "@tiptap/core": "3.23.6" } }, "sha512-KG8KXFYyLrtYvT7AZ1WGV61ofx8pDe5g9pH658MERxqQGii+Pyfc6xkz04l7XeBts/7+571UQp/0O7i/z560TA=="], + + "@tiptap/extension-code-block": ["@tiptap/extension-code-block@3.23.6", "https://registry.npmmirror.com/@tiptap/extension-code-block/-/extension-code-block-3.23.6.tgz", { "peerDependencies": { "@tiptap/core": "3.23.6", "@tiptap/pm": "3.23.6" } }, "sha512-4kccgcn5yHThxrzsIhJny3EwfEZYIk+BjUCL4uIuzOyWvExtGhZ6JMHVCZeMhI8D1/bX1LNkkAKN5DXPzH4lXQ=="], + + "@tiptap/extension-document": ["@tiptap/extension-document@3.23.6", "https://registry.npmmirror.com/@tiptap/extension-document/-/extension-document-3.23.6.tgz", { "peerDependencies": { "@tiptap/core": "3.23.6" } }, "sha512-XDAIgG9KcKumFM9KJWUEUhXPbFIhhl47bfy5GknareWTRKke85rcoj/oxKKO9ihLZr8JfpbXjqnS4SCm5yhYPw=="], + + "@tiptap/extension-dropcursor": ["@tiptap/extension-dropcursor@3.23.6", "https://registry.npmmirror.com/@tiptap/extension-dropcursor/-/extension-dropcursor-3.23.6.tgz", { "peerDependencies": { "@tiptap/extensions": "3.23.6" } }, "sha512-+XWEoRKf3lXxi7Le1aOM2xU1XHwxICGpXjT3m4QaYqUgIpsq8gQEuso6kVg8DnTD7biKQs6+oIQ0o2b/gTW9WA=="], + + "@tiptap/extension-floating-menu": ["@tiptap/extension-floating-menu@3.23.6", "https://registry.npmmirror.com/@tiptap/extension-floating-menu/-/extension-floating-menu-3.23.6.tgz", { "peerDependencies": { "@floating-ui/dom": "^1.0.0", "@tiptap/core": "3.23.6", "@tiptap/pm": "3.23.6" } }, "sha512-2kjuDcEq69lEcECl75xqY5MyzUSh2zcC5aLrpwP1WwhJz5bxsIFHiaps5AP6h9R4A+ZBj5b2haay2Y1wDUU3VA=="], + + "@tiptap/extension-gapcursor": ["@tiptap/extension-gapcursor@3.23.6", "https://registry.npmmirror.com/@tiptap/extension-gapcursor/-/extension-gapcursor-3.23.6.tgz", { "peerDependencies": { "@tiptap/extensions": "3.23.6" } }, "sha512-wbKmxXsszxWacEkrHucRpSQbiKjz4fmOebD6OVyL9AcrmlbxNk8vcM3iyh/8cVeRy09XY+morM165t/u7/z4IQ=="], + + "@tiptap/extension-hard-break": ["@tiptap/extension-hard-break@3.23.6", "https://registry.npmmirror.com/@tiptap/extension-hard-break/-/extension-hard-break-3.23.6.tgz", { "peerDependencies": { "@tiptap/core": "3.23.6" } }, "sha512-KeUm+tkUfIVSX9QM9XOIhaay0Fn36sLKUo5NVYjN3uJaxFvaZXZmTlxdO85OTdgF2P5sqh9LomrIgliaFRGk4w=="], + + "@tiptap/extension-heading": ["@tiptap/extension-heading@3.23.6", "https://registry.npmmirror.com/@tiptap/extension-heading/-/extension-heading-3.23.6.tgz", { "peerDependencies": { "@tiptap/core": "3.23.6" } }, "sha512-A/0jPhxnUh9THSZymlu0OGPZe1wdFdwHAXnRCmqvYUCwJjrG7LCC/ahzmcj1tcNzI9hgHyuYPSfev8RXYrNu/w=="], + + "@tiptap/extension-horizontal-rule": ["@tiptap/extension-horizontal-rule@3.23.6", "https://registry.npmmirror.com/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.23.6.tgz", { "peerDependencies": { "@tiptap/core": "3.23.6", "@tiptap/pm": "3.23.6" } }, "sha512-hEUlz4H+I64r+TH6LCuNCRgO7JTHncXGmx9+WbU69EOfY8O0ZurcgeJc8HeiAKL+r9YuC1e5YHfFxgCaaC0jlg=="], + + "@tiptap/extension-image": ["@tiptap/extension-image@3.23.6", "https://registry.npmmirror.com/@tiptap/extension-image/-/extension-image-3.23.6.tgz", { "peerDependencies": { "@tiptap/core": "3.23.6" } }, "sha512-vvNGxArvD2dW+XvV0KdYovRVUzCy8QVNulc2r5pV7umnG1E6cCmMkiHiif8J2ePJu2KtysAvJQe0iF+UqueGMw=="], + + "@tiptap/extension-italic": ["@tiptap/extension-italic@3.23.6", "https://registry.npmmirror.com/@tiptap/extension-italic/-/extension-italic-3.23.6.tgz", { "peerDependencies": { "@tiptap/core": "3.23.6" } }, "sha512-wol5KdwCPAvpiYhH9PLlvO8ZnJHwZtIboVevrfOGgBcKlXRA3dedR4OAMXHnUtkkzu9KtliLg1+TYzEx4JZG9Q=="], + + "@tiptap/extension-link": ["@tiptap/extension-link@3.23.6", "https://registry.npmmirror.com/@tiptap/extension-link/-/extension-link-3.23.6.tgz", { "dependencies": { "linkifyjs": "^4.3.3" }, "peerDependencies": { "@tiptap/core": "3.23.6", "@tiptap/pm": "3.23.6" } }, "sha512-KNZz7z7P2/qbQsx5bPAbSPjrKDg1VHsedGlLHJCr8U2VRD5VgmDLkMpkouP1CsDg15qgyUKv/nDib5KgPpLNWA=="], + + "@tiptap/extension-list": ["@tiptap/extension-list@3.23.6", "https://registry.npmmirror.com/@tiptap/extension-list/-/extension-list-3.23.6.tgz", { "peerDependencies": { "@tiptap/core": "3.23.6", "@tiptap/pm": "3.23.6" } }, "sha512-z6vj9+Qht2sjdQkyyHcUpsC/yCIZqTrQiyHDhs/HGKrfvoANyAZGpqdNeKf1wSyjIso+27tQuIH5NDfk8ygyNw=="], + + "@tiptap/extension-list-item": ["@tiptap/extension-list-item@3.23.6", "https://registry.npmmirror.com/@tiptap/extension-list-item/-/extension-list-item-3.23.6.tgz", { "peerDependencies": { "@tiptap/extension-list": "3.23.6" } }, "sha512-3zzyhdkUWcHVpXuvy6KiIwjh29rbH6gEDEqPQqHLrl1XGnO9pnShC7pSHctlCDjmcx3O4n9cd4QMtVBlUerbiA=="], + + "@tiptap/extension-list-keymap": ["@tiptap/extension-list-keymap@3.23.6", "https://registry.npmmirror.com/@tiptap/extension-list-keymap/-/extension-list-keymap-3.23.6.tgz", { "peerDependencies": { "@tiptap/extension-list": "3.23.6" } }, "sha512-x8bPcLViGzg/RAmQM/XtmfqIwQ/Pv9Q8mkd+OgfUiTqjeJqKwVQmiqbLFNa7zw81+H61M+HDU+qGAaQ3vRIMjw=="], + + "@tiptap/extension-mention": ["@tiptap/extension-mention@3.23.6", "https://registry.npmmirror.com/@tiptap/extension-mention/-/extension-mention-3.23.6.tgz", { "peerDependencies": { "@tiptap/core": "3.23.6", "@tiptap/pm": "3.23.6", "@tiptap/suggestion": "3.23.6" } }, "sha512-rSjeAAtuMwMA1lj4nbxz3rbmM06yPFUc8TFzhrEpmA4/l5XNWOk/PQef6uiGN+Isv2Z2PrIhr8XrR7Me8OSCiA=="], + + "@tiptap/extension-ordered-list": ["@tiptap/extension-ordered-list@3.23.6", "https://registry.npmmirror.com/@tiptap/extension-ordered-list/-/extension-ordered-list-3.23.6.tgz", { "peerDependencies": { "@tiptap/extension-list": "3.23.6" } }, "sha512-1m/wWB/ZtXcmG2vNdiUkCqsOgqv5vBjCv/mVaHhF9OvV+zQS8YDjoWE7zEuT/GgELdT77Xq8lHrn4nCDudB3/A=="], + + "@tiptap/extension-paragraph": ["@tiptap/extension-paragraph@3.23.6", "https://registry.npmmirror.com/@tiptap/extension-paragraph/-/extension-paragraph-3.23.6.tgz", { "peerDependencies": { "@tiptap/core": "3.23.6" } }, "sha512-+7m58LUSncodjrIyXks4RZ3tLNYrvgT77wRR4l3HnM5OABY3GDsDTqi7c1t1yI29NVOSk/DUacqy6UwYAj1DGg=="], + + "@tiptap/extension-strike": ["@tiptap/extension-strike@3.23.6", "https://registry.npmmirror.com/@tiptap/extension-strike/-/extension-strike-3.23.6.tgz", { "peerDependencies": { "@tiptap/core": "3.23.6" } }, "sha512-oF7FEZ37f15aCe5kPgzGDYf/m+hr7VdQ/Ko/Hds/UM9pX7AG1fdtmRrl6wqkRqDM/incZaC/AQR2/Dpo2VCNGQ=="], + + "@tiptap/extension-text": ["@tiptap/extension-text@3.23.6", "https://registry.npmmirror.com/@tiptap/extension-text/-/extension-text-3.23.6.tgz", { "peerDependencies": { "@tiptap/core": "3.23.6" } }, "sha512-ipoC2TkIAIOTiF5ByiGgvQB1DqDyfP90wrUB3mohBcgvp7lQnwHszCDGv8dNnmcUek8uXV/uoLu2VXeVQlxjPA=="], + + "@tiptap/extension-text-align": ["@tiptap/extension-text-align@3.23.6", "https://registry.npmmirror.com/@tiptap/extension-text-align/-/extension-text-align-3.23.6.tgz", { "peerDependencies": { "@tiptap/core": "3.23.6" } }, "sha512-pE+gDmvnrSMWHADDnDSzaO7YUi9kOywQkyrqZ9bEPLddZred7ICoJ4NtD2DqLjDSur+HijaJuByResWb78c6FA=="], + + "@tiptap/extension-text-style": ["@tiptap/extension-text-style@3.23.6", "https://registry.npmmirror.com/@tiptap/extension-text-style/-/extension-text-style-3.23.6.tgz", { "peerDependencies": { "@tiptap/core": "3.23.6" } }, "sha512-Auasgj469wkQ5ip+Zi2gaRzvqxx9qKG58+1mkT8yPE3QAGnOIg/AaKyQ7pTV77UyL3FHvLnU+KsWCad+qcobww=="], + + "@tiptap/extension-underline": ["@tiptap/extension-underline@3.23.6", "https://registry.npmmirror.com/@tiptap/extension-underline/-/extension-underline-3.23.6.tgz", { "peerDependencies": { "@tiptap/core": "3.23.6" } }, "sha512-P55wGIZGYTVH92Fq0cgI4/O9AhLCaJC3hhxg15RSERP5/YegM9eJHDK/GQ1EE/DvYA+xpYGOV6agKwAUqfA/Iw=="], + + "@tiptap/extensions": ["@tiptap/extensions@3.23.6", "https://registry.npmmirror.com/@tiptap/extensions/-/extensions-3.23.6.tgz", { "peerDependencies": { "@tiptap/core": "3.23.6", "@tiptap/pm": "3.23.6" } }, "sha512-X09/Db1teB+ifXzDGVVFmOeQRx7wTAayE9/280spxpsHkHZvJ5bHRvWIzUzviMIjbBz+NPDIKYPK7gMfh9iaig=="], + + "@tiptap/pm": ["@tiptap/pm@3.23.6", "https://registry.npmmirror.com/@tiptap/pm/-/pm-3.23.6.tgz", { "dependencies": { "prosemirror-changeset": "^2.3.0", "prosemirror-commands": "^1.6.2", "prosemirror-dropcursor": "^1.8.1", "prosemirror-gapcursor": "^1.3.2", "prosemirror-history": "^1.4.1", "prosemirror-keymap": "^1.2.2", "prosemirror-model": "^1.24.1", "prosemirror-schema-list": "^1.5.0", "prosemirror-state": "^1.4.3", "prosemirror-tables": "^1.6.4", "prosemirror-transform": "^1.10.2", "prosemirror-view": "^1.38.1" } }, "sha512-in5CaMaWlJcH2A1q6GJKFtrodE8WLS3M9tIi/f89jPmIVHJShpodC0KZDNyJkrVBQomYk0DEh86Utm6ASXzQww=="], + + "@tiptap/react": ["@tiptap/react@3.23.6", "https://registry.npmmirror.com/@tiptap/react/-/react-3.23.6.tgz", { "dependencies": { "@types/use-sync-external-store": "^0.0.6", "fast-equals": "^5.3.3", "use-sync-external-store": "^1.4.0" }, "optionalDependencies": { "@tiptap/extension-bubble-menu": "^3.23.6", "@tiptap/extension-floating-menu": "^3.23.6" }, "peerDependencies": { "@tiptap/core": "3.23.6", "@tiptap/pm": "3.23.6", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "@types/react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Tw9KZkYqFMk3vaJAEQKqEYIO/iq3cSJe7OUEGBul4k4GaMQeLItLf5EYhUd0GIPXci1WVVPNntKJsHfX25M37w=="], + + "@tiptap/starter-kit": ["@tiptap/starter-kit@3.23.6", "https://registry.npmmirror.com/@tiptap/starter-kit/-/starter-kit-3.23.6.tgz", { "dependencies": { "@tiptap/core": "^3.23.6", "@tiptap/extension-blockquote": "^3.23.6", "@tiptap/extension-bold": "^3.23.6", "@tiptap/extension-bullet-list": "^3.23.6", "@tiptap/extension-code": "^3.23.6", "@tiptap/extension-code-block": "^3.23.6", "@tiptap/extension-document": "^3.23.6", "@tiptap/extension-dropcursor": "^3.23.6", "@tiptap/extension-gapcursor": "^3.23.6", "@tiptap/extension-hard-break": "^3.23.6", "@tiptap/extension-heading": "^3.23.6", "@tiptap/extension-horizontal-rule": "^3.23.6", "@tiptap/extension-italic": "^3.23.6", "@tiptap/extension-link": "^3.23.6", "@tiptap/extension-list": "^3.23.6", "@tiptap/extension-list-item": "^3.23.6", "@tiptap/extension-list-keymap": "^3.23.6", "@tiptap/extension-ordered-list": "^3.23.6", "@tiptap/extension-paragraph": "^3.23.6", "@tiptap/extension-strike": "^3.23.6", "@tiptap/extension-text": "^3.23.6", "@tiptap/extension-underline": "^3.23.6", "@tiptap/extensions": "^3.23.6", "@tiptap/pm": "^3.23.6" } }, "sha512-gykwtGWrnWCmtql1hid3opac/KV8zQvOAnu3bTqIqcHrn1FusbUwKmNzavSbfGvcktHM3hFjb35W48JyVLyu/A=="], + + "@tiptap/suggestion": ["@tiptap/suggestion@3.23.6", "https://registry.npmmirror.com/@tiptap/suggestion/-/suggestion-3.23.6.tgz", { "peerDependencies": { "@tiptap/core": "3.23.6", "@tiptap/pm": "3.23.6" } }, "sha512-YAoI2jctPClcyUhIcpxb1QlrUFG2a1Xsv1gS4tIfgh5KoOuEfGfCoeCq89TKgz/rHeP+ktRhzg1E2E4EY68HEA=="], + + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "https://registry.npmmirror.com/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="], + + "@types/d3-array": ["@types/d3-array@3.2.2", "https://registry.npmmirror.com/@types/d3-array/-/d3-array-3.2.2.tgz", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="], + + "@types/d3-color": ["@types/d3-color@3.1.3", "https://registry.npmmirror.com/@types/d3-color/-/d3-color-3.1.3.tgz", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="], + + "@types/d3-ease": ["@types/d3-ease@3.0.2", "https://registry.npmmirror.com/@types/d3-ease/-/d3-ease-3.0.2.tgz", {}, "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="], + + "@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "https://registry.npmmirror.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="], + + "@types/d3-path": ["@types/d3-path@3.1.1", "https://registry.npmmirror.com/@types/d3-path/-/d3-path-3.1.1.tgz", {}, "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="], + + "@types/d3-scale": ["@types/d3-scale@4.0.9", "https://registry.npmmirror.com/@types/d3-scale/-/d3-scale-4.0.9.tgz", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="], + + "@types/d3-shape": ["@types/d3-shape@3.1.8", "https://registry.npmmirror.com/@types/d3-shape/-/d3-shape-3.1.8.tgz", { "dependencies": { "@types/d3-path": "*" } }, "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w=="], + + "@types/d3-time": ["@types/d3-time@3.0.4", "https://registry.npmmirror.com/@types/d3-time/-/d3-time-3.0.4.tgz", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="], + + "@types/d3-timer": ["@types/d3-timer@3.0.2", "https://registry.npmmirror.com/@types/d3-timer/-/d3-timer-3.0.2.tgz", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="], + + "@types/debug": ["@types/debug@4.1.13", "https://registry.npmmirror.com/@types/debug/-/debug-4.1.13.tgz", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="], + + "@types/estree": ["@types/estree@1.0.9", "https://registry.npmmirror.com/@types/estree/-/estree-1.0.9.tgz", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], + + "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "https://registry.npmmirror.com/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="], + + "@types/hast": ["@types/hast@3.0.4", "https://registry.npmmirror.com/@types/hast/-/hast-3.0.4.tgz", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], + + "@types/json-schema": ["@types/json-schema@7.0.15", "https://registry.npmmirror.com/@types/json-schema/-/json-schema-7.0.15.tgz", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + + "@types/json5": ["@types/json5@0.0.29", "https://registry.npmmirror.com/@types/json5/-/json5-0.0.29.tgz", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="], + + "@types/mdast": ["@types/mdast@4.0.4", "https://registry.npmmirror.com/@types/mdast/-/mdast-4.0.4.tgz", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], + + "@types/mdx": ["@types/mdx@2.0.13", "https://registry.npmmirror.com/@types/mdx/-/mdx-2.0.13.tgz", {}, "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw=="], + + "@types/ms": ["@types/ms@2.1.0", "https://registry.npmmirror.com/@types/ms/-/ms-2.1.0.tgz", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], + + "@types/node": ["@types/node@20.19.41", "https://registry.npmmirror.com/@types/node/-/node-20.19.41.tgz", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ=="], + + "@types/pngjs": ["@types/pngjs@6.0.5", "https://registry.npmmirror.com/@types/pngjs/-/pngjs-6.0.5.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-0k5eKfrA83JOZPppLtS2C7OUtyNAl2wKNxfyYl9Q5g9lPkgBl/9hNyAu6HuEH2J4XmIv2znEpkDd0SaZVxW6iQ=="], + + "@types/react": ["@types/react@19.2.15", "https://registry.npmmirror.com/@types/react/-/react-19.2.15.tgz", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q=="], + + "@types/react-dom": ["@types/react-dom@19.2.3", "https://registry.npmmirror.com/@types/react-dom/-/react-dom-19.2.3.tgz", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], + + "@types/unist": ["@types/unist@3.0.3", "https://registry.npmmirror.com/@types/unist/-/unist-3.0.3.tgz", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], + + "@types/use-sync-external-store": ["@types/use-sync-external-store@0.0.6", "https://registry.npmmirror.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", {}, "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="], + + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.60.0", "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.60.0.tgz", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.60.0", "@typescript-eslint/type-utils": "8.60.0", "@typescript-eslint/utils": "8.60.0", "@typescript-eslint/visitor-keys": "8.60.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.60.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-QYb/sa74/s7OKMbACMjrYnGspj9Hs5YI5aaffSL65UfeBUzVzBJfVo3oWSpbzPurvm7yaCCo2Lk7lVj610HqKw=="], + + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.60.0", "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-8.60.0.tgz", { "dependencies": { "@typescript-eslint/scope-manager": "8.60.0", "@typescript-eslint/types": "8.60.0", "@typescript-eslint/typescript-estree": "8.60.0", "@typescript-eslint/visitor-keys": "8.60.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-fcqpj/MyK4sxDPcbe7STNPbpQL4RLZOPWuaTmwZYuc+hJKzRf58yRxfhqGpc6PIq9ZyfSBpfHgmUHmHs0KwHwg=="], + + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.60.0", "https://registry.npmmirror.com/@typescript-eslint/project-service/-/project-service-8.60.0.tgz", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.60.0", "@typescript-eslint/types": "^8.60.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-aZu74NNKJeUWqCjDddzdiKaS82dgYgV/vmf+Ui3ZdZejmgfXR/q+pRumgobnQ2cCJTgGTWp4ypiwsuofFubavg=="], + + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.60.0", "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-8.60.0.tgz", { "dependencies": { "@typescript-eslint/types": "8.60.0", "@typescript-eslint/visitor-keys": "8.60.0" } }, "sha512-pFzqhllJMs+jghLQWzV00ds39xLzuyqPSev5pd8f4Ir0rtKR3ZLUB4/4dhjOFighWb9larvtfJvqL+4yKDI3Xw=="], + + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.60.0", "https://registry.npmmirror.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.0.tgz", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-BZPR3RGYlAXnly6ymAxfkVn5rCbZzQNou0rxv3GfWZ8cTQp+hhVd73khbGLAd8k1TlAPLISH337M+tAgAnaJDQ=="], + + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.60.0", "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-8.60.0.tgz", { "dependencies": { "@typescript-eslint/types": "8.60.0", "@typescript-eslint/typescript-estree": "8.60.0", "@typescript-eslint/utils": "8.60.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-SX46wEUtitCpq7AN38HkUU/+zvUpdKf7ephtWAFgckH8O7PQIyL5gvrhQgBLuEYgLfuKWOVvWVskMbuFHAz5xg=="], + + "@typescript-eslint/types": ["@typescript-eslint/types@8.60.0", "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.60.0.tgz", {}, "sha512-AsE7x2XaAK+CVbeih0Fvbn+r1qHxtpLDJ3XUuFcIinT318T90yHMJC+Zgv+jUuDjQQd06HKwxnDu6sz1IcTilA=="], + + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.60.0", "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.0.tgz", { "dependencies": { "@typescript-eslint/project-service": "8.60.0", "@typescript-eslint/tsconfig-utils": "8.60.0", "@typescript-eslint/types": "8.60.0", "@typescript-eslint/visitor-keys": "8.60.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-3AcZNBGMClm6CXDyo8kYvVGT/sx29sS0oBsIb9oZI2gunA4Vm2M3YHzRLPvsUBBsl+yB5FPtltq7gGH0iTlp9g=="], + + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.60.0", "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-8.60.0.tgz", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.60.0", "@typescript-eslint/types": "8.60.0", "@typescript-eslint/typescript-estree": "8.60.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-HtXuPfrHTyBDkameWpl+vJb1Uevu2tznAyahM1Oc4AENidCLTPiZDWIo4GfcxNdC/RcfGcadzzkqbRG87dUrQA=="], + + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.60.0", "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.60.0.tgz", { "dependencies": { "@typescript-eslint/types": "8.60.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-9WI52t8ZGLVGrPMBet25yAftqY/n95+zmoUUtJBBQTKDSKUu7OsPTroT2op7U9JatkoRccL0YkWDNMFfC4Sjxg=="], + + "@ungap/structured-clone": ["@ungap/structured-clone@1.3.1", "https://registry.npmmirror.com/@ungap/structured-clone/-/structured-clone-1.3.1.tgz", {}, "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ=="], + + "@unrs/resolver-binding-android-arm-eabi": ["@unrs/resolver-binding-android-arm-eabi@1.12.2", "https://registry.npmmirror.com/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.12.2.tgz", { "os": "android", "cpu": "arm" }, "sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w=="], + + "@unrs/resolver-binding-android-arm64": ["@unrs/resolver-binding-android-arm64@1.12.2", "https://registry.npmmirror.com/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.12.2.tgz", { "os": "android", "cpu": "arm64" }, "sha512-YGCRZv/9GLhwmz6mYDeTsm/92BAyR28l6c2ReweVW5pWgfsitWLY8upvfRlGdoyD8HjeTHSYJWyZGD4KJA/nFQ=="], + + "@unrs/resolver-binding-darwin-arm64": ["@unrs/resolver-binding-darwin-arm64@1.12.2", "https://registry.npmmirror.com/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.12.2.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-u9DiNT1auQMO20A9SyTuG3wUgQWB9Z7KjAg0uFuCDR1FsAY8A0CG2S6JpHS1xwm/w1G08bjXZDcyOCjv1WAm2w=="], + + "@unrs/resolver-binding-darwin-x64": ["@unrs/resolver-binding-darwin-x64@1.12.2", "https://registry.npmmirror.com/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.12.2.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-f7rPLi/T1HVKZu/u6t87lroib16n8vrSzcyxI7lg4BGO9UF26KhQL44sd9eOUgrTYhvRXtWOIZT5PejdPyJfUA=="], + + "@unrs/resolver-binding-freebsd-x64": ["@unrs/resolver-binding-freebsd-x64@1.12.2", "https://registry.npmmirror.com/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.12.2.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg=="], + + "@unrs/resolver-binding-linux-arm-gnueabihf": ["@unrs/resolver-binding-linux-arm-gnueabihf@1.12.2", "https://registry.npmmirror.com/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.12.2.tgz", { "os": "linux", "cpu": "arm" }, "sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A=="], + + "@unrs/resolver-binding-linux-arm-musleabihf": ["@unrs/resolver-binding-linux-arm-musleabihf@1.12.2", "https://registry.npmmirror.com/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.12.2.tgz", { "os": "linux", "cpu": "arm" }, "sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g=="], + + "@unrs/resolver-binding-linux-arm64-gnu": ["@unrs/resolver-binding-linux-arm64-gnu@1.12.2", "https://registry.npmmirror.com/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.12.2.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg=="], + + "@unrs/resolver-binding-linux-arm64-musl": ["@unrs/resolver-binding-linux-arm64-musl@1.12.2", "https://registry.npmmirror.com/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.12.2.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA=="], + + "@unrs/resolver-binding-linux-loong64-gnu": ["@unrs/resolver-binding-linux-loong64-gnu@1.12.2", "https://registry.npmmirror.com/@unrs/resolver-binding-linux-loong64-gnu/-/resolver-binding-linux-loong64-gnu-1.12.2.tgz", { "os": "linux", "cpu": "none" }, "sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q=="], + + "@unrs/resolver-binding-linux-loong64-musl": ["@unrs/resolver-binding-linux-loong64-musl@1.12.2", "https://registry.npmmirror.com/@unrs/resolver-binding-linux-loong64-musl/-/resolver-binding-linux-loong64-musl-1.12.2.tgz", { "os": "linux", "cpu": "none" }, "sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew=="], + + "@unrs/resolver-binding-linux-ppc64-gnu": ["@unrs/resolver-binding-linux-ppc64-gnu@1.12.2", "https://registry.npmmirror.com/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.12.2.tgz", { "os": "linux", "cpu": "ppc64" }, "sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg=="], + + "@unrs/resolver-binding-linux-riscv64-gnu": ["@unrs/resolver-binding-linux-riscv64-gnu@1.12.2", "https://registry.npmmirror.com/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.12.2.tgz", { "os": "linux", "cpu": "none" }, "sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A=="], + + "@unrs/resolver-binding-linux-riscv64-musl": ["@unrs/resolver-binding-linux-riscv64-musl@1.12.2", "https://registry.npmmirror.com/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.12.2.tgz", { "os": "linux", "cpu": "none" }, "sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w=="], + + "@unrs/resolver-binding-linux-s390x-gnu": ["@unrs/resolver-binding-linux-s390x-gnu@1.12.2", "https://registry.npmmirror.com/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.12.2.tgz", { "os": "linux", "cpu": "s390x" }, "sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw=="], + + "@unrs/resolver-binding-linux-x64-gnu": ["@unrs/resolver-binding-linux-x64-gnu@1.12.2", "https://registry.npmmirror.com/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.12.2.tgz", { "os": "linux", "cpu": "x64" }, "sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ=="], + + "@unrs/resolver-binding-linux-x64-musl": ["@unrs/resolver-binding-linux-x64-musl@1.12.2", "https://registry.npmmirror.com/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.12.2.tgz", { "os": "linux", "cpu": "x64" }, "sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A=="], + + "@unrs/resolver-binding-openharmony-arm64": ["@unrs/resolver-binding-openharmony-arm64@1.12.2", "https://registry.npmmirror.com/@unrs/resolver-binding-openharmony-arm64/-/resolver-binding-openharmony-arm64-1.12.2.tgz", { "os": "none", "cpu": "arm64" }, "sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ=="], + + "@unrs/resolver-binding-wasm32-wasi": ["@unrs/resolver-binding-wasm32-wasi@1.12.2", "https://registry.npmmirror.com/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.12.2.tgz", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A=="], + + "@unrs/resolver-binding-win32-arm64-msvc": ["@unrs/resolver-binding-win32-arm64-msvc@1.12.2", "https://registry.npmmirror.com/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.12.2.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g=="], + + "@unrs/resolver-binding-win32-ia32-msvc": ["@unrs/resolver-binding-win32-ia32-msvc@1.12.2", "https://registry.npmmirror.com/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.12.2.tgz", { "os": "win32", "cpu": "ia32" }, "sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g=="], + + "@unrs/resolver-binding-win32-x64-msvc": ["@unrs/resolver-binding-win32-x64-msvc@1.12.2", "https://registry.npmmirror.com/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.12.2.tgz", { "os": "win32", "cpu": "x64" }, "sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA=="], + + "acorn": ["acorn@8.16.0", "https://registry.npmmirror.com/acorn/-/acorn-8.16.0.tgz", { "bin": "bin/acorn" }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], + + "acorn-jsx": ["acorn-jsx@5.3.2", "https://registry.npmmirror.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + + "ajv": ["ajv@6.15.0", "https://registry.npmmirror.com/ajv/-/ajv-6.15.0.tgz", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="], + + "ansi-styles": ["ansi-styles@4.3.0", "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "argparse": ["argparse@2.0.1", "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "aria-query": ["aria-query@5.3.2", "https://registry.npmmirror.com/aria-query/-/aria-query-5.3.2.tgz", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], + + "array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "https://registry.npmmirror.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="], + + "array-includes": ["array-includes@3.1.9", "https://registry.npmmirror.com/array-includes/-/array-includes-3.1.9.tgz", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.0", "es-object-atoms": "^1.1.1", "get-intrinsic": "^1.3.0", "is-string": "^1.1.1", "math-intrinsics": "^1.1.0" } }, "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ=="], + + "array.prototype.findlast": ["array.prototype.findlast@1.2.5", "https://registry.npmmirror.com/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-shim-unscopables": "^1.0.2" } }, "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ=="], + + "array.prototype.findlastindex": ["array.prototype.findlastindex@1.2.6", "https://registry.npmmirror.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-shim-unscopables": "^1.1.0" } }, "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ=="], + + "array.prototype.flat": ["array.prototype.flat@1.3.3", "https://registry.npmmirror.com/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg=="], + + "array.prototype.flatmap": ["array.prototype.flatmap@1.3.3", "https://registry.npmmirror.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg=="], + + "array.prototype.tosorted": ["array.prototype.tosorted@1.1.4", "https://registry.npmmirror.com/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.3", "es-errors": "^1.3.0", "es-shim-unscopables": "^1.0.2" } }, "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA=="], + + "arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "https://registry.npmmirror.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="], + + "ast-types-flow": ["ast-types-flow@0.0.8", "https://registry.npmmirror.com/ast-types-flow/-/ast-types-flow-0.0.8.tgz", {}, "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ=="], + + "astring": ["astring@1.9.0", "https://registry.npmmirror.com/astring/-/astring-1.9.0.tgz", { "bin": "bin/astring" }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="], + + "async-function": ["async-function@1.0.0", "https://registry.npmmirror.com/async-function/-/async-function-1.0.0.tgz", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="], + + "async-validator": ["async-validator@3.5.2", "https://registry.npmmirror.com/async-validator/-/async-validator-3.5.2.tgz", {}, "sha512-8eLCg00W9pIRZSB781UUX/H6Oskmm8xloZfr09lz5bikRpBVDlJ3hRVuxxP1SxcwsEYfJ4IU8Q19Y8/893r3rQ=="], + + "available-typed-arrays": ["available-typed-arrays@1.0.7", "https://registry.npmmirror.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], + + "axe-core": ["axe-core@4.11.4", "https://registry.npmmirror.com/axe-core/-/axe-core-4.11.4.tgz", {}, "sha512-KunSNx+TVpkAw/6ULfhnx+HWRecjqZGTOyquAoWHYLRSdK1tB5Ihce1ZW+UY3fj33bYAFWPu7W/GRSmmrCGuxA=="], + + "axobject-query": ["axobject-query@4.1.0", "https://registry.npmmirror.com/axobject-query/-/axobject-query-4.1.0.tgz", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], + + "bail": ["bail@2.0.2", "https://registry.npmmirror.com/bail/-/bail-2.0.2.tgz", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], + + "balanced-match": ["balanced-match@1.0.2", "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.32", "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.32.tgz", { "bin": "dist/cli.cjs" }, "sha512-wbPvpyjJPC0zdfdKXxqEL3Ea+bOMD/87X4lftiJkkaBiuG6ALQy1SLmEd7BSmVCuwCQsBrCamgBoLyfFDD1EPg=="], + + "bezier-easing": ["bezier-easing@2.1.0", "https://registry.npmmirror.com/bezier-easing/-/bezier-easing-2.1.0.tgz", {}, "sha512-gbIqZ/eslnUFC1tjEvtz0sgx+xTK20wDnYMIA27VA04R7w6xxXQPZDbibjA9DTWZRA2CXtwHykkVzlCaAJAZig=="], + + "brace-expansion": ["brace-expansion@1.1.15", "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.15.tgz", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg=="], + + "braces": ["braces@3.0.3", "https://registry.npmmirror.com/braces/-/braces-3.0.3.tgz", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + + "browserslist": ["browserslist@4.28.2", "https://registry.npmmirror.com/browserslist/-/browserslist-4.28.2.tgz", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": "cli.js" }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="], + + "call-bind": ["call-bind@1.0.9", "https://registry.npmmirror.com/call-bind/-/call-bind-1.0.9.tgz", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "get-intrinsic": "^1.3.0", "set-function-length": "^1.2.2" } }, "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "call-bound": ["call-bound@1.0.4", "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + + "callsites": ["callsites@3.1.0", "https://registry.npmmirror.com/callsites/-/callsites-3.1.0.tgz", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001793", "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", {}, "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA=="], + + "ccount": ["ccount@2.0.1", "https://registry.npmmirror.com/ccount/-/ccount-2.0.1.tgz", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], + + "chalk": ["chalk@4.1.2", "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "character-entities": ["character-entities@2.0.2", "https://registry.npmmirror.com/character-entities/-/character-entities-2.0.2.tgz", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], + + "character-entities-html4": ["character-entities-html4@2.1.0", "https://registry.npmmirror.com/character-entities-html4/-/character-entities-html4-2.1.0.tgz", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="], + + "character-entities-legacy": ["character-entities-legacy@3.0.0", "https://registry.npmmirror.com/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="], + + "character-reference-invalid": ["character-reference-invalid@2.0.1", "https://registry.npmmirror.com/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="], + + "classnames": ["classnames@2.5.1", "https://registry.npmmirror.com/classnames/-/classnames-2.5.1.tgz", {}, "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow=="], + + "client-only": ["client-only@0.0.1", "https://registry.npmmirror.com/client-only/-/client-only-0.0.1.tgz", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="], + + "clsx": ["clsx@2.1.1", "https://registry.npmmirror.com/clsx/-/clsx-2.1.1.tgz", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + + "collapse-white-space": ["collapse-white-space@2.1.0", "https://registry.npmmirror.com/collapse-white-space/-/collapse-white-space-2.1.0.tgz", {}, "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw=="], + + "color-convert": ["color-convert@2.0.1", "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "https://registry.npmmirror.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], + + "compute-scroll-into-view": ["compute-scroll-into-view@1.0.20", "https://registry.npmmirror.com/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz", {}, "sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg=="], + + "concat-map": ["concat-map@0.0.1", "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + + "convert-source-map": ["convert-source-map@2.0.0", "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + + "copy-text-to-clipboard": ["copy-text-to-clipboard@2.2.0", "https://registry.npmmirror.com/copy-text-to-clipboard/-/copy-text-to-clipboard-2.2.0.tgz", {}, "sha512-WRvoIdnTs1rgPMkgA2pUOa/M4Enh2uzCwdKsOMYNAJiz/4ZvEJgmbF4OmninPmlFdAWisfeh0tH+Cpf7ni3RqQ=="], + + "cross-spawn": ["cross-spawn@7.0.6", "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "csstype": ["csstype@3.2.3", "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + + "d3-array": ["d3-array@3.2.4", "https://registry.npmmirror.com/d3-array/-/d3-array-3.2.4.tgz", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="], + + "d3-color": ["d3-color@3.1.0", "https://registry.npmmirror.com/d3-color/-/d3-color-3.1.0.tgz", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="], + + "d3-ease": ["d3-ease@3.0.1", "https://registry.npmmirror.com/d3-ease/-/d3-ease-3.0.1.tgz", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="], + + "d3-format": ["d3-format@3.1.2", "https://registry.npmmirror.com/d3-format/-/d3-format-3.1.2.tgz", {}, "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg=="], + + "d3-interpolate": ["d3-interpolate@3.0.1", "https://registry.npmmirror.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="], + + "d3-path": ["d3-path@3.1.0", "https://registry.npmmirror.com/d3-path/-/d3-path-3.1.0.tgz", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="], + + "d3-scale": ["d3-scale@4.0.2", "https://registry.npmmirror.com/d3-scale/-/d3-scale-4.0.2.tgz", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="], + + "d3-shape": ["d3-shape@3.2.0", "https://registry.npmmirror.com/d3-shape/-/d3-shape-3.2.0.tgz", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="], + + "d3-time": ["d3-time@3.1.0", "https://registry.npmmirror.com/d3-time/-/d3-time-3.1.0.tgz", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="], + + "d3-time-format": ["d3-time-format@4.1.0", "https://registry.npmmirror.com/d3-time-format/-/d3-time-format-4.1.0.tgz", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="], + + "d3-timer": ["d3-timer@3.0.1", "https://registry.npmmirror.com/d3-timer/-/d3-timer-3.0.1.tgz", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="], + + "damerau-levenshtein": ["damerau-levenshtein@1.0.8", "https://registry.npmmirror.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", {}, "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA=="], + + "data-view-buffer": ["data-view-buffer@1.0.2", "https://registry.npmmirror.com/data-view-buffer/-/data-view-buffer-1.0.2.tgz", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="], + + "data-view-byte-length": ["data-view-byte-length@1.0.2", "https://registry.npmmirror.com/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ=="], + + "data-view-byte-offset": ["data-view-byte-offset@1.0.1", "https://registry.npmmirror.com/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="], + + "date-fns": ["date-fns@2.30.0", "https://registry.npmmirror.com/date-fns/-/date-fns-2.30.0.tgz", { "dependencies": { "@babel/runtime": "^7.21.0" } }, "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw=="], + + "date-fns-tz": ["date-fns-tz@1.3.8", "https://registry.npmmirror.com/date-fns-tz/-/date-fns-tz-1.3.8.tgz", { "peerDependencies": { "date-fns": ">=2.0.0" } }, "sha512-qwNXUFtMHTTU6CFSFjoJ80W8Fzzp24LntbjFFBgL/faqds4e5mo9mftoRLgr3Vi1trISsg4awSpYVsOQCRnapQ=="], + + "debug": ["debug@4.4.3", "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "decimal.js-light": ["decimal.js-light@2.5.1", "https://registry.npmmirror.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="], + + "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "https://registry.npmmirror.com/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="], + + "deep-is": ["deep-is@0.1.4", "https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + + "define-data-property": ["define-data-property@1.1.4", "https://registry.npmmirror.com/define-data-property/-/define-data-property-1.1.4.tgz", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], + + "define-properties": ["define-properties@1.2.1", "https://registry.npmmirror.com/define-properties/-/define-properties-1.2.1.tgz", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="], + + "dequal": ["dequal@2.0.3", "https://registry.npmmirror.com/dequal/-/dequal-2.0.3.tgz", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], + + "detect-libc": ["detect-libc@2.1.2", "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "devlop": ["devlop@1.1.0", "https://registry.npmmirror.com/devlop/-/devlop-1.1.0.tgz", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], + + "doctrine": ["doctrine@2.1.0", "https://registry.npmmirror.com/doctrine/-/doctrine-2.1.0.tgz", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="], + + "dom-helpers": ["dom-helpers@5.2.1", "https://registry.npmmirror.com/dom-helpers/-/dom-helpers-5.2.1.tgz", { "dependencies": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" } }, "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA=="], + + "dunder-proto": ["dunder-proto@1.0.1", "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "electron-to-chromium": ["electron-to-chromium@1.5.362", "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.362.tgz", {}, "sha512-PUY2DrLvkjkUuWqq+KPL2iWshrJsZOcIojzRQ7eXFacc9dWga7MGMJAa15VbiejSZB1PAXaRLAiKgruHP8LB1w=="], + + "emoji-regex": ["emoji-regex@9.2.2", "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-9.2.2.tgz", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + + "enhanced-resolve": ["enhanced-resolve@5.22.0", "https://registry.npmmirror.com/enhanced-resolve/-/enhanced-resolve-5.22.0.tgz", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-xYcDWrpELkFzz9SpZ3PlI6Eu6eD93Yf0WLDRxikGhWJ3MAir2SNZTIVCVZqZ/NUyx8AdMc2gT9C0gPiw18kG+A=="], + + "es-abstract": ["es-abstract@1.24.2", "https://registry.npmmirror.com/es-abstract/-/es-abstract-1.24.2.tgz", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg=="], + + "es-define-property": ["es-define-property@1.0.1", "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-iterator-helpers": ["es-iterator-helpers@1.3.2", "https://registry.npmmirror.com/es-iterator-helpers/-/es-iterator-helpers-1.3.2.tgz", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.2", "es-errors": "^1.3.0", "es-set-tostringtag": "^2.1.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.3.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "iterator.prototype": "^1.1.5", "math-intrinsics": "^1.1.0" } }, "sha512-HVLACW1TppGYjJ8H6/jqH/pqOtKRw6wMlrB23xfExmFWxFquAIWCmwoLsOyN96K4a5KbmOf5At9ZUO3GZbetAw=="], + + "es-object-atoms": ["es-object-atoms@1.1.2", "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.2.tgz", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw=="], + + "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], + + "es-shim-unscopables": ["es-shim-unscopables@1.1.0", "https://registry.npmmirror.com/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw=="], + + "es-to-primitive": ["es-to-primitive@1.3.0", "https://registry.npmmirror.com/es-to-primitive/-/es-to-primitive-1.3.0.tgz", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="], + + "esast-util-from-estree": ["esast-util-from-estree@2.0.0", "https://registry.npmmirror.com/esast-util-from-estree/-/esast-util-from-estree-2.0.0.tgz", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "unist-util-position-from-estree": "^2.0.0" } }, "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ=="], + + "esast-util-from-js": ["esast-util-from-js@2.0.1", "https://registry.npmmirror.com/esast-util-from-js/-/esast-util-from-js-2.0.1.tgz", { "dependencies": { "@types/estree-jsx": "^1.0.0", "acorn": "^8.0.0", "esast-util-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw=="], + + "escalade": ["escalade@3.2.0", "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "escape-string-regexp": ["escape-string-regexp@4.0.0", "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + + "eslint": ["eslint@9.39.4", "https://registry.npmmirror.com/eslint/-/eslint-9.39.4.tgz", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.5", "@eslint/js": "9.39.4", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "bin": "bin/eslint.js" }, "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ=="], + + "eslint-config-next": ["eslint-config-next@16.2.6", "https://registry.npmmirror.com/eslint-config-next/-/eslint-config-next-16.2.6.tgz", { "dependencies": { "@next/eslint-plugin-next": "16.2.6", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.32.0", "eslint-plugin-jsx-a11y": "^6.10.0", "eslint-plugin-react": "^7.37.0", "eslint-plugin-react-hooks": "^7.0.0", "globals": "16.4.0", "typescript-eslint": "^8.46.0" }, "peerDependencies": { "eslint": ">=9.0.0", "typescript": ">=3.3.1" } }, "sha512-z2ELYSkyrrJ6cuunTU8vhsT/RpouPkjaSah06nVW6Rg2Hpg0Vs8s497/e5s8G8qtdp4ccsiovz5P1rv+5VSW2Q=="], + + "eslint-import-resolver-node": ["eslint-import-resolver-node@0.3.10", "https://registry.npmmirror.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.10.tgz", { "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.16.1", "resolve": "^2.0.0-next.6" } }, "sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ=="], + + "eslint-import-resolver-typescript": ["eslint-import-resolver-typescript@3.10.1", "https://registry.npmmirror.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz", { "dependencies": { "@nolyfill/is-core-module": "1.0.39", "debug": "^4.4.0", "get-tsconfig": "^4.10.0", "is-bun-module": "^2.0.0", "stable-hash": "^0.0.5", "tinyglobby": "^0.2.13", "unrs-resolver": "^1.6.2" }, "peerDependencies": { "eslint": "*", "eslint-plugin-import": "*", "eslint-plugin-import-x": "*" }, "optionalPeers": ["eslint-plugin-import-x"] }, "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ=="], + + "eslint-module-utils": ["eslint-module-utils@2.12.1", "https://registry.npmmirror.com/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", { "dependencies": { "debug": "^3.2.7" } }, "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw=="], + + "eslint-plugin-import": ["eslint-plugin-import@2.32.0", "https://registry.npmmirror.com/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", { "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", "array.prototype.findlastindex": "^1.2.6", "array.prototype.flat": "^1.3.3", "array.prototype.flatmap": "^1.3.3", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", "eslint-module-utils": "^2.12.1", "hasown": "^2.0.2", "is-core-module": "^2.16.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "object.groupby": "^1.0.3", "object.values": "^1.2.1", "semver": "^6.3.1", "string.prototype.trimend": "^1.0.9", "tsconfig-paths": "^3.15.0" }, "peerDependencies": { "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA=="], + + "eslint-plugin-jsx-a11y": ["eslint-plugin-jsx-a11y@6.10.2", "https://registry.npmmirror.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", { "dependencies": { "aria-query": "^5.3.2", "array-includes": "^3.1.8", "array.prototype.flatmap": "^1.3.2", "ast-types-flow": "^0.0.8", "axe-core": "^4.10.0", "axobject-query": "^4.1.0", "damerau-levenshtein": "^1.0.8", "emoji-regex": "^9.2.2", "hasown": "^2.0.2", "jsx-ast-utils": "^3.3.5", "language-tags": "^1.0.9", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "safe-regex-test": "^1.0.3", "string.prototype.includes": "^2.0.1" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" } }, "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q=="], + + "eslint-plugin-react": ["eslint-plugin-react@7.37.5", "https://registry.npmmirror.com/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", { "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", "array.prototype.flatmap": "^1.3.3", "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", "es-iterator-helpers": "^1.2.1", "estraverse": "^5.3.0", "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", "object.entries": "^1.1.9", "object.fromentries": "^2.0.8", "object.values": "^1.2.1", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.5", "semver": "^6.3.1", "string.prototype.matchall": "^4.0.12", "string.prototype.repeat": "^1.0.0" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA=="], + + "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.1.1", "https://registry.npmmirror.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.25.0 || ^4.0.0", "zod-validation-error": "^3.5.0 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" } }, "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g=="], + + "eslint-scope": ["eslint-scope@8.4.0", "https://registry.npmmirror.com/eslint-scope/-/eslint-scope-8.4.0.tgz", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], + + "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], + + "espree": ["espree@10.4.0", "https://registry.npmmirror.com/espree/-/espree-10.4.0.tgz", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], + + "esquery": ["esquery@1.7.0", "https://registry.npmmirror.com/esquery/-/esquery-1.7.0.tgz", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="], + + "esrecurse": ["esrecurse@4.3.0", "https://registry.npmmirror.com/esrecurse/-/esrecurse-4.3.0.tgz", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], + + "estraverse": ["estraverse@5.3.0", "https://registry.npmmirror.com/estraverse/-/estraverse-5.3.0.tgz", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + + "estree-util-attach-comments": ["estree-util-attach-comments@3.0.0", "https://registry.npmmirror.com/estree-util-attach-comments/-/estree-util-attach-comments-3.0.0.tgz", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw=="], + + "estree-util-build-jsx": ["estree-util-build-jsx@3.0.1", "https://registry.npmmirror.com/estree-util-build-jsx/-/estree-util-build-jsx-3.0.1.tgz", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-walker": "^3.0.0" } }, "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ=="], + + "estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "https://registry.npmmirror.com/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="], + + "estree-util-scope": ["estree-util-scope@1.0.0", "https://registry.npmmirror.com/estree-util-scope/-/estree-util-scope-1.0.0.tgz", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0" } }, "sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ=="], + + "estree-util-to-js": ["estree-util-to-js@2.0.0", "https://registry.npmmirror.com/estree-util-to-js/-/estree-util-to-js-2.0.0.tgz", { "dependencies": { "@types/estree-jsx": "^1.0.0", "astring": "^1.8.0", "source-map": "^0.7.0" } }, "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg=="], + + "estree-util-visit": ["estree-util-visit@2.0.0", "https://registry.npmmirror.com/estree-util-visit/-/estree-util-visit-2.0.0.tgz", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/unist": "^3.0.0" } }, "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww=="], + + "estree-walker": ["estree-walker@3.0.3", "https://registry.npmmirror.com/estree-walker/-/estree-walker-3.0.3.tgz", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + + "esutils": ["esutils@2.0.3", "https://registry.npmmirror.com/esutils/-/esutils-2.0.3.tgz", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + + "eventemitter3": ["eventemitter3@4.0.7", "https://registry.npmmirror.com/eventemitter3/-/eventemitter3-4.0.7.tgz", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="], + + "extend": ["extend@3.0.2", "https://registry.npmmirror.com/extend/-/extend-3.0.2.tgz", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], + + "fast-copy": ["fast-copy@3.0.2", "https://registry.npmmirror.com/fast-copy/-/fast-copy-3.0.2.tgz", {}, "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-equals": ["fast-equals@5.4.0", "https://registry.npmmirror.com/fast-equals/-/fast-equals-5.4.0.tgz", {}, "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw=="], + + "fast-glob": ["fast-glob@3.3.1", "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.1.tgz", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.4" } }, "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg=="], + + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "https://registry.npmmirror.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + + "fast-levenshtein": ["fast-levenshtein@2.0.6", "https://registry.npmmirror.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + + "fastq": ["fastq@1.20.1", "https://registry.npmmirror.com/fastq/-/fastq-1.20.1.tgz", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], + + "fdir": ["fdir@6.5.0", "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", { "peerDependencies": { "picomatch": "^3 || ^4" } }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "file-entry-cache": ["file-entry-cache@8.0.0", "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], + + "fill-range": ["fill-range@7.1.1", "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + + "find-up": ["find-up@5.0.0", "https://registry.npmmirror.com/find-up/-/find-up-5.0.0.tgz", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], + + "flat-cache": ["flat-cache@4.0.1", "https://registry.npmmirror.com/flat-cache/-/flat-cache-4.0.1.tgz", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], + + "flatted": ["flatted@3.4.2", "https://registry.npmmirror.com/flatted/-/flatted-3.4.2.tgz", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="], + + "for-each": ["for-each@0.3.5", "https://registry.npmmirror.com/for-each/-/for-each-0.3.5.tgz", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="], + + "fsevents": ["fsevents@2.3.2", "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.2.tgz", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], + + "function-bind": ["function-bind@1.1.2", "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "function.prototype.name": ["function.prototype.name@1.1.8", "https://registry.npmmirror.com/function.prototype.name/-/function.prototype.name-1.1.8.tgz", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "functions-have-names": "^1.2.3", "hasown": "^2.0.2", "is-callable": "^1.2.7" } }, "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q=="], + + "functions-have-names": ["functions-have-names@1.2.3", "https://registry.npmmirror.com/functions-have-names/-/functions-have-names-1.2.3.tgz", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="], + + "generator-function": ["generator-function@2.0.1", "https://registry.npmmirror.com/generator-function/-/generator-function-2.0.1.tgz", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="], + + "gensync": ["gensync@1.0.0-beta.2", "https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + + "get-intrinsic": ["get-intrinsic@1.3.0", "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-proto": ["get-proto@1.0.1", "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "get-symbol-description": ["get-symbol-description@1.1.0", "https://registry.npmmirror.com/get-symbol-description/-/get-symbol-description-1.1.0.tgz", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="], + + "get-tsconfig": ["get-tsconfig@4.14.0", "https://registry.npmmirror.com/get-tsconfig/-/get-tsconfig-4.14.0.tgz", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA=="], + + "glob-parent": ["glob-parent@6.0.2", "https://registry.npmmirror.com/glob-parent/-/glob-parent-6.0.2.tgz", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + + "globals": ["globals@16.4.0", "https://registry.npmmirror.com/globals/-/globals-16.4.0.tgz", {}, "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw=="], + + "globalthis": ["globalthis@1.0.4", "https://registry.npmmirror.com/globalthis/-/globalthis-1.0.4.tgz", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="], + + "gopd": ["gopd@1.2.0", "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "graceful-fs": ["graceful-fs@4.2.11", "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "has-bigints": ["has-bigints@1.1.0", "https://registry.npmmirror.com/has-bigints/-/has-bigints-1.1.0.tgz", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="], + + "has-flag": ["has-flag@4.0.0", "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "has-property-descriptors": ["has-property-descriptors@1.0.2", "https://registry.npmmirror.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], + + "has-proto": ["has-proto@1.2.0", "https://registry.npmmirror.com/has-proto/-/has-proto-1.2.0.tgz", { "dependencies": { "dunder-proto": "^1.0.0" } }, "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ=="], + + "has-symbols": ["has-symbols@1.1.0", "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "has-tostringtag": ["has-tostringtag@1.0.2", "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], + + "hasown": ["hasown@2.0.3", "https://registry.npmmirror.com/hasown/-/hasown-2.0.3.tgz", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="], + + "hast-util-to-estree": ["hast-util-to-estree@3.1.3", "https://registry.npmmirror.com/hast-util-to-estree/-/hast-util-to-estree-3.1.3.tgz", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-attach-comments": "^3.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w=="], + + "hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "https://registry.npmmirror.com/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="], + + "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "https://registry.npmmirror.com/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], + + "hermes-estree": ["hermes-estree@0.25.1", "https://registry.npmmirror.com/hermes-estree/-/hermes-estree-0.25.1.tgz", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="], + + "hermes-parser": ["hermes-parser@0.25.1", "https://registry.npmmirror.com/hermes-parser/-/hermes-parser-0.25.1.tgz", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="], + + "ignore": ["ignore@5.3.2", "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "import-fresh": ["import-fresh@3.3.1", "https://registry.npmmirror.com/import-fresh/-/import-fresh-3.3.1.tgz", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], + + "imurmurhash": ["imurmurhash@0.1.4", "https://registry.npmmirror.com/imurmurhash/-/imurmurhash-0.1.4.tgz", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + + "inline-style-parser": ["inline-style-parser@0.2.7", "https://registry.npmmirror.com/inline-style-parser/-/inline-style-parser-0.2.7.tgz", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="], + + "internal-slot": ["internal-slot@1.1.0", "https://registry.npmmirror.com/internal-slot/-/internal-slot-1.1.0.tgz", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="], + + "internmap": ["internmap@2.0.3", "https://registry.npmmirror.com/internmap/-/internmap-2.0.3.tgz", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="], + + "is-alphabetical": ["is-alphabetical@2.0.1", "https://registry.npmmirror.com/is-alphabetical/-/is-alphabetical-2.0.1.tgz", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="], + + "is-alphanumerical": ["is-alphanumerical@2.0.1", "https://registry.npmmirror.com/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="], + + "is-array-buffer": ["is-array-buffer@3.0.5", "https://registry.npmmirror.com/is-array-buffer/-/is-array-buffer-3.0.5.tgz", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="], + + "is-async-function": ["is-async-function@2.1.1", "https://registry.npmmirror.com/is-async-function/-/is-async-function-2.1.1.tgz", { "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ=="], + + "is-bigint": ["is-bigint@1.1.0", "https://registry.npmmirror.com/is-bigint/-/is-bigint-1.1.0.tgz", { "dependencies": { "has-bigints": "^1.0.2" } }, "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ=="], + + "is-boolean-object": ["is-boolean-object@1.2.2", "https://registry.npmmirror.com/is-boolean-object/-/is-boolean-object-1.2.2.tgz", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A=="], + + "is-bun-module": ["is-bun-module@2.0.0", "https://registry.npmmirror.com/is-bun-module/-/is-bun-module-2.0.0.tgz", { "dependencies": { "semver": "^7.7.1" } }, "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ=="], + + "is-callable": ["is-callable@1.2.7", "https://registry.npmmirror.com/is-callable/-/is-callable-1.2.7.tgz", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="], + + "is-core-module": ["is-core-module@2.16.2", "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.16.2.tgz", { "dependencies": { "hasown": "^2.0.3" } }, "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA=="], + + "is-data-view": ["is-data-view@1.0.2", "https://registry.npmmirror.com/is-data-view/-/is-data-view-1.0.2.tgz", { "dependencies": { "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "is-typed-array": "^1.1.13" } }, "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw=="], + + "is-date-object": ["is-date-object@1.1.0", "https://registry.npmmirror.com/is-date-object/-/is-date-object-1.1.0.tgz", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg=="], + + "is-decimal": ["is-decimal@2.0.1", "https://registry.npmmirror.com/is-decimal/-/is-decimal-2.0.1.tgz", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="], + + "is-extglob": ["is-extglob@2.1.1", "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-finalizationregistry": ["is-finalizationregistry@1.1.1", "https://registry.npmmirror.com/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg=="], + + "is-generator-function": ["is-generator-function@1.1.2", "https://registry.npmmirror.com/is-generator-function/-/is-generator-function-1.1.2.tgz", { "dependencies": { "call-bound": "^1.0.4", "generator-function": "^2.0.0", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA=="], + + "is-glob": ["is-glob@4.0.3", "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-hexadecimal": ["is-hexadecimal@2.0.1", "https://registry.npmmirror.com/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="], + + "is-map": ["is-map@2.0.3", "https://registry.npmmirror.com/is-map/-/is-map-2.0.3.tgz", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="], + + "is-negative-zero": ["is-negative-zero@2.0.3", "https://registry.npmmirror.com/is-negative-zero/-/is-negative-zero-2.0.3.tgz", {}, "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw=="], + + "is-number": ["is-number@7.0.0", "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "is-number-object": ["is-number-object@1.1.1", "https://registry.npmmirror.com/is-number-object/-/is-number-object-1.1.1.tgz", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="], + + "is-plain-obj": ["is-plain-obj@4.1.0", "https://registry.npmmirror.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], + + "is-regex": ["is-regex@1.2.1", "https://registry.npmmirror.com/is-regex/-/is-regex-1.2.1.tgz", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="], + + "is-set": ["is-set@2.0.3", "https://registry.npmmirror.com/is-set/-/is-set-2.0.3.tgz", {}, "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg=="], + + "is-shared-array-buffer": ["is-shared-array-buffer@1.0.4", "https://registry.npmmirror.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A=="], + + "is-string": ["is-string@1.1.1", "https://registry.npmmirror.com/is-string/-/is-string-1.1.1.tgz", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA=="], + + "is-symbol": ["is-symbol@1.1.1", "https://registry.npmmirror.com/is-symbol/-/is-symbol-1.1.1.tgz", { "dependencies": { "call-bound": "^1.0.2", "has-symbols": "^1.1.0", "safe-regex-test": "^1.1.0" } }, "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w=="], + + "is-typed-array": ["is-typed-array@1.1.15", "https://registry.npmmirror.com/is-typed-array/-/is-typed-array-1.1.15.tgz", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="], + + "is-weakmap": ["is-weakmap@2.0.2", "https://registry.npmmirror.com/is-weakmap/-/is-weakmap-2.0.2.tgz", {}, "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w=="], + + "is-weakref": ["is-weakref@1.1.1", "https://registry.npmmirror.com/is-weakref/-/is-weakref-1.1.1.tgz", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew=="], + + "is-weakset": ["is-weakset@2.0.4", "https://registry.npmmirror.com/is-weakset/-/is-weakset-2.0.4.tgz", { "dependencies": { "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ=="], + + "isarray": ["isarray@2.0.5", "https://registry.npmmirror.com/isarray/-/isarray-2.0.5.tgz", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], + + "isexe": ["isexe@2.0.0", "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "iterator.prototype": ["iterator.prototype@1.1.5", "https://registry.npmmirror.com/iterator.prototype/-/iterator.prototype-1.1.5.tgz", { "dependencies": { "define-data-property": "^1.1.4", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "get-proto": "^1.0.0", "has-symbols": "^1.1.0", "set-function-name": "^2.0.2" } }, "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g=="], + + "jiti": ["jiti@2.7.0", "https://registry.npmmirror.com/jiti/-/jiti-2.7.0.tgz", { "bin": "lib/jiti-cli.mjs" }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], + + "js-tokens": ["js-tokens@4.0.0", "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "js-yaml": ["js-yaml@4.1.1", "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.1.tgz", { "dependencies": { "argparse": "^2.0.1" }, "bin": "bin/js-yaml.js" }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + + "jsesc": ["jsesc@3.1.0", "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz", { "bin": "bin/jsesc" }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + + "json-buffer": ["json-buffer@3.0.1", "https://registry.npmmirror.com/json-buffer/-/json-buffer-3.0.1.tgz", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + + "json-schema-traverse": ["json-schema-traverse@0.4.1", "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + + "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "https://registry.npmmirror.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], + + "json5": ["json5@1.0.2", "https://registry.npmmirror.com/json5/-/json5-1.0.2.tgz", { "dependencies": { "minimist": "^1.2.0" }, "bin": "lib/cli.js" }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="], + + "jsonc-parser": ["jsonc-parser@3.3.1", "https://registry.npmmirror.com/jsonc-parser/-/jsonc-parser-3.3.1.tgz", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="], + + "jsx-ast-utils": ["jsx-ast-utils@3.3.5", "https://registry.npmmirror.com/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", { "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", "object.assign": "^4.1.4", "object.values": "^1.1.6" } }, "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ=="], + + "keyv": ["keyv@4.5.4", "https://registry.npmmirror.com/keyv/-/keyv-4.5.4.tgz", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + + "language-subtag-registry": ["language-subtag-registry@0.3.23", "https://registry.npmmirror.com/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", {}, "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ=="], + + "language-tags": ["language-tags@1.0.9", "https://registry.npmmirror.com/language-tags/-/language-tags-1.0.9.tgz", { "dependencies": { "language-subtag-registry": "^0.3.20" } }, "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA=="], + + "levn": ["levn@0.4.1", "https://registry.npmmirror.com/levn/-/levn-0.4.1.tgz", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + + "lightningcss": ["lightningcss@1.32.0", "https://registry.npmmirror.com/lightningcss/-/lightningcss-1.32.0.tgz", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], + + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "https://registry.npmmirror.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "https://registry.npmmirror.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "https://registry.npmmirror.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "https://registry.npmmirror.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "https://registry.npmmirror.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "https://registry.npmmirror.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "https://registry.npmmirror.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "https://registry.npmmirror.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "https://registry.npmmirror.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "https://registry.npmmirror.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "https://registry.npmmirror.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], + + "linkifyjs": ["linkifyjs@4.3.3", "https://registry.npmmirror.com/linkifyjs/-/linkifyjs-4.3.3.tgz", {}, "sha512-P8aEP5U/D1/IlTY2OeYsErdwh9bGuLE30NcXtKEjgdHcahveQoQwM2yZNsioQHsWFz0P7KKudisbrzCgR0sDHg=="], + + "locate-path": ["locate-path@6.0.0", "https://registry.npmmirror.com/locate-path/-/locate-path-6.0.0.tgz", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + + "lodash": ["lodash@4.18.1", "https://registry.npmmirror.com/lodash/-/lodash-4.18.1.tgz", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="], + + "lodash.merge": ["lodash.merge@4.6.2", "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], + + "longest-streak": ["longest-streak@3.1.0", "https://registry.npmmirror.com/longest-streak/-/longest-streak-3.1.0.tgz", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], + + "loose-envify": ["loose-envify@1.4.0", "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": "cli.js" }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], + + "lottie-web": ["lottie-web@5.13.0", "https://registry.npmmirror.com/lottie-web/-/lottie-web-5.13.0.tgz", {}, "sha512-+gfBXl6sxXMPe8tKQm7qzLnUy5DUPJPKIyRHwtpCpyUEYjHYRJC/5gjUvdkuO2c3JllrPtHXH5UJJK8LRYl5yQ=="], + + "lru-cache": ["lru-cache@5.1.1", "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + + "lucide-react": ["lucide-react@0.511.0", "https://registry.npmmirror.com/lucide-react/-/lucide-react-0.511.0.tgz", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-VK5a2ydJ7xm8GvBeKLS9mu1pVK6ucef9780JVUjw6bAjJL/QXnd4Y0p7SPeOUMC27YhzNCZvm5d/QX0Tp3rc0w=="], + + "magic-string": ["magic-string@0.30.21", "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "markdown-extensions": ["markdown-extensions@2.0.0", "https://registry.npmmirror.com/markdown-extensions/-/markdown-extensions-2.0.0.tgz", {}, "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q=="], + + "markdown-table": ["markdown-table@3.0.4", "https://registry.npmmirror.com/markdown-table/-/markdown-table-3.0.4.tgz", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "https://registry.npmmirror.com/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="], + + "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "https://registry.npmmirror.com/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="], + + "mdast-util-gfm": ["mdast-util-gfm@3.1.0", "https://registry.npmmirror.com/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="], + + "mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@2.0.1", "https://registry.npmmirror.com/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", { "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" } }, "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ=="], + + "mdast-util-gfm-footnote": ["mdast-util-gfm-footnote@2.1.0", "https://registry.npmmirror.com/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0" } }, "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ=="], + + "mdast-util-gfm-strikethrough": ["mdast-util-gfm-strikethrough@2.0.0", "https://registry.npmmirror.com/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg=="], + + "mdast-util-gfm-table": ["mdast-util-gfm-table@2.0.0", "https://registry.npmmirror.com/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "markdown-table": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg=="], + + "mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "https://registry.npmmirror.com/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="], + + "mdast-util-mdx": ["mdast-util-mdx@3.0.0", "https://registry.npmmirror.com/mdast-util-mdx/-/mdast-util-mdx-3.0.0.tgz", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w=="], + + "mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "https://registry.npmmirror.com/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="], + + "mdast-util-mdx-jsx": ["mdast-util-mdx-jsx@3.2.0", "https://registry.npmmirror.com/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" } }, "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q=="], + + "mdast-util-mdxjs-esm": ["mdast-util-mdxjs-esm@2.0.1", "https://registry.npmmirror.com/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg=="], + + "mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "https://registry.npmmirror.com/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="], + + "mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "https://registry.npmmirror.com/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="], + + "mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "https://registry.npmmirror.com/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="], + + "mdast-util-to-string": ["mdast-util-to-string@4.0.0", "https://registry.npmmirror.com/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="], + + "memoize-one": ["memoize-one@5.2.1", "https://registry.npmmirror.com/memoize-one/-/memoize-one-5.2.1.tgz", {}, "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q=="], + + "merge2": ["merge2@1.4.1", "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + + "micromark": ["micromark@4.0.2", "https://registry.npmmirror.com/micromark/-/micromark-4.0.2.tgz", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="], + + "micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "https://registry.npmmirror.com/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="], + + "micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "https://registry.npmmirror.com/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="], + + "micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "https://registry.npmmirror.com/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="], + + "micromark-extension-gfm-footnote": ["micromark-extension-gfm-footnote@2.1.0", "https://registry.npmmirror.com/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", { "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw=="], + + "micromark-extension-gfm-strikethrough": ["micromark-extension-gfm-strikethrough@2.1.0", "https://registry.npmmirror.com/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw=="], + + "micromark-extension-gfm-table": ["micromark-extension-gfm-table@2.1.1", "https://registry.npmmirror.com/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg=="], + + "micromark-extension-gfm-tagfilter": ["micromark-extension-gfm-tagfilter@2.0.0", "https://registry.npmmirror.com/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg=="], + + "micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "https://registry.npmmirror.com/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="], + + "micromark-extension-mdx-expression": ["micromark-extension-mdx-expression@3.0.1", "https://registry.npmmirror.com/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.1.tgz", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q=="], + + "micromark-extension-mdx-jsx": ["micromark-extension-mdx-jsx@3.0.2", "https://registry.npmmirror.com/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-3.0.2.tgz", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ=="], + + "micromark-extension-mdx-md": ["micromark-extension-mdx-md@2.0.0", "https://registry.npmmirror.com/micromark-extension-mdx-md/-/micromark-extension-mdx-md-2.0.0.tgz", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ=="], + + "micromark-extension-mdxjs": ["micromark-extension-mdxjs@3.0.0", "https://registry.npmmirror.com/micromark-extension-mdxjs/-/micromark-extension-mdxjs-3.0.0.tgz", { "dependencies": { "acorn": "^8.0.0", "acorn-jsx": "^5.0.0", "micromark-extension-mdx-expression": "^3.0.0", "micromark-extension-mdx-jsx": "^3.0.0", "micromark-extension-mdx-md": "^2.0.0", "micromark-extension-mdxjs-esm": "^3.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ=="], + + "micromark-extension-mdxjs-esm": ["micromark-extension-mdxjs-esm@3.0.0", "https://registry.npmmirror.com/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-3.0.0.tgz", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-position-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A=="], + + "micromark-factory-destination": ["micromark-factory-destination@2.0.1", "https://registry.npmmirror.com/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="], + + "micromark-factory-label": ["micromark-factory-label@2.0.1", "https://registry.npmmirror.com/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="], + + "micromark-factory-mdx-expression": ["micromark-factory-mdx-expression@2.0.3", "https://registry.npmmirror.com/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-2.0.3.tgz", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-position-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ=="], + + "micromark-factory-space": ["micromark-factory-space@2.0.1", "https://registry.npmmirror.com/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="], + + "micromark-factory-title": ["micromark-factory-title@2.0.1", "https://registry.npmmirror.com/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="], + + "micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "https://registry.npmmirror.com/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="], + + "micromark-util-character": ["micromark-util-character@2.1.1", "https://registry.npmmirror.com/micromark-util-character/-/micromark-util-character-2.1.1.tgz", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], + + "micromark-util-chunked": ["micromark-util-chunked@2.0.1", "https://registry.npmmirror.com/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="], + + "micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "https://registry.npmmirror.com/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="], + + "micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.1", "https://registry.npmmirror.com/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg=="], + + "micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.2", "https://registry.npmmirror.com/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw=="], + + "micromark-util-decode-string": ["micromark-util-decode-string@2.0.1", "https://registry.npmmirror.com/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ=="], + + "micromark-util-encode": ["micromark-util-encode@2.0.1", "https://registry.npmmirror.com/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="], + + "micromark-util-events-to-acorn": ["micromark-util-events-to-acorn@2.0.3", "https://registry.npmmirror.com/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-2.0.3.tgz", { "dependencies": { "@types/estree": "^1.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg=="], + + "micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "https://registry.npmmirror.com/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="], + + "micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "https://registry.npmmirror.com/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="], + + "micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.1", "https://registry.npmmirror.com/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg=="], + + "micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "https://registry.npmmirror.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="], + + "micromark-util-subtokenize": ["micromark-util-subtokenize@2.1.0", "https://registry.npmmirror.com/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA=="], + + "micromark-util-symbol": ["micromark-util-symbol@2.0.1", "https://registry.npmmirror.com/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], + + "micromark-util-types": ["micromark-util-types@2.0.2", "https://registry.npmmirror.com/micromark-util-types/-/micromark-util-types-2.0.2.tgz", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="], + + "micromatch": ["micromatch@4.0.8", "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.8.tgz", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + + "minimatch": ["minimatch@3.1.5", "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.5.tgz", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], + + "minimist": ["minimist@1.2.8", "https://registry.npmmirror.com/minimist/-/minimist-1.2.8.tgz", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "ms": ["ms@2.1.3", "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "nanoid": ["nanoid@3.3.12", "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.12.tgz", { "bin": "bin/nanoid.cjs" }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="], + + "napi-postinstall": ["napi-postinstall@0.3.4", "https://registry.npmmirror.com/napi-postinstall/-/napi-postinstall-0.3.4.tgz", { "bin": "lib/cli.js" }, "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ=="], + + "natural-compare": ["natural-compare@1.4.0", "https://registry.npmmirror.com/natural-compare/-/natural-compare-1.4.0.tgz", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + + "next": ["next@16.2.6", "https://registry.npmmirror.com/next/-/next-16.2.6.tgz", { "dependencies": { "@next/env": "16.2.6", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.2.6", "@next/swc-darwin-x64": "16.2.6", "@next/swc-linux-arm64-gnu": "16.2.6", "@next/swc-linux-arm64-musl": "16.2.6", "@next/swc-linux-x64-gnu": "16.2.6", "@next/swc-linux-x64-musl": "16.2.6", "@next/swc-win32-arm64-msvc": "16.2.6", "@next/swc-win32-x64-msvc": "16.2.6", "sharp": "^0.34.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": "dist/bin/next" }, "sha512-qOVgKJg1+At15NpeUP+eJgCHvTCgXsogweq87Ri/Ix7PkqQHg4sdaXmSFqKlgaIXE4kW0g25LE68W87UANlHtw=="], + + "node-exports-info": ["node-exports-info@1.6.0", "https://registry.npmmirror.com/node-exports-info/-/node-exports-info-1.6.0.tgz", { "dependencies": { "array.prototype.flatmap": "^1.3.3", "es-errors": "^1.3.0", "object.entries": "^1.1.9", "semver": "^6.3.1" } }, "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw=="], + + "node-releases": ["node-releases@2.0.46", "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.46.tgz", {}, "sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ=="], + + "object-assign": ["object-assign@4.1.1", "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + + "object-inspect": ["object-inspect@1.13.4", "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + + "object-keys": ["object-keys@1.1.1", "https://registry.npmmirror.com/object-keys/-/object-keys-1.1.1.tgz", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="], + + "object.assign": ["object.assign@4.1.7", "https://registry.npmmirror.com/object.assign/-/object.assign-4.1.7.tgz", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0", "has-symbols": "^1.1.0", "object-keys": "^1.1.1" } }, "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw=="], + + "object.entries": ["object.entries@1.1.9", "https://registry.npmmirror.com/object.entries/-/object.entries-1.1.9.tgz", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-object-atoms": "^1.1.1" } }, "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw=="], + + "object.fromentries": ["object.fromentries@2.0.8", "https://registry.npmmirror.com/object.fromentries/-/object.fromentries-2.0.8.tgz", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-object-atoms": "^1.0.0" } }, "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ=="], + + "object.groupby": ["object.groupby@1.0.3", "https://registry.npmmirror.com/object.groupby/-/object.groupby-1.0.3.tgz", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2" } }, "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ=="], + + "object.values": ["object.values@1.2.1", "https://registry.npmmirror.com/object.values/-/object.values-1.2.1.tgz", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="], + + "optionator": ["optionator@0.9.4", "https://registry.npmmirror.com/optionator/-/optionator-0.9.4.tgz", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], + + "orderedmap": ["orderedmap@2.1.1", "https://registry.npmmirror.com/orderedmap/-/orderedmap-2.1.1.tgz", {}, "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g=="], + + "own-keys": ["own-keys@1.0.1", "https://registry.npmmirror.com/own-keys/-/own-keys-1.0.1.tgz", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="], + + "p-limit": ["p-limit@3.1.0", "https://registry.npmmirror.com/p-limit/-/p-limit-3.1.0.tgz", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + + "p-locate": ["p-locate@5.0.0", "https://registry.npmmirror.com/p-locate/-/p-locate-5.0.0.tgz", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + + "parent-module": ["parent-module@1.0.1", "https://registry.npmmirror.com/parent-module/-/parent-module-1.0.1.tgz", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + + "parse-entities": ["parse-entities@4.0.2", "https://registry.npmmirror.com/parse-entities/-/parse-entities-4.0.2.tgz", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="], + + "path-exists": ["path-exists@4.0.0", "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + + "path-key": ["path-key@3.1.1", "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "path-parse": ["path-parse@1.0.7", "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + + "picocolors": ["picocolors@1.1.1", "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.4", "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.4.tgz", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + + "pixelmatch": ["pixelmatch@7.2.0", "https://registry.npmmirror.com/pixelmatch/-/pixelmatch-7.2.0.tgz", { "dependencies": { "pngjs": "^7.0.0" }, "bin": { "pixelmatch": "bin/pixelmatch" } }, "sha512-xhcb4yHu9sM/G7foGzoLtXYcC0zHEaOXXjRKhGup0fw78Nf2Tkiapv4EQyMzrbcmQPsllAI7DbFY2UT7PlI9Pg=="], + + "playwright": ["playwright@1.60.0", "https://registry.npmmirror.com/playwright/-/playwright-1.60.0.tgz", { "dependencies": { "playwright-core": "1.60.0" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA=="], + + "playwright-core": ["playwright-core@1.60.0", "https://registry.npmmirror.com/playwright-core/-/playwright-core-1.60.0.tgz", { "bin": { "playwright-core": "cli.js" } }, "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA=="], + + "pngjs": ["pngjs@7.0.0", "https://registry.npmmirror.com/pngjs/-/pngjs-7.0.0.tgz", {}, "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow=="], + + "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "https://registry.npmmirror.com/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], + + "postcss": ["postcss@8.5.15", "https://registry.npmmirror.com/postcss/-/postcss-8.5.15.tgz", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="], + + "prelude-ls": ["prelude-ls@1.2.1", "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + + "prismjs": ["prismjs@1.30.0", "https://registry.npmmirror.com/prismjs/-/prismjs-1.30.0.tgz", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="], + + "prop-types": ["prop-types@15.8.1", "https://registry.npmmirror.com/prop-types/-/prop-types-15.8.1.tgz", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], + + "property-information": ["property-information@7.1.0", "https://registry.npmmirror.com/property-information/-/property-information-7.1.0.tgz", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], + + "prosemirror-changeset": ["prosemirror-changeset@2.4.1", "https://registry.npmmirror.com/prosemirror-changeset/-/prosemirror-changeset-2.4.1.tgz", { "dependencies": { "prosemirror-transform": "^1.0.0" } }, "sha512-96WBLhOaYhJ+kPhLg3uW359Tz6I/MfcrQfL4EGv4SrcqKEMC1gmoGrXHecPE8eOwTVCJ4IwgfzM8fFad25wNfw=="], + + "prosemirror-commands": ["prosemirror-commands@1.7.1", "https://registry.npmmirror.com/prosemirror-commands/-/prosemirror-commands-1.7.1.tgz", { "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.10.2" } }, "sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w=="], + + "prosemirror-dropcursor": ["prosemirror-dropcursor@1.8.2", "https://registry.npmmirror.com/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.2.tgz", { "dependencies": { "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.1.0", "prosemirror-view": "^1.1.0" } }, "sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw=="], + + "prosemirror-gapcursor": ["prosemirror-gapcursor@1.4.1", "https://registry.npmmirror.com/prosemirror-gapcursor/-/prosemirror-gapcursor-1.4.1.tgz", { "dependencies": { "prosemirror-keymap": "^1.0.0", "prosemirror-model": "^1.0.0", "prosemirror-state": "^1.0.0", "prosemirror-view": "^1.0.0" } }, "sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw=="], + + "prosemirror-history": ["prosemirror-history@1.5.0", "https://registry.npmmirror.com/prosemirror-history/-/prosemirror-history-1.5.0.tgz", { "dependencies": { "prosemirror-state": "^1.2.2", "prosemirror-transform": "^1.0.0", "prosemirror-view": "^1.31.0", "rope-sequence": "^1.3.0" } }, "sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg=="], + + "prosemirror-keymap": ["prosemirror-keymap@1.2.3", "https://registry.npmmirror.com/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz", { "dependencies": { "prosemirror-state": "^1.0.0", "w3c-keyname": "^2.2.0" } }, "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw=="], + + "prosemirror-model": ["prosemirror-model@1.25.7", "https://registry.npmmirror.com/prosemirror-model/-/prosemirror-model-1.25.7.tgz", { "dependencies": { "orderedmap": "^2.0.0" } }, "sha512-A79aN8QEFUwI6cax8Yq4Rpcx1TJZ3Kagn+ii7qLo4/V8H3mMiHrhFyhTyHHvpSnOgMPpWiDGSwM3etwrxE50ug=="], + + "prosemirror-schema-list": ["prosemirror-schema-list@1.5.1", "https://registry.npmmirror.com/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz", { "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.7.3" } }, "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q=="], + + "prosemirror-state": ["prosemirror-state@1.4.4", "https://registry.npmmirror.com/prosemirror-state/-/prosemirror-state-1.4.4.tgz", { "dependencies": { "prosemirror-model": "^1.0.0", "prosemirror-transform": "^1.0.0", "prosemirror-view": "^1.27.0" } }, "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw=="], + + "prosemirror-tables": ["prosemirror-tables@1.8.5", "https://registry.npmmirror.com/prosemirror-tables/-/prosemirror-tables-1.8.5.tgz", { "dependencies": { "prosemirror-keymap": "^1.2.3", "prosemirror-model": "^1.25.4", "prosemirror-state": "^1.4.4", "prosemirror-transform": "^1.10.5", "prosemirror-view": "^1.41.4" } }, "sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw=="], + + "prosemirror-transform": ["prosemirror-transform@1.12.0", "https://registry.npmmirror.com/prosemirror-transform/-/prosemirror-transform-1.12.0.tgz", { "dependencies": { "prosemirror-model": "^1.21.0" } }, "sha512-GxboyN4AMIsoHNtz5uf2r2Ru551i5hWeCMD6E2Ib4Eogqoub0NflniaBPVQ4MrGE5yZ8JV9tUHg9qcZTTrcN4w=="], + + "prosemirror-view": ["prosemirror-view@1.41.8", "https://registry.npmmirror.com/prosemirror-view/-/prosemirror-view-1.41.8.tgz", { "dependencies": { "prosemirror-model": "^1.20.0", "prosemirror-state": "^1.0.0", "prosemirror-transform": "^1.1.0" } }, "sha512-TnKDdohEatgyZNGCDWIdccOHXhYloJwbwU+phw/a23KBvJIR9lWQWW7WHHK3vBdOLDNuF7TaX98GObUZOWkOnA=="], + + "punycode": ["punycode@2.3.1", "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + + "qrcode.react": ["qrcode.react@4.2.0", "https://registry.npmmirror.com/qrcode.react/-/qrcode.react-4.2.0.tgz", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA=="], + + "queue-microtask": ["queue-microtask@1.2.3", "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + + "react": ["react@19.2.4", "https://registry.npmmirror.com/react/-/react-19.2.4.tgz", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="], + + "react-dom": ["react-dom@19.2.4", "https://registry.npmmirror.com/react-dom/-/react-dom-19.2.4.tgz", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="], + + "react-draggable": ["react-draggable@4.5.0", "https://registry.npmmirror.com/react-draggable/-/react-draggable-4.5.0.tgz", { "dependencies": { "clsx": "^2.1.1", "prop-types": "^15.8.1" }, "peerDependencies": { "react": ">= 16.3.0", "react-dom": ">= 16.3.0" } }, "sha512-VC+HBLEZ0XJxnOxVAZsdRi8rD04Iz3SiiKOoYzamjylUcju/hP9np/aZdLHf/7WOD268WMoNJMvYfB5yAK45cw=="], + + "react-is": ["react-is@18.3.1", "https://registry.npmmirror.com/react-is/-/react-is-18.3.1.tgz", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], + + "react-resizable": ["react-resizable@3.2.0", "https://registry.npmmirror.com/react-resizable/-/react-resizable-3.2.0.tgz", { "dependencies": { "prop-types": "15.x", "react-draggable": "^4.5.0" }, "peerDependencies": { "react": ">= 16.3", "react-dom": ">= 16.3" } }, "sha512-3NKQ0SLZV7rs3LQHeXlOzDSRQfFrkX6TVet77/Qk03zqiZyee37b7N8/gwDJAA8UUjRz7PdWCCy49hcso45SMQ=="], + + "react-smooth": ["react-smooth@4.0.4", "https://registry.npmmirror.com/react-smooth/-/react-smooth-4.0.4.tgz", { "dependencies": { "fast-equals": "^5.0.1", "prop-types": "^15.8.1", "react-transition-group": "^4.4.5" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q=="], + + "react-transition-group": ["react-transition-group@4.4.5", "https://registry.npmmirror.com/react-transition-group/-/react-transition-group-4.4.5.tgz", { "dependencies": { "@babel/runtime": "^7.5.5", "dom-helpers": "^5.0.1", "loose-envify": "^1.4.0", "prop-types": "^15.6.2" }, "peerDependencies": { "react": ">=16.6.0", "react-dom": ">=16.6.0" } }, "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g=="], + + "react-window": ["react-window@1.8.11", "https://registry.npmmirror.com/react-window/-/react-window-1.8.11.tgz", { "dependencies": { "@babel/runtime": "^7.0.0", "memoize-one": ">=3.1.1 <6" }, "peerDependencies": { "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-+SRbUVT2scadgFSWx+R1P754xHPEqvcfSfVX10QYg6POOz+WNgkN48pS+BtZNIMGiL1HYrSEiCkwsMS15QogEQ=="], + + "recharts": ["recharts@2.15.4", "https://registry.npmmirror.com/recharts/-/recharts-2.15.4.tgz", { "dependencies": { "clsx": "^2.0.0", "eventemitter3": "^4.0.1", "lodash": "^4.17.21", "react-is": "^18.3.1", "react-smooth": "^4.0.4", "recharts-scale": "^0.4.4", "tiny-invariant": "^1.3.1", "victory-vendor": "^36.6.8" }, "peerDependencies": { "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw=="], + + "recharts-scale": ["recharts-scale@0.4.5", "https://registry.npmmirror.com/recharts-scale/-/recharts-scale-0.4.5.tgz", { "dependencies": { "decimal.js-light": "^2.4.1" } }, "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w=="], + + "recma-build-jsx": ["recma-build-jsx@1.0.0", "https://registry.npmmirror.com/recma-build-jsx/-/recma-build-jsx-1.0.0.tgz", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-build-jsx": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew=="], + + "recma-jsx": ["recma-jsx@1.0.1", "https://registry.npmmirror.com/recma-jsx/-/recma-jsx-1.0.1.tgz", { "dependencies": { "acorn-jsx": "^5.0.0", "estree-util-to-js": "^2.0.0", "recma-parse": "^1.0.0", "recma-stringify": "^1.0.0", "unified": "^11.0.0" }, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w=="], + + "recma-parse": ["recma-parse@1.0.0", "https://registry.npmmirror.com/recma-parse/-/recma-parse-1.0.0.tgz", { "dependencies": { "@types/estree": "^1.0.0", "esast-util-from-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ=="], + + "recma-stringify": ["recma-stringify@1.0.0", "https://registry.npmmirror.com/recma-stringify/-/recma-stringify-1.0.0.tgz", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-to-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g=="], + + "reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "https://registry.npmmirror.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="], + + "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "https://registry.npmmirror.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="], + + "rehype-recma": ["rehype-recma@1.0.0", "https://registry.npmmirror.com/rehype-recma/-/rehype-recma-1.0.0.tgz", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "hast-util-to-estree": "^3.0.0" } }, "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw=="], + + "remark-gfm": ["remark-gfm@4.0.1", "https://registry.npmmirror.com/remark-gfm/-/remark-gfm-4.0.1.tgz", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="], + + "remark-mdx": ["remark-mdx@3.1.1", "https://registry.npmmirror.com/remark-mdx/-/remark-mdx-3.1.1.tgz", { "dependencies": { "mdast-util-mdx": "^3.0.0", "micromark-extension-mdxjs": "^3.0.0" } }, "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg=="], + + "remark-parse": ["remark-parse@11.0.0", "https://registry.npmmirror.com/remark-parse/-/remark-parse-11.0.0.tgz", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="], + + "remark-rehype": ["remark-rehype@11.1.2", "https://registry.npmmirror.com/remark-rehype/-/remark-rehype-11.1.2.tgz", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="], + + "remark-stringify": ["remark-stringify@11.0.0", "https://registry.npmmirror.com/remark-stringify/-/remark-stringify-11.0.0.tgz", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="], + + "resolve": ["resolve@2.0.0-next.7", "https://registry.npmmirror.com/resolve/-/resolve-2.0.0-next.7.tgz", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.2", "node-exports-info": "^1.6.0", "object-keys": "^1.1.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": "bin/resolve" }, "sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ=="], + + "resolve-from": ["resolve-from@4.0.0", "https://registry.npmmirror.com/resolve-from/-/resolve-from-4.0.0.tgz", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "https://registry.npmmirror.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + + "reusify": ["reusify@1.1.0", "https://registry.npmmirror.com/reusify/-/reusify-1.1.0.tgz", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + + "rope-sequence": ["rope-sequence@1.3.4", "https://registry.npmmirror.com/rope-sequence/-/rope-sequence-1.3.4.tgz", {}, "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ=="], + + "run-parallel": ["run-parallel@1.2.0", "https://registry.npmmirror.com/run-parallel/-/run-parallel-1.2.0.tgz", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + + "safe-array-concat": ["safe-array-concat@1.1.4", "https://registry.npmmirror.com/safe-array-concat/-/safe-array-concat-1.1.4.tgz", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "get-intrinsic": "^1.3.0", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg=="], + + "safe-push-apply": ["safe-push-apply@1.0.0", "https://registry.npmmirror.com/safe-push-apply/-/safe-push-apply-1.0.0.tgz", { "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" } }, "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA=="], + + "safe-regex-test": ["safe-regex-test@1.1.0", "https://registry.npmmirror.com/safe-regex-test/-/safe-regex-test-1.1.0.tgz", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="], + + "scheduler": ["scheduler@0.27.0", "https://registry.npmmirror.com/scheduler/-/scheduler-0.27.0.tgz", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + + "scroll-into-view-if-needed": ["scroll-into-view-if-needed@2.2.31", "https://registry.npmmirror.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.31.tgz", { "dependencies": { "compute-scroll-into-view": "^1.0.20" } }, "sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA=="], + + "semver": ["semver@6.3.1", "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", { "bin": "bin/semver.js" }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "set-function-length": ["set-function-length@1.2.2", "https://registry.npmmirror.com/set-function-length/-/set-function-length-1.2.2.tgz", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], + + "set-function-name": ["set-function-name@2.0.2", "https://registry.npmmirror.com/set-function-name/-/set-function-name-2.0.2.tgz", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="], + + "set-proto": ["set-proto@1.0.0", "https://registry.npmmirror.com/set-proto/-/set-proto-1.0.0.tgz", { "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0" } }, "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw=="], + + "sharp": ["sharp@0.34.5", "https://registry.npmmirror.com/sharp/-/sharp-0.34.5.tgz", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], + + "shebang-command": ["shebang-command@2.0.0", "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "side-channel": ["side-channel@1.1.0", "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.0.tgz", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], + + "side-channel-list": ["side-channel-list@1.0.1", "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.1.tgz", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="], + + "side-channel-map": ["side-channel-map@1.0.1", "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], + + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + + "source-map": ["source-map@0.7.6", "https://registry.npmmirror.com/source-map/-/source-map-0.7.6.tgz", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], + + "source-map-js": ["source-map-js@1.2.1", "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "space-separated-tokens": ["space-separated-tokens@2.0.2", "https://registry.npmmirror.com/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], + + "stable-hash": ["stable-hash@0.0.5", "https://registry.npmmirror.com/stable-hash/-/stable-hash-0.0.5.tgz", {}, "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA=="], + + "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "https://registry.npmmirror.com/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="], + + "string.prototype.includes": ["string.prototype.includes@2.0.1", "https://registry.npmmirror.com/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.3" } }, "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg=="], + + "string.prototype.matchall": ["string.prototype.matchall@4.0.12", "https://registry.npmmirror.com/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "regexp.prototype.flags": "^1.5.3", "set-function-name": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA=="], + + "string.prototype.repeat": ["string.prototype.repeat@1.0.0", "https://registry.npmmirror.com/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", { "dependencies": { "define-properties": "^1.1.3", "es-abstract": "^1.17.5" } }, "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w=="], + + "string.prototype.trim": ["string.prototype.trim@1.2.10", "https://registry.npmmirror.com/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-data-property": "^1.1.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-object-atoms": "^1.0.0", "has-property-descriptors": "^1.0.2" } }, "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA=="], + + "string.prototype.trimend": ["string.prototype.trimend@1.0.9", "https://registry.npmmirror.com/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ=="], + + "string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "https://registry.npmmirror.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="], + + "stringify-entities": ["stringify-entities@4.0.4", "https://registry.npmmirror.com/stringify-entities/-/stringify-entities-4.0.4.tgz", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], + + "strip-bom": ["strip-bom@3.0.0", "https://registry.npmmirror.com/strip-bom/-/strip-bom-3.0.0.tgz", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], + + "strip-json-comments": ["strip-json-comments@3.1.1", "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + + "style-to-js": ["style-to-js@1.1.21", "https://registry.npmmirror.com/style-to-js/-/style-to-js-1.1.21.tgz", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="], + + "style-to-object": ["style-to-object@1.0.14", "https://registry.npmmirror.com/style-to-object/-/style-to-object-1.0.14.tgz", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="], + + "styled-jsx": ["styled-jsx@5.1.6", "https://registry.npmmirror.com/styled-jsx/-/styled-jsx-5.1.6.tgz", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="], + + "supports-color": ["supports-color@7.2.0", "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + + "tailwindcss": ["tailwindcss@4.3.0", "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-4.3.0.tgz", {}, "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q=="], + + "tapable": ["tapable@2.3.3", "https://registry.npmmirror.com/tapable/-/tapable-2.3.3.tgz", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="], + + "tiny-invariant": ["tiny-invariant@1.3.3", "https://registry.npmmirror.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], + + "tinyglobby": ["tinyglobby@0.2.16", "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.16.tgz", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], + + "to-regex-range": ["to-regex-range@5.0.1", "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + + "trim-lines": ["trim-lines@3.0.1", "https://registry.npmmirror.com/trim-lines/-/trim-lines-3.0.1.tgz", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], + + "trough": ["trough@2.2.0", "https://registry.npmmirror.com/trough/-/trough-2.2.0.tgz", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], + + "ts-api-utils": ["ts-api-utils@2.5.0", "https://registry.npmmirror.com/ts-api-utils/-/ts-api-utils-2.5.0.tgz", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="], + + "tsconfig-paths": ["tsconfig-paths@3.15.0", "https://registry.npmmirror.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", { "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg=="], + + "tslib": ["tslib@2.8.1", "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "type-check": ["type-check@0.4.0", "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], + + "typed-array-buffer": ["typed-array-buffer@1.0.3", "https://registry.npmmirror.com/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="], + + "typed-array-byte-length": ["typed-array-byte-length@1.0.3", "https://registry.npmmirror.com/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", { "dependencies": { "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.14" } }, "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg=="], + + "typed-array-byte-offset": ["typed-array-byte-offset@1.0.4", "https://registry.npmmirror.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.15", "reflect.getprototypeof": "^1.0.9" } }, "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ=="], + + "typed-array-length": ["typed-array-length@1.0.7", "https://registry.npmmirror.com/typed-array-length/-/typed-array-length-1.0.7.tgz", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0", "reflect.getprototypeof": "^1.0.6" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="], + + "typescript": ["typescript@5.9.3", "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "typescript-eslint": ["typescript-eslint@8.60.0", "https://registry.npmmirror.com/typescript-eslint/-/typescript-eslint-8.60.0.tgz", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.60.0", "@typescript-eslint/parser": "8.60.0", "@typescript-eslint/typescript-estree": "8.60.0", "@typescript-eslint/utils": "8.60.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-9f65qWLZdAW9m1JaxBDUHcqRUfL8bkxxXL7XxEfI+F09q56PkBvIfCjLF3yInsDM/BBmwkqmCQdCZe/RYlIWEw=="], + + "unbox-primitive": ["unbox-primitive@1.1.0", "https://registry.npmmirror.com/unbox-primitive/-/unbox-primitive-1.1.0.tgz", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], + + "undici-types": ["undici-types@6.21.0", "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + + "unified": ["unified@11.0.5", "https://registry.npmmirror.com/unified/-/unified-11.0.5.tgz", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], + + "unist-util-is": ["unist-util-is@6.0.1", "https://registry.npmmirror.com/unist-util-is/-/unist-util-is-6.0.1.tgz", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], + + "unist-util-position": ["unist-util-position@5.0.0", "https://registry.npmmirror.com/unist-util-position/-/unist-util-position-5.0.0.tgz", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="], + + "unist-util-position-from-estree": ["unist-util-position-from-estree@2.0.0", "https://registry.npmmirror.com/unist-util-position-from-estree/-/unist-util-position-from-estree-2.0.0.tgz", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ=="], + + "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "https://registry.npmmirror.com/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="], + + "unist-util-visit": ["unist-util-visit@5.1.0", "https://registry.npmmirror.com/unist-util-visit/-/unist-util-visit-5.1.0.tgz", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="], + + "unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "https://registry.npmmirror.com/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="], + + "unrs-resolver": ["unrs-resolver@1.12.2", "https://registry.npmmirror.com/unrs-resolver/-/unrs-resolver-1.12.2.tgz", { "dependencies": { "napi-postinstall": "^0.3.4" }, "optionalDependencies": { "@unrs/resolver-binding-android-arm-eabi": "1.12.2", "@unrs/resolver-binding-android-arm64": "1.12.2", "@unrs/resolver-binding-darwin-arm64": "1.12.2", "@unrs/resolver-binding-darwin-x64": "1.12.2", "@unrs/resolver-binding-freebsd-x64": "1.12.2", "@unrs/resolver-binding-linux-arm-gnueabihf": "1.12.2", "@unrs/resolver-binding-linux-arm-musleabihf": "1.12.2", "@unrs/resolver-binding-linux-arm64-gnu": "1.12.2", "@unrs/resolver-binding-linux-arm64-musl": "1.12.2", "@unrs/resolver-binding-linux-loong64-gnu": "1.12.2", "@unrs/resolver-binding-linux-loong64-musl": "1.12.2", "@unrs/resolver-binding-linux-ppc64-gnu": "1.12.2", "@unrs/resolver-binding-linux-riscv64-gnu": "1.12.2", "@unrs/resolver-binding-linux-riscv64-musl": "1.12.2", "@unrs/resolver-binding-linux-s390x-gnu": "1.12.2", "@unrs/resolver-binding-linux-x64-gnu": "1.12.2", "@unrs/resolver-binding-linux-x64-musl": "1.12.2", "@unrs/resolver-binding-openharmony-arm64": "1.12.2", "@unrs/resolver-binding-wasm32-wasi": "1.12.2", "@unrs/resolver-binding-win32-arm64-msvc": "1.12.2", "@unrs/resolver-binding-win32-ia32-msvc": "1.12.2", "@unrs/resolver-binding-win32-x64-msvc": "1.12.2" } }, "sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ=="], + + "update-browserslist-db": ["update-browserslist-db@1.2.3", "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], + + "uri-js": ["uri-js@4.4.1", "https://registry.npmmirror.com/uri-js/-/uri-js-4.4.1.tgz", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + + "use-sync-external-store": ["use-sync-external-store@1.6.0", "https://registry.npmmirror.com/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], + + "utility-types": ["utility-types@3.11.0", "https://registry.npmmirror.com/utility-types/-/utility-types-3.11.0.tgz", {}, "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw=="], + + "vfile": ["vfile@6.0.3", "https://registry.npmmirror.com/vfile/-/vfile-6.0.3.tgz", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], + + "vfile-message": ["vfile-message@4.0.3", "https://registry.npmmirror.com/vfile-message/-/vfile-message-4.0.3.tgz", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], + + "victory-vendor": ["victory-vendor@36.9.2", "https://registry.npmmirror.com/victory-vendor/-/victory-vendor-36.9.2.tgz", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ=="], + + "w3c-keyname": ["w3c-keyname@2.2.8", "https://registry.npmmirror.com/w3c-keyname/-/w3c-keyname-2.2.8.tgz", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="], + + "which": ["which@2.0.2", "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "which-boxed-primitive": ["which-boxed-primitive@1.1.1", "https://registry.npmmirror.com/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="], + + "which-builtin-type": ["which-builtin-type@1.2.1", "https://registry.npmmirror.com/which-builtin-type/-/which-builtin-type-1.2.1.tgz", { "dependencies": { "call-bound": "^1.0.2", "function.prototype.name": "^1.1.6", "has-tostringtag": "^1.0.2", "is-async-function": "^2.0.0", "is-date-object": "^1.1.0", "is-finalizationregistry": "^1.1.0", "is-generator-function": "^1.0.10", "is-regex": "^1.2.1", "is-weakref": "^1.0.2", "isarray": "^2.0.5", "which-boxed-primitive": "^1.1.0", "which-collection": "^1.0.2", "which-typed-array": "^1.1.16" } }, "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q=="], + + "which-collection": ["which-collection@1.0.2", "https://registry.npmmirror.com/which-collection/-/which-collection-1.0.2.tgz", { "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", "is-weakmap": "^2.0.2", "is-weakset": "^2.0.3" } }, "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw=="], + + "which-typed-array": ["which-typed-array@1.1.21", "https://registry.npmmirror.com/which-typed-array/-/which-typed-array-1.1.21.tgz", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.9", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-zbRA8cVm6io/d5W8uIe2hblzN76/Wm3v/yiythQvr+dpBWeqhPSWIDNj4zOyHi4zKbMK6DN34Xsr9jPHJERAEw=="], + + "word-wrap": ["word-wrap@1.2.5", "https://registry.npmmirror.com/word-wrap/-/word-wrap-1.2.5.tgz", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + + "yallist": ["yallist@3.1.1", "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + + "yocto-queue": ["yocto-queue@0.1.0", "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-0.1.0.tgz", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + + "zod": ["zod@4.4.3", "https://registry.npmmirror.com/zod/-/zod-4.4.3.tgz", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], + + "zod-validation-error": ["zod-validation-error@4.0.2", "https://registry.npmmirror.com/zod-validation-error/-/zod-validation-error-4.0.2.tgz", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="], + + "zwitch": ["zwitch@2.0.4", "https://registry.npmmirror.com/zwitch/-/zwitch-2.0.4.tgz", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], + + "@babel/core/json5": ["json5@2.2.3", "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz", { "bin": "lib/cli.js" }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "@eslint/eslintrc/globals": ["globals@14.0.0", "https://registry.npmmirror.com/globals/-/globals-14.0.0.tgz", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], + + "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "https://registry.npmmirror.com/ignore/-/ignore-7.0.5.tgz", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + + "@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.5", "https://registry.npmmirror.com/minimatch/-/minimatch-10.2.5.tgz", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], + + "@typescript-eslint/typescript-estree/semver": ["semver@7.8.1", "https://registry.npmmirror.com/semver/-/semver-7.8.1.tgz", { "bin": "bin/semver.js" }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="], + + "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], + + "eslint-import-resolver-node/debug": ["debug@3.2.7", "https://registry.npmmirror.com/debug/-/debug-3.2.7.tgz", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + + "eslint-module-utils/debug": ["debug@3.2.7", "https://registry.npmmirror.com/debug/-/debug-3.2.7.tgz", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + + "eslint-plugin-import/debug": ["debug@3.2.7", "https://registry.npmmirror.com/debug/-/debug-3.2.7.tgz", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + + "fast-glob/glob-parent": ["glob-parent@5.1.2", "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "is-bun-module/semver": ["semver@7.8.1", "https://registry.npmmirror.com/semver/-/semver-7.8.1.tgz", { "bin": "bin/semver.js" }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="], + + "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], + + "micromatch/picomatch": ["picomatch@2.3.2", "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.2.tgz", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + + "next/postcss": ["postcss@8.4.31", "https://registry.npmmirror.com/postcss/-/postcss-8.4.31.tgz", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], + + "parse-entities/@types/unist": ["@types/unist@2.0.11", "https://registry.npmmirror.com/@types/unist/-/unist-2.0.11.tgz", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], + + "prop-types/react-is": ["react-is@16.13.1", "https://registry.npmmirror.com/react-is/-/react-is-16.13.1.tgz", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], + + "sharp/semver": ["semver@7.8.1", "https://registry.npmmirror.com/semver/-/semver-7.8.1.tgz", { "bin": "bin/semver.js" }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="], + + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.6", "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-5.0.6.tgz", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="], + + "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "https://registry.npmmirror.com/balanced-match/-/balanced-match-4.0.4.tgz", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + } +} diff --git a/portal/components/announcement/announcement-modal.tsx b/portal/components/announcement/announcement-modal.tsx new file mode 100644 index 0000000..7eb044d --- /dev/null +++ b/portal/components/announcement/announcement-modal.tsx @@ -0,0 +1,75 @@ +"use client"; + +import { Button, Modal, Typography } from "@douyinfe/semi-ui"; +import { useCallback, useState } from "react"; + +import type { AnnouncementItem } from "@/lib/api/wallet-types"; +import { gatewayFetch } from "@/lib/api/client"; +import { useDeferredEffect } from "@/lib/hooks/use-deferred-effect"; + +const { Paragraph, Title } = Typography; + +const DISMISS_PREFIX = "portal_announcement_dismiss_"; + +function todayKey() { + return new Date().toISOString().slice(0, 10); +} + +type AnnouncementModalProps = { + placement?: string; +}; + +export function AnnouncementModal({ placement = "home" }: AnnouncementModalProps) { + const [visible, setVisible] = useState(false); + const [item, setItem] = useState(null); + + const load = useCallback(async () => { + try { + const res = await gatewayFetch<{ success: boolean; data: AnnouncementItem[] }>( + `/api/announcements?placement=${placement}`, + ); + const first = res.data[0]; + if (!first) return; + const key = `${DISMISS_PREFIX}${first.id}_${todayKey()}`; + if (localStorage.getItem(key) === "1") return; + setItem(first); + setVisible(true); + } catch { + // ignore when gateway offline + } + }, [placement]); + + useDeferredEffect(() => load(), [load]); + + if (!item) return null; + + return ( + setVisible(false)} + footer={ +
+ + +
+ } + > + {item.content} + {item.level === "warning" && ( + + 请关注系统通知 + + )} +
+ ); +} diff --git a/portal/components/brand-logo.tsx b/portal/components/brand-logo.tsx new file mode 100644 index 0000000..2873fd6 --- /dev/null +++ b/portal/components/brand-logo.tsx @@ -0,0 +1,29 @@ +import Link from "next/link"; + +type BrandLogoProps = { + href?: string; + className?: string; +}; + +export function BrandLogo({ href = "/", className = "" }: BrandLogoProps) { + const inner = ( + + + 蓝 + + + 蓝移 API + + + ); + + if (href) { + return ( + + {inner} + + ); + } + + return inner; +} diff --git a/portal/components/config-error.tsx b/portal/components/config-error.tsx new file mode 100644 index 0000000..3ec7fae --- /dev/null +++ b/portal/components/config-error.tsx @@ -0,0 +1,24 @@ +"use client"; + +import { Banner } from "@douyinfe/semi-ui"; + +export function ConfigError() { + return ( +
+ + 请在 portal/.env.local{" "} + 中设置 NEXT_PUBLIC_GATEWAY_API_URL + (无尾斜杠),例如 http://localhost:8080 + + } + /> +
+ ); +} diff --git a/portal/components/config-gate.tsx b/portal/components/config-gate.tsx new file mode 100644 index 0000000..a379429 --- /dev/null +++ b/portal/components/config-gate.tsx @@ -0,0 +1,12 @@ +"use client"; + +import { isGatewayConfigured } from "@/lib/gateway-config"; + +import { ConfigError } from "./config-error"; + +export function ConfigGate({ children }: { children: React.ReactNode }) { + if (!isGatewayConfigured()) { + return ; + } + return <>{children}; +} diff --git a/portal/components/console-guard.tsx b/portal/components/console-guard.tsx new file mode 100644 index 0000000..f62c26e --- /dev/null +++ b/portal/components/console-guard.tsx @@ -0,0 +1,34 @@ +"use client"; + +import { Toast } from "@douyinfe/semi-ui"; +import { useRouter } from "next/navigation"; +import { useEffect } from "react"; + +import { useAuth } from "@/contexts/auth-context"; + +export function ConsoleGuard({ children }: { children: React.ReactNode }) { + const { token, ready } = useAuth(); + const router = useRouter(); + + useEffect(() => { + if (!ready) return; + if (!token) { + Toast.warning("未登录或登录已过期"); + router.replace("/login?expired=true"); + } + }, [ready, token, router]); + + if (!ready) { + return ( +
+ 加载中… +
+ ); + } + + if (!token) { + return null; + } + + return <>{children}; +} diff --git a/portal/components/console-sidebar.tsx b/portal/components/console-sidebar.tsx new file mode 100644 index 0000000..d072acf --- /dev/null +++ b/portal/components/console-sidebar.tsx @@ -0,0 +1,103 @@ +"use client"; + +import { Button, Layout, Nav } from "@douyinfe/semi-ui"; +import { + IconHistogram, + IconKey, + IconList, + IconSetting, + IconTicketCode, + IconUser, + IconChevronLeft, +} from "@douyinfe/semi-icons"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { useState } from "react"; + +const { Sider } = Layout; + +const MENU = [ + { + itemKey: "chat", + text: "聊天", + items: [{ itemKey: "/console/playground", text: "操练场", icon: }], + }, + { + itemKey: "console", + text: "控制台", + items: [ + { itemKey: "/console", text: "数据看板", icon: }, + { itemKey: "/console/token", text: "令牌管理", icon: }, + { itemKey: "/console/log", text: "使用日志", icon: }, + { itemKey: "/console/task", text: "任务日志", icon: }, + ], + }, + { + itemKey: "account", + text: "个人中心", + items: [ + { itemKey: "/console/wallet", text: "钱包管理", icon: }, + { itemKey: "/console/setting", text: "个人设置", icon: }, + ], + }, +]; + +function resolveSelectedKey(pathname: string): string { + if (pathname.startsWith("/console/playground")) return "/console/playground"; + if (pathname.startsWith("/console/token")) return "/console/token"; + if (pathname.startsWith("/console/log")) return "/console/log"; + if (pathname.startsWith("/console/task")) return "/console/task"; + if (pathname.startsWith("/console/wallet")) return "/console/wallet"; + if (pathname.startsWith("/console/setting")) return "/console/setting"; + if (pathname.startsWith("/console")) return "/console"; + return "/console"; +} + +export function ConsoleSidebar() { + const pathname = usePathname(); + const selectedKey = resolveSelectedKey(pathname); + const [collapsed, setCollapsed] = useState(false); + + return ( + +
+
+
+ ); +} diff --git a/portal/components/dashboard/console-dashboard-page.tsx b/portal/components/dashboard/console-dashboard-page.tsx new file mode 100644 index 0000000..ea59712 --- /dev/null +++ b/portal/components/dashboard/console-dashboard-page.tsx @@ -0,0 +1,353 @@ +"use client"; + +import Link from "next/link"; +import { + Button, + Card, + Collapse, + TabPane, + Tabs, + Tag, + Toast, + Typography, +} from "@douyinfe/semi-ui"; +import { IconCopy, IconExternalOpen } from "@douyinfe/semi-icons"; +import { useCallback, useMemo, useState } from "react"; +import { + Bar, + BarChart, + CartesianGrid, + Line, + LineChart, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from "recharts"; + +import type { + DashboardChartsResponse, + DashboardStatsResponse, + NodePingResult, + NodesPingResponse, + PortalNode, +} from "@/lib/api/dashboard-types"; +import { DashboardAnnouncementsCard } from "@/components/dashboard/dashboard-announcements-card"; +import { StatSparkline } from "@/components/dashboard/stat-sparkline"; +import { AnnouncementModal } from "@/components/announcement/announcement-modal"; +import { usePortalLocale } from "@/contexts/portal-locale-context"; +import { ApiError } from "@/lib/api/client"; +import { useAuth } from "@/contexts/auth-context"; +import { useDeferredEffect } from "@/lib/hooks/use-deferred-effect"; +import { useGatewayRequest } from "@/lib/hooks/use-gateway-request"; + +const { Title, Text } = Typography; + +const SERVICES = [ + { name: "API 网关", status: "ok" as const }, + { name: "OpenAI 兼容接口", status: "ok" as const }, + { name: "操练场", status: "ok" as const }, +]; + +function greetingLabel(): string { + const h = new Date().getHours(); + if (h < 6) return "凌晨好"; + if (h < 12) return "早上好"; + if (h < 18) return "下午好"; + return "晚上好"; +} + +export function ConsoleDashboardPage() { + const request = useGatewayRequest(); + const { user } = useAuth(); + const { messages: dm } = usePortalLocale(); + const [stats, setStats] = useState(null); + const [charts, setCharts] = useState(null); + const [nodes, setNodes] = useState([]); + const [pingMap, setPingMap] = useState>({}); + const [pinging, setPinging] = useState(false); + const [range, setRange] = useState("7d"); + const [loading, setLoading] = useState(true); + + const load = useCallback(async () => { + setLoading(true); + try { + const [s, c, n] = await Promise.all([ + request("/api/dashboard/stats"), + request(`/api/dashboard/charts?range=${range}`), + request<{ success: boolean; data: PortalNode[] }>("/api/dashboard/nodes"), + ]); + setStats(s.data); + setCharts(c.data); + setNodes(n.data); + } catch (e) { + Toast.error(e instanceof ApiError ? e.message : "加载看板失败"); + } finally { + setLoading(false); + } + }, [request, range]); + + useDeferredEffect(() => load(), [load]); + + const runPing = useCallback(async () => { + setPinging(true); + try { + const res = await request("/api/nodes/ping"); + const next: Record = {}; + for (const row of res.data) { + next[row.url] = row; + } + setPingMap(next); + } catch (e) { + Toast.error(e instanceof ApiError ? e.message : "测速失败"); + } finally { + setPinging(false); + } + }, [request]); + + const metricCards = useMemo(() => { + if (!stats) return []; + return [ + { + key: "account", + title: "账户余额", + value: stats.account.quota, + sub: `已消耗 ${stats.account.used_quota} · 分组 ${stats.account.group}`, + action: ( + + + + ), + }, + { + key: "requests", + title: "24h 请求", + value: stats.usage.request_count_24h, + spark: stats.sparklines.requests, + color: "#007AFF", + }, + { + key: "tokens", + title: "24h Tokens", + value: stats.usage.total_tokens_24h, + }, + { + key: "cost", + title: "24h 消耗额度", + value: stats.consumption.cost_quota_24h, + spark: stats.sparklines.cost, + color: "#7C3AED", + }, + ]; + }, [stats]); + + const trendData = useMemo( + () => + (charts?.call_trend ?? []).map((p) => ({ + hour: p.hour.slice(11, 16), + count: p.count, + cost: p.cost, + })), + [charts], + ); + + const modelData = useMemo( + () => charts?.consumption_by_model?.slice(0, 10) ?? [], + [charts], + ); + + return ( +
+ +
+
+
+ + 👋 {greetingLabel()} + {user?.display_name || user?.username + ? `,${user.display_name || user.username}` + : ""} + + + 数据看板 · RPM {stats?.performance.rpm ?? "—"} · TPM{" "} + {stats?.performance.tpm ?? "—"} · 平均延迟{" "} + {stats?.performance.avg_latency_ms ?? "—"} ms + +
+ + + + + +
+ +
+ {metricCards.map((card) => ( + +
+ + {card.title} + + {"action" in card ? card.action : null} +
+ + {card.value} + + {"sub" in card && card.sub ? ( + + {card.sub} + + ) : null} + {"spark" in card && card.spark ? ( + + ) : null} +
+ ))} +
+ + + + +
+ + + + + + + + + +
+
+ +
+ + + + + + + + + +
+
+ +
+ {(charts?.ranking ?? []).slice(0, 8).map((row) => ( +
+ {row.model} + {row.cost} +
+ ))} +
+
+
+
+
+ + +
+ ); +} diff --git a/portal/components/dashboard/dashboard-announcements-card.tsx b/portal/components/dashboard/dashboard-announcements-card.tsx new file mode 100644 index 0000000..19b74a2 --- /dev/null +++ b/portal/components/dashboard/dashboard-announcements-card.tsx @@ -0,0 +1,104 @@ +"use client"; + +import { Card, Tag, Typography } from "@douyinfe/semi-ui"; +import { useCallback, useState } from "react"; + +import type { AnnouncementItem } from "@/lib/api/wallet-types"; +import { ApiError } from "@/lib/api/client"; +import { useDeferredEffect } from "@/lib/hooks/use-deferred-effect"; +import { useGatewayRequest } from "@/lib/hooks/use-gateway-request"; + +const { Text } = Typography; + +const LEVEL_META: Record< + string, + { label: string; color: "grey" | "blue" | "green" | "orange" | "red" } +> = { + default: { label: "默认", color: "grey" }, + info: { label: "进行中", color: "blue" }, + success: { label: "成功", color: "green" }, + warning: { label: "警告", color: "orange" }, + error: { label: "错误", color: "red" }, +}; + +function levelMeta(level: string) { + return LEVEL_META[level] ?? LEVEL_META.default; +} + +type DashboardAnnouncementsCardProps = { + placement?: string; +}; + +export function DashboardAnnouncementsCard({ + placement = "console", +}: DashboardAnnouncementsCardProps) { + const request = useGatewayRequest(); + const [items, setItems] = useState([]); + const [loading, setLoading] = useState(true); + + const load = useCallback(async () => { + setLoading(true); + try { + const res = await request<{ success: boolean; data: AnnouncementItem[] }>( + `/api/announcements?placement=${placement}`, + ); + let list = res.data ?? []; + if (list.length === 0 && placement !== "home") { + const fallback = await request<{ success: boolean; data: AnnouncementItem[] }>( + "/api/announcements?placement=home", + ); + list = fallback.data ?? []; + } + setItems(list.slice(0, 6)); + } catch (e) { + if (!(e instanceof ApiError)) { + setItems([]); + } + } finally { + setLoading(false); + } + }, [request, placement]); + + useDeferredEffect(() => load(), [load]); + + return ( + +
+ {Object.entries(LEVEL_META).map(([key, meta]) => ( + + {meta.label} + + ))} +
+ {items.length === 0 ? ( + + 暂无公告 + + ) : ( +
    + {items.map((item) => { + const meta = levelMeta(item.level); + return ( +
  • +
    + + {meta.label} + + + {item.title} + +
    + + {item.content} + +
  • + ); + })} +
+ )} +
+ ); +} diff --git a/portal/components/dashboard/stat-sparkline.tsx b/portal/components/dashboard/stat-sparkline.tsx new file mode 100644 index 0000000..95b65d1 --- /dev/null +++ b/portal/components/dashboard/stat-sparkline.tsx @@ -0,0 +1,28 @@ +"use client"; + +import { Line, LineChart, ResponsiveContainer } from "recharts"; + +type StatSparklineProps = { + data: { t: string; v: number }[]; + color?: string; +}; + +export function StatSparkline({ data, color = "#007AFF" }: StatSparklineProps) { + if (!data.length) return null; + return ( +
+ + + + + +
+ ); +} diff --git a/portal/components/logs/task-log-page.tsx b/portal/components/logs/task-log-page.tsx new file mode 100644 index 0000000..13e8f15 --- /dev/null +++ b/portal/components/logs/task-log-page.tsx @@ -0,0 +1,113 @@ +"use client"; + +import { Button, Empty, Form, Input, Table, Tag, Toast, Typography } from "@douyinfe/semi-ui"; +import { useCallback, useState } from "react"; + +import type { TaskLogRow } from "@/lib/api/dashboard-types"; +import { ApiError } from "@/lib/api/client"; +import { useDeferredEffect } from "@/lib/hooks/use-deferred-effect"; +import { useGatewayRequest } from "@/lib/hooks/use-gateway-request"; + +const { Title } = Typography; + +type FilterValues = { + task_id?: string; + range?: Date[]; +}; + +export function TaskLogPage() { + const request = useGatewayRequest(); + const [items, setItems] = useState([]); + const [total, setTotal] = useState(0); + const [page, setPage] = useState(1); + const [loading, setLoading] = useState(false); + const [filters, setFilters] = useState({}); + + const load = useCallback(async () => { + setLoading(true); + const params = new URLSearchParams({ page: String(page), page_size: "10" }); + if (filters.task_id) params.set("task_id", filters.task_id); + const [start, end] = filters.range ?? []; + if (start) params.set("start", start.toISOString()); + if (end) params.set("end", end.toISOString()); + try { + const res = await request<{ + success: boolean; + data: { items: TaskLogRow[]; total: number }; + }>(`/api/logs/tasks?${params}`); + setItems(res.data.items); + setTotal(res.data.total); + } catch (e) { + Toast.error(e instanceof ApiError ? e.message : "加载失败"); + } finally { + setLoading(false); + } + }, [request, page, filters]); + + useDeferredEffect(() => load(), [load]); + + return ( +
+ 任务日志 +
{ + setFilters(v as FilterValues); + setPage(1); + }} + > + + + + + + {!loading && items.length === 0 ? ( + + ) : ( + new Date(v).toLocaleString(), + }, + { + title: "结束时间", + dataIndex: "finished_at", + render: (v: string | null) => (v ? new Date(v).toLocaleString() : "—"), + }, + { + title: "耗时(ms)", + dataIndex: "duration_ms", + render: (v: number | null) => v ?? "—", + }, + { title: "平台", dataIndex: "platform" }, + { title: "类型", dataIndex: "type" }, + { title: "任务 ID", dataIndex: "task_id" }, + { + title: "状态", + dataIndex: "status", + render: (v: string) => {v}, + }, + { + title: "进度", + dataIndex: "progress", + render: (v: number) => `${v}%`, + }, + ]} + /> + )} + + ); +} diff --git a/portal/components/logs/usage-log-page.tsx b/portal/components/logs/usage-log-page.tsx new file mode 100644 index 0000000..fb9de1f --- /dev/null +++ b/portal/components/logs/usage-log-page.tsx @@ -0,0 +1,126 @@ +"use client"; + +import { Button, Form, Table, Tag, Toast, Typography } from "@douyinfe/semi-ui"; +import { useCallback, useState } from "react"; + +import type { UsageLogRow } from "@/lib/api/dashboard-types"; +import { ApiError } from "@/lib/api/client"; +import { useDeferredEffect } from "@/lib/hooks/use-deferred-effect"; +import { useGatewayRequest } from "@/lib/hooks/use-gateway-request"; + +const { Title } = Typography; + +type FilterValues = { + token_name?: string; + model?: string; + request_id?: string; + token_group?: string; + range?: Date[]; +}; + +function toISO(d?: Date) { + return d ? d.toISOString() : undefined; +} + +export function UsageLogPage() { + const request = useGatewayRequest(); + const [items, setItems] = useState([]); + const [total, setTotal] = useState(0); + const [page, setPage] = useState(1); + const [loading, setLoading] = useState(false); + const [filters, setFilters] = useState({}); + + const load = useCallback(async () => { + setLoading(true); + const params = new URLSearchParams({ + page: String(page), + page_size: "10", + }); + if (filters.token_name) params.set("token_name", filters.token_name); + if (filters.model) params.set("model", filters.model); + if (filters.request_id) params.set("request_id", filters.request_id); + if (filters.token_group) params.set("token_group", filters.token_group); + const [start, end] = filters.range ?? []; + if (start) params.set("start", toISO(start)!); + if (end) params.set("end", toISO(end)!); + try { + const res = await request<{ + success: boolean; + data: { items: UsageLogRow[]; total: number }; + }>(`/api/logs/usage?${params}`); + setItems(res.data.items); + setTotal(res.data.total); + } catch (e) { + Toast.error(e instanceof ApiError ? e.message : "加载失败"); + } finally { + setLoading(false); + } + }, [request, page, filters]); + + useDeferredEffect(() => load(), [load]); + + return ( +
+ 使用日志 +
{ + setFilters(v as FilterValues); + setPage(1); + }} + > + + + + + + + + +
new Date(v).toLocaleString(), + }, + { title: "令牌", dataIndex: "token_name", width: 120 }, + { + title: "分组", + dataIndex: "token_group", + render: (v: string) => {v || "—"}, + }, + { title: "模型", dataIndex: "model" }, + { title: "Request ID", dataIndex: "request_id", width: 200 }, + { + title: "首字(ms)", + dataIndex: "time_to_first_ms", + render: (v: number | null) => v ?? "—", + }, + { title: "输入 Tok", dataIndex: "prompt_tokens" }, + { + title: "消耗额度", + dataIndex: "cost_quota", + }, + ]} + /> + + ); +} diff --git a/portal/components/marketing/about-page.tsx b/portal/components/marketing/about-page.tsx new file mode 100644 index 0000000..4d33158 --- /dev/null +++ b/portal/components/marketing/about-page.tsx @@ -0,0 +1,46 @@ +"use client"; + +import { Card, Typography } from "@douyinfe/semi-ui"; +import Link from "next/link"; + +import { usePortalLocale } from "@/contexts/portal-locale-context"; + +const { Title, Paragraph, Text } = Typography; + +export function AboutPageContent() { + const { messages: m } = usePortalLocale(); + + return ( +
+ {m.about.title} + {m.about.intro} + + + {m.about.mission} + + + +
    + {m.about.features.map((item) => ( +
  • + {item} +
  • + ))} +
+
+ + + {m.about.contact} + + + {m.nav.docs} + + {" · "} + + {m.nav.pricing} + + + +
+ ); +} diff --git a/portal/components/marketing/docs-page.tsx b/portal/components/marketing/docs-page.tsx new file mode 100644 index 0000000..5a379f9 --- /dev/null +++ b/portal/components/marketing/docs-page.tsx @@ -0,0 +1,83 @@ +"use client"; + +import { Button, Card, Typography } from "@douyinfe/semi-ui"; +import { IconExternalOpen } from "@douyinfe/semi-icons"; +import Link from "next/link"; + +import { usePortalLocale } from "@/contexts/portal-locale-context"; +import { getGatewayBaseUrl } from "@/lib/gateway-config"; +import { getDocsUrl } from "@/lib/portal-config"; + +const { Title, Paragraph, Text } = Typography; + +export function DocsPageContent() { + const { messages: m } = usePortalLocale(); + const externalDocs = getDocsUrl(); + const gatewayBase = getGatewayBaseUrl().replace(/\/$/, ""); + + return ( +
+
+ {m.docs.title} + {externalDocs ? ( + + + + ) : null} +
+ {m.docs.intro} + + +
+
+ {m.docs.authTitle} + {m.docs.authBody} +
+
+ {m.docs.baseUrlTitle} + + {m.docs.baseUrlBody} + {gatewayBase ? ( + <> +
+ + {gatewayBase}/v1 + + + ) : null} +
+
+
+ {m.docs.errorsTitle} + {m.docs.errorsBody} +
+
+
+ + + + OpenAPI + + {gatewayBase ? ( + <> + {" "} + ( + + {gatewayBase}/openapi.yaml + + ) + + ) : ( + "(配置 NEXT_PUBLIC_GATEWAY_API_URL 后显示网关地址)" + )} + +
+ ); +} diff --git a/portal/components/marketing/partner-marquee.tsx b/portal/components/marketing/partner-marquee.tsx new file mode 100644 index 0000000..e4a20a4 --- /dev/null +++ b/portal/components/marketing/partner-marquee.tsx @@ -0,0 +1,45 @@ +"use client"; + +import { Typography } from "@douyinfe/semi-ui"; + +import { usePortalLocale } from "@/contexts/portal-locale-context"; + +const { Text } = Typography; + +const PARTNERS = [ + "OpenAI", + "Anthropic", + "Google", + "DeepSeek", + "Meta", + "Mistral", + "Qwen", + "Moonshot", + "Zhipu", + "MiniMax", +]; + +export function PartnerMarquee() { + const { messages } = usePortalLocale(); + const row = [...PARTNERS, ...PARTNERS]; + + return ( +
+ + {messages.home.partnersTitle} + +
+
+ {row.map((name, i) => ( + + {name} + + ))} +
+
+
+ ); +} diff --git a/portal/components/placeholder-page.tsx b/portal/components/placeholder-page.tsx new file mode 100644 index 0000000..ca104c2 --- /dev/null +++ b/portal/components/placeholder-page.tsx @@ -0,0 +1,21 @@ +"use client"; + +import { Card, Typography } from "@douyinfe/semi-ui"; + +const { Title, Text } = Typography; + +type PlaceholderPageProps = { + title: string; + phase: number; +}; + +export function PlaceholderPage({ title, phase }: PlaceholderPageProps) { + return ( +
+ {title} + + 该页面将在 OpenSpec Phase {phase} 实现。 + +
+ ); +} diff --git a/portal/components/playground/playground-page.tsx b/portal/components/playground/playground-page.tsx new file mode 100644 index 0000000..fd9ebf7 --- /dev/null +++ b/portal/components/playground/playground-page.tsx @@ -0,0 +1,662 @@ +"use client"; + +import { + Button, + Input, + Select, + Slider, + Switch, + TextArea, + Toast, + Typography, +} from "@douyinfe/semi-ui"; +import { + IconCopy, + IconDelete, + IconDownload, + IconEdit, + IconRefresh, + IconUpload, +} from "@douyinfe/semi-icons"; +import { useSearchParams } from "next/navigation"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; + +import type { CatalogListResponse, TokenGroupItem } from "@/lib/api/catalog-types"; +import { ApiError } from "@/lib/api/client"; +import type { + PlaygroundConfig, + PlaygroundMessage, +} from "@/lib/api/playground-types"; +import { streamPlaygroundChat } from "@/lib/api/playground-stream"; +import { useAuth } from "@/contexts/auth-context"; +import { useDeferredEffect } from "@/lib/hooks/use-deferred-effect"; +import { useGatewayRequest } from "@/lib/hooks/use-gateway-request"; + +const { Title, Text } = Typography; + +const CONFIG_STORAGE_KEY = "portal_playground_config_v1"; + +function newId(): string { + return `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`; +} + +const defaultConfig = (): PlaygroundConfig => ({ + version: 1, + token_group: "default", + model: "", + custom_body: false, + custom_body_raw: "", + image_url: "", + max_tokens: 2048, + temperature: 1, + temperature_enabled: true, + top_p: 1, + top_p_enabled: false, + frequency_penalty: 0, + frequency_penalty_enabled: false, + presence_penalty: 0, + presence_penalty_enabled: false, +}); + +export function PlaygroundPage() { + const { token } = useAuth(); + const request = useGatewayRequest(); + const searchParams = useSearchParams(); + const abortRef = useRef(null); + const bottomRef = useRef(null); + + const [config, setConfig] = useState(defaultConfig); + const [messages, setMessages] = useState([]); + const [input, setInput] = useState(""); + const [streaming, setStreaming] = useState(false); + const [models, setModels] = useState([]); + const [groups, setGroups] = useState([]); + const [apiKeyId, setApiKeyId] = useState(); + const [tokenHint, setTokenHint] = useState(null); + const [debugOpen, setDebugOpen] = useState(false); + const [debugPayload, setDebugPayload] = useState(null); + const [editingId, setEditingId] = useState(null); + const [editDraft, setEditDraft] = useState(""); + + useDeferredEffect(() => { + const raw = sessionStorage.getItem(CONFIG_STORAGE_KEY); + if (raw) { + try { + const parsed = JSON.parse(raw) as PlaygroundConfig; + setConfig((c) => ({ ...c, ...parsed, version: 1 })); + } catch { + /* ignore */ + } + } + const fromQuery = searchParams.get("token_id"); + if (fromQuery) { + sessionStorage.setItem("playground_token_id", fromQuery); + } + const idStr = sessionStorage.getItem("playground_token_id"); + const name = sessionStorage.getItem("playground_token_name"); + if (idStr) { + const id = Number(idStr); + if (!Number.isNaN(id)) setApiKeyId(id); + setTokenHint(name ? `${name} (#${idStr})` : `#${idStr}`); + } + }, [searchParams]); + + useEffect(() => { + sessionStorage.setItem(CONFIG_STORAGE_KEY, JSON.stringify(config)); + }, [config]); + + const loadCatalog = useCallback(async () => { + try { + const group = config.token_group || "default"; + const [catalogRes, groupRes] = await Promise.all([ + request( + `/api/models?page=1&page_size=200&token_group=${encodeURIComponent(group)}`, + ), + request<{ success: boolean; data: TokenGroupItem[] }>("/api/token-groups"), + ]); + const names = catalogRes.data.items.map((m) => m.model); + setModels(names); + setGroups(groupRes.data); + setConfig((c) => ({ + ...c, + model: c.model && names.includes(c.model) ? c.model : (names[0] ?? ""), + })); + } catch (e) { + Toast.error(e instanceof ApiError ? e.message : "加载模型失败"); + } + }, [request, config.token_group]); + + useDeferredEffect(() => loadCatalog(), [loadCatalog]); + + const patchConfig = (partial: Partial) => { + setConfig((c) => ({ ...c, ...partial })); + }; + + const buildApiMessages = useCallback( + (list: PlaygroundMessage[]) => { + const out: { role: PlaygroundMessage["role"]; content: string }[] = []; + for (const m of list) { + if (m.role === "assistant" && !m.content.trim()) continue; + if (m.role === "user" && config.image_url?.trim()) { + out.push({ + role: "user", + content: m.content, + }); + continue; + } + out.push({ role: m.role, content: m.content }); + } + return out; + }, + [config.image_url], + ); + + const runCompletion = useCallback( + async (baseMessages: PlaygroundMessage[], regenerate = false) => { + if (!token) { + Toast.error("请先登录"); + return; + } + if (!config.model && !config.custom_body) { + Toast.error("请选择模型"); + return; + } + if (streaming) return; + + let contextMessages = [...baseMessages]; + if (regenerate) { + while ( + contextMessages.length > 0 && + contextMessages[contextMessages.length - 1].role === "assistant" + ) { + contextMessages = contextMessages.slice(0, -1); + } + } + + const last = contextMessages[contextMessages.length - 1]; + if (!last || last.role !== "user" || !last.content.trim()) { + Toast.warning("请输入消息"); + return; + } + + const assistantId = newId(); + setMessages([ + ...contextMessages, + { id: assistantId, role: "assistant", content: "" }, + ]); + setStreaming(true); + abortRef.current?.abort(); + abortRef.current = new AbortController(); + + let customRaw: unknown; + if (config.custom_body && config.custom_body_raw.trim()) { + try { + customRaw = JSON.parse(config.custom_body_raw) as unknown; + } catch { + setStreaming(false); + Toast.error("自定义 Body JSON 无效"); + return; + } + } + + const reqBody = { + model: config.model, + messages: buildApiMessages(contextMessages), + max_tokens: config.max_tokens, + api_key_id: apiKeyId, + token_group: config.token_group, + custom_body: config.custom_body, + ...(customRaw !== undefined ? { custom_body_raw: customRaw } : {}), + ...(config.temperature_enabled ? { temperature: config.temperature } : {}), + ...(config.top_p_enabled ? { top_p: config.top_p } : {}), + ...(config.frequency_penalty_enabled + ? { frequency_penalty: config.frequency_penalty } + : {}), + ...(config.presence_penalty_enabled + ? { presence_penalty: config.presence_penalty } + : {}), + }; + + setDebugPayload(reqBody); + + await streamPlaygroundChat( + token, + reqBody, + { + onDelta: (text) => { + setMessages((prev) => + prev.map((m) => + m.id === assistantId ? { ...m, content: m.content + text } : m, + ), + ); + }, + onDone: () => { + setStreaming(false); + setMessages((prev) => + prev.map((m) => + m.id === assistantId + ? { + ...m, + debug: { + request: reqBody, + finished_at: new Date().toISOString(), + }, + } + : m, + ), + ); + }, + onError: (message) => { + setStreaming(false); + Toast.error(message); + }, + }, + abortRef.current.signal, + ); + }, + [token, config, apiKeyId, streaming, buildApiMessages], + ); + + const sendMessage = async () => { + const text = input.trim(); + if (!text) return; + const userMsg: PlaygroundMessage = { id: newId(), role: "user", content: text }; + setInput(""); + const next = [...messages, userMsg]; + setMessages(next); + await runCompletion(next); + }; + + const regenerate = async () => { + if (!messages.some((m) => m.role === "user")) { + Toast.warning("没有可重新生成的对话"); + return; + } + await runCompletion(messages, true); + }; + + const copyMessage = async (content: string) => { + try { + await navigator.clipboard.writeText(content); + Toast.success("已复制"); + } catch { + Toast.error("复制失败"); + } + }; + + const deleteMessage = (id: string) => { + setMessages((prev) => prev.filter((m) => m.id !== id)); + }; + + const startEdit = (m: PlaygroundMessage) => { + setEditingId(m.id); + setEditDraft(m.content); + }; + + const saveEdit = () => { + if (!editingId) return; + setMessages((prev) => + prev.map((m) => (m.id === editingId ? { ...m, content: editDraft } : m)), + ); + setEditingId(null); + setEditDraft(""); + }; + + const exportConfig = () => { + const blob = new Blob([JSON.stringify(config, null, 2)], { + type: "application/json", + }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = "playground-config.json"; + a.click(); + URL.revokeObjectURL(url); + Toast.success("配置已导出"); + }; + + const importConfig = () => { + const inputEl = document.createElement("input"); + inputEl.type = "file"; + inputEl.accept = "application/json"; + inputEl.onchange = async () => { + const file = inputEl.files?.[0]; + if (!file) return; + try { + const text = await file.text(); + const parsed = JSON.parse(text) as PlaygroundConfig; + setConfig({ ...defaultConfig(), ...parsed, version: 1 }); + Toast.success("配置已导入"); + } catch { + Toast.error("无效的配置文件"); + } + }; + inputEl.click(); + }; + + const groupOptions = useMemo( + () => groups.map((g) => ({ value: g.slug, label: `${g.name} (${g.multiplier}x)` })), + [groups], + ); + + const modelOptions = useMemo( + () => models.map((m) => ({ value: m, label: m })), + [models], + ); + + useEffect(() => { + bottomRef.current?.scrollIntoView({ behavior: "smooth" }); + }, [messages, streaming]); + + return ( +
+ {tokenHint && ( + + 当前令牌:{tokenHint} + + )} + +
+