diff --git a/backend/internal/handler/admin/account_data.go b/backend/internal/handler/admin/account_data.go index 20cc09ee..00da4821 100644 --- a/backend/internal/handler/admin/account_data.go +++ b/backend/internal/handler/admin/account_data.go @@ -10,6 +10,7 @@ import ( "log/slog" + infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors" "github.com/Wei-Shaw/sub2api/internal/pkg/openai" "github.com/Wei-Shaw/sub2api/internal/pkg/response" "github.com/Wei-Shaw/sub2api/internal/service" @@ -359,7 +360,7 @@ func (h *AccountHandler) listAllProxies(ctx context.Context) ([]service.Proxy, e pageSize := dataPageCap var out []service.Proxy for { - items, total, err := h.adminService.ListProxies(ctx, page, pageSize, "", "", "") + items, total, err := h.adminService.ListProxies(ctx, page, pageSize, "", "", "", "created_at", "desc") if err != nil { return nil, err } @@ -372,12 +373,12 @@ func (h *AccountHandler) listAllProxies(ctx context.Context) ([]service.Proxy, e return out, nil } -func (h *AccountHandler) listAccountsFiltered(ctx context.Context, platform, accountType, status, search string) ([]service.Account, error) { +func (h *AccountHandler) listAccountsFiltered(ctx context.Context, platform, accountType, status, search string, groupID int64, privacyMode, sortBy, sortOrder string) ([]service.Account, error) { page := 1 pageSize := dataPageCap var out []service.Account for { - items, total, err := h.adminService.ListAccounts(ctx, page, pageSize, platform, accountType, status, search, 0, "") + items, total, err := h.adminService.ListAccounts(ctx, page, pageSize, platform, accountType, status, search, groupID, privacyMode, sortBy, sortOrder) if err != nil { return nil, err } @@ -409,11 +410,28 @@ func (h *AccountHandler) resolveExportAccounts(ctx context.Context, ids []int64, platform := c.Query("platform") accountType := c.Query("type") status := c.Query("status") + privacyMode := strings.TrimSpace(c.Query("privacy_mode")) search := strings.TrimSpace(c.Query("search")) + sortBy := c.DefaultQuery("sort_by", "name") + sortOrder := c.DefaultQuery("sort_order", "asc") if len(search) > 100 { search = search[:100] } - return h.listAccountsFiltered(ctx, platform, accountType, status, search) + + groupID := int64(0) + if groupIDStr := c.Query("group"); groupIDStr != "" { + if groupIDStr == accountListGroupUngroupedQueryValue { + groupID = service.AccountListGroupUngrouped + } else { + parsedGroupID, parseErr := strconv.ParseInt(groupIDStr, 10, 64) + if parseErr != nil || parsedGroupID <= 0 { + return nil, infraerrors.BadRequest("INVALID_GROUP_FILTER", "invalid group filter") + } + groupID = parsedGroupID + } + } + + return h.listAccountsFiltered(ctx, platform, accountType, status, search, groupID, privacyMode, sortBy, sortOrder) } func (h *AccountHandler) resolveExportProxies(ctx context.Context, accounts []service.Account) ([]service.Proxy, error) { diff --git a/backend/internal/handler/admin/account_data_handler_test.go b/backend/internal/handler/admin/account_data_handler_test.go index 285033a1..5793983c 100644 --- a/backend/internal/handler/admin/account_data_handler_test.go +++ b/backend/internal/handler/admin/account_data_handler_test.go @@ -172,6 +172,51 @@ func TestExportDataWithoutProxies(t *testing.T) { require.Nil(t, resp.Data.Accounts[0].ProxyKey) } +func TestExportDataPassesAccountFiltersAndSort(t *testing.T) { + router, adminSvc := setupAccountDataRouter() + adminSvc.accounts = []service.Account{ + {ID: 1, Name: "acc-1", Status: service.StatusActive}, + } + + rec := httptest.NewRecorder() + req := httptest.NewRequest( + http.MethodGet, + "/api/v1/admin/accounts/data?platform=openai&type=oauth&status=active&group=12&privacy_mode=blocked&search=keyword&sort_by=priority&sort_order=desc", + nil, + ) + router.ServeHTTP(rec, req) + require.Equal(t, http.StatusOK, rec.Code) + + require.Equal(t, 1, adminSvc.lastListAccounts.calls) + require.Equal(t, "openai", adminSvc.lastListAccounts.platform) + require.Equal(t, "oauth", adminSvc.lastListAccounts.accountType) + require.Equal(t, "active", adminSvc.lastListAccounts.status) + require.Equal(t, int64(12), adminSvc.lastListAccounts.groupID) + require.Equal(t, "blocked", adminSvc.lastListAccounts.privacyMode) + require.Equal(t, "keyword", adminSvc.lastListAccounts.search) + require.Equal(t, "priority", adminSvc.lastListAccounts.sortBy) + require.Equal(t, "desc", adminSvc.lastListAccounts.sortOrder) +} + +func TestExportDataSelectedIDsOverrideFilters(t *testing.T) { + router, adminSvc := setupAccountDataRouter() + + rec := httptest.NewRecorder() + req := httptest.NewRequest( + http.MethodGet, + "/api/v1/admin/accounts/data?ids=1,2&platform=openai&search=keyword&sort_by=priority&sort_order=desc", + nil, + ) + router.ServeHTTP(rec, req) + require.Equal(t, http.StatusOK, rec.Code) + + var resp dataResponse + require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp)) + require.Equal(t, 0, resp.Code) + require.Len(t, resp.Data.Accounts, 2) + require.Equal(t, 0, adminSvc.lastListAccounts.calls) +} + func TestImportDataReusesProxyAndSkipsDefaultGroup(t *testing.T) { router, adminSvc := setupAccountDataRouter() diff --git a/backend/internal/handler/admin/account_handler.go b/backend/internal/handler/admin/account_handler.go index 9aed64d5..9e985a79 100644 --- a/backend/internal/handler/admin/account_handler.go +++ b/backend/internal/handler/admin/account_handler.go @@ -221,6 +221,8 @@ func (h *AccountHandler) List(c *gin.Context) { status := c.Query("status") search := c.Query("search") privacyMode := strings.TrimSpace(c.Query("privacy_mode")) + sortBy := c.DefaultQuery("sort_by", "name") + sortOrder := c.DefaultQuery("sort_order", "asc") // 标准化和验证 search 参数 search = strings.TrimSpace(search) if len(search) > 100 { @@ -246,7 +248,7 @@ func (h *AccountHandler) List(c *gin.Context) { } } - accounts, total, err := h.adminService.ListAccounts(c.Request.Context(), page, pageSize, platform, accountType, status, search, groupID, privacyMode) + accounts, total, err := h.adminService.ListAccounts(c.Request.Context(), page, pageSize, platform, accountType, status, search, groupID, privacyMode, sortBy, sortOrder) if err != nil { response.ErrorFrom(c, err) return @@ -2029,7 +2031,7 @@ func (h *AccountHandler) BatchRefreshTier(c *gin.Context) { accounts := make([]*service.Account, 0) if len(req.AccountIDs) == 0 { - allAccounts, _, err := h.adminService.ListAccounts(ctx, 1, 10000, "gemini", "oauth", "", "", 0, "") + allAccounts, _, err := h.adminService.ListAccounts(ctx, 1, 10000, "gemini", "oauth", "", "", 0, "", "name", "asc") if err != nil { response.ErrorFrom(c, err) return diff --git a/backend/internal/handler/admin/admin_service_stub_test.go b/backend/internal/handler/admin/admin_service_stub_test.go index 60d68913..6d1ef1b6 100644 --- a/backend/internal/handler/admin/admin_service_stub_test.go +++ b/backend/internal/handler/admin/admin_service_stub_test.go @@ -31,6 +31,33 @@ type stubAdminService struct { platform string groupIDs []int64 } + lastListAccounts struct { + platform string + accountType string + status string + search string + groupID int64 + privacyMode string + sortBy string + sortOrder string + calls int + } + lastListProxies struct { + protocol string + status string + search string + sortBy string + sortOrder string + calls int + } + lastListRedeemCodes struct { + codeType string + status string + search string + sortBy string + sortOrder string + calls int + } mu sync.Mutex } @@ -99,7 +126,7 @@ func newStubAdminService() *stubAdminService { } } -func (s *stubAdminService) ListUsers(ctx context.Context, page, pageSize int, filters service.UserListFilters) ([]service.User, int64, error) { +func (s *stubAdminService) ListUsers(ctx context.Context, page, pageSize int, filters service.UserListFilters, sortBy, sortOrder string) ([]service.User, int64, error) { return s.users, int64(len(s.users)), nil } @@ -132,7 +159,7 @@ func (s *stubAdminService) UpdateUserBalance(ctx context.Context, userID int64, return &user, nil } -func (s *stubAdminService) GetUserAPIKeys(ctx context.Context, userID int64, page, pageSize int) ([]service.APIKey, int64, error) { +func (s *stubAdminService) GetUserAPIKeys(ctx context.Context, userID int64, page, pageSize int, sortBy, sortOrder string) ([]service.APIKey, int64, error) { return s.apiKeys, int64(len(s.apiKeys)), nil } @@ -140,7 +167,7 @@ func (s *stubAdminService) GetUserUsageStats(ctx context.Context, userID int64, return map[string]any{"user_id": userID}, nil } -func (s *stubAdminService) ListGroups(ctx context.Context, page, pageSize int, platform, status, search string, isExclusive *bool) ([]service.Group, int64, error) { +func (s *stubAdminService) ListGroups(ctx context.Context, page, pageSize int, platform, status, search string, isExclusive *bool, sortBy, sortOrder string) ([]service.Group, int64, error) { return s.groups, int64(len(s.groups)), nil } @@ -187,7 +214,16 @@ func (s *stubAdminService) BatchSetGroupRateMultipliers(_ context.Context, _ int return nil } -func (s *stubAdminService) ListAccounts(ctx context.Context, page, pageSize int, platform, accountType, status, search string, groupID int64, privacyMode string) ([]service.Account, int64, error) { +func (s *stubAdminService) ListAccounts(ctx context.Context, page, pageSize int, platform, accountType, status, search string, groupID int64, privacyMode string, sortBy, sortOrder string) ([]service.Account, int64, error) { + s.lastListAccounts.platform = platform + s.lastListAccounts.accountType = accountType + s.lastListAccounts.status = status + s.lastListAccounts.search = search + s.lastListAccounts.groupID = groupID + s.lastListAccounts.privacyMode = privacyMode + s.lastListAccounts.sortBy = sortBy + s.lastListAccounts.sortOrder = sortOrder + s.lastListAccounts.calls++ return s.accounts, int64(len(s.accounts)), nil } @@ -261,7 +297,13 @@ func (s *stubAdminService) CheckMixedChannelRisk(ctx context.Context, currentAcc return s.checkMixedErr } -func (s *stubAdminService) ListProxies(ctx context.Context, page, pageSize int, protocol, status, search string) ([]service.Proxy, int64, error) { +func (s *stubAdminService) ListProxies(ctx context.Context, page, pageSize int, protocol, status, search string, sortBy, sortOrder string) ([]service.Proxy, int64, error) { + s.lastListProxies.protocol = protocol + s.lastListProxies.status = status + s.lastListProxies.search = search + s.lastListProxies.sortBy = sortBy + s.lastListProxies.sortOrder = sortOrder + s.lastListProxies.calls++ search = strings.TrimSpace(strings.ToLower(search)) filtered := make([]service.Proxy, 0, len(s.proxies)) for _, proxy := range s.proxies { @@ -283,7 +325,7 @@ func (s *stubAdminService) ListProxies(ctx context.Context, page, pageSize int, return filtered, int64(len(filtered)), nil } -func (s *stubAdminService) ListProxiesWithAccountCount(ctx context.Context, page, pageSize int, protocol, status, search string) ([]service.ProxyWithAccountCount, int64, error) { +func (s *stubAdminService) ListProxiesWithAccountCount(ctx context.Context, page, pageSize int, protocol, status, search string, sortBy, sortOrder string) ([]service.ProxyWithAccountCount, int64, error) { return s.proxyCounts, int64(len(s.proxyCounts)), nil } @@ -384,7 +426,13 @@ func (s *stubAdminService) CheckProxyQuality(ctx context.Context, id int64) (*se }, nil } -func (s *stubAdminService) ListRedeemCodes(ctx context.Context, page, pageSize int, codeType, status, search string) ([]service.RedeemCode, int64, error) { +func (s *stubAdminService) ListRedeemCodes(ctx context.Context, page, pageSize int, codeType, status, search string, sortBy, sortOrder string) ([]service.RedeemCode, int64, error) { + s.lastListRedeemCodes.codeType = codeType + s.lastListRedeemCodes.status = status + s.lastListRedeemCodes.search = search + s.lastListRedeemCodes.sortBy = sortBy + s.lastListRedeemCodes.sortOrder = sortOrder + s.lastListRedeemCodes.calls++ return s.redeems, int64(len(s.redeems)), nil } diff --git a/backend/internal/handler/admin/announcement_handler.go b/backend/internal/handler/admin/announcement_handler.go index d1312bc0..d3b9d173 100644 --- a/backend/internal/handler/admin/announcement_handler.go +++ b/backend/internal/handler/admin/announcement_handler.go @@ -52,13 +52,17 @@ func (h *AnnouncementHandler) List(c *gin.Context) { page, pageSize := response.ParsePagination(c) status := strings.TrimSpace(c.Query("status")) search := strings.TrimSpace(c.Query("search")) + sortBy := c.DefaultQuery("sort_by", "created_at") + sortOrder := c.DefaultQuery("sort_order", "desc") if len(search) > 200 { search = search[:200] } params := pagination.PaginationParams{ - Page: page, - PageSize: pageSize, + Page: page, + PageSize: pageSize, + SortBy: sortBy, + SortOrder: sortOrder, } items, paginationResult, err := h.announcementService.List( @@ -227,8 +231,10 @@ func (h *AnnouncementHandler) ListReadStatus(c *gin.Context) { page, pageSize := response.ParsePagination(c) params := pagination.PaginationParams{ - Page: page, - PageSize: pageSize, + Page: page, + PageSize: pageSize, + SortBy: c.DefaultQuery("sort_by", "email"), + SortOrder: c.DefaultQuery("sort_order", "asc"), } search := strings.TrimSpace(c.Query("search")) if len(search) > 200 { diff --git a/backend/internal/handler/admin/announcement_handler_sort_test.go b/backend/internal/handler/admin/announcement_handler_sort_test.go new file mode 100644 index 00000000..545e619e --- /dev/null +++ b/backend/internal/handler/admin/announcement_handler_sort_test.go @@ -0,0 +1,138 @@ +package admin + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/Wei-Shaw/sub2api/internal/pkg/pagination" + "github.com/Wei-Shaw/sub2api/internal/service" + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/require" +) + +type announcementRepoCapture struct { + service.AnnouncementRepository + listParams pagination.PaginationParams +} + +func (r *announcementRepoCapture) List(ctx context.Context, params pagination.PaginationParams, filters service.AnnouncementListFilters) ([]service.Announcement, *pagination.PaginationResult, error) { + r.listParams = params + return []service.Announcement{}, &pagination.PaginationResult{ + Total: 0, + Page: params.Page, + PageSize: params.PageSize, + Pages: 0, + }, nil +} + +func (r *announcementRepoCapture) GetByID(ctx context.Context, id int64) (*service.Announcement, error) { + return &service.Announcement{ + ID: id, + Title: "announcement", + Content: "content", + Status: service.AnnouncementStatusActive, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }, nil +} + +type announcementUserRepoCapture struct { + service.UserRepository + listParams pagination.PaginationParams +} + +func (r *announcementUserRepoCapture) ListWithFilters(ctx context.Context, params pagination.PaginationParams, filters service.UserListFilters) ([]service.User, *pagination.PaginationResult, error) { + r.listParams = params + return []service.User{}, &pagination.PaginationResult{ + Total: 0, + Page: params.Page, + PageSize: params.PageSize, + Pages: 0, + }, nil +} + +type announcementReadRepoCapture struct { + service.AnnouncementReadRepository +} + +func (r *announcementReadRepoCapture) GetReadMapByUsers(ctx context.Context, announcementID int64, userIDs []int64) (map[int64]time.Time, error) { + return map[int64]time.Time{}, nil +} + +type announcementUserSubRepoCapture struct { + service.UserSubscriptionRepository +} + +func newAnnouncementSortTestRouter(announcementRepo *announcementRepoCapture, userRepo *announcementUserRepoCapture) *gin.Engine { + gin.SetMode(gin.TestMode) + svc := service.NewAnnouncementService( + announcementRepo, + &announcementReadRepoCapture{}, + userRepo, + &announcementUserSubRepoCapture{}, + ) + handler := NewAnnouncementHandler(svc) + router := gin.New() + router.GET("/admin/announcements", handler.List) + router.GET("/admin/announcements/:id/read-status", handler.ListReadStatus) + return router +} + +func TestAdminAnnouncementListSortParams(t *testing.T) { + announcementRepo := &announcementRepoCapture{} + userRepo := &announcementUserRepoCapture{} + router := newAnnouncementSortTestRouter(announcementRepo, userRepo) + + req := httptest.NewRequest(http.MethodGet, "/admin/announcements?sort_by=title&sort_order=ASC", nil) + rec := httptest.NewRecorder() + router.ServeHTTP(rec, req) + + require.Equal(t, http.StatusOK, rec.Code) + require.Equal(t, "title", announcementRepo.listParams.SortBy) + require.Equal(t, "ASC", announcementRepo.listParams.SortOrder) +} + +func TestAdminAnnouncementListSortDefaults(t *testing.T) { + announcementRepo := &announcementRepoCapture{} + userRepo := &announcementUserRepoCapture{} + router := newAnnouncementSortTestRouter(announcementRepo, userRepo) + + req := httptest.NewRequest(http.MethodGet, "/admin/announcements", nil) + rec := httptest.NewRecorder() + router.ServeHTTP(rec, req) + + require.Equal(t, http.StatusOK, rec.Code) + require.Equal(t, "created_at", announcementRepo.listParams.SortBy) + require.Equal(t, "desc", announcementRepo.listParams.SortOrder) +} + +func TestAdminAnnouncementReadStatusSortParams(t *testing.T) { + announcementRepo := &announcementRepoCapture{} + userRepo := &announcementUserRepoCapture{} + router := newAnnouncementSortTestRouter(announcementRepo, userRepo) + + req := httptest.NewRequest(http.MethodGet, "/admin/announcements/1/read-status?sort_by=balance&sort_order=DESC", nil) + rec := httptest.NewRecorder() + router.ServeHTTP(rec, req) + + require.Equal(t, http.StatusOK, rec.Code) + require.Equal(t, "balance", userRepo.listParams.SortBy) + require.Equal(t, "DESC", userRepo.listParams.SortOrder) +} + +func TestAdminAnnouncementReadStatusSortDefaults(t *testing.T) { + announcementRepo := &announcementRepoCapture{} + userRepo := &announcementUserRepoCapture{} + router := newAnnouncementSortTestRouter(announcementRepo, userRepo) + + req := httptest.NewRequest(http.MethodGet, "/admin/announcements/1/read-status", nil) + rec := httptest.NewRecorder() + router.ServeHTTP(rec, req) + + require.Equal(t, http.StatusOK, rec.Code) + require.Equal(t, "email", userRepo.listParams.SortBy) + require.Equal(t, "asc", userRepo.listParams.SortOrder) +} diff --git a/backend/internal/handler/admin/channel_handler.go b/backend/internal/handler/admin/channel_handler.go index b503e5c3..c92b35bb 100644 --- a/backend/internal/handler/admin/channel_handler.go +++ b/backend/internal/handler/admin/channel_handler.go @@ -245,7 +245,12 @@ func (h *ChannelHandler) List(c *gin.Context) { search = search[:100] } - channels, pag, err := h.channelService.List(c.Request.Context(), pagination.PaginationParams{Page: page, PageSize: pageSize}, status, search) + channels, pag, err := h.channelService.List(c.Request.Context(), pagination.PaginationParams{ + Page: page, + PageSize: pageSize, + SortBy: c.DefaultQuery("sort_by", "created_at"), + SortOrder: c.DefaultQuery("sort_order", "desc"), + }, status, search) if err != nil { response.ErrorFrom(c, err) return diff --git a/backend/internal/handler/admin/group_handler.go b/backend/internal/handler/admin/group_handler.go index 8b6b056d..cb2bd201 100644 --- a/backend/internal/handler/admin/group_handler.go +++ b/backend/internal/handler/admin/group_handler.go @@ -162,6 +162,8 @@ func (h *GroupHandler) List(c *gin.Context) { search = search[:100] } isExclusiveStr := c.Query("is_exclusive") + sortBy := c.DefaultQuery("sort_by", "sort_order") + sortOrder := c.DefaultQuery("sort_order", "asc") var isExclusive *bool if isExclusiveStr != "" { @@ -169,7 +171,7 @@ func (h *GroupHandler) List(c *gin.Context) { isExclusive = &val } - groups, total, err := h.adminService.ListGroups(c.Request.Context(), page, pageSize, platform, status, search, isExclusive) + groups, total, err := h.adminService.ListGroups(c.Request.Context(), page, pageSize, platform, status, search, isExclusive, sortBy, sortOrder) if err != nil { response.ErrorFrom(c, err) return diff --git a/backend/internal/handler/admin/promo_handler.go b/backend/internal/handler/admin/promo_handler.go index 3eafa380..77d5f171 100644 --- a/backend/internal/handler/admin/promo_handler.go +++ b/backend/internal/handler/admin/promo_handler.go @@ -55,8 +55,10 @@ func (h *PromoHandler) List(c *gin.Context) { } params := pagination.PaginationParams{ - Page: page, - PageSize: pageSize, + Page: page, + PageSize: pageSize, + SortBy: c.DefaultQuery("sort_by", "created_at"), + SortOrder: c.DefaultQuery("sort_order", "desc"), } codes, paginationResult, err := h.promoService.List(c.Request.Context(), params, status, search) diff --git a/backend/internal/handler/admin/proxy_data.go b/backend/internal/handler/admin/proxy_data.go index 72ecd6c1..8149ce3b 100644 --- a/backend/internal/handler/admin/proxy_data.go +++ b/backend/internal/handler/admin/proxy_data.go @@ -33,11 +33,13 @@ func (h *ProxyHandler) ExportData(c *gin.Context) { protocol := c.Query("protocol") status := c.Query("status") search := strings.TrimSpace(c.Query("search")) + sortBy := c.DefaultQuery("sort_by", "id") + sortOrder := c.DefaultQuery("sort_order", "desc") if len(search) > 100 { search = search[:100] } - proxies, err = h.listProxiesFiltered(ctx, protocol, status, search) + proxies, err = h.listProxiesFiltered(ctx, protocol, status, search, sortBy, sortOrder) if err != nil { response.ErrorFrom(c, err) return @@ -89,7 +91,7 @@ func (h *ProxyHandler) ImportData(c *gin.Context) { ctx := c.Request.Context() result := DataImportResult{} - existingProxies, err := h.listProxiesFiltered(ctx, "", "", "") + existingProxies, err := h.listProxiesFiltered(ctx, "", "", "", "id", "desc") if err != nil { response.ErrorFrom(c, err) return @@ -220,18 +222,33 @@ func parseProxyIDs(c *gin.Context) ([]int64, error) { return ids, nil } -func (h *ProxyHandler) listProxiesFiltered(ctx context.Context, protocol, status, search string) ([]service.Proxy, error) { +func (h *ProxyHandler) listProxiesFiltered(ctx context.Context, protocol, status, search, sortBy, sortOrder string) ([]service.Proxy, error) { page := 1 pageSize := dataPageCap var out []service.Proxy + sortBy = strings.TrimSpace(sortBy) + useAccountCountSort := strings.EqualFold(sortBy, "account_count") for { - items, total, err := h.adminService.ListProxies(ctx, page, pageSize, protocol, status, search) - if err != nil { - return nil, err - } - out = append(out, items...) - if len(out) >= int(total) || len(items) == 0 { - break + if useAccountCountSort { + items, total, err := h.adminService.ListProxiesWithAccountCount(ctx, page, pageSize, protocol, status, search, sortBy, sortOrder) + if err != nil { + return nil, err + } + for i := range items { + out = append(out, items[i].Proxy) + } + if len(out) >= int(total) || len(items) == 0 { + break + } + } else { + items, total, err := h.adminService.ListProxies(ctx, page, pageSize, protocol, status, search, sortBy, sortOrder) + if err != nil { + return nil, err + } + out = append(out, items...) + if len(out) >= int(total) || len(items) == 0 { + break + } } page++ } diff --git a/backend/internal/handler/admin/proxy_data_handler_test.go b/backend/internal/handler/admin/proxy_data_handler_test.go index 803f9b61..8cd035ed 100644 --- a/backend/internal/handler/admin/proxy_data_handler_test.go +++ b/backend/internal/handler/admin/proxy_data_handler_test.go @@ -74,6 +74,10 @@ func TestProxyExportDataRespectsFilters(t *testing.T) { require.Len(t, resp.Data.Proxies, 1) require.Len(t, resp.Data.Accounts, 0) require.Equal(t, "https", resp.Data.Proxies[0].Protocol) + require.Equal(t, 1, adminSvc.lastListProxies.calls) + require.Equal(t, "https", adminSvc.lastListProxies.protocol) + require.Equal(t, "id", adminSvc.lastListProxies.sortBy) + require.Equal(t, "desc", adminSvc.lastListProxies.sortOrder) } func TestProxyExportDataWithSelectedIDs(t *testing.T) { @@ -113,6 +117,96 @@ func TestProxyExportDataWithSelectedIDs(t *testing.T) { require.Len(t, resp.Data.Proxies, 1) require.Equal(t, "https", resp.Data.Proxies[0].Protocol) require.Equal(t, "10.0.0.2", resp.Data.Proxies[0].Host) + require.Equal(t, 0, adminSvc.lastListProxies.calls) +} + +func TestProxyExportDataPassesSortParams(t *testing.T) { + router, adminSvc := setupProxyDataRouter() + + adminSvc.proxies = []service.Proxy{ + { + ID: 1, + Name: "proxy-a", + Protocol: "http", + Host: "127.0.0.1", + Port: 8080, + Username: "user", + Password: "pass", + Status: service.StatusActive, + }, + } + + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/proxies/data?protocol=http&status=active&search=proxy&sort_by=name&sort_order=asc", nil) + router.ServeHTTP(rec, req) + require.Equal(t, http.StatusOK, rec.Code) + + require.Equal(t, 1, adminSvc.lastListProxies.calls) + require.Equal(t, "http", adminSvc.lastListProxies.protocol) + require.Equal(t, "active", adminSvc.lastListProxies.status) + require.Equal(t, "proxy", adminSvc.lastListProxies.search) + require.Equal(t, "name", adminSvc.lastListProxies.sortBy) + require.Equal(t, "asc", adminSvc.lastListProxies.sortOrder) +} + +func TestProxyExportDataSortByAccountCountUsesAccountCountListing(t *testing.T) { + router, adminSvc := setupProxyDataRouter() + + adminSvc.proxies = []service.Proxy{ + { + ID: 1, + Name: "proxy-id-1", + Protocol: "http", + Host: "127.0.0.1", + Port: 8080, + Status: service.StatusActive, + }, + { + ID: 2, + Name: "proxy-id-2", + Protocol: "http", + Host: "127.0.0.2", + Port: 8081, + Status: service.StatusActive, + }, + } + adminSvc.proxyCounts = []service.ProxyWithAccountCount{ + { + Proxy: service.Proxy{ + ID: 2, + Name: "proxy-count-high", + Protocol: "http", + Host: "127.0.0.2", + Port: 8081, + Status: service.StatusActive, + }, + AccountCount: 9, + }, + { + Proxy: service.Proxy{ + ID: 1, + Name: "proxy-count-low", + Protocol: "http", + Host: "127.0.0.1", + Port: 8080, + Status: service.StatusActive, + }, + AccountCount: 1, + }, + } + + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/proxies/data?sort_by=account_count&sort_order=desc", nil) + router.ServeHTTP(rec, req) + require.Equal(t, http.StatusOK, rec.Code) + + var resp proxyDataResponse + require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp)) + require.Equal(t, 0, resp.Code) + require.Len(t, resp.Data.Proxies, 2) + require.Equal(t, "proxy-count-high", resp.Data.Proxies[0].Name) + require.Equal(t, "proxy-count-low", resp.Data.Proxies[1].Name) + require.Equal(t, 0, adminSvc.lastListProxies.calls) } func TestProxyImportDataReusesAndTriggersLatencyProbe(t *testing.T) { diff --git a/backend/internal/handler/admin/proxy_handler.go b/backend/internal/handler/admin/proxy_handler.go index e8ae0ce2..f97fcb0a 100644 --- a/backend/internal/handler/admin/proxy_handler.go +++ b/backend/internal/handler/admin/proxy_handler.go @@ -52,13 +52,15 @@ func (h *ProxyHandler) List(c *gin.Context) { protocol := c.Query("protocol") status := c.Query("status") search := c.Query("search") + sortBy := c.DefaultQuery("sort_by", "id") + sortOrder := c.DefaultQuery("sort_order", "desc") // 标准化和验证 search 参数 search = strings.TrimSpace(search) if len(search) > 100 { search = search[:100] } - proxies, total, err := h.adminService.ListProxiesWithAccountCount(c.Request.Context(), page, pageSize, protocol, status, search) + proxies, total, err := h.adminService.ListProxiesWithAccountCount(c.Request.Context(), page, pageSize, protocol, status, search, sortBy, sortOrder) if err != nil { response.ErrorFrom(c, err) return diff --git a/backend/internal/handler/admin/redeem_export_handler_test.go b/backend/internal/handler/admin/redeem_export_handler_test.go new file mode 100644 index 00000000..9983fe31 --- /dev/null +++ b/backend/internal/handler/admin/redeem_export_handler_test.go @@ -0,0 +1,49 @@ +package admin + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/require" +) + +func setupRedeemExportRouter() (*gin.Engine, *stubAdminService) { + gin.SetMode(gin.TestMode) + router := gin.New() + adminSvc := newStubAdminService() + + h := NewRedeemHandler(adminSvc, nil) + router.GET("/api/v1/admin/redeem-codes/export", h.Export) + return router, adminSvc +} + +func TestRedeemExportPassesSearchAndSort(t *testing.T) { + router, adminSvc := setupRedeemExportRouter() + + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/redeem-codes/export?type=balance&status=unused&search=ABC&sort_by=value&sort_order=asc", nil) + router.ServeHTTP(rec, req) + require.Equal(t, http.StatusOK, rec.Code) + + require.Equal(t, 1, adminSvc.lastListRedeemCodes.calls) + require.Equal(t, "balance", adminSvc.lastListRedeemCodes.codeType) + require.Equal(t, "unused", adminSvc.lastListRedeemCodes.status) + require.Equal(t, "ABC", adminSvc.lastListRedeemCodes.search) + require.Equal(t, "value", adminSvc.lastListRedeemCodes.sortBy) + require.Equal(t, "asc", adminSvc.lastListRedeemCodes.sortOrder) +} + +func TestRedeemExportSortDefaults(t *testing.T) { + router, adminSvc := setupRedeemExportRouter() + + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/redeem-codes/export", nil) + router.ServeHTTP(rec, req) + require.Equal(t, http.StatusOK, rec.Code) + + require.Equal(t, 1, adminSvc.lastListRedeemCodes.calls) + require.Equal(t, "id", adminSvc.lastListRedeemCodes.sortBy) + require.Equal(t, "desc", adminSvc.lastListRedeemCodes.sortOrder) +} diff --git a/backend/internal/handler/admin/redeem_handler.go b/backend/internal/handler/admin/redeem_handler.go index c494e5fb..24365f3d 100644 --- a/backend/internal/handler/admin/redeem_handler.go +++ b/backend/internal/handler/admin/redeem_handler.go @@ -59,13 +59,15 @@ func (h *RedeemHandler) List(c *gin.Context) { codeType := c.Query("type") status := c.Query("status") search := c.Query("search") + sortBy := c.DefaultQuery("sort_by", "id") + sortOrder := c.DefaultQuery("sort_order", "desc") // 标准化和验证 search 参数 search = strings.TrimSpace(search) if len(search) > 100 { search = search[:100] } - codes, total, err := h.adminService.ListRedeemCodes(c.Request.Context(), page, pageSize, codeType, status, search) + codes, total, err := h.adminService.ListRedeemCodes(c.Request.Context(), page, pageSize, codeType, status, search, sortBy, sortOrder) if err != nil { response.ErrorFrom(c, err) return @@ -300,9 +302,15 @@ func (h *RedeemHandler) GetStats(c *gin.Context) { func (h *RedeemHandler) Export(c *gin.Context) { codeType := c.Query("type") status := c.Query("status") + search := strings.TrimSpace(c.Query("search")) + sortBy := c.DefaultQuery("sort_by", "id") + sortOrder := c.DefaultQuery("sort_order", "desc") + if len(search) > 100 { + search = search[:100] + } // Get all codes without pagination (use large page size) - codes, _, err := h.adminService.ListRedeemCodes(c.Request.Context(), 1, 10000, codeType, status, "") + codes, _, err := h.adminService.ListRedeemCodes(c.Request.Context(), 1, 10000, codeType, status, search, sortBy, sortOrder) if err != nil { response.ErrorFrom(c, err) return diff --git a/backend/internal/handler/admin/setting_handler.go b/backend/internal/handler/admin/setting_handler.go index abae75d9..a43e84c4 100644 --- a/backend/internal/handler/admin/setting_handler.go +++ b/backend/internal/handler/admin/setting_handler.go @@ -137,6 +137,8 @@ func (h *SettingHandler) GetSettings(c *gin.Context) { HideCcsImportButton: settings.HideCcsImportButton, PurchaseSubscriptionEnabled: settings.PurchaseSubscriptionEnabled, PurchaseSubscriptionURL: settings.PurchaseSubscriptionURL, + TableDefaultPageSize: settings.TableDefaultPageSize, + TablePageSizeOptions: settings.TablePageSizeOptions, CustomMenuItems: dto.ParseCustomMenuItems(settings.CustomMenuItems), CustomEndpoints: dto.ParseCustomEndpoints(settings.CustomEndpoints), DefaultConcurrency: settings.DefaultConcurrency, @@ -230,6 +232,8 @@ type UpdateSettingsRequest struct { HideCcsImportButton bool `json:"hide_ccs_import_button"` PurchaseSubscriptionEnabled *bool `json:"purchase_subscription_enabled"` PurchaseSubscriptionURL *string `json:"purchase_subscription_url"` + TableDefaultPageSize int `json:"table_default_page_size"` + TablePageSizeOptions []int `json:"table_page_size_options"` CustomMenuItems *[]dto.CustomMenuItem `json:"custom_menu_items"` CustomEndpoints *[]dto.CustomEndpoint `json:"custom_endpoints"` @@ -292,6 +296,13 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) { if req.DefaultBalance < 0 { req.DefaultBalance = 0 } + // 通用表格配置:兼容旧客户端未传字段时保留当前值。 + if req.TableDefaultPageSize <= 0 { + req.TableDefaultPageSize = previousSettings.TableDefaultPageSize + } + if req.TablePageSizeOptions == nil { + req.TablePageSizeOptions = previousSettings.TablePageSizeOptions + } req.SMTPHost = strings.TrimSpace(req.SMTPHost) req.SMTPUsername = strings.TrimSpace(req.SMTPUsername) req.SMTPPassword = strings.TrimSpace(req.SMTPPassword) @@ -757,6 +768,8 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) { HideCcsImportButton: req.HideCcsImportButton, PurchaseSubscriptionEnabled: purchaseEnabled, PurchaseSubscriptionURL: purchaseURL, + TableDefaultPageSize: req.TableDefaultPageSize, + TablePageSizeOptions: req.TablePageSizeOptions, CustomMenuItems: customMenuJSON, CustomEndpoints: customEndpointsJSON, DefaultConcurrency: req.DefaultConcurrency, @@ -894,6 +907,8 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) { HideCcsImportButton: updatedSettings.HideCcsImportButton, PurchaseSubscriptionEnabled: updatedSettings.PurchaseSubscriptionEnabled, PurchaseSubscriptionURL: updatedSettings.PurchaseSubscriptionURL, + TableDefaultPageSize: updatedSettings.TableDefaultPageSize, + TablePageSizeOptions: updatedSettings.TablePageSizeOptions, CustomMenuItems: dto.ParseCustomMenuItems(updatedSettings.CustomMenuItems), CustomEndpoints: dto.ParseCustomEndpoints(updatedSettings.CustomEndpoints), DefaultConcurrency: updatedSettings.DefaultConcurrency, @@ -1152,6 +1167,12 @@ func diffSettings(before *service.SystemSettings, after *service.SystemSettings, if before.PurchaseSubscriptionURL != after.PurchaseSubscriptionURL { changed = append(changed, "purchase_subscription_url") } + if before.TableDefaultPageSize != after.TableDefaultPageSize { + changed = append(changed, "table_default_page_size") + } + if !equalIntSlice(before.TablePageSizeOptions, after.TablePageSizeOptions) { + changed = append(changed, "table_page_size_options") + } if before.CustomMenuItems != after.CustomMenuItems { changed = append(changed, "custom_menu_items") } @@ -1208,6 +1229,18 @@ func equalDefaultSubscriptions(a, b []service.DefaultSubscriptionSetting) bool { return true } +func equalIntSlice(a, b []int) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} + // TestSMTPRequest 测试SMTP连接请求 type TestSMTPRequest struct { SMTPHost string `json:"smtp_host"` diff --git a/backend/internal/handler/admin/usage_handler.go b/backend/internal/handler/admin/usage_handler.go index 2967b384..0857a138 100644 --- a/backend/internal/handler/admin/usage_handler.go +++ b/backend/internal/handler/admin/usage_handler.go @@ -165,7 +165,12 @@ func (h *UsageHandler) List(c *gin.Context) { endTime = &t } - params := pagination.PaginationParams{Page: page, PageSize: pageSize} + params := pagination.PaginationParams{ + Page: page, + PageSize: pageSize, + SortBy: c.DefaultQuery("sort_by", "created_at"), + SortOrder: c.DefaultQuery("sort_order", "desc"), + } filters := usagestats.UsageLogFilters{ UserID: userID, APIKeyID: apiKeyID, @@ -339,7 +344,7 @@ func (h *UsageHandler) SearchUsers(c *gin.Context) { } // Limit to 30 results - users, _, err := h.adminService.ListUsers(c.Request.Context(), 1, 30, service.UserListFilters{Search: keyword}) + users, _, err := h.adminService.ListUsers(c.Request.Context(), 1, 30, service.UserListFilters{Search: keyword}, "email", "asc") if err != nil { response.ErrorFrom(c, err) return diff --git a/backend/internal/handler/admin/usage_handler_request_type_test.go b/backend/internal/handler/admin/usage_handler_request_type_test.go index 3f158316..882cbe93 100644 --- a/backend/internal/handler/admin/usage_handler_request_type_test.go +++ b/backend/internal/handler/admin/usage_handler_request_type_test.go @@ -15,11 +15,13 @@ import ( type adminUsageRepoCapture struct { service.UsageLogRepository + listParams pagination.PaginationParams listFilters usagestats.UsageLogFilters statsFilters usagestats.UsageLogFilters } func (s *adminUsageRepoCapture) ListWithFilters(ctx context.Context, params pagination.PaginationParams, filters usagestats.UsageLogFilters) ([]service.UsageLog, *pagination.PaginationResult, error) { + s.listParams = params s.listFilters = filters return []service.UsageLog{}, &pagination.PaginationResult{ Total: 0, diff --git a/backend/internal/handler/admin/usage_handler_sort_test.go b/backend/internal/handler/admin/usage_handler_sort_test.go new file mode 100644 index 00000000..dac82676 --- /dev/null +++ b/backend/internal/handler/admin/usage_handler_sort_test.go @@ -0,0 +1,35 @@ +package admin + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestAdminUsageListSortParams(t *testing.T) { + repo := &adminUsageRepoCapture{} + router := newAdminUsageRequestTypeTestRouter(repo) + + req := httptest.NewRequest(http.MethodGet, "/admin/usage?sort_by=model&sort_order=ASC", nil) + rec := httptest.NewRecorder() + router.ServeHTTP(rec, req) + + require.Equal(t, http.StatusOK, rec.Code) + require.Equal(t, "model", repo.listParams.SortBy) + require.Equal(t, "ASC", repo.listParams.SortOrder) +} + +func TestAdminUsageListSortDefaults(t *testing.T) { + repo := &adminUsageRepoCapture{} + router := newAdminUsageRequestTypeTestRouter(repo) + + req := httptest.NewRequest(http.MethodGet, "/admin/usage", nil) + rec := httptest.NewRecorder() + router.ServeHTTP(rec, req) + + require.Equal(t, http.StatusOK, rec.Code) + require.Equal(t, "created_at", repo.listParams.SortBy) + require.Equal(t, "desc", repo.listParams.SortOrder) +} diff --git a/backend/internal/handler/admin/user_handler.go b/backend/internal/handler/admin/user_handler.go index a357657e..1453bd07 100644 --- a/backend/internal/handler/admin/user_handler.go +++ b/backend/internal/handler/admin/user_handler.go @@ -91,12 +91,14 @@ func (h *UserHandler) List(c *gin.Context) { GroupName: strings.TrimSpace(c.Query("group_name")), Attributes: parseAttributeFilters(c), } + sortBy := c.DefaultQuery("sort_by", "created_at") + sortOrder := c.DefaultQuery("sort_order", "desc") if raw, ok := c.GetQuery("include_subscriptions"); ok { includeSubscriptions := parseBoolQueryWithDefault(raw, true) filters.IncludeSubscriptions = &includeSubscriptions } - users, total, err := h.adminService.ListUsers(c.Request.Context(), page, pageSize, filters) + users, total, err := h.adminService.ListUsers(c.Request.Context(), page, pageSize, filters, sortBy, sortOrder) if err != nil { response.ErrorFrom(c, err) return @@ -290,8 +292,10 @@ func (h *UserHandler) GetUserAPIKeys(c *gin.Context) { } page, pageSize := response.ParsePagination(c) + sortBy := c.DefaultQuery("sort_by", "created_at") + sortOrder := c.DefaultQuery("sort_order", "desc") - keys, total, err := h.adminService.GetUserAPIKeys(c.Request.Context(), userID, page, pageSize) + keys, total, err := h.adminService.GetUserAPIKeys(c.Request.Context(), userID, page, pageSize, sortBy, sortOrder) if err != nil { response.ErrorFrom(c, err) return diff --git a/backend/internal/handler/api_key_handler.go b/backend/internal/handler/api_key_handler.go index 951aed08..9d6c6c15 100644 --- a/backend/internal/handler/api_key_handler.go +++ b/backend/internal/handler/api_key_handler.go @@ -72,7 +72,12 @@ func (h *APIKeyHandler) List(c *gin.Context) { } page, pageSize := response.ParsePagination(c) - params := pagination.PaginationParams{Page: page, PageSize: pageSize} + params := pagination.PaginationParams{ + Page: page, + PageSize: pageSize, + SortBy: c.DefaultQuery("sort_by", "created_at"), + SortOrder: c.DefaultQuery("sort_order", "desc"), + } // Parse filter parameters var filters service.APIKeyListFilters diff --git a/backend/internal/handler/dto/settings.go b/backend/internal/handler/dto/settings.go index c8fc3b5d..28f45fa9 100644 --- a/backend/internal/handler/dto/settings.go +++ b/backend/internal/handler/dto/settings.go @@ -84,6 +84,8 @@ type SystemSettings struct { HideCcsImportButton bool `json:"hide_ccs_import_button"` PurchaseSubscriptionEnabled bool `json:"purchase_subscription_enabled"` PurchaseSubscriptionURL string `json:"purchase_subscription_url"` + TableDefaultPageSize int `json:"table_default_page_size"` + TablePageSizeOptions []int `json:"table_page_size_options"` CustomMenuItems []CustomMenuItem `json:"custom_menu_items"` CustomEndpoints []CustomEndpoint `json:"custom_endpoints"` @@ -148,6 +150,8 @@ type PublicSettings struct { HideCcsImportButton bool `json:"hide_ccs_import_button"` PurchaseSubscriptionEnabled bool `json:"purchase_subscription_enabled"` PurchaseSubscriptionURL string `json:"purchase_subscription_url"` + TableDefaultPageSize int `json:"table_default_page_size"` + TablePageSizeOptions []int `json:"table_page_size_options"` CustomMenuItems []CustomMenuItem `json:"custom_menu_items"` CustomEndpoints []CustomEndpoint `json:"custom_endpoints"` LinuxDoOAuthEnabled bool `json:"linuxdo_oauth_enabled"` diff --git a/backend/internal/handler/setting_handler.go b/backend/internal/handler/setting_handler.go index 1db104c1..0f411ac7 100644 --- a/backend/internal/handler/setting_handler.go +++ b/backend/internal/handler/setting_handler.go @@ -51,6 +51,8 @@ func (h *SettingHandler) GetPublicSettings(c *gin.Context) { HideCcsImportButton: settings.HideCcsImportButton, PurchaseSubscriptionEnabled: settings.PurchaseSubscriptionEnabled, PurchaseSubscriptionURL: settings.PurchaseSubscriptionURL, + TableDefaultPageSize: settings.TableDefaultPageSize, + TablePageSizeOptions: settings.TablePageSizeOptions, CustomMenuItems: dto.ParseUserVisibleMenuItems(settings.CustomMenuItems), CustomEndpoints: dto.ParseCustomEndpoints(settings.CustomEndpoints), LinuxDoOAuthEnabled: settings.LinuxDoOAuthEnabled, diff --git a/backend/internal/handler/usage_handler.go b/backend/internal/handler/usage_handler.go index 483f5105..b8506154 100644 --- a/backend/internal/handler/usage_handler.go +++ b/backend/internal/handler/usage_handler.go @@ -119,7 +119,12 @@ func (h *UsageHandler) List(c *gin.Context) { endTime = &t } - params := pagination.PaginationParams{Page: page, PageSize: pageSize} + params := pagination.PaginationParams{ + Page: page, + PageSize: pageSize, + SortBy: c.DefaultQuery("sort_by", "created_at"), + SortOrder: c.DefaultQuery("sort_order", "desc"), + } filters := usagestats.UsageLogFilters{ UserID: subject.UserID, // Always filter by current user for security APIKeyID: apiKeyID, diff --git a/backend/internal/handler/usage_handler_request_type_test.go b/backend/internal/handler/usage_handler_request_type_test.go index 7c4c7913..b49ed59b 100644 --- a/backend/internal/handler/usage_handler_request_type_test.go +++ b/backend/internal/handler/usage_handler_request_type_test.go @@ -16,10 +16,12 @@ import ( type userUsageRepoCapture struct { service.UsageLogRepository + listParams pagination.PaginationParams listFilters usagestats.UsageLogFilters } func (s *userUsageRepoCapture) ListWithFilters(ctx context.Context, params pagination.PaginationParams, filters usagestats.UsageLogFilters) ([]service.UsageLog, *pagination.PaginationResult, error) { + s.listParams = params s.listFilters = filters return []service.UsageLog{}, &pagination.PaginationResult{ Total: 0, diff --git a/backend/internal/handler/usage_handler_sort_test.go b/backend/internal/handler/usage_handler_sort_test.go new file mode 100644 index 00000000..1af313b0 --- /dev/null +++ b/backend/internal/handler/usage_handler_sort_test.go @@ -0,0 +1,35 @@ +package handler + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestUserUsageListSortParams(t *testing.T) { + repo := &userUsageRepoCapture{} + router := newUserUsageRequestTypeTestRouter(repo) + + req := httptest.NewRequest(http.MethodGet, "/usage?sort_by=model&sort_order=ASC", nil) + rec := httptest.NewRecorder() + router.ServeHTTP(rec, req) + + require.Equal(t, http.StatusOK, rec.Code) + require.Equal(t, "model", repo.listParams.SortBy) + require.Equal(t, "ASC", repo.listParams.SortOrder) +} + +func TestUserUsageListSortDefaults(t *testing.T) { + repo := &userUsageRepoCapture{} + router := newUserUsageRequestTypeTestRouter(repo) + + req := httptest.NewRequest(http.MethodGet, "/usage", nil) + rec := httptest.NewRecorder() + router.ServeHTTP(rec, req) + + require.Equal(t, http.StatusOK, rec.Code) + require.Equal(t, "created_at", repo.listParams.SortBy) + require.Equal(t, "desc", repo.listParams.SortOrder) +} diff --git a/backend/internal/pkg/pagination/pagination.go b/backend/internal/pkg/pagination/pagination.go index c162588a..ce8e74b8 100644 --- a/backend/internal/pkg/pagination/pagination.go +++ b/backend/internal/pkg/pagination/pagination.go @@ -1,10 +1,19 @@ // Package pagination provides types and helpers for paginated responses. package pagination +import "strings" + +const ( + SortOrderAsc = "asc" + SortOrderDesc = "desc" +) + // PaginationParams 分页参数 type PaginationParams struct { - Page int - PageSize int + Page int + PageSize int + SortBy string + SortOrder string } // PaginationResult 分页结果 @@ -18,8 +27,9 @@ type PaginationResult struct { // DefaultPagination 默认分页参数 func DefaultPagination() PaginationParams { return PaginationParams{ - Page: 1, - PageSize: 20, + Page: 1, + PageSize: 20, + SortOrder: SortOrderDesc, } } @@ -36,8 +46,32 @@ func (p PaginationParams) Limit() int { if p.PageSize < 1 { return 20 } - if p.PageSize > 100 { - return 100 + if p.PageSize > 1000 { + return 1000 } return p.PageSize } + +// NormalizeSortOrder normalizes sort order to asc/desc and falls back to defaultOrder. +func NormalizeSortOrder(order string, defaultOrder string) string { + switch strings.ToLower(strings.TrimSpace(defaultOrder)) { + case SortOrderAsc: + defaultOrder = SortOrderAsc + default: + defaultOrder = SortOrderDesc + } + + switch strings.ToLower(strings.TrimSpace(order)) { + case SortOrderAsc: + return SortOrderAsc + case SortOrderDesc: + return SortOrderDesc + default: + return defaultOrder + } +} + +// NormalizedSortOrder returns the normalized sort order using defaultOrder as fallback. +func (p PaginationParams) NormalizedSortOrder(defaultOrder string) string { + return NormalizeSortOrder(p.SortOrder, defaultOrder) +} diff --git a/backend/internal/pkg/pagination/pagination_test.go b/backend/internal/pkg/pagination/pagination_test.go new file mode 100644 index 00000000..9a3b069d --- /dev/null +++ b/backend/internal/pkg/pagination/pagination_test.go @@ -0,0 +1,71 @@ +package pagination + +import "testing" + +func TestNormalizeSortOrder(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + input string + defaultOrder string + want string + }{ + {name: "asc", input: "asc", defaultOrder: "desc", want: "asc"}, + {name: "uppercase asc", input: "ASC", defaultOrder: "desc", want: "asc"}, + {name: "desc", input: "desc", defaultOrder: "asc", want: "desc"}, + {name: "trim spaces", input: " desc ", defaultOrder: "asc", want: "desc"}, + {name: "invalid falls back", input: "sideways", defaultOrder: "asc", want: "asc"}, + {name: "empty falls back", input: "", defaultOrder: "desc", want: "desc"}, + {name: "invalid default falls back to desc", input: "", defaultOrder: "wat", want: "desc"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + if got := NormalizeSortOrder(tt.input, tt.defaultOrder); got != tt.want { + t.Fatalf("NormalizeSortOrder(%q, %q) = %q, want %q", tt.input, tt.defaultOrder, got, tt.want) + } + }) + } +} + +func TestPaginationParamsNormalizedSortOrder(t *testing.T) { + t.Parallel() + + params := PaginationParams{SortOrder: "ASC"} + if got := params.NormalizedSortOrder("desc"); got != "asc" { + t.Fatalf("NormalizedSortOrder = %q, want asc", got) + } + + params = PaginationParams{SortOrder: "bad"} + if got := params.NormalizedSortOrder("asc"); got != "asc" { + t.Fatalf("NormalizedSortOrder invalid fallback = %q, want asc", got) + } +} + +func TestPaginationParamsLimit(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + pageSize int + want int + }{ + {name: "non-positive falls back to default", pageSize: 0, want: 20}, + {name: "negative falls back to default", pageSize: -1, want: 20}, + {name: "normal value keeps", pageSize: 50, want: 50}, + {name: "max value keeps", pageSize: 1000, want: 1000}, + {name: "beyond max clamps to 1000", pageSize: 1500, want: 1000}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + p := PaginationParams{PageSize: tt.pageSize} + if got := p.Limit(); got != tt.want { + t.Fatalf("Limit() for PageSize=%d = %d, want %d", tt.pageSize, got, tt.want) + } + }) + } +} diff --git a/backend/internal/repository/account_repo.go b/backend/internal/repository/account_repo.go index 14498715..24115c33 100644 --- a/backend/internal/repository/account_repo.go +++ b/backend/internal/repository/account_repo.go @@ -471,21 +471,58 @@ func (r *accountRepository) ListWithFilters(ctx context.Context, params paginati case service.StatusActive: q = q.Where( dbaccount.StatusEQ(status), + dbaccount.SchedulableEQ(true), dbaccount.Or( dbaccount.RateLimitResetAtIsNil(), dbaccount.RateLimitResetAtLTE(time.Now()), ), + dbpredicate.Account(func(s *entsql.Selector) { + col := s.C("temp_unschedulable_until") + s.Where(entsql.Or( + entsql.IsNull(col), + entsql.LTE(col, entsql.Expr("NOW()")), + )) + }), ) case "rate_limited": - q = q.Where(dbaccount.RateLimitResetAtGT(time.Now())) + q = q.Where( + dbaccount.StatusEQ(service.StatusActive), + dbaccount.RateLimitResetAtGT(time.Now()), + dbpredicate.Account(func(s *entsql.Selector) { + col := s.C("temp_unschedulable_until") + s.Where(entsql.Or( + entsql.IsNull(col), + entsql.LTE(col, entsql.Expr("NOW()")), + )) + }), + ) case "temp_unschedulable": - q = q.Where(dbpredicate.Account(func(s *entsql.Selector) { - col := s.C("temp_unschedulable_until") - s.Where(entsql.And( - entsql.Not(entsql.IsNull(col)), - entsql.GT(col, entsql.Expr("NOW()")), - )) - })) + q = q.Where( + dbaccount.StatusEQ(service.StatusActive), + dbpredicate.Account(func(s *entsql.Selector) { + col := s.C("temp_unschedulable_until") + s.Where(entsql.And( + entsql.Not(entsql.IsNull(col)), + entsql.GT(col, entsql.Expr("NOW()")), + )) + }), + ) + case "unschedulable": + q = q.Where( + dbaccount.StatusEQ(service.StatusActive), + dbaccount.SchedulableEQ(false), + dbaccount.Or( + dbaccount.RateLimitResetAtIsNil(), + dbaccount.RateLimitResetAtLTE(time.Now()), + ), + dbpredicate.Account(func(s *entsql.Selector) { + col := s.C("temp_unschedulable_until") + s.Where(entsql.Or( + entsql.IsNull(col), + entsql.LTE(col, entsql.Expr("NOW()")), + )) + }), + ) default: q = q.Where(dbaccount.StatusEQ(status)) } @@ -518,11 +555,14 @@ func (r *accountRepository) ListWithFilters(ctx context.Context, params paginati return nil, nil, err } - accounts, err := q. + accountsQuery := q. Offset(params.Offset()). - Limit(params.Limit()). - Order(dbent.Desc(dbaccount.FieldID)). - All(ctx) + Limit(params.Limit()) + for _, order := range accountListOrder(params) { + accountsQuery = accountsQuery.Order(order) + } + + accounts, err := accountsQuery.All(ctx) if err != nil { return nil, nil, err } @@ -534,6 +574,50 @@ func (r *accountRepository) ListWithFilters(ctx context.Context, params paginati return outAccounts, paginationResultFromTotal(int64(total), params), nil } +func accountListOrder(params pagination.PaginationParams) []func(*entsql.Selector) { + sortBy := strings.ToLower(strings.TrimSpace(params.SortBy)) + sortOrder := params.NormalizedSortOrder(pagination.SortOrderAsc) + + field := dbaccount.FieldName + defaultOrder := true + switch sortBy { + case "", "name": + field = dbaccount.FieldName + case "id": + field = dbaccount.FieldID + defaultOrder = false + case "status": + field = dbaccount.FieldStatus + defaultOrder = false + case "schedulable": + field = dbaccount.FieldSchedulable + defaultOrder = false + case "priority": + field = dbaccount.FieldPriority + defaultOrder = false + case "rate_multiplier": + field = dbaccount.FieldRateMultiplier + defaultOrder = false + case "last_used_at": + field = dbaccount.FieldLastUsedAt + defaultOrder = false + case "expires_at": + field = dbaccount.FieldExpiresAt + defaultOrder = false + case "created_at": + field = dbaccount.FieldCreatedAt + defaultOrder = false + } + + if sortOrder == pagination.SortOrderDesc { + return []func(*entsql.Selector){dbent.Desc(field), dbent.Desc(dbaccount.FieldID)} + } + if defaultOrder { + return []func(*entsql.Selector){dbent.Asc(dbaccount.FieldName), dbent.Asc(dbaccount.FieldID)} + } + return []func(*entsql.Selector){dbent.Asc(field), dbent.Asc(dbaccount.FieldID)} +} + func (r *accountRepository) ListByGroup(ctx context.Context, groupID int64) ([]service.Account, error) { accounts, err := r.queryAccountsByGroup(ctx, groupID, accountGroupQueryOptions{ status: service.StatusActive, diff --git a/backend/internal/repository/account_repo_integration_test.go b/backend/internal/repository/account_repo_integration_test.go index f3e3f745..b249bb61 100644 --- a/backend/internal/repository/account_repo_integration_test.go +++ b/backend/internal/repository/account_repo_integration_test.go @@ -256,7 +256,7 @@ func (s *AccountRepoSuite) TestListWithFilters() { }, }, { - name: "filter_by_status_active_excludes_rate_limited", + name: "filter_by_status_active_excludes_runtime_blocked_accounts", setup: func(client *dbent.Client) { mustCreateAccount(s.T(), client, &service.Account{Name: "active-normal", Status: service.StatusActive}) rateLimited := mustCreateAccount(s.T(), client, &service.Account{Name: "active-rate-limited", Status: service.StatusActive}) @@ -264,6 +264,16 @@ func (s *AccountRepoSuite) TestListWithFilters() { SetRateLimitResetAt(time.Now().Add(10 * time.Minute)). Exec(context.Background()) s.Require().NoError(err) + tempUnsched := mustCreateAccount(s.T(), client, &service.Account{Name: "active-temp-unsched", Status: service.StatusActive}) + err = client.Account.UpdateOneID(tempUnsched.ID). + SetTempUnschedulableUntil(time.Now().Add(15 * time.Minute)). + Exec(context.Background()) + s.Require().NoError(err) + unsched := mustCreateAccount(s.T(), client, &service.Account{Name: "active-unsched", Status: service.StatusActive}) + err = client.Account.UpdateOneID(unsched.ID). + SetSchedulable(false). + Exec(context.Background()) + s.Require().NoError(err) }, status: service.StatusActive, wantCount: 1, @@ -271,6 +281,75 @@ func (s *AccountRepoSuite) TestListWithFilters() { s.Require().Equal("active-normal", accounts[0].Name) }, }, + { + name: "filter_by_status_unschedulable_excludes_rate_limited_and_temp_unschedulable", + setup: func(client *dbent.Client) { + mustCreateAccount(s.T(), client, &service.Account{Name: "active-normal", Status: service.StatusActive, Schedulable: true}) + unsched := mustCreateAccount(s.T(), client, &service.Account{Name: "active-unsched", Status: service.StatusActive}) + err := client.Account.UpdateOneID(unsched.ID). + SetSchedulable(false). + Exec(context.Background()) + s.Require().NoError(err) + rateLimited := mustCreateAccount(s.T(), client, &service.Account{Name: "active-rate-limited", Status: service.StatusActive}) + err = client.Account.UpdateOneID(rateLimited.ID). + SetSchedulable(false). + SetRateLimitResetAt(time.Now().Add(10 * time.Minute)). + Exec(context.Background()) + s.Require().NoError(err) + tempUnsched := mustCreateAccount(s.T(), client, &service.Account{Name: "active-temp-unsched", Status: service.StatusActive}) + err = client.Account.UpdateOneID(tempUnsched.ID). + SetSchedulable(false). + SetTempUnschedulableUntil(time.Now().Add(15 * time.Minute)). + Exec(context.Background()) + s.Require().NoError(err) + }, + status: "unschedulable", + wantCount: 1, + validate: func(accounts []service.Account) { + s.Require().Equal("active-unsched", accounts[0].Name) + }, + }, + { + name: "filter_by_status_rate_limited_excludes_temp_unschedulable", + setup: func(client *dbent.Client) { + rateLimited := mustCreateAccount(s.T(), client, &service.Account{Name: "active-rate-limited", Status: service.StatusActive}) + err := client.Account.UpdateOneID(rateLimited.ID). + SetRateLimitResetAt(time.Now().Add(10 * time.Minute)). + Exec(context.Background()) + s.Require().NoError(err) + tempUnsched := mustCreateAccount(s.T(), client, &service.Account{Name: "active-temp-unsched", Status: service.StatusActive}) + err = client.Account.UpdateOneID(tempUnsched.ID). + SetRateLimitResetAt(time.Now().Add(20 * time.Minute)). + SetTempUnschedulableUntil(time.Now().Add(15 * time.Minute)). + Exec(context.Background()) + s.Require().NoError(err) + }, + status: "rate_limited", + wantCount: 1, + validate: func(accounts []service.Account) { + s.Require().Equal("active-rate-limited", accounts[0].Name) + }, + }, + { + name: "filter_by_status_temp_unschedulable_excludes_manually_unschedulable", + setup: func(client *dbent.Client) { + tempUnsched := mustCreateAccount(s.T(), client, &service.Account{Name: "active-temp-unsched", Status: service.StatusActive, Schedulable: true}) + err := client.Account.UpdateOneID(tempUnsched.ID). + SetTempUnschedulableUntil(time.Now().Add(15 * time.Minute)). + Exec(context.Background()) + s.Require().NoError(err) + unsched := mustCreateAccount(s.T(), client, &service.Account{Name: "active-unsched", Status: service.StatusActive}) + err = client.Account.UpdateOneID(unsched.ID). + SetSchedulable(false). + Exec(context.Background()) + s.Require().NoError(err) + }, + status: "temp_unschedulable", + wantCount: 1, + validate: func(accounts []service.Account) { + s.Require().Equal("active-temp-unsched", accounts[0].Name) + }, + }, { name: "filter_by_search", setup: func(client *dbent.Client) { diff --git a/backend/internal/repository/account_repo_sort_integration_test.go b/backend/internal/repository/account_repo_sort_integration_test.go new file mode 100644 index 00000000..098dde7b --- /dev/null +++ b/backend/internal/repository/account_repo_sort_integration_test.go @@ -0,0 +1,35 @@ +//go:build integration + +package repository + +import ( + "github.com/Wei-Shaw/sub2api/internal/pkg/pagination" + "github.com/Wei-Shaw/sub2api/internal/service" +) + +func (s *AccountRepoSuite) TestList_DefaultSortByNameAsc() { + mustCreateAccount(s.T(), s.client, &service.Account{Name: "z-account"}) + mustCreateAccount(s.T(), s.client, &service.Account{Name: "a-account"}) + + accounts, _, err := s.repo.List(s.ctx, pagination.PaginationParams{Page: 1, PageSize: 10}) + s.Require().NoError(err) + s.Require().Len(accounts, 2) + s.Require().Equal("a-account", accounts[0].Name) + s.Require().Equal("z-account", accounts[1].Name) +} + +func (s *AccountRepoSuite) TestListWithFilters_SortByPriorityDesc() { + mustCreateAccount(s.T(), s.client, &service.Account{Name: "low-priority", Priority: 10}) + mustCreateAccount(s.T(), s.client, &service.Account{Name: "high-priority", Priority: 90}) + + accounts, _, err := s.repo.ListWithFilters(s.ctx, pagination.PaginationParams{ + Page: 1, + PageSize: 10, + SortBy: "priority", + SortOrder: "desc", + }, "", "", "", "", 0, "") + s.Require().NoError(err) + s.Require().Len(accounts, 2) + s.Require().Equal("high-priority", accounts[0].Name) + s.Require().Equal("low-priority", accounts[1].Name) +} diff --git a/backend/internal/repository/announcement_repo.go b/backend/internal/repository/announcement_repo.go index 53dc335f..afe1fb25 100644 --- a/backend/internal/repository/announcement_repo.go +++ b/backend/internal/repository/announcement_repo.go @@ -2,12 +2,15 @@ package repository import ( "context" + "strings" "time" dbent "github.com/Wei-Shaw/sub2api/ent" "github.com/Wei-Shaw/sub2api/ent/announcement" "github.com/Wei-Shaw/sub2api/internal/pkg/pagination" "github.com/Wei-Shaw/sub2api/internal/service" + + entsql "entgo.io/ent/dialect/sql" ) type announcementRepository struct { @@ -128,11 +131,14 @@ func (r *announcementRepository) List( return nil, nil, err } - items, err := q. + itemsQuery := q. Offset(params.Offset()). - Limit(params.Limit()). - Order(dbent.Desc(announcement.FieldID)). - All(ctx) + Limit(params.Limit()) + for _, order := range announcementListOrders(params) { + itemsQuery = itemsQuery.Order(order) + } + + items, err := itemsQuery.All(ctx) if err != nil { return nil, nil, err } @@ -141,6 +147,56 @@ func (r *announcementRepository) List( return out, paginationResultFromTotal(int64(total), params), nil } +func announcementListOrder(params pagination.PaginationParams) (string, string) { + sortBy := strings.ToLower(strings.TrimSpace(params.SortBy)) + sortOrder := params.NormalizedSortOrder(pagination.SortOrderDesc) + + switch sortBy { + case "title": + return announcement.FieldTitle, sortOrder + case "status": + return announcement.FieldStatus, sortOrder + case "notify_mode": + return announcement.FieldNotifyMode, sortOrder + case "starts_at": + return announcement.FieldStartsAt, sortOrder + case "ends_at": + return announcement.FieldEndsAt, sortOrder + case "id": + return announcement.FieldID, sortOrder + case "", "created_at": + return announcement.FieldCreatedAt, sortOrder + default: + return announcement.FieldCreatedAt, pagination.SortOrderDesc + } +} + +func announcementListOrders(params pagination.PaginationParams) []func(*entsql.Selector) { + field, sortOrder := announcementListOrder(params) + + if sortOrder == pagination.SortOrderAsc { + if field == announcement.FieldID { + return []func(*entsql.Selector){ + dbent.Asc(field), + } + } + return []func(*entsql.Selector){ + dbent.Asc(field), + dbent.Asc(announcement.FieldID), + } + } + + if field == announcement.FieldID { + return []func(*entsql.Selector){ + dbent.Desc(field), + } + } + return []func(*entsql.Selector){ + dbent.Desc(field), + dbent.Desc(announcement.FieldID), + } +} + func (r *announcementRepository) ListActive(ctx context.Context, now time.Time) ([]service.Announcement, error) { q := r.client.Announcement.Query(). Where( diff --git a/backend/internal/repository/announcement_repo_sort_test.go b/backend/internal/repository/announcement_repo_sort_test.go new file mode 100644 index 00000000..e47f98dc --- /dev/null +++ b/backend/internal/repository/announcement_repo_sort_test.go @@ -0,0 +1,63 @@ +package repository + +import ( + "testing" + + "github.com/Wei-Shaw/sub2api/internal/pkg/pagination" +) + +func TestAnnouncementListOrder(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + params pagination.PaginationParams + wantBy string + want string + }{ + { + name: "default created_at desc", + params: pagination.PaginationParams{}, + wantBy: "created_at", + want: "desc", + }, + { + name: "title asc", + params: pagination.PaginationParams{ + SortBy: "title", + SortOrder: "ASC", + }, + wantBy: "title", + want: "asc", + }, + { + name: "status desc", + params: pagination.PaginationParams{ + SortBy: "status", + SortOrder: "desc", + }, + wantBy: "status", + want: "desc", + }, + { + name: "invalid falls back", + params: pagination.PaginationParams{ + SortBy: "sideways", + SortOrder: "wat", + }, + wantBy: "created_at", + want: "desc", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + gotBy, gotOrder := announcementListOrder(tt.params) + if gotBy != tt.wantBy || gotOrder != tt.want { + t.Fatalf("announcementListOrder(%+v) = (%q, %q), want (%q, %q)", tt.params, gotBy, gotOrder, tt.wantBy, tt.want) + } + }) + } +} diff --git a/backend/internal/repository/api_key_repo.go b/backend/internal/repository/api_key_repo.go index b3b12e81..7fd98855 100644 --- a/backend/internal/repository/api_key_repo.go +++ b/backend/internal/repository/api_key_repo.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "fmt" + "strings" "time" dbent "github.com/Wei-Shaw/sub2api/ent" @@ -14,6 +15,8 @@ import ( "github.com/Wei-Shaw/sub2api/internal/service" "github.com/Wei-Shaw/sub2api/internal/pkg/pagination" + + entsql "entgo.io/ent/dialect/sql" ) type apiKeyRepository struct { @@ -164,6 +167,7 @@ func (r *apiKeyRepository) GetByKeyForAuth(ctx context.Context, key string) (*se group.FieldSupportedModelScopes, group.FieldAllowMessagesDispatch, group.FieldDefaultMappedModel, + group.FieldMessagesDispatchModelConfig, ) }). Only(ctx) @@ -309,12 +313,15 @@ func (r *apiKeyRepository) ListByUserID(ctx context.Context, userID int64, param return nil, nil, err } - keys, err := q. + keysQuery := q. WithGroup(). Offset(params.Offset()). - Limit(params.Limit()). - Order(dbent.Desc(apikey.FieldID)). - All(ctx) + Limit(params.Limit()) + for _, order := range apiKeyListOrder(params) { + keysQuery = keysQuery.Order(order) + } + + keys, err := keysQuery.All(ctx) if err != nil { return nil, nil, err } @@ -359,12 +366,15 @@ func (r *apiKeyRepository) ListByGroupID(ctx context.Context, groupID int64, par return nil, nil, err } - keys, err := q. + keysQuery := q. WithUser(). Offset(params.Offset()). - Limit(params.Limit()). - Order(dbent.Desc(apikey.FieldID)). - All(ctx) + Limit(params.Limit()) + for _, order := range apiKeyListOrder(params) { + keysQuery = keysQuery.Order(order) + } + + keys, err := keysQuery.All(ctx) if err != nil { return nil, nil, err } @@ -377,6 +387,32 @@ func (r *apiKeyRepository) ListByGroupID(ctx context.Context, groupID int64, par return outKeys, paginationResultFromTotal(int64(total), params), nil } +func apiKeyListOrder(params pagination.PaginationParams) []func(*entsql.Selector) { + sortBy := strings.ToLower(strings.TrimSpace(params.SortBy)) + sortOrder := params.NormalizedSortOrder(pagination.SortOrderDesc) + + var field string + switch sortBy { + case "name": + field = apikey.FieldName + case "status": + field = apikey.FieldStatus + case "expires_at": + field = apikey.FieldExpiresAt + case "last_used_at": + field = apikey.FieldLastUsedAt + case "created_at": + field = apikey.FieldCreatedAt + default: + field = apikey.FieldID + } + + if sortOrder == pagination.SortOrderAsc { + return []func(*entsql.Selector){dbent.Asc(field), dbent.Asc(apikey.FieldID)} + } + return []func(*entsql.Selector){dbent.Desc(field), dbent.Desc(apikey.FieldID)} +} + // SearchAPIKeys searches API keys by user ID and/or keyword (name) func (r *apiKeyRepository) SearchAPIKeys(ctx context.Context, userID int64, keyword string, limit int) ([]service.APIKey, error) { q := r.activeQuery() @@ -654,6 +690,7 @@ func groupEntityToService(g *dbent.Group) *service.Group { RequireOAuthOnly: g.RequireOauthOnly, RequirePrivacySet: g.RequirePrivacySet, DefaultMappedModel: g.DefaultMappedModel, + MessagesDispatchModelConfig: g.MessagesDispatchModelConfig, CreatedAt: g.CreatedAt, UpdatedAt: g.UpdatedAt, } diff --git a/backend/internal/repository/api_key_repo_integration_test.go b/backend/internal/repository/api_key_repo_integration_test.go index 7d5c1826..e926ed86 100644 --- a/backend/internal/repository/api_key_repo_integration_test.go +++ b/backend/internal/repository/api_key_repo_integration_test.go @@ -86,6 +86,45 @@ func (s *APIKeyRepoSuite) TestGetByKey_NotFound() { s.Require().Error(err, "expected error for non-existent key") } +func (s *APIKeyRepoSuite) TestGetByKeyForAuth_PreservesMessagesDispatchModelConfig() { + user := s.mustCreateUser("getbykey-auth-dispatch@test.com") + group, err := s.client.Group.Create(). + SetName("g-auth-dispatch"). + SetPlatform(service.PlatformOpenAI). + SetStatus(service.StatusActive). + SetSubscriptionType(service.SubscriptionTypeStandard). + SetRateMultiplier(1). + SetAllowMessagesDispatch(true). + SetDefaultMappedModel("gpt-5.4"). + SetMessagesDispatchModelConfig(service.OpenAIMessagesDispatchModelConfig{ + OpusMappedModel: "gpt-5.4-nano", + SonnetMappedModel: "gpt-5.3-codex", + HaikuMappedModel: "gpt-5.4-mini", + ExactModelMappings: map[string]string{ + "claude-sonnet-4.5": "gpt-5.4-nano", + }, + }). + Save(s.ctx) + s.Require().NoError(err) + + key := &service.APIKey{ + UserID: user.ID, + Key: "sk-getbykey-auth-dispatch", + Name: "Dispatch Key", + GroupID: &group.ID, + Status: service.StatusActive, + } + s.Require().NoError(s.repo.Create(s.ctx, key)) + + got, err := s.repo.GetByKeyForAuth(s.ctx, key.Key) + s.Require().NoError(err) + s.Require().NotNil(got.Group) + s.Require().True(got.Group.AllowMessagesDispatch) + s.Require().Equal("gpt-5.4", got.Group.DefaultMappedModel) + s.Require().Equal("gpt-5.4-nano", got.Group.MessagesDispatchModelConfig.OpusMappedModel) + s.Require().Equal("gpt-5.4-nano", got.Group.MessagesDispatchModelConfig.ExactModelMappings["claude-sonnet-4.5"]) +} + // --- Update --- func (s *APIKeyRepoSuite) TestUpdate() { diff --git a/backend/internal/repository/api_key_repo_messages_dispatch_unit_test.go b/backend/internal/repository/api_key_repo_messages_dispatch_unit_test.go new file mode 100644 index 00000000..aba62ead --- /dev/null +++ b/backend/internal/repository/api_key_repo_messages_dispatch_unit_test.go @@ -0,0 +1,74 @@ +package repository + +import ( + "context" + "testing" + + dbent "github.com/Wei-Shaw/sub2api/ent" + "github.com/Wei-Shaw/sub2api/internal/service" + "github.com/stretchr/testify/require" +) + +func TestGroupEntityToService_PreservesMessagesDispatchModelConfig(t *testing.T) { + group := &dbent.Group{ + ID: 1, + Name: "openai-dispatch", + Platform: service.PlatformOpenAI, + Status: service.StatusActive, + SubscriptionType: service.SubscriptionTypeStandard, + RateMultiplier: 1, + AllowMessagesDispatch: true, + DefaultMappedModel: "gpt-5.4", + MessagesDispatchModelConfig: service.OpenAIMessagesDispatchModelConfig{ + OpusMappedModel: "gpt-5.4-nano", + SonnetMappedModel: "gpt-5.3-codex", + HaikuMappedModel: "gpt-5.4-mini", + ExactModelMappings: map[string]string{ + "claude-sonnet-4.5": "gpt-5.4-nano", + }, + }, + } + + got := groupEntityToService(group) + require.NotNil(t, got) + require.Equal(t, group.MessagesDispatchModelConfig, got.MessagesDispatchModelConfig) +} + +func TestAPIKeyRepository_GetByKeyForAuth_PreservesMessagesDispatchModelConfig_SQLite(t *testing.T) { + repo, client := newAPIKeyRepoSQLite(t) + ctx := context.Background() + user := mustCreateAPIKeyRepoUser(t, ctx, client, "getbykey-auth-dispatch-unit@test.com") + + group, err := client.Group.Create(). + SetName("g-auth-dispatch-unit"). + SetPlatform(service.PlatformOpenAI). + SetStatus(service.StatusActive). + SetSubscriptionType(service.SubscriptionTypeStandard). + SetRateMultiplier(1). + SetAllowMessagesDispatch(true). + SetDefaultMappedModel("gpt-5.4"). + SetMessagesDispatchModelConfig(service.OpenAIMessagesDispatchModelConfig{ + OpusMappedModel: "gpt-5.4-nano", + SonnetMappedModel: "gpt-5.3-codex", + HaikuMappedModel: "gpt-5.4-mini", + ExactModelMappings: map[string]string{ + "claude-sonnet-4.5": "gpt-5.4-nano", + }, + }). + Save(ctx) + require.NoError(t, err) + + key := &service.APIKey{ + UserID: user.ID, + Key: "sk-getbykey-auth-dispatch-unit", + Name: "Dispatch Key Unit", + GroupID: &group.ID, + Status: service.StatusActive, + } + require.NoError(t, repo.Create(ctx, key)) + + got, err := repo.GetByKeyForAuth(ctx, key.Key) + require.NoError(t, err) + require.NotNil(t, got.Group) + require.Equal(t, group.MessagesDispatchModelConfig, got.Group.MessagesDispatchModelConfig) +} diff --git a/backend/internal/repository/api_key_repo_sort_integration_test.go b/backend/internal/repository/api_key_repo_sort_integration_test.go new file mode 100644 index 00000000..69812882 --- /dev/null +++ b/backend/internal/repository/api_key_repo_sort_integration_test.go @@ -0,0 +1,25 @@ +//go:build integration + +package repository + +import ( + "github.com/Wei-Shaw/sub2api/internal/pkg/pagination" + "github.com/Wei-Shaw/sub2api/internal/service" +) + +func (s *APIKeyRepoSuite) TestListByUserID_SortByNameAsc() { + user := s.mustCreateUser("sort-name@example.com") + s.mustCreateApiKey(user.ID, "sk-z", "z-key", nil) + s.mustCreateApiKey(user.ID, "sk-a", "a-key", nil) + + keys, _, err := s.repo.ListByUserID(s.ctx, user.ID, pagination.PaginationParams{ + Page: 1, + PageSize: 10, + SortBy: "name", + SortOrder: "asc", + }, service.APIKeyListFilters{}) + s.Require().NoError(err) + s.Require().Len(keys, 2) + s.Require().Equal("a-key", keys[0].Name) + s.Require().Equal("z-key", keys[1].Name) +} diff --git a/backend/internal/repository/channel_repo.go b/backend/internal/repository/channel_repo.go index 1e2c2e4c..49c2d8d9 100644 --- a/backend/internal/repository/channel_repo.go +++ b/backend/internal/repository/channel_repo.go @@ -188,8 +188,8 @@ func (r *channelRepository) List(ctx context.Context, params pagination.Paginati // 查询 channel 列表 dataQuery := fmt.Sprintf( `SELECT c.id, c.name, c.description, c.status, c.model_mapping, c.billing_model_source, c.restrict_models, c.created_at, c.updated_at - FROM channels c WHERE %s ORDER BY c.id ASC LIMIT $%d OFFSET $%d`, - whereClause, argIdx, argIdx+1, + FROM channels c WHERE %s ORDER BY %s LIMIT $%d OFFSET $%d`, + whereClause, channelListOrderBy(params), argIdx, argIdx+1, ) args = append(args, pageSize, offset) @@ -246,6 +246,31 @@ func (r *channelRepository) List(ctx context.Context, params pagination.Paginati return channels, paginationResult, nil } +func channelListOrderBy(params pagination.PaginationParams) string { + sortBy := strings.ToLower(strings.TrimSpace(params.SortBy)) + sortOrder := strings.ToUpper(params.NormalizedSortOrder(pagination.SortOrderAsc)) + + var column string + switch sortBy { + case "": + column = "c.id" + sortOrder = "ASC" + case "id": + column = "c.id" + case "name": + column = "c.name" + case "status": + column = "c.status" + case "created_at": + column = "c.created_at" + default: + column = "c.id" + sortOrder = "ASC" + } + + return fmt.Sprintf("%s %s, c.id %s", column, sortOrder, sortOrder) +} + func (r *channelRepository) ListAll(ctx context.Context) ([]service.Channel, error) { rows, err := r.db.QueryContext(ctx, `SELECT id, name, description, status, model_mapping, billing_model_source, restrict_models, created_at, updated_at FROM channels ORDER BY id`, diff --git a/backend/internal/repository/channel_repo_test.go b/backend/internal/repository/channel_repo_test.go index 5a59948d..e761866d 100644 --- a/backend/internal/repository/channel_repo_test.go +++ b/backend/internal/repository/channel_repo_test.go @@ -8,6 +8,7 @@ import ( "fmt" "testing" + "github.com/Wei-Shaw/sub2api/internal/pkg/pagination" "github.com/lib/pq" "github.com/stretchr/testify/require" ) @@ -225,3 +226,12 @@ func TestIsUniqueViolation(t *testing.T) { }) } } + +func TestChannelListOrderBy_AllowsDescendingIDSort(t *testing.T) { + params := pagination.PaginationParams{ + SortBy: "id", + SortOrder: "desc", + } + + require.Equal(t, "c.id DESC, c.id DESC", channelListOrderBy(params)) +} diff --git a/backend/internal/repository/group_repo.go b/backend/internal/repository/group_repo.go index 1803cf30..c17e3365 100644 --- a/backend/internal/repository/group_repo.go +++ b/backend/internal/repository/group_repo.go @@ -5,6 +5,7 @@ import ( "database/sql" "errors" "fmt" + "sort" "strings" dbent "github.com/Wei-Shaw/sub2api/ent" @@ -14,6 +15,8 @@ import ( "github.com/Wei-Shaw/sub2api/internal/pkg/pagination" "github.com/Wei-Shaw/sub2api/internal/service" "github.com/lib/pq" + + entsql "entgo.io/ent/dialect/sql" ) type sqlExecutor interface { @@ -40,6 +43,7 @@ func (r *groupRepository) Create(ctx context.Context, groupIn *service.Group) er SetDescription(groupIn.Description). SetPlatform(groupIn.Platform). SetRateMultiplier(groupIn.RateMultiplier). + SetSortOrder(groupIn.SortOrder). SetIsExclusive(groupIn.IsExclusive). SetStatus(groupIn.Status). SetSubscriptionType(groupIn.SubscriptionType). @@ -233,11 +237,18 @@ func (r *groupRepository) ListWithFilters(ctx context.Context, params pagination return nil, nil, err } - groups, err := q. + if strings.EqualFold(strings.TrimSpace(params.SortBy), "account_count") { + return r.listWithAccountCountSort(ctx, q, params, total) + } + + groupsQuery := q. Offset(params.Offset()). - Limit(params.Limit()). - Order(dbent.Asc(group.FieldSortOrder), dbent.Asc(group.FieldID)). - All(ctx) + Limit(params.Limit()) + for _, order := range groupListOrder(params) { + groupsQuery = groupsQuery.Order(order) + } + + groups, err := groupsQuery.All(ctx) if err != nil { return nil, nil, err } @@ -263,6 +274,104 @@ func (r *groupRepository) ListWithFilters(ctx context.Context, params pagination return outGroups, paginationResultFromTotal(int64(total), params), nil } +func (r *groupRepository) listWithAccountCountSort(ctx context.Context, q *dbent.GroupQuery, params pagination.PaginationParams, total int) ([]service.Group, *pagination.PaginationResult, error) { + groups, err := q. + Order(dbent.Asc(group.FieldSortOrder), dbent.Asc(group.FieldID)). + All(ctx) + if err != nil { + return nil, nil, err + } + + groupIDs := make([]int64, 0, len(groups)) + outGroups := make([]service.Group, 0, len(groups)) + for i := range groups { + g := groupEntityToService(groups[i]) + outGroups = append(outGroups, *g) + groupIDs = append(groupIDs, g.ID) + } + + counts, err := r.loadAccountCounts(ctx, groupIDs) + if err != nil { + return nil, nil, err + } + for i := range outGroups { + c := counts[outGroups[i].ID] + outGroups[i].AccountCount = c.Total + outGroups[i].ActiveAccountCount = c.Active + outGroups[i].RateLimitedAccountCount = c.RateLimited + } + + sortOrder := params.NormalizedSortOrder(pagination.SortOrderDesc) + sort.SliceStable(outGroups, func(i, j int) bool { + if outGroups[i].AccountCount == outGroups[j].AccountCount { + if outGroups[i].SortOrder == outGroups[j].SortOrder { + return outGroups[i].ID < outGroups[j].ID + } + return outGroups[i].SortOrder < outGroups[j].SortOrder + } + if sortOrder == pagination.SortOrderAsc { + return outGroups[i].AccountCount < outGroups[j].AccountCount + } + return outGroups[i].AccountCount > outGroups[j].AccountCount + }) + + return paginateSlice(outGroups, params), paginationResultFromTotal(int64(total), params), nil +} + +func groupListOrder(params pagination.PaginationParams) []func(*entsql.Selector) { + sortBy := strings.ToLower(strings.TrimSpace(params.SortBy)) + sortOrder := params.NormalizedSortOrder(pagination.SortOrderAsc) + + var field string + tieField := group.FieldID + defaultOrder := true + switch sortBy { + case "", "sort_order": + field = group.FieldSortOrder + case "name": + field = group.FieldName + defaultOrder = false + case "platform": + field = group.FieldPlatform + defaultOrder = false + case "billing_type", "subscription_type": + field = group.FieldSubscriptionType + defaultOrder = false + case "rate_multiplier": + field = group.FieldRateMultiplier + defaultOrder = false + case "is_exclusive": + field = group.FieldIsExclusive + defaultOrder = false + case "status": + field = group.FieldStatus + defaultOrder = false + case "created_at": + field = group.FieldCreatedAt + defaultOrder = false + case "id": + field = group.FieldID + defaultOrder = false + tieField = "" + default: + field = group.FieldSortOrder + } + + if sortOrder == pagination.SortOrderDesc && sortBy != "" { + if tieField == "" { + return []func(*entsql.Selector){dbent.Desc(field)} + } + return []func(*entsql.Selector){dbent.Desc(field), dbent.Desc(tieField)} + } + if defaultOrder { + return []func(*entsql.Selector){dbent.Asc(group.FieldSortOrder), dbent.Asc(group.FieldID)} + } + if tieField == "" { + return []func(*entsql.Selector){dbent.Asc(field)} + } + return []func(*entsql.Selector){dbent.Asc(field), dbent.Asc(tieField)} +} + func (r *groupRepository) ListActive(ctx context.Context) ([]service.Group, error) { groups, err := r.client.Group.Query(). Where(group.StatusEQ(service.StatusActive)). diff --git a/backend/internal/repository/group_repo_integration_test.go b/backend/internal/repository/group_repo_integration_test.go index eccf5cea..f91dae43 100644 --- a/backend/internal/repository/group_repo_integration_test.go +++ b/backend/internal/repository/group_repo_integration_test.go @@ -113,6 +113,33 @@ func (s *GroupRepoSuite) TestUpdate() { s.Require().Equal("updated", got.Name) } +func (s *GroupRepoSuite) TestGetByID_PreservesMessagesDispatchModelConfig() { + group := &service.Group{ + Name: "openai-dispatch", + Platform: service.PlatformOpenAI, + RateMultiplier: 1.0, + IsExclusive: false, + Status: service.StatusActive, + SubscriptionType: service.SubscriptionTypeStandard, + AllowMessagesDispatch: true, + DefaultMappedModel: "gpt-5.4", + MessagesDispatchModelConfig: service.OpenAIMessagesDispatchModelConfig{ + OpusMappedModel: "gpt-5.4", + SonnetMappedModel: "gpt-5.3-codex", + HaikuMappedModel: "gpt-5.4-mini", + ExactModelMappings: map[string]string{ + "claude-sonnet-4.5": "gpt-5.4-nano", + }, + }, + } + + s.Require().NoError(s.repo.Create(s.ctx, group)) + + got, err := s.repo.GetByID(s.ctx, group.ID) + s.Require().NoError(err) + s.Require().Equal(group.MessagesDispatchModelConfig, got.MessagesDispatchModelConfig) +} + func (s *GroupRepoSuite) TestDelete() { group := &service.Group{ Name: "to-delete", diff --git a/backend/internal/repository/group_repo_sort_integration_test.go b/backend/internal/repository/group_repo_sort_integration_test.go new file mode 100644 index 00000000..85b2efcc --- /dev/null +++ b/backend/internal/repository/group_repo_sort_integration_test.go @@ -0,0 +1,50 @@ +//go:build integration + +package repository + +import ( + "github.com/Wei-Shaw/sub2api/internal/pkg/pagination" + "github.com/Wei-Shaw/sub2api/internal/service" +) + +func (s *GroupRepoSuite) TestList_DefaultSortBySortOrderAsc() { + g1 := &service.Group{Name: "g1", Platform: service.PlatformAnthropic, RateMultiplier: 1, Status: service.StatusActive, SubscriptionType: service.SubscriptionTypeStandard, SortOrder: 20} + g2 := &service.Group{Name: "g2", Platform: service.PlatformAnthropic, RateMultiplier: 1, Status: service.StatusActive, SubscriptionType: service.SubscriptionTypeStandard, SortOrder: 10} + s.Require().NoError(s.repo.Create(s.ctx, g1)) + s.Require().NoError(s.repo.Create(s.ctx, g2)) + + groups, _, err := s.repo.List(s.ctx, pagination.PaginationParams{Page: 1, PageSize: 100}) + s.Require().NoError(err) + s.Require().GreaterOrEqual(len(groups), 2) + indexByID := make(map[int64]int, len(groups)) + for i, g := range groups { + indexByID[g.ID] = i + } + s.Require().Contains(indexByID, g1.ID) + s.Require().Contains(indexByID, g2.ID) + // g2 has SortOrder=10, g1 has SortOrder=20; ascending means g2 comes first + s.Require().Less(indexByID[g2.ID], indexByID[g1.ID]) +} + +func (s *GroupRepoSuite) TestList_SortBySortOrderDesc() { + g1 := &service.Group{Name: "g1", Platform: service.PlatformAnthropic, RateMultiplier: 1, Status: service.StatusActive, SubscriptionType: service.SubscriptionTypeStandard, SortOrder: 40} + g2 := &service.Group{Name: "g2", Platform: service.PlatformAnthropic, RateMultiplier: 1, Status: service.StatusActive, SubscriptionType: service.SubscriptionTypeStandard, SortOrder: 50} + s.Require().NoError(s.repo.Create(s.ctx, g1)) + s.Require().NoError(s.repo.Create(s.ctx, g2)) + + groups, _, err := s.repo.List(s.ctx, pagination.PaginationParams{ + Page: 1, + PageSize: 10, + SortBy: "sort_order", + SortOrder: "desc", + }) + s.Require().NoError(err) + s.Require().GreaterOrEqual(len(groups), 2) + indexByID := make(map[int64]int, len(groups)) + for i, group := range groups { + indexByID[group.ID] = i + } + s.Require().Contains(indexByID, g1.ID) + s.Require().Contains(indexByID, g2.ID) + s.Require().Less(indexByID[g2.ID], indexByID[g1.ID]) +} diff --git a/backend/internal/repository/pagination.go b/backend/internal/repository/pagination.go index ff08c34b..87c42a59 100644 --- a/backend/internal/repository/pagination.go +++ b/backend/internal/repository/pagination.go @@ -14,3 +14,22 @@ func paginationResultFromTotal(total int64, params pagination.PaginationParams) Pages: pages, } } + +func paginateSlice[T any](items []T, params pagination.PaginationParams) []T { + if len(items) == 0 { + return []T{} + } + + offset := params.Offset() + if offset >= len(items) { + return []T{} + } + + limit := params.Limit() + end := offset + limit + if end > len(items) { + end = len(items) + } + + return items[offset:end] +} diff --git a/backend/internal/repository/promo_code_repo.go b/backend/internal/repository/promo_code_repo.go index 95ce687a..d9c76bb3 100644 --- a/backend/internal/repository/promo_code_repo.go +++ b/backend/internal/repository/promo_code_repo.go @@ -2,12 +2,15 @@ package repository import ( "context" + "strings" dbent "github.com/Wei-Shaw/sub2api/ent" "github.com/Wei-Shaw/sub2api/ent/promocode" "github.com/Wei-Shaw/sub2api/ent/promocodeusage" "github.com/Wei-Shaw/sub2api/internal/pkg/pagination" "github.com/Wei-Shaw/sub2api/internal/service" + + entsql "entgo.io/ent/dialect/sql" ) type promoCodeRepository struct { @@ -137,11 +140,14 @@ func (r *promoCodeRepository) ListWithFilters(ctx context.Context, params pagina return nil, nil, err } - codes, err := q. + codesQuery := q. Offset(params.Offset()). - Limit(params.Limit()). - Order(dbent.Desc(promocode.FieldID)). - All(ctx) + Limit(params.Limit()) + for _, order := range promoCodeListOrder(params) { + codesQuery = codesQuery.Order(order) + } + + codes, err := codesQuery.All(ctx) if err != nil { return nil, nil, err } @@ -151,6 +157,32 @@ func (r *promoCodeRepository) ListWithFilters(ctx context.Context, params pagina return outCodes, paginationResultFromTotal(int64(total), params), nil } +func promoCodeListOrder(params pagination.PaginationParams) []func(*entsql.Selector) { + sortBy := strings.ToLower(strings.TrimSpace(params.SortBy)) + sortOrder := params.NormalizedSortOrder(pagination.SortOrderDesc) + + var field string + switch sortBy { + case "bonus_amount": + field = promocode.FieldBonusAmount + case "status": + field = promocode.FieldStatus + case "expires_at": + field = promocode.FieldExpiresAt + case "created_at": + field = promocode.FieldCreatedAt + case "code": + field = promocode.FieldCode + default: + field = promocode.FieldID + } + + if sortOrder == pagination.SortOrderAsc { + return []func(*entsql.Selector){dbent.Asc(field), dbent.Asc(promocode.FieldID)} + } + return []func(*entsql.Selector){dbent.Desc(field), dbent.Desc(promocode.FieldID)} +} + func (r *promoCodeRepository) CreateUsage(ctx context.Context, usage *service.PromoCodeUsage) error { client := clientFromContext(ctx, r.client) created, err := client.PromoCodeUsage.Create(). diff --git a/backend/internal/repository/proxy_repo.go b/backend/internal/repository/proxy_repo.go index 07c2a204..60b2f069 100644 --- a/backend/internal/repository/proxy_repo.go +++ b/backend/internal/repository/proxy_repo.go @@ -3,12 +3,16 @@ package repository import ( "context" "database/sql" + "sort" + "strings" dbent "github.com/Wei-Shaw/sub2api/ent" "github.com/Wei-Shaw/sub2api/ent/proxy" "github.com/Wei-Shaw/sub2api/internal/service" "github.com/Wei-Shaw/sub2api/internal/pkg/pagination" + + entsql "entgo.io/ent/dialect/sql" ) type sqlQuerier interface { @@ -135,11 +139,14 @@ func (r *proxyRepository) ListWithFilters(ctx context.Context, params pagination return nil, nil, err } - proxies, err := q. + proxiesQuery := q. Offset(params.Offset()). - Limit(params.Limit()). - Order(dbent.Desc(proxy.FieldID)). - All(ctx) + Limit(params.Limit()) + for _, order := range proxyListOrder(params) { + proxiesQuery = proxiesQuery.Order(order) + } + + proxies, err := proxiesQuery.All(ctx) if err != nil { return nil, nil, err } @@ -170,22 +177,58 @@ func (r *proxyRepository) ListWithFiltersAndAccountCount(ctx context.Context, pa return nil, nil, err } - proxies, err := q. + if strings.EqualFold(strings.TrimSpace(params.SortBy), "account_count") { + return r.listWithAccountCountSort(ctx, q, params, total) + } + + proxiesQuery := q. Offset(params.Offset()). - Limit(params.Limit()). + Limit(params.Limit()) + for _, order := range proxyListOrder(params) { + proxiesQuery = proxiesQuery.Order(order) + } + + proxies, err := proxiesQuery.All(ctx) + if err != nil { + return nil, nil, err + } + + return r.buildProxyWithAccountCountResult(ctx, proxies, params, int64(total)) +} + +func (r *proxyRepository) listWithAccountCountSort(ctx context.Context, q *dbent.ProxyQuery, params pagination.PaginationParams, total int) ([]service.ProxyWithAccountCount, *pagination.PaginationResult, error) { + proxies, err := q. Order(dbent.Desc(proxy.FieldID)). All(ctx) if err != nil { return nil, nil, err } - // Get account counts + result, _, err := r.buildProxyWithAccountCountResult(ctx, proxies, params, int64(total)) + if err != nil { + return nil, nil, err + } + + sortOrder := params.NormalizedSortOrder(pagination.SortOrderDesc) + sort.SliceStable(result, func(i, j int) bool { + if result[i].AccountCount == result[j].AccountCount { + return result[i].ID > result[j].ID + } + if sortOrder == pagination.SortOrderAsc { + return result[i].AccountCount < result[j].AccountCount + } + return result[i].AccountCount > result[j].AccountCount + }) + + return paginateSlice(result, params), paginationResultFromTotal(int64(total), params), nil +} + +func (r *proxyRepository) buildProxyWithAccountCountResult(ctx context.Context, proxies []*dbent.Proxy, params pagination.PaginationParams, total int64) ([]service.ProxyWithAccountCount, *pagination.PaginationResult, error) { counts, err := r.GetAccountCountsForProxies(ctx) if err != nil { return nil, nil, err } - // Build result with account counts result := make([]service.ProxyWithAccountCount, 0, len(proxies)) for i := range proxies { proxyOut := proxyEntityToService(proxies[i]) @@ -198,7 +241,31 @@ func (r *proxyRepository) ListWithFiltersAndAccountCount(ctx context.Context, pa }) } - return result, paginationResultFromTotal(int64(total), params), nil + return result, paginationResultFromTotal(total, params), nil +} + +func proxyListOrder(params pagination.PaginationParams) []func(*entsql.Selector) { + sortBy := strings.ToLower(strings.TrimSpace(params.SortBy)) + sortOrder := params.NormalizedSortOrder(pagination.SortOrderDesc) + + var field string + switch sortBy { + case "name": + field = proxy.FieldName + case "protocol": + field = proxy.FieldProtocol + case "status": + field = proxy.FieldStatus + case "created_at": + field = proxy.FieldCreatedAt + default: + field = proxy.FieldID + } + + if sortOrder == pagination.SortOrderAsc { + return []func(*entsql.Selector){dbent.Asc(field), dbent.Asc(proxy.FieldID)} + } + return []func(*entsql.Selector){dbent.Desc(field), dbent.Desc(proxy.FieldID)} } func (r *proxyRepository) ListActive(ctx context.Context) ([]service.Proxy, error) { diff --git a/backend/internal/repository/proxy_repo_sort_integration_test.go b/backend/internal/repository/proxy_repo_sort_integration_test.go new file mode 100644 index 00000000..fe1c2873 --- /dev/null +++ b/backend/internal/repository/proxy_repo_sort_integration_test.go @@ -0,0 +1,28 @@ +//go:build integration + +package repository + +import ( + "github.com/Wei-Shaw/sub2api/internal/pkg/pagination" + "github.com/Wei-Shaw/sub2api/internal/service" +) + +func (s *ProxyRepoSuite) TestListWithFiltersAndAccountCount_SortByAccountCountDesc() { + p1 := s.mustCreateProxy(&service.Proxy{Name: "p1", Protocol: "http", Host: "127.0.0.1", Port: 8080, Status: service.StatusActive}) + p2 := s.mustCreateProxy(&service.Proxy{Name: "p2", Protocol: "http", Host: "127.0.0.1", Port: 8081, Status: service.StatusActive}) + s.mustInsertAccount("a1", &p1.ID) + s.mustInsertAccount("a2", &p1.ID) + s.mustInsertAccount("a3", &p2.ID) + + proxies, _, err := s.repo.ListWithFiltersAndAccountCount(s.ctx, pagination.PaginationParams{ + Page: 1, + PageSize: 10, + SortBy: "account_count", + SortOrder: "desc", + }, "", "", "") + s.Require().NoError(err) + s.Require().Len(proxies, 2) + s.Require().Equal(p1.ID, proxies[0].ID) + s.Require().Equal(int64(2), proxies[0].AccountCount) + s.Require().Equal(p2.ID, proxies[1].ID) +} diff --git a/backend/internal/repository/redeem_code_repo.go b/backend/internal/repository/redeem_code_repo.go index 934a3095..07975970 100644 --- a/backend/internal/repository/redeem_code_repo.go +++ b/backend/internal/repository/redeem_code_repo.go @@ -2,6 +2,7 @@ package repository import ( "context" + "strings" "time" dbent "github.com/Wei-Shaw/sub2api/ent" @@ -9,6 +10,8 @@ import ( "github.com/Wei-Shaw/sub2api/ent/user" "github.com/Wei-Shaw/sub2api/internal/pkg/pagination" "github.com/Wei-Shaw/sub2api/internal/service" + + entsql "entgo.io/ent/dialect/sql" ) type redeemCodeRepository struct { @@ -120,13 +123,16 @@ func (r *redeemCodeRepository) ListWithFilters(ctx context.Context, params pagin return nil, nil, err } - codes, err := q. + codesQuery := q. WithUser(). WithGroup(). Offset(params.Offset()). - Limit(params.Limit()). - Order(dbent.Desc(redeemcode.FieldID)). - All(ctx) + Limit(params.Limit()) + for _, order := range redeemCodeListOrder(params) { + codesQuery = codesQuery.Order(order) + } + + codes, err := codesQuery.All(ctx) if err != nil { return nil, nil, err } @@ -136,6 +142,34 @@ func (r *redeemCodeRepository) ListWithFilters(ctx context.Context, params pagin return outCodes, paginationResultFromTotal(int64(total), params), nil } +func redeemCodeListOrder(params pagination.PaginationParams) []func(*entsql.Selector) { + sortBy := strings.ToLower(strings.TrimSpace(params.SortBy)) + sortOrder := params.NormalizedSortOrder(pagination.SortOrderDesc) + + var field string + switch sortBy { + case "type": + field = redeemcode.FieldType + case "value": + field = redeemcode.FieldValue + case "status": + field = redeemcode.FieldStatus + case "used_at": + field = redeemcode.FieldUsedAt + case "created_at": + field = redeemcode.FieldCreatedAt + case "code": + field = redeemcode.FieldCode + default: + field = redeemcode.FieldID + } + + if sortOrder == pagination.SortOrderAsc { + return []func(*entsql.Selector){dbent.Asc(field), dbent.Asc(redeemcode.FieldID)} + } + return []func(*entsql.Selector){dbent.Desc(field), dbent.Desc(redeemcode.FieldID)} +} + func (r *redeemCodeRepository) Update(ctx context.Context, code *service.RedeemCode) error { up := r.client.RedeemCode.UpdateOneID(code.ID). SetCode(code.Code). diff --git a/backend/internal/repository/redeem_code_repo_sort_integration_test.go b/backend/internal/repository/redeem_code_repo_sort_integration_test.go new file mode 100644 index 00000000..30d32f4c --- /dev/null +++ b/backend/internal/repository/redeem_code_repo_sort_integration_test.go @@ -0,0 +1,24 @@ +//go:build integration + +package repository + +import ( + "github.com/Wei-Shaw/sub2api/internal/pkg/pagination" + "github.com/Wei-Shaw/sub2api/internal/service" +) + +func (s *RedeemCodeRepoSuite) TestListWithFilters_SortByValueAsc() { + s.Require().NoError(s.repo.Create(s.ctx, &service.RedeemCode{Code: "VALUE-20", Type: service.RedeemTypeBalance, Value: 20, Status: service.StatusUnused})) + s.Require().NoError(s.repo.Create(s.ctx, &service.RedeemCode{Code: "VALUE-10", Type: service.RedeemTypeBalance, Value: 10, Status: service.StatusUnused})) + + codes, _, err := s.repo.ListWithFilters(s.ctx, pagination.PaginationParams{ + Page: 1, + PageSize: 10, + SortBy: "value", + SortOrder: "asc", + }, "", "", "") + s.Require().NoError(err) + s.Require().Len(codes, 2) + s.Require().Equal("VALUE-10", codes[0].Code) + s.Require().Equal("VALUE-20", codes[1].Code) +} diff --git a/backend/internal/repository/usage_log_repo.go b/backend/internal/repository/usage_log_repo.go index d7bcd094..3ba2191e 100644 --- a/backend/internal/repository/usage_log_repo.go +++ b/backend/internal/repository/usage_log_repo.go @@ -3771,7 +3771,7 @@ func (r *usageLogRepository) listUsageLogsWithPagination(ctx context.Context, wh limitPos := len(args) + 1 offsetPos := len(args) + 2 listArgs := append(append([]any{}, args...), params.Limit(), params.Offset()) - query := fmt.Sprintf("SELECT %s FROM usage_logs %s ORDER BY id DESC LIMIT $%d OFFSET $%d", usageLogSelectColumns, whereClause, limitPos, offsetPos) + query := fmt.Sprintf("SELECT %s FROM usage_logs %s ORDER BY %s LIMIT $%d OFFSET $%d", usageLogSelectColumns, whereClause, usageLogOrderBy(params), limitPos, offsetPos) logs, err := r.queryUsageLogs(ctx, query, listArgs...) if err != nil { return nil, nil, err @@ -3786,7 +3786,7 @@ func (r *usageLogRepository) listUsageLogsWithFastPagination(ctx context.Context limitPos := len(args) + 1 offsetPos := len(args) + 2 listArgs := append(append([]any{}, args...), limit+1, offset) - query := fmt.Sprintf("SELECT %s FROM usage_logs %s ORDER BY id DESC LIMIT $%d OFFSET $%d", usageLogSelectColumns, whereClause, limitPos, offsetPos) + query := fmt.Sprintf("SELECT %s FROM usage_logs %s ORDER BY %s LIMIT $%d OFFSET $%d", usageLogSelectColumns, whereClause, usageLogOrderBy(params), limitPos, offsetPos) logs, err := r.queryUsageLogs(ctx, query, listArgs...) if err != nil { @@ -3808,6 +3808,26 @@ func (r *usageLogRepository) listUsageLogsWithFastPagination(ctx context.Context return logs, paginationResultFromTotal(total, params), nil } +func usageLogOrderBy(params pagination.PaginationParams) string { + sortBy := strings.ToLower(strings.TrimSpace(params.SortBy)) + sortOrder := strings.ToUpper(params.NormalizedSortOrder(pagination.SortOrderDesc)) + + var column string + switch sortBy { + case "model": + column = "COALESCE(NULLIF(TRIM(requested_model), ''), model)" + case "created_at": + column = "created_at" + default: + column = "id" + } + + if column == "id" { + return fmt.Sprintf("id %s", sortOrder) + } + return fmt.Sprintf("%s %s, id %s", column, sortOrder, sortOrder) +} + func (r *usageLogRepository) queryUsageLogs(ctx context.Context, query string, args ...any) (logs []service.UsageLog, err error) { rows, err := r.sql.QueryContext(ctx, query, args...) if err != nil { diff --git a/backend/internal/repository/usage_log_repo_request_type_test.go b/backend/internal/repository/usage_log_repo_request_type_test.go index ce0c5f00..b9cb6a13 100644 --- a/backend/internal/repository/usage_log_repo_request_type_test.go +++ b/backend/internal/repository/usage_log_repo_request_type_test.go @@ -330,6 +330,15 @@ func TestUsageLogRepositoryGetStatsWithFiltersRequestTypePriority(t *testing.T) "total_account_cost", "avg_duration_ms", }).AddRow(int64(1), int64(2), int64(3), int64(4), 1.2, 1.0, 1.2, 20.0)) + mock.ExpectQuery("SELECT COALESCE\\(NULLIF\\(TRIM\\(inbound_endpoint\\), ''\\), 'unknown'\\) AS endpoint"). + WithArgs(sqlmock.AnyArg(), sqlmock.AnyArg(), requestType). + WillReturnRows(sqlmock.NewRows([]string{"endpoint", "requests", "total_tokens", "cost", "actual_cost"})) + mock.ExpectQuery("SELECT COALESCE\\(NULLIF\\(TRIM\\(upstream_endpoint\\), ''\\), 'unknown'\\) AS endpoint"). + WithArgs(sqlmock.AnyArg(), sqlmock.AnyArg(), requestType). + WillReturnRows(sqlmock.NewRows([]string{"endpoint", "requests", "total_tokens", "cost", "actual_cost"})) + mock.ExpectQuery("SELECT CONCAT\\("). + WithArgs(sqlmock.AnyArg(), sqlmock.AnyArg(), requestType). + WillReturnRows(sqlmock.NewRows([]string{"endpoint", "requests", "total_tokens", "cost", "actual_cost"})) stats, err := repo.GetStatsWithFilters(context.Background(), filters) require.NoError(t, err) diff --git a/backend/internal/repository/usage_log_repo_sort_integration_test.go b/backend/internal/repository/usage_log_repo_sort_integration_test.go new file mode 100644 index 00000000..4c69f975 --- /dev/null +++ b/backend/internal/repository/usage_log_repo_sort_integration_test.go @@ -0,0 +1,61 @@ +//go:build integration + +package repository + +import ( + "time" + + "github.com/Wei-Shaw/sub2api/internal/pkg/pagination" + "github.com/Wei-Shaw/sub2api/internal/pkg/usagestats" + "github.com/Wei-Shaw/sub2api/internal/service" + "github.com/google/uuid" +) + +func (s *UsageLogRepoSuite) TestListWithFilters_SortByModelAsc() { + user := mustCreateUser(s.T(), s.client, &service.User{Email: "usage-sort@example.com"}) + apiKey := mustCreateApiKey(s.T(), s.client, &service.APIKey{UserID: user.ID, Key: "sk-usage-sort", Name: "k"}) + account := mustCreateAccount(s.T(), s.client, &service.Account{Name: "usage-sort-account"}) + + first := &service.UsageLog{ + UserID: user.ID, + APIKeyID: apiKey.ID, + AccountID: account.ID, + RequestID: uuid.New().String(), + Model: "z-model", + RequestedModel: "z-model", + InputTokens: 10, + OutputTokens: 20, + TotalCost: 0.5, + ActualCost: 0.5, + CreatedAt: time.Now(), + } + _, err := s.repo.Create(s.ctx, first) + s.Require().NoError(err) + + second := &service.UsageLog{ + UserID: user.ID, + APIKeyID: apiKey.ID, + AccountID: account.ID, + RequestID: uuid.New().String(), + Model: "a-model", + RequestedModel: "a-model", + InputTokens: 10, + OutputTokens: 20, + TotalCost: 0.5, + ActualCost: 0.5, + CreatedAt: time.Now().Add(time.Second), + } + _, err = s.repo.Create(s.ctx, second) + s.Require().NoError(err) + + logs, _, err := s.repo.ListWithFilters(s.ctx, pagination.PaginationParams{ + Page: 1, + PageSize: 10, + SortBy: "model", + SortOrder: "asc", + }, usagestats.UsageLogFilters{UserID: user.ID}) + s.Require().NoError(err) + s.Require().Len(logs, 2) + s.Require().Equal("a-model", logs[0].RequestedModel) + s.Require().Equal("z-model", logs[1].RequestedModel) +} diff --git a/backend/internal/repository/user_repo.go b/backend/internal/repository/user_repo.go index 06c79113..d5a13607 100644 --- a/backend/internal/repository/user_repo.go +++ b/backend/internal/repository/user_repo.go @@ -17,6 +17,8 @@ import ( "github.com/Wei-Shaw/sub2api/ent/usersubscription" "github.com/Wei-Shaw/sub2api/internal/pkg/pagination" "github.com/Wei-Shaw/sub2api/internal/service" + + entsql "entgo.io/ent/dialect/sql" ) type userRepository struct { @@ -224,11 +226,14 @@ func (r *userRepository) ListWithFilters(ctx context.Context, params pagination. return nil, nil, err } - users, err := q. + usersQuery := q. Offset(params.Offset()). - Limit(params.Limit()). - Order(dbent.Desc(dbuser.FieldID)). - All(ctx) + Limit(params.Limit()) + for _, order := range userListOrder(params) { + usersQuery = usersQuery.Order(order) + } + + users, err := usersQuery.All(ctx) if err != nil { return nil, nil, err } @@ -281,6 +286,50 @@ func (r *userRepository) ListWithFilters(ctx context.Context, params pagination. return outUsers, paginationResultFromTotal(int64(total), params), nil } +func userListOrder(params pagination.PaginationParams) []func(*entsql.Selector) { + sortBy := strings.ToLower(strings.TrimSpace(params.SortBy)) + sortOrder := params.NormalizedSortOrder(pagination.SortOrderDesc) + + var field string + defaultField := true + switch sortBy { + case "email": + field = dbuser.FieldEmail + defaultField = false + case "username": + field = dbuser.FieldUsername + defaultField = false + case "role": + field = dbuser.FieldRole + defaultField = false + case "balance": + field = dbuser.FieldBalance + defaultField = false + case "concurrency": + field = dbuser.FieldConcurrency + defaultField = false + case "status": + field = dbuser.FieldStatus + defaultField = false + case "created_at": + field = dbuser.FieldCreatedAt + defaultField = false + default: + field = dbuser.FieldID + } + + if sortOrder == pagination.SortOrderAsc { + if defaultField && field == dbuser.FieldID { + return []func(*entsql.Selector){dbent.Asc(dbuser.FieldID)} + } + return []func(*entsql.Selector){dbent.Asc(field), dbent.Asc(dbuser.FieldID)} + } + if defaultField && field == dbuser.FieldID { + return []func(*entsql.Selector){dbent.Desc(dbuser.FieldID)} + } + return []func(*entsql.Selector){dbent.Desc(field), dbent.Desc(dbuser.FieldID)} +} + // filterUsersByAttributes returns user IDs that match ALL the given attribute filters func (r *userRepository) filterUsersByAttributes(ctx context.Context, attrs map[int64]string) ([]int64, error) { if len(attrs) == 0 { diff --git a/backend/internal/repository/user_repo_sort_integration_test.go b/backend/internal/repository/user_repo_sort_integration_test.go new file mode 100644 index 00000000..ab84b0e9 --- /dev/null +++ b/backend/internal/repository/user_repo_sort_integration_test.go @@ -0,0 +1,39 @@ +//go:build integration + +package repository + +import ( + "testing" + + "github.com/Wei-Shaw/sub2api/internal/pkg/pagination" + "github.com/Wei-Shaw/sub2api/internal/service" +) + +func (s *UserRepoSuite) TestListWithFilters_SortByEmailAsc() { + s.mustCreateUser(&service.User{Email: "z-last@example.com", Username: "z-user"}) + s.mustCreateUser(&service.User{Email: "a-first@example.com", Username: "a-user"}) + + users, _, err := s.repo.ListWithFilters(s.ctx, pagination.PaginationParams{ + Page: 1, + PageSize: 10, + SortBy: "email", + SortOrder: "asc", + }, service.UserListFilters{}) + s.Require().NoError(err) + s.Require().Len(users, 2) + s.Require().Equal("a-first@example.com", users[0].Email) + s.Require().Equal("z-last@example.com", users[1].Email) +} + +func (s *UserRepoSuite) TestList_DefaultSortByNewestFirst() { + first := s.mustCreateUser(&service.User{Email: "first@example.com"}) + second := s.mustCreateUser(&service.User{Email: "second@example.com"}) + + users, _, err := s.repo.List(s.ctx, pagination.PaginationParams{Page: 1, PageSize: 10}) + s.Require().NoError(err) + s.Require().Len(users, 2) + s.Require().Equal(second.ID, users[0].ID) + s.Require().Equal(first.ID, users[1].ID) +} + +func TestUserRepoSortSuiteSmoke(_ *testing.T) {} diff --git a/backend/internal/server/api_contract_test.go b/backend/internal/server/api_contract_test.go index fee879a4..9b37956a 100644 --- a/backend/internal/server/api_contract_test.go +++ b/backend/internal/server/api_contract_test.go @@ -491,8 +491,10 @@ func TestAPIContracts(t *testing.T) { service.SettingKeyContactInfo: "support", service.SettingKeyDocURL: "https://docs.example.com", - service.SettingKeyDefaultConcurrency: "5", - service.SettingKeyDefaultBalance: "1.25", + service.SettingKeyDefaultConcurrency: "5", + service.SettingKeyDefaultBalance: "1.25", + service.SettingKeyTableDefaultPageSize: "20", + service.SettingKeyTablePageSizeOptions: "[10,20,50,100]", service.SettingKeyOpsMonitoringEnabled: "false", service.SettingKeyOpsRealtimeMonitoringEnabled: "true", @@ -576,6 +578,8 @@ func TestAPIContracts(t *testing.T) { "hide_ccs_import_button": false, "purchase_subscription_enabled": false, "purchase_subscription_url": "", + "table_default_page_size": 20, + "table_page_size_options": [10, 20, 50, 100], "min_claude_code_version": "", "max_claude_code_version": "", "allow_ungrouped_key_scheduling": false, diff --git a/backend/internal/service/admin_service.go b/backend/internal/service/admin_service.go index c2553eee..97b42c24 100644 --- a/backend/internal/service/admin_service.go +++ b/backend/internal/service/admin_service.go @@ -21,13 +21,13 @@ import ( // AdminService interface defines admin management operations type AdminService interface { // User management - ListUsers(ctx context.Context, page, pageSize int, filters UserListFilters) ([]User, int64, error) + ListUsers(ctx context.Context, page, pageSize int, filters UserListFilters, sortBy, sortOrder string) ([]User, int64, error) GetUser(ctx context.Context, id int64) (*User, error) CreateUser(ctx context.Context, input *CreateUserInput) (*User, error) UpdateUser(ctx context.Context, id int64, input *UpdateUserInput) (*User, error) DeleteUser(ctx context.Context, id int64) error UpdateUserBalance(ctx context.Context, userID int64, balance float64, operation string, notes string) (*User, error) - GetUserAPIKeys(ctx context.Context, userID int64, page, pageSize int) ([]APIKey, int64, error) + GetUserAPIKeys(ctx context.Context, userID int64, page, pageSize int, sortBy, sortOrder string) ([]APIKey, int64, error) GetUserUsageStats(ctx context.Context, userID int64, period string) (any, error) // GetUserBalanceHistory returns paginated balance/concurrency change records for a user. // codeType is optional - pass empty string to return all types. @@ -35,7 +35,7 @@ type AdminService interface { GetUserBalanceHistory(ctx context.Context, userID int64, page, pageSize int, codeType string) ([]RedeemCode, int64, float64, error) // Group management - ListGroups(ctx context.Context, page, pageSize int, platform, status, search string, isExclusive *bool) ([]Group, int64, error) + ListGroups(ctx context.Context, page, pageSize int, platform, status, search string, isExclusive *bool, sortBy, sortOrder string) ([]Group, int64, error) GetAllGroups(ctx context.Context) ([]Group, error) GetAllGroupsByPlatform(ctx context.Context, platform string) ([]Group, error) GetGroup(ctx context.Context, id int64) (*Group, error) @@ -55,7 +55,7 @@ type AdminService interface { ReplaceUserGroup(ctx context.Context, userID, oldGroupID, newGroupID int64) (*ReplaceUserGroupResult, error) // Account management - ListAccounts(ctx context.Context, page, pageSize int, platform, accountType, status, search string, groupID int64, privacyMode string) ([]Account, int64, error) + ListAccounts(ctx context.Context, page, pageSize int, platform, accountType, status, search string, groupID int64, privacyMode string, sortBy, sortOrder string) ([]Account, int64, error) GetAccount(ctx context.Context, id int64) (*Account, error) GetAccountsByIDs(ctx context.Context, ids []int64) ([]*Account, error) CreateAccount(ctx context.Context, input *CreateAccountInput) (*Account, error) @@ -77,8 +77,8 @@ type AdminService interface { CheckMixedChannelRisk(ctx context.Context, currentAccountID int64, currentAccountPlatform string, groupIDs []int64) error // Proxy management - ListProxies(ctx context.Context, page, pageSize int, protocol, status, search string) ([]Proxy, int64, error) - ListProxiesWithAccountCount(ctx context.Context, page, pageSize int, protocol, status, search string) ([]ProxyWithAccountCount, int64, error) + ListProxies(ctx context.Context, page, pageSize int, protocol, status, search string, sortBy, sortOrder string) ([]Proxy, int64, error) + ListProxiesWithAccountCount(ctx context.Context, page, pageSize int, protocol, status, search string, sortBy, sortOrder string) ([]ProxyWithAccountCount, int64, error) GetAllProxies(ctx context.Context) ([]Proxy, error) GetAllProxiesWithAccountCount(ctx context.Context) ([]ProxyWithAccountCount, error) GetProxy(ctx context.Context, id int64) (*Proxy, error) @@ -93,7 +93,7 @@ type AdminService interface { CheckProxyQuality(ctx context.Context, id int64) (*ProxyQualityCheckResult, error) // Redeem code management - ListRedeemCodes(ctx context.Context, page, pageSize int, codeType, status, search string) ([]RedeemCode, int64, error) + ListRedeemCodes(ctx context.Context, page, pageSize int, codeType, status, search string, sortBy, sortOrder string) ([]RedeemCode, int64, error) GetRedeemCode(ctx context.Context, id int64) (*RedeemCode, error) GenerateRedeemCodes(ctx context.Context, input *GenerateRedeemCodesInput) ([]RedeemCode, error) DeleteRedeemCode(ctx context.Context, id int64) error @@ -485,8 +485,8 @@ func NewAdminService( } // User management implementations -func (s *adminServiceImpl) ListUsers(ctx context.Context, page, pageSize int, filters UserListFilters) ([]User, int64, error) { - params := pagination.PaginationParams{Page: page, PageSize: pageSize} +func (s *adminServiceImpl) ListUsers(ctx context.Context, page, pageSize int, filters UserListFilters, sortBy, sortOrder string) ([]User, int64, error) { + params := pagination.PaginationParams{Page: page, PageSize: pageSize, SortBy: sortBy, SortOrder: sortOrder} users, result, err := s.userRepo.ListWithFilters(ctx, params, filters) if err != nil { return nil, 0, err @@ -753,8 +753,8 @@ func (s *adminServiceImpl) UpdateUserBalance(ctx context.Context, userID int64, return user, nil } -func (s *adminServiceImpl) GetUserAPIKeys(ctx context.Context, userID int64, page, pageSize int) ([]APIKey, int64, error) { - params := pagination.PaginationParams{Page: page, PageSize: pageSize} +func (s *adminServiceImpl) GetUserAPIKeys(ctx context.Context, userID int64, page, pageSize int, sortBy, sortOrder string) ([]APIKey, int64, error) { + params := pagination.PaginationParams{Page: page, PageSize: pageSize, SortBy: sortBy, SortOrder: sortOrder} keys, result, err := s.apiKeyRepo.ListByUserID(ctx, userID, params, APIKeyListFilters{}) if err != nil { return nil, 0, err @@ -789,8 +789,8 @@ func (s *adminServiceImpl) GetUserBalanceHistory(ctx context.Context, userID int } // Group management implementations -func (s *adminServiceImpl) ListGroups(ctx context.Context, page, pageSize int, platform, status, search string, isExclusive *bool) ([]Group, int64, error) { - params := pagination.PaginationParams{Page: page, PageSize: pageSize} +func (s *adminServiceImpl) ListGroups(ctx context.Context, page, pageSize int, platform, status, search string, isExclusive *bool, sortBy, sortOrder string) ([]Group, int64, error) { + params := pagination.PaginationParams{Page: page, PageSize: pageSize, SortBy: sortBy, SortOrder: sortOrder} groups, result, err := s.groupRepo.ListWithFilters(ctx, params, platform, status, search, isExclusive) if err != nil { return nil, 0, err @@ -1464,8 +1464,8 @@ func (s *adminServiceImpl) ReplaceUserGroup(ctx context.Context, userID, oldGrou } // Account management implementations -func (s *adminServiceImpl) ListAccounts(ctx context.Context, page, pageSize int, platform, accountType, status, search string, groupID int64, privacyMode string) ([]Account, int64, error) { - params := pagination.PaginationParams{Page: page, PageSize: pageSize} +func (s *adminServiceImpl) ListAccounts(ctx context.Context, page, pageSize int, platform, accountType, status, search string, groupID int64, privacyMode string, sortBy, sortOrder string) ([]Account, int64, error) { + params := pagination.PaginationParams{Page: page, PageSize: pageSize, SortBy: sortBy, SortOrder: sortOrder} accounts, result, err := s.accountRepo.ListWithFilters(ctx, params, platform, accountType, status, search, groupID, privacyMode) if err != nil { return nil, 0, err @@ -1893,8 +1893,8 @@ func (s *adminServiceImpl) SetAccountSchedulable(ctx context.Context, id int64, } // Proxy management implementations -func (s *adminServiceImpl) ListProxies(ctx context.Context, page, pageSize int, protocol, status, search string) ([]Proxy, int64, error) { - params := pagination.PaginationParams{Page: page, PageSize: pageSize} +func (s *adminServiceImpl) ListProxies(ctx context.Context, page, pageSize int, protocol, status, search string, sortBy, sortOrder string) ([]Proxy, int64, error) { + params := pagination.PaginationParams{Page: page, PageSize: pageSize, SortBy: sortBy, SortOrder: sortOrder} proxies, result, err := s.proxyRepo.ListWithFilters(ctx, params, protocol, status, search) if err != nil { return nil, 0, err @@ -1902,8 +1902,8 @@ func (s *adminServiceImpl) ListProxies(ctx context.Context, page, pageSize int, return proxies, result.Total, nil } -func (s *adminServiceImpl) ListProxiesWithAccountCount(ctx context.Context, page, pageSize int, protocol, status, search string) ([]ProxyWithAccountCount, int64, error) { - params := pagination.PaginationParams{Page: page, PageSize: pageSize} +func (s *adminServiceImpl) ListProxiesWithAccountCount(ctx context.Context, page, pageSize int, protocol, status, search string, sortBy, sortOrder string) ([]ProxyWithAccountCount, int64, error) { + params := pagination.PaginationParams{Page: page, PageSize: pageSize, SortBy: sortBy, SortOrder: sortOrder} proxies, result, err := s.proxyRepo.ListWithFiltersAndAccountCount(ctx, params, protocol, status, search) if err != nil { return nil, 0, err @@ -2040,8 +2040,8 @@ func (s *adminServiceImpl) CheckProxyExists(ctx context.Context, host string, po } // Redeem code management implementations -func (s *adminServiceImpl) ListRedeemCodes(ctx context.Context, page, pageSize int, codeType, status, search string) ([]RedeemCode, int64, error) { - params := pagination.PaginationParams{Page: page, PageSize: pageSize} +func (s *adminServiceImpl) ListRedeemCodes(ctx context.Context, page, pageSize int, codeType, status, search string, sortBy, sortOrder string) ([]RedeemCode, int64, error) { + params := pagination.PaginationParams{Page: page, PageSize: pageSize, SortBy: sortBy, SortOrder: sortOrder} codes, result, err := s.redeemCodeRepo.ListWithFilters(ctx, params, codeType, status, search) if err != nil { return nil, 0, err diff --git a/backend/internal/service/admin_service_group_test.go b/backend/internal/service/admin_service_group_test.go index 364022bd..a4c6d0ca 100644 --- a/backend/internal/service/admin_service_group_test.go +++ b/backend/internal/service/admin_service_group_test.go @@ -125,6 +125,22 @@ func (s *groupRepoStubForAdmin) UpdateSortOrders(_ context.Context, _ []GroupSor return nil } +func TestAdminService_ListGroups_PassesSortParams(t *testing.T) { + repo := &groupRepoStubForAdmin{ + listWithFiltersGroups: []Group{{ID: 1, Name: "g1"}}, + } + svc := &adminServiceImpl{groupRepo: repo} + + _, _, err := svc.ListGroups(context.Background(), 3, 25, PlatformOpenAI, StatusActive, "needle", nil, "account_count", "ASC") + require.NoError(t, err) + require.Equal(t, pagination.PaginationParams{ + Page: 3, + PageSize: 25, + SortBy: "account_count", + SortOrder: "ASC", + }, repo.listWithFiltersParams) +} + // TestAdminService_CreateGroup_WithImagePricing 测试创建分组时 ImagePrice 字段正确传递 func TestAdminService_CreateGroup_WithImagePricing(t *testing.T) { repo := &groupRepoStubForAdmin{} @@ -373,7 +389,7 @@ func TestAdminService_ListGroups_WithSearch(t *testing.T) { } svc := &adminServiceImpl{groupRepo: repo} - groups, total, err := svc.ListGroups(context.Background(), 1, 20, "", "", "alpha", nil) + groups, total, err := svc.ListGroups(context.Background(), 1, 20, "", "", "alpha", nil, "", "") require.NoError(t, err) require.Equal(t, int64(1), total) require.Equal(t, []Group{{ID: 1, Name: "alpha"}}, groups) @@ -391,7 +407,7 @@ func TestAdminService_ListGroups_WithSearch(t *testing.T) { } svc := &adminServiceImpl{groupRepo: repo} - groups, total, err := svc.ListGroups(context.Background(), 2, 10, "", "", "", nil) + groups, total, err := svc.ListGroups(context.Background(), 2, 10, "", "", "", nil, "", "") require.NoError(t, err) require.Empty(t, groups) require.Equal(t, int64(0), total) @@ -410,7 +426,7 @@ func TestAdminService_ListGroups_WithSearch(t *testing.T) { } svc := &adminServiceImpl{groupRepo: repo} - groups, total, err := svc.ListGroups(context.Background(), 3, 50, PlatformAntigravity, StatusActive, "beta", &isExclusive) + groups, total, err := svc.ListGroups(context.Background(), 3, 50, PlatformAntigravity, StatusActive, "beta", &isExclusive, "", "") require.NoError(t, err) require.Equal(t, int64(42), total) require.Equal(t, []Group{{ID: 2, Name: "beta"}}, groups) diff --git a/backend/internal/service/admin_service_list_users_test.go b/backend/internal/service/admin_service_list_users_test.go index 37f348df..ceeb52c2 100644 --- a/backend/internal/service/admin_service_list_users_test.go +++ b/backend/internal/service/admin_service_list_users_test.go @@ -13,11 +13,13 @@ import ( type userRepoStubForListUsers struct { userRepoStub - users []User - err error + users []User + err error + listWithFiltersParams pagination.PaginationParams } func (s *userRepoStubForListUsers) ListWithFilters(_ context.Context, params pagination.PaginationParams, _ UserListFilters) ([]User, *pagination.PaginationResult, error) { + s.listWithFiltersParams = params if s.err != nil { return nil, nil, s.err } @@ -103,7 +105,7 @@ func TestAdminService_ListUsers_BatchRateFallbackToSingle(t *testing.T) { userGroupRateRepo: rateRepo, } - users, total, err := svc.ListUsers(context.Background(), 1, 20, UserListFilters{}) + users, total, err := svc.ListUsers(context.Background(), 1, 20, UserListFilters{}, "", "") require.NoError(t, err) require.Equal(t, int64(2), total) require.Len(t, users, 2) @@ -112,3 +114,19 @@ func TestAdminService_ListUsers_BatchRateFallbackToSingle(t *testing.T) { require.Equal(t, 1.1, users[0].GroupRates[11]) require.Equal(t, 2.2, users[1].GroupRates[22]) } + +func TestAdminService_ListUsers_PassesSortParams(t *testing.T) { + userRepo := &userRepoStubForListUsers{ + users: []User{{ID: 1, Email: "a@example.com"}}, + } + svc := &adminServiceImpl{userRepo: userRepo} + + _, _, err := svc.ListUsers(context.Background(), 2, 50, UserListFilters{}, "email", "ASC") + require.NoError(t, err) + require.Equal(t, pagination.PaginationParams{ + Page: 2, + PageSize: 50, + SortBy: "email", + SortOrder: "ASC", + }, userRepo.listWithFiltersParams) +} diff --git a/backend/internal/service/admin_service_search_test.go b/backend/internal/service/admin_service_search_test.go index eb213e6a..595e99e3 100644 --- a/backend/internal/service/admin_service_search_test.go +++ b/backend/internal/service/admin_service_search_test.go @@ -170,13 +170,13 @@ func TestAdminService_ListAccounts_WithSearch(t *testing.T) { } svc := &adminServiceImpl{accountRepo: repo} - accounts, total, err := svc.ListAccounts(context.Background(), 1, 20, PlatformGemini, AccountTypeOAuth, StatusActive, "acc", 0, "") + accounts, total, err := svc.ListAccounts(context.Background(), 1, 20, PlatformGemini, AccountTypeOAuth, StatusActive, "acc", 0, "", "name", "ASC") require.NoError(t, err) require.Equal(t, int64(10), total) require.Equal(t, []Account{{ID: 1, Name: "acc"}}, accounts) require.Equal(t, 1, repo.listWithFiltersCalls) - require.Equal(t, pagination.PaginationParams{Page: 1, PageSize: 20}, repo.listWithFiltersParams) + require.Equal(t, pagination.PaginationParams{Page: 1, PageSize: 20, SortBy: "name", SortOrder: "ASC"}, repo.listWithFiltersParams) require.Equal(t, PlatformGemini, repo.listWithFiltersPlatform) require.Equal(t, AccountTypeOAuth, repo.listWithFiltersType) require.Equal(t, StatusActive, repo.listWithFiltersStatus) @@ -192,7 +192,7 @@ func TestAdminService_ListAccounts_WithPrivacyMode(t *testing.T) { } svc := &adminServiceImpl{accountRepo: repo} - accounts, total, err := svc.ListAccounts(context.Background(), 1, 20, PlatformOpenAI, AccountTypeOAuth, StatusActive, "acc2", 0, PrivacyModeCFBlocked) + accounts, total, err := svc.ListAccounts(context.Background(), 1, 20, PlatformOpenAI, AccountTypeOAuth, StatusActive, "acc2", 0, PrivacyModeCFBlocked, "", "") require.NoError(t, err) require.Equal(t, int64(1), total) require.Equal(t, []Account{{ID: 2, Name: "acc2"}}, accounts) @@ -208,13 +208,13 @@ func TestAdminService_ListProxies_WithSearch(t *testing.T) { } svc := &adminServiceImpl{proxyRepo: repo} - proxies, total, err := svc.ListProxies(context.Background(), 3, 50, "http", StatusActive, "p1") + proxies, total, err := svc.ListProxies(context.Background(), 3, 50, "http", StatusActive, "p1", "name", "ASC") require.NoError(t, err) require.Equal(t, int64(7), total) require.Equal(t, []Proxy{{ID: 2, Name: "p1"}}, proxies) require.Equal(t, 1, repo.listWithFiltersCalls) - require.Equal(t, pagination.PaginationParams{Page: 3, PageSize: 50}, repo.listWithFiltersParams) + require.Equal(t, pagination.PaginationParams{Page: 3, PageSize: 50, SortBy: "name", SortOrder: "ASC"}, repo.listWithFiltersParams) require.Equal(t, "http", repo.listWithFiltersProtocol) require.Equal(t, StatusActive, repo.listWithFiltersStatus) require.Equal(t, "p1", repo.listWithFiltersSearch) @@ -229,13 +229,13 @@ func TestAdminService_ListProxiesWithAccountCount_WithSearch(t *testing.T) { } svc := &adminServiceImpl{proxyRepo: repo} - proxies, total, err := svc.ListProxiesWithAccountCount(context.Background(), 2, 10, "socks5", StatusDisabled, "p2") + proxies, total, err := svc.ListProxiesWithAccountCount(context.Background(), 2, 10, "socks5", StatusDisabled, "p2", "account_count", "DESC") require.NoError(t, err) require.Equal(t, int64(9), total) require.Equal(t, []ProxyWithAccountCount{{Proxy: Proxy{ID: 3, Name: "p2"}, AccountCount: 5}}, proxies) require.Equal(t, 1, repo.listWithFiltersAndAccountCountCalls) - require.Equal(t, pagination.PaginationParams{Page: 2, PageSize: 10}, repo.listWithFiltersAndAccountCountParams) + require.Equal(t, pagination.PaginationParams{Page: 2, PageSize: 10, SortBy: "account_count", SortOrder: "DESC"}, repo.listWithFiltersAndAccountCountParams) require.Equal(t, "socks5", repo.listWithFiltersAndAccountCountProtocol) require.Equal(t, StatusDisabled, repo.listWithFiltersAndAccountCountStatus) require.Equal(t, "p2", repo.listWithFiltersAndAccountCountSearch) @@ -250,13 +250,13 @@ func TestAdminService_ListRedeemCodes_WithSearch(t *testing.T) { } svc := &adminServiceImpl{redeemCodeRepo: repo} - codes, total, err := svc.ListRedeemCodes(context.Background(), 1, 20, RedeemTypeBalance, StatusUnused, "ABC") + codes, total, err := svc.ListRedeemCodes(context.Background(), 1, 20, RedeemTypeBalance, StatusUnused, "ABC", "value", "ASC") require.NoError(t, err) require.Equal(t, int64(3), total) require.Equal(t, []RedeemCode{{ID: 4, Code: "ABC"}}, codes) require.Equal(t, 1, repo.listWithFiltersCalls) - require.Equal(t, pagination.PaginationParams{Page: 1, PageSize: 20}, repo.listWithFiltersParams) + require.Equal(t, pagination.PaginationParams{Page: 1, PageSize: 20, SortBy: "value", SortOrder: "ASC"}, repo.listWithFiltersParams) require.Equal(t, RedeemTypeBalance, repo.listWithFiltersType) require.Equal(t, StatusUnused, repo.listWithFiltersStatus) require.Equal(t, "ABC", repo.listWithFiltersSearch) diff --git a/backend/internal/service/api_key_auth_cache.go b/backend/internal/service/api_key_auth_cache.go index ad6ba0e9..c2e96df1 100644 --- a/backend/internal/service/api_key_auth_cache.go +++ b/backend/internal/service/api_key_auth_cache.go @@ -4,6 +4,7 @@ import "time" // APIKeyAuthSnapshot API Key 认证缓存快照(仅包含认证所需字段) type APIKeyAuthSnapshot struct { + Version int `json:"version"` APIKeyID int64 `json:"api_key_id"` UserID int64 `json:"user_id"` GroupID *int64 `json:"group_id,omitempty"` @@ -63,8 +64,9 @@ type APIKeyAuthGroupSnapshot struct { SupportedModelScopes []string `json:"supported_model_scopes,omitempty"` // OpenAI Messages 调度配置(仅 openai 平台使用) - AllowMessagesDispatch bool `json:"allow_messages_dispatch"` - DefaultMappedModel string `json:"default_mapped_model,omitempty"` + AllowMessagesDispatch bool `json:"allow_messages_dispatch"` + DefaultMappedModel string `json:"default_mapped_model,omitempty"` + MessagesDispatchModelConfig OpenAIMessagesDispatchModelConfig `json:"messages_dispatch_model_config,omitempty"` } // APIKeyAuthCacheEntry 缓存条目,支持负缓存 diff --git a/backend/internal/service/api_key_auth_cache_impl.go b/backend/internal/service/api_key_auth_cache_impl.go index 64a70e8c..8069ed4f 100644 --- a/backend/internal/service/api_key_auth_cache_impl.go +++ b/backend/internal/service/api_key_auth_cache_impl.go @@ -13,6 +13,8 @@ import ( "github.com/dgraph-io/ristretto" ) +const apiKeyAuthSnapshotVersion = 3 + type apiKeyAuthCacheConfig struct { l1Size int l1TTL time.Duration @@ -192,6 +194,9 @@ func (s *APIKeyService) applyAuthCacheEntry(key string, entry *APIKeyAuthCacheEn if entry.Snapshot == nil { return nil, false, nil } + if entry.Snapshot.Version != apiKeyAuthSnapshotVersion { + return nil, false, nil + } return s.snapshotToAPIKey(key, entry.Snapshot), true, nil } @@ -200,6 +205,7 @@ func (s *APIKeyService) snapshotFromAPIKey(apiKey *APIKey) *APIKeyAuthSnapshot { return nil } snapshot := &APIKeyAuthSnapshot{ + Version: apiKeyAuthSnapshotVersion, APIKeyID: apiKey.ID, UserID: apiKey.UserID, GroupID: apiKey.GroupID, @@ -243,6 +249,7 @@ func (s *APIKeyService) snapshotFromAPIKey(apiKey *APIKey) *APIKeyAuthSnapshot { SupportedModelScopes: apiKey.Group.SupportedModelScopes, AllowMessagesDispatch: apiKey.Group.AllowMessagesDispatch, DefaultMappedModel: apiKey.Group.DefaultMappedModel, + MessagesDispatchModelConfig: apiKey.Group.MessagesDispatchModelConfig, } } return snapshot @@ -298,6 +305,7 @@ func (s *APIKeyService) snapshotToAPIKey(key string, snapshot *APIKeyAuthSnapsho SupportedModelScopes: snapshot.Group.SupportedModelScopes, AllowMessagesDispatch: snapshot.Group.AllowMessagesDispatch, DefaultMappedModel: snapshot.Group.DefaultMappedModel, + MessagesDispatchModelConfig: snapshot.Group.MessagesDispatchModelConfig, } } s.compileAPIKeyIPRules(apiKey) diff --git a/backend/internal/service/api_key_service_cache_test.go b/backend/internal/service/api_key_service_cache_test.go index 357f8def..3c2f7dbb 100644 --- a/backend/internal/service/api_key_service_cache_test.go +++ b/backend/internal/service/api_key_service_cache_test.go @@ -188,6 +188,7 @@ func TestAPIKeyService_GetByKey_UsesL2Cache(t *testing.T) { groupID := int64(9) cacheEntry := &APIKeyAuthCacheEntry{ Snapshot: &APIKeyAuthSnapshot{ + Version: apiKeyAuthSnapshotVersion, APIKeyID: 1, UserID: 2, GroupID: &groupID, @@ -226,6 +227,129 @@ func TestAPIKeyService_GetByKey_UsesL2Cache(t *testing.T) { require.Equal(t, map[string][]int64{"claude-opus-*": {1, 2}}, apiKey.Group.ModelRouting) } +func TestAPIKeyService_SnapshotRoundTrip_PreservesMessagesDispatchModelConfig(t *testing.T) { + svc := NewAPIKeyService(nil, nil, nil, nil, nil, nil, &config.Config{}) + groupID := int64(9) + apiKey := &APIKey{ + ID: 1, + UserID: 2, + GroupID: &groupID, + Key: "k-roundtrip", + Status: StatusActive, + User: &User{ + ID: 2, + Status: StatusActive, + Role: RoleUser, + Balance: 10, + Concurrency: 3, + }, + Group: &Group{ + ID: groupID, + Name: "openai", + Platform: PlatformOpenAI, + Status: StatusActive, + SubscriptionType: SubscriptionTypeStandard, + RateMultiplier: 1, + AllowMessagesDispatch: true, + DefaultMappedModel: "gpt-5.4", + MessagesDispatchModelConfig: OpenAIMessagesDispatchModelConfig{ + OpusMappedModel: "gpt-5.4-nano", + SonnetMappedModel: "gpt-5.3-codex", + HaikuMappedModel: "gpt-5.4-mini", + ExactModelMappings: map[string]string{ + "claude-sonnet-4.5": "gpt-5.4-nano", + }, + }, + }, + } + + snapshot := svc.snapshotFromAPIKey(apiKey) + roundTrip := svc.snapshotToAPIKey(apiKey.Key, snapshot) + + require.NotNil(t, roundTrip) + require.NotNil(t, roundTrip.Group) + require.Equal(t, apiKey.Group.MessagesDispatchModelConfig, roundTrip.Group.MessagesDispatchModelConfig) +} + +func TestAPIKeyService_GetByKey_IgnoresLegacyAuthCacheSnapshotWithoutMessagesDispatchConfig(t *testing.T) { + cache := &authCacheStub{} + var repoCalls int32 + repo := &authRepoStub{ + getByKeyForAuth: func(ctx context.Context, key string) (*APIKey, error) { + atomic.AddInt32(&repoCalls, 1) + groupID := int64(9) + return &APIKey{ + ID: 1, + UserID: 2, + GroupID: &groupID, + Status: StatusActive, + User: &User{ + ID: 2, + Status: StatusActive, + Role: RoleUser, + Balance: 10, + Concurrency: 3, + }, + Group: &Group{ + ID: groupID, + Name: "openai", + Platform: PlatformOpenAI, + Status: StatusActive, + Hydrated: true, + SubscriptionType: SubscriptionTypeStandard, + RateMultiplier: 1, + AllowMessagesDispatch: true, + DefaultMappedModel: "gpt-5.4", + MessagesDispatchModelConfig: OpenAIMessagesDispatchModelConfig{ + OpusMappedModel: "gpt-5.4-nano", + }, + }, + }, nil + }, + } + cfg := &config.Config{ + APIKeyAuth: config.APIKeyAuthCacheConfig{ + L2TTLSeconds: 60, + }, + } + svc := NewAPIKeyService(repo, nil, nil, nil, nil, cache, cfg) + + groupID := int64(9) + cache.getAuthCache = func(ctx context.Context, key string) (*APIKeyAuthCacheEntry, error) { + return &APIKeyAuthCacheEntry{ + Snapshot: &APIKeyAuthSnapshot{ + APIKeyID: 1, + UserID: 2, + GroupID: &groupID, + Status: StatusActive, + User: APIKeyAuthUserSnapshot{ + ID: 2, + Status: StatusActive, + Role: RoleUser, + Balance: 10, + Concurrency: 3, + }, + Group: &APIKeyAuthGroupSnapshot{ + ID: groupID, + Name: "openai", + Platform: PlatformOpenAI, + Status: StatusActive, + SubscriptionType: SubscriptionTypeStandard, + RateMultiplier: 1, + AllowMessagesDispatch: true, + DefaultMappedModel: "gpt-5.4", + }, + }, + }, nil + } + + apiKey, err := svc.GetByKey(context.Background(), "k-legacy") + require.NoError(t, err) + require.Equal(t, int32(1), atomic.LoadInt32(&repoCalls)) + require.NotNil(t, apiKey.Group) + require.Equal(t, "gpt-5.4-nano", apiKey.Group.MessagesDispatchModelConfig.OpusMappedModel) +} + func TestAPIKeyService_GetByKey_NegativeCache(t *testing.T) { cache := &authCacheStub{} repo := &authRepoStub{ diff --git a/backend/internal/service/domain_constants.go b/backend/internal/service/domain_constants.go index e194f921..68d7da3b 100644 --- a/backend/internal/service/domain_constants.go +++ b/backend/internal/service/domain_constants.go @@ -143,6 +143,8 @@ const ( SettingKeyHideCcsImportButton = "hide_ccs_import_button" // 是否隐藏 API Keys 页面的导入 CCS 按钮 SettingKeyPurchaseSubscriptionEnabled = "purchase_subscription_enabled" // 是否展示"购买订阅"页面入口 SettingKeyPurchaseSubscriptionURL = "purchase_subscription_url" // "购买订阅"页面 URL(作为 iframe src) + SettingKeyTableDefaultPageSize = "table_default_page_size" // 表格默认每页条数 + SettingKeyTablePageSizeOptions = "table_page_size_options" // 表格可选每页条数(JSON 数组) SettingKeyCustomMenuItems = "custom_menu_items" // 自定义菜单项(JSON 数组) SettingKeyCustomEndpoints = "custom_endpoints" // 自定义端点列表(JSON 数组) diff --git a/backend/internal/service/openai_ws_ratelimit_signal_test.go b/backend/internal/service/openai_ws_ratelimit_signal_test.go index ffe79152..6313d0c0 100644 --- a/backend/internal/service/openai_ws_ratelimit_signal_test.go +++ b/backend/internal/service/openai_ws_ratelimit_signal_test.go @@ -492,7 +492,7 @@ func TestAdminService_ListAccounts_ExhaustedCodexExtraReturnsRateLimitedAccount( } svc := &adminServiceImpl{accountRepo: repo} - accounts, total, err := svc.ListAccounts(context.Background(), 1, 20, PlatformOpenAI, AccountTypeOAuth, "", "", 0, "") + accounts, total, err := svc.ListAccounts(context.Background(), 1, 20, PlatformOpenAI, AccountTypeOAuth, "", "", 0, "", "", "") require.NoError(t, err) require.Equal(t, int64(1), total) require.Len(t, accounts, 1) diff --git a/backend/internal/service/setting_service.go b/backend/internal/service/setting_service.go index 9a179d67..06daaa21 100644 --- a/backend/internal/service/setting_service.go +++ b/backend/internal/service/setting_service.go @@ -9,6 +9,7 @@ import ( "fmt" "log/slog" "net/url" + "sort" "strconv" "strings" "sync/atomic" @@ -161,6 +162,8 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings SettingKeyHideCcsImportButton, SettingKeyPurchaseSubscriptionEnabled, SettingKeyPurchaseSubscriptionURL, + SettingKeyTableDefaultPageSize, + SettingKeyTablePageSizeOptions, SettingKeyCustomMenuItems, SettingKeyCustomEndpoints, SettingKeyLinuxDoConnectEnabled, @@ -200,6 +203,10 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings registrationEmailSuffixWhitelist := ParseRegistrationEmailSuffixWhitelist( settings[SettingKeyRegistrationEmailSuffixWhitelist], ) + tableDefaultPageSize, tablePageSizeOptions := parseTablePreferences( + settings[SettingKeyTableDefaultPageSize], + settings[SettingKeyTablePageSizeOptions], + ) return &PublicSettings{ RegistrationEnabled: settings[SettingKeyRegistrationEnabled] == "true", @@ -221,6 +228,8 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings HideCcsImportButton: settings[SettingKeyHideCcsImportButton] == "true", PurchaseSubscriptionEnabled: settings[SettingKeyPurchaseSubscriptionEnabled] == "true", PurchaseSubscriptionURL: strings.TrimSpace(settings[SettingKeyPurchaseSubscriptionURL]), + TableDefaultPageSize: tableDefaultPageSize, + TablePageSizeOptions: tablePageSizeOptions, CustomMenuItems: settings[SettingKeyCustomMenuItems], CustomEndpoints: settings[SettingKeyCustomEndpoints], LinuxDoOAuthEnabled: linuxDoEnabled, @@ -270,6 +279,8 @@ func (s *SettingService) GetPublicSettingsForInjection(ctx context.Context) (any HideCcsImportButton bool `json:"hide_ccs_import_button"` PurchaseSubscriptionEnabled bool `json:"purchase_subscription_enabled"` PurchaseSubscriptionURL string `json:"purchase_subscription_url,omitempty"` + TableDefaultPageSize int `json:"table_default_page_size"` + TablePageSizeOptions []int `json:"table_page_size_options"` CustomMenuItems json.RawMessage `json:"custom_menu_items"` CustomEndpoints json.RawMessage `json:"custom_endpoints"` LinuxDoOAuthEnabled bool `json:"linuxdo_oauth_enabled"` @@ -297,6 +308,8 @@ func (s *SettingService) GetPublicSettingsForInjection(ctx context.Context) (any HideCcsImportButton: settings.HideCcsImportButton, PurchaseSubscriptionEnabled: settings.PurchaseSubscriptionEnabled, PurchaseSubscriptionURL: settings.PurchaseSubscriptionURL, + TableDefaultPageSize: settings.TableDefaultPageSize, + TablePageSizeOptions: settings.TablePageSizeOptions, CustomMenuItems: filterUserVisibleMenuItems(settings.CustomMenuItems), CustomEndpoints: safeRawJSONArray(settings.CustomEndpoints), LinuxDoOAuthEnabled: settings.LinuxDoOAuthEnabled, @@ -522,6 +535,16 @@ func (s *SettingService) UpdateSettings(ctx context.Context, settings *SystemSet updates[SettingKeyHideCcsImportButton] = strconv.FormatBool(settings.HideCcsImportButton) updates[SettingKeyPurchaseSubscriptionEnabled] = strconv.FormatBool(settings.PurchaseSubscriptionEnabled) updates[SettingKeyPurchaseSubscriptionURL] = strings.TrimSpace(settings.PurchaseSubscriptionURL) + tableDefaultPageSize, tablePageSizeOptions := normalizeTablePreferences( + settings.TableDefaultPageSize, + settings.TablePageSizeOptions, + ) + updates[SettingKeyTableDefaultPageSize] = strconv.Itoa(tableDefaultPageSize) + tablePageSizeOptionsJSON, err := json.Marshal(tablePageSizeOptions) + if err != nil { + return fmt.Errorf("marshal table page size options: %w", err) + } + updates[SettingKeyTablePageSizeOptions] = string(tablePageSizeOptionsJSON) updates[SettingKeyCustomMenuItems] = settings.CustomMenuItems updates[SettingKeyCustomEndpoints] = settings.CustomEndpoints @@ -875,6 +898,8 @@ func (s *SettingService) InitializeDefaultSettings(ctx context.Context) error { SettingKeySiteLogo: "", SettingKeyPurchaseSubscriptionEnabled: "false", SettingKeyPurchaseSubscriptionURL: "", + SettingKeyTableDefaultPageSize: "20", + SettingKeyTablePageSizeOptions: "[10,20,50,100]", SettingKeyCustomMenuItems: "[]", SettingKeyCustomEndpoints: "[]", SettingKeyOIDCConnectEnabled: "false", @@ -946,6 +971,10 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin CustomEndpoints: settings[SettingKeyCustomEndpoints], BackendModeEnabled: settings[SettingKeyBackendModeEnabled] == "true", } + result.TableDefaultPageSize, result.TablePageSizeOptions = parseTablePreferences( + settings[SettingKeyTableDefaultPageSize], + settings[SettingKeyTablePageSizeOptions], + ) // 解析整数类型 if port, err := strconv.Atoi(settings[SettingKeySMTPPort]); err == nil { @@ -1221,6 +1250,50 @@ func parseDefaultSubscriptions(raw string) []DefaultSubscriptionSetting { return normalized } +func parseTablePreferences(defaultPageSizeRaw, optionsRaw string) (int, []int) { + defaultPageSize := 20 + if v, err := strconv.Atoi(strings.TrimSpace(defaultPageSizeRaw)); err == nil { + defaultPageSize = v + } + + var options []int + if strings.TrimSpace(optionsRaw) != "" { + _ = json.Unmarshal([]byte(optionsRaw), &options) + } + + return normalizeTablePreferences(defaultPageSize, options) +} + +func normalizeTablePreferences(defaultPageSize int, options []int) (int, []int) { + const minPageSize = 5 + const maxPageSize = 1000 + const fallbackPageSize = 20 + + seen := make(map[int]struct{}, len(options)) + normalizedOptions := make([]int, 0, len(options)) + for _, option := range options { + if option < minPageSize || option > maxPageSize { + continue + } + if _, ok := seen[option]; ok { + continue + } + seen[option] = struct{}{} + normalizedOptions = append(normalizedOptions, option) + } + sort.Ints(normalizedOptions) + + if defaultPageSize < minPageSize || defaultPageSize > maxPageSize { + defaultPageSize = fallbackPageSize + } + + if len(normalizedOptions) == 0 { + normalizedOptions = []int{10, 20, 50} + } + + return defaultPageSize, normalizedOptions +} + // getStringOrDefault 获取字符串值或默认值 func (s *SettingService) getStringOrDefault(settings map[string]string, key, defaultValue string) string { if value, ok := settings[key]; ok && value != "" { diff --git a/backend/internal/service/setting_service_public_test.go b/backend/internal/service/setting_service_public_test.go index b511cd29..6dfa627c 100644 --- a/backend/internal/service/setting_service_public_test.go +++ b/backend/internal/service/setting_service_public_test.go @@ -62,3 +62,18 @@ func TestSettingService_GetPublicSettings_ExposesRegistrationEmailSuffixWhitelis require.NoError(t, err) require.Equal(t, []string{"@example.com", "@foo.bar"}, settings.RegistrationEmailSuffixWhitelist) } + +func TestSettingService_GetPublicSettings_ExposesTablePreferences(t *testing.T) { + repo := &settingPublicRepoStub{ + values: map[string]string{ + SettingKeyTableDefaultPageSize: "50", + SettingKeyTablePageSizeOptions: "[20,50,100]", + }, + } + svc := NewSettingService(repo, &config.Config{}) + + settings, err := svc.GetPublicSettings(context.Background()) + require.NoError(t, err) + require.Equal(t, 50, settings.TableDefaultPageSize) + require.Equal(t, []int{20, 50, 100}, settings.TablePageSizeOptions) +} diff --git a/backend/internal/service/setting_service_update_test.go b/backend/internal/service/setting_service_update_test.go index 1de08611..28c7ad02 100644 --- a/backend/internal/service/setting_service_update_test.go +++ b/backend/internal/service/setting_service_update_test.go @@ -202,3 +202,24 @@ func TestParseDefaultSubscriptions_NormalizesValues(t *testing.T) { {GroupID: 12, ValidityDays: MaxValidityDays}, }, got) } + +func TestSettingService_UpdateSettings_TablePreferences(t *testing.T) { + repo := &settingUpdateRepoStub{} + svc := NewSettingService(repo, &config.Config{}) + + err := svc.UpdateSettings(context.Background(), &SystemSettings{ + TableDefaultPageSize: 50, + TablePageSizeOptions: []int{20, 50, 100}, + }) + require.NoError(t, err) + require.Equal(t, "50", repo.updates[SettingKeyTableDefaultPageSize]) + require.Equal(t, "[20,50,100]", repo.updates[SettingKeyTablePageSizeOptions]) + + err = svc.UpdateSettings(context.Background(), &SystemSettings{ + TableDefaultPageSize: 1000, + TablePageSizeOptions: []int{20, 100}, + }) + require.NoError(t, err) + require.Equal(t, "1000", repo.updates[SettingKeyTableDefaultPageSize]) + require.Equal(t, "[20,100]", repo.updates[SettingKeyTablePageSizeOptions]) +} diff --git a/backend/internal/service/settings_view.go b/backend/internal/service/settings_view.go index 80932e9d..5a96f086 100644 --- a/backend/internal/service/settings_view.go +++ b/backend/internal/service/settings_view.go @@ -66,6 +66,8 @@ type SystemSettings struct { HideCcsImportButton bool PurchaseSubscriptionEnabled bool PurchaseSubscriptionURL string + TableDefaultPageSize int + TablePageSizeOptions []int CustomMenuItems string // JSON array of custom menu items CustomEndpoints string // JSON array of custom endpoints @@ -132,6 +134,8 @@ type PublicSettings struct { PurchaseSubscriptionEnabled bool PurchaseSubscriptionURL string + TableDefaultPageSize int + TablePageSizeOptions []int CustomMenuItems string // JSON array of custom menu items CustomEndpoints string // JSON array of custom endpoints diff --git a/frontend/package.json b/frontend/package.json index d2a6dede..49868cd9 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -18,7 +18,7 @@ "@lobehub/icons": "^4.0.2", "@tanstack/vue-virtual": "^3.13.23", "@vueuse/core": "^10.7.0", - "axios": "^1.13.5", + "axios": "^1.15.0", "chart.js": "^4.4.1", "dompurify": "^3.3.1", "driver.js": "^1.4.0", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 505b72f3..ea96892c 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -18,8 +18,8 @@ importers: specifier: ^10.7.0 version: 10.11.1(vue@3.5.26(typescript@5.6.3)) axios: - specifier: ^1.13.5 - version: 1.13.5 + specifier: ^1.15.0 + version: 1.15.0 chart.js: specifier: ^4.4.1 version: 4.5.1 @@ -134,11 +134,11 @@ packages: resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} - '@ant-design/colors@8.0.0': - resolution: {integrity: sha512-6YzkKCw30EI/E9kHOIXsQDHmMvTllT8STzjMb4K2qzit33RW2pqCJP0sk+hidBntXxE+Vz4n1+RvCTfBw6OErw==} + '@ant-design/colors@8.0.1': + resolution: {integrity: sha512-foPVl0+SWIslGUtD/xBr1p9U4AKzPhNYEseXYRRo5QSzGACYZrQbe11AYJbYfAWnWSpGBx6JjBmSeugUsD9vqQ==} - '@ant-design/cssinjs-utils@2.0.2': - resolution: {integrity: sha512-Mq3Hm6fJuQeFNKSp3+yT4bjuhVbdrsyXE2RyfpJFL0xiYNZdaJ6oFaE3zFrzmHbmvTd2Wp3HCbRtkD4fU+v2ZA==} + '@ant-design/cssinjs-utils@2.1.2': + resolution: {integrity: sha512-5fTHQ158jJJ5dC/ECeyIdZUzKxE/mpEMRZxthyG1sw/AKRHKgJBg00Yi6ACVXgycdje7KahRNvNET/uBccwCnA==} peerDependencies: react: '>=18' react-dom: '>=18' @@ -149,15 +149,21 @@ packages: react: '>=16.0.0' react-dom: '>=16.0.0' - '@ant-design/fast-color@3.0.0': - resolution: {integrity: sha512-eqvpP7xEDm2S7dUzl5srEQCBTXZMmY3ekf97zI+M2DHOYyKdJGH0qua0JACHTqbkRnD/KHFQP9J1uMJ/XWVzzA==} + '@ant-design/cssinjs@2.1.2': + resolution: {integrity: sha512-2Hy8BnCEH31xPeSLbhhB2ctCPXE2ZnASdi+KbSeS79BNbUhL9hAEe20SkUk+BR8aKTmqb6+FKFruk7w8z0VoRQ==} + peerDependencies: + react: '>=16.0.0' + react-dom: '>=16.0.0' + + '@ant-design/fast-color@3.0.1': + resolution: {integrity: sha512-esKJegpW4nckh0o6kV3Tkb7NPIZYbPnnFxmQDUmL08ukXZAvV85TZBr70eGuke/CIArLaP6aw8lt9KILjnWuOw==} engines: {node: '>=8.x'} '@ant-design/icons-svg@4.4.2': resolution: {integrity: sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==} - '@ant-design/icons@6.1.0': - resolution: {integrity: sha512-KrWMu1fIg3w/1F2zfn+JlfNDU8dDqILfA5Tg85iqs1lf8ooyGlbkA+TkwfOKKgqpUmAiRY1PTFpuOU2DAIgSUg==} + '@ant-design/icons@6.1.1': + resolution: {integrity: sha512-AMT4N2y++TZETNHiM77fs4a0uPVCJGuL5MTonk13Pvv7UN7sID1cNEZOc1qNqx6zLKAOilTEFAdAoAFKa0U//Q==} engines: {node: '>=8'} peerDependencies: react: '>=16.0.0' @@ -208,6 +214,10 @@ packages: resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} engines: {node: '>=6.9.0'} + '@babel/runtime@7.29.2': + resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} + engines: {node: '>=6.9.0'} + '@babel/template@7.27.2': resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} engines: {node: '>=6.9.0'} @@ -220,8 +230,8 @@ packages: resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} engines: {node: '>=6.9.0'} - '@base-ui/react@1.0.0': - resolution: {integrity: sha512-4USBWz++DUSLTuIYpbYkSgy1F9ZmNG9S/lXvlUN6qMK0P0RlW+6eQmDUB4DgZ7HVvtXl4pvi4z5J2fv6Z3+9hg==} + '@base-ui/react@1.3.0': + resolution: {integrity: sha512-FwpKqZbPz14AITp1CVgf4AjhKPe1OeeVKSBMdgD10zbFlj3QSWelmtCMLi2+/PFZZcIm3l87G7rwtCZJwHyXWA==} engines: {node: '>=14.0.0'} peerDependencies: '@types/react': ^17 || ^18 || ^19 @@ -231,8 +241,8 @@ packages: '@types/react': optional: true - '@base-ui/utils@0.2.3': - resolution: {integrity: sha512-/CguQ2PDaOzeVOkllQR8nocJ0FFIDqsWIcURsVmm53QGo8NhFNpePjNlyPIB41luxfOqnG7PU0xicMEw3ls7XQ==} + '@base-ui/utils@0.2.6': + resolution: {integrity: sha512-yQ+qeuqohwhsNpoYDqqXaLllYAkPCP4vYdDrVo8FQXaAPfHWm1pG/Vm+jmGTA5JFS0BAIjookyapuJFY8F9PIw==} peerDependencies: '@types/react': ^17 || ^18 || ^19 react: ^17 || ^18 || ^19 @@ -244,23 +254,23 @@ packages: '@bcoe/v8-coverage@0.2.3': resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} - '@braintree/sanitize-url@7.1.1': - resolution: {integrity: sha512-i1L7noDNxtFyL5DmZafWy1wRVhGehQmzZaz1HiN5e7iylJMSZR7ekOV7NsIqa5qBldlLrsKv4HbgFUVlQrz8Mw==} + '@braintree/sanitize-url@7.1.2': + resolution: {integrity: sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==} - '@chevrotain/cst-dts-gen@11.0.3': - resolution: {integrity: sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==} + '@chevrotain/cst-dts-gen@12.0.0': + resolution: {integrity: sha512-fSL4KXjTl7cDgf0B5Rip9Q05BOrYvkJV/RrBTE/bKDN096E4hN/ySpcBK5B24T76dlQ2i32Zc3PAE27jFnFrKg==} - '@chevrotain/gast@11.0.3': - resolution: {integrity: sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==} + '@chevrotain/gast@12.0.0': + resolution: {integrity: sha512-1ne/m3XsIT8aEdrvT33so0GUC+wkctpUPK6zU9IlOyJLUbR0rg4G7ZiApiJbggpgPir9ERy3FRjT6T7lpgetnQ==} - '@chevrotain/regexp-to-ast@11.0.3': - resolution: {integrity: sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==} + '@chevrotain/regexp-to-ast@12.0.0': + resolution: {integrity: sha512-p+EW9MaJwgaHguhoqwOtx/FwuGr+DnNn857sXWOi/mClXIkPGl3rn7hGNWvo31HA3vyeQxjqe+H36yZJwYU8cA==} - '@chevrotain/types@11.0.3': - resolution: {integrity: sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==} + '@chevrotain/types@12.0.0': + resolution: {integrity: sha512-S+04vjFQKeuYw0/eW3U52LkAHQsB1ASxsPGsLPUyQgrZ2iNNibQrsidruDzjEX2JYfespXMG0eZmXlhA6z7nWA==} - '@chevrotain/utils@11.0.3': - resolution: {integrity: sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==} + '@chevrotain/utils@12.0.0': + resolution: {integrity: sha512-lB59uJoaGIfOOL9knQqQRfhl9g7x8/wqFkp13zTdkRu1huG9kg6IJs1O8hqj9rs6h7orGxHJUKb+mX3rPbWGhA==} '@csstools/color-helpers@5.1.0': resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} @@ -536,26 +546,26 @@ packages: resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - '@floating-ui/core@1.7.3': - resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==} + '@floating-ui/core@1.7.5': + resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} - '@floating-ui/dom@1.7.4': - resolution: {integrity: sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==} + '@floating-ui/dom@1.7.6': + resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} - '@floating-ui/react-dom@2.1.6': - resolution: {integrity: sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==} + '@floating-ui/react-dom@2.1.8': + resolution: {integrity: sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==} peerDependencies: react: '>=16.8.0' react-dom: '>=16.8.0' - '@floating-ui/react@0.27.16': - resolution: {integrity: sha512-9O8N4SeG2z++TSM8QA/KTeKFBVCNEz/AGS7gWPJf6KFRzmRWixFRnCnkPHRDwSVZW6QPDO6uT0P2SpWNKCc9/g==} + '@floating-ui/react@0.27.19': + resolution: {integrity: sha512-31B8h5mm8YxotlE7/AU/PhNAl8eWxAmjL/v2QOxroDNkTFLk3Uu82u63N3b6TXa4EGJeeZLVcd/9AlNlVqzeog==} peerDependencies: react: '>=17.0.0' react-dom: '>=17.0.0' - '@floating-ui/utils@0.2.10': - resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + '@floating-ui/utils@0.2.11': + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} '@giscus/react@3.1.0': resolution: {integrity: sha512-0TCO2TvL43+oOdyVVGHDItwxD1UMKP2ZYpT6gXmhFOqfAJtZxTzJ9hkn34iAF/b6YzyJ4Um89QIt9z/ajmAEeg==} @@ -618,8 +628,8 @@ packages: '@kurkle/color@0.3.4': resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==} - '@lit-labs/ssr-dom-shim@1.5.0': - resolution: {integrity: sha512-HLomZXMmrCFHSRKESF5vklAKsDY7/fsT/ZhqCu3V0UoW/Qbv8wxmO4W9bx4KnCCF2Zak4yuk+AGraK/bPmI4kA==} + '@lit-labs/ssr-dom-shim@1.5.1': + resolution: {integrity: sha512-Aou5UdlSpr5whQe8AA/bZG0jMj96CoJIWbGfZ91qieWu5AWUMKw8VR/pAkQkJYvBNhmCcWnZlyyk5oze8JIqYA==} '@lit/reactive-element@2.1.2': resolution: {integrity: sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A==} @@ -660,8 +670,8 @@ packages: '@types/react': '>=16' react: '>=16' - '@mermaid-js/parser@0.6.3': - resolution: {integrity: sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA==} + '@mermaid-js/parser@1.1.0': + resolution: {integrity: sha512-gxK9ZX2+Fex5zu8LhRQoMeMPEHbc73UKZ0FQ54YrQtUxE1VVhMwzeNtKRPAu5aXks4FasbMe4xB4bWrmq6Jlxw==} '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} @@ -682,8 +692,8 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} - '@primer/octicons@19.21.1': - resolution: {integrity: sha512-7tgtBkCNcg75YJnckinzvES+uxysYQCe+CHSEnzr3VYgxttzKRvfmrnVogl3aEuHCQP4xhiE9k2lFDhYwGtTzQ==} + '@primer/octicons@19.23.1': + resolution: {integrity: sha512-CzjGmxkmNhyst6EekrS3SJPdtzgIkUMP/LSJch65y99/kmiFXbO1a+q7zoYe3hnI9NaOM0IN+ydDIbOmd8YqcA==} '@radix-ui/primitive@1.1.3': resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} @@ -929,8 +939,8 @@ packages: '@radix-ui/rect@1.1.1': resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} - '@rc-component/async-validator@5.0.4': - resolution: {integrity: sha512-qgGdcVIF604M9EqjNF0hbUTz42bz/RDtxWdWuU5EQe3hi7M8ob54B6B35rOsvX5eSvIHIzT9iH1R3n+hk3CGfg==} + '@rc-component/async-validator@5.1.0': + resolution: {integrity: sha512-n4HcR5siNUXRX23nDizbZBQPO0ZM/5oTtmKZ6/eqL0L2bo747cklFdZGRN2f+c9qWGICwDzrhW0H7tE9PptdcA==} engines: {node: '>=14.x'} '@rc-component/cascader@1.10.0': @@ -981,8 +991,8 @@ packages: react: '>=16.11.0' react-dom: '>=16.11.0' - '@rc-component/form@1.6.0': - resolution: {integrity: sha512-A7vrN8kExtw4sW06mrsgCb1rowhvBFFvQU6Bk/NL0Fj6Wet/5GF0QnGCxBu/sG3JI9FEhsJWES0D44BW2d0hzg==} + '@rc-component/form@1.6.2': + resolution: {integrity: sha512-OgIn2RAoaSBqaIgzJf/X6iflIa9LpTozci1lagLBdURDFhGA370v0+T0tXxOi8YShMjTha531sFhwtnrv+EJaQ==} engines: {node: '>=8.x'} peerDependencies: react: '>=16.9.0' @@ -1018,8 +1028,8 @@ packages: react: '>=16.9.0' react-dom: '>=16.9.0' - '@rc-component/mini-decimal@1.1.0': - resolution: {integrity: sha512-jS4E7T9Li2GuYwI6PyiVXmxTiM6b07rlD9Ge8uGZSCz3WlzcG5ZK7g5bbuKNeZ9pgUuPK/5guV781ujdVpm4HQ==} + '@rc-component/mini-decimal@1.1.3': + resolution: {integrity: sha512-bk/FJ09fLf+NLODMAFll6CfYrHPBioTedhW6lxDBuuWucJEqFUd4l/D/5JgIi3dina6sYahB8iuPAZTNz2pMxw==} engines: {node: '>=8.x'} '@rc-component/motion@1.1.6': @@ -1054,8 +1064,8 @@ packages: react: '>=16.9.0' react-dom: '>=16.9.0' - '@rc-component/picker@1.9.0': - resolution: {integrity: sha512-OLisdk8AWVCG9goBU1dWzuH5QlBQk8jktmQ6p0/IyBFwdKGwyIZOSjnBYo8hooHiTdl0lU+wGf/OfMtVBw02KQ==} + '@rc-component/picker@1.9.1': + resolution: {integrity: sha512-9FBYYsvH3HMLICaPDA/1Th5FLaDkFa7qAtangIdlhKb3ZALaR745e9PsOhheJb6asS4QXc12ffiAcjdkZ4C5/g==} engines: {node: '>=12.x'} peerDependencies: date-fns: '>= 2.x' @@ -1108,8 +1118,8 @@ packages: react: '>=16.9.0' react-dom: '>=16.9.0' - '@rc-component/resize-observer@1.0.1': - resolution: {integrity: sha512-r+w+Mz1EiueGk1IgjB3ptNXLYSLZ5vnEfKHH+gfgj7JMupftyzvUUl3fRcMZe5uMM04x0n8+G2o/c6nlO2+Wag==} + '@rc-component/resize-observer@1.1.2': + resolution: {integrity: sha512-t/Bb0W8uvL4PYKAB3YcChC+DlHh0Wt5kM7q/J+0qpVEUMLe7Hk5zuvc9km0hMnTFPSx5Z7Wu/fzCLN6erVLE8Q==} peerDependencies: react: '>=16.9.0' react-dom: '>=16.9.0' @@ -1193,15 +1203,15 @@ packages: react: '*' react-dom: '*' - '@rc-component/trigger@2.3.0': - resolution: {integrity: sha512-iwaxZyzOuK0D7lS+0AQEtW52zUWxoGqTGkke3dRyb8pYiShmRpCjB/8TzPI4R6YySCH7Vm9BZj/31VPiiQTLBg==} + '@rc-component/trigger@2.3.1': + resolution: {integrity: sha512-ORENF39PeXTzM+gQEshuk460Z8N4+6DkjpxlpE7Q3gYy1iBpLrx0FOJz3h62ryrJZ/3zCAUIkT1Pb/8hHWpb3A==} engines: {node: '>=8.x'} peerDependencies: react: '>=16.9.0' react-dom: '>=16.9.0' - '@rc-component/trigger@3.8.1': - resolution: {integrity: sha512-walnDJnKq+OcPQFHBMN+YZmdHV8+6z75+Rgpc0dW1c+Dmy6O7tRueDs4LdbwjlryQfTdsw84PIkNPzcx5yQ7qQ==} + '@rc-component/trigger@3.9.0': + resolution: {integrity: sha512-X8btpwfrT27AgrZVOz4swclhEHTZcqaHeQMXXBgveagOiakTa36uObXbdwerXffgV8G9dH1fAAE0DHtVQs8EHg==} engines: {node: '>=8.x'} peerDependencies: react: '>=18.0.0' @@ -1213,6 +1223,12 @@ packages: react: '>=16.9.0' react-dom: '>=16.9.0' + '@rc-component/util@1.10.1': + resolution: {integrity: sha512-q++9S6rUa5Idb/xIBNz6jtvumw5+O5YV5V0g4iK9mn9jWs4oGJheE3ZN1kAnE723AXyaD8v95yeOASmdk8Jnng==} + peerDependencies: + react: '>=18.0.0' + react-dom: '>=18.0.0' + '@rc-component/util@1.7.0': resolution: {integrity: sha512-tIvIGj4Vl6fsZFvWSkYw9sAfiCKUXMyhVz6kpKyZbwyZyRPqv2vxYZROdaO1VB4gqTNvUZFXh6i3APUiterw5g==} peerDependencies: @@ -1347,26 +1363,26 @@ packages: cpu: [x64] os: [win32] - '@shikijs/core@3.20.0': - resolution: {integrity: sha512-f2ED7HYV4JEk827mtMDwe/yQ25pRiXZmtHjWF8uzZKuKiEsJR7Ce1nuQ+HhV9FzDcbIo4ObBCD9GPTzNuy9S1g==} + '@shikijs/core@3.23.0': + resolution: {integrity: sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA==} - '@shikijs/engine-javascript@3.20.0': - resolution: {integrity: sha512-OFx8fHAZuk7I42Z9YAdZ95To6jDePQ9Rnfbw9uSRTSbBhYBp1kEOKv/3jOimcj3VRUKusDYM6DswLauwfhboLg==} + '@shikijs/engine-javascript@3.23.0': + resolution: {integrity: sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA==} - '@shikijs/engine-oniguruma@3.20.0': - resolution: {integrity: sha512-Yx3gy7xLzM0ZOjqoxciHjA7dAt5tyzJE3L4uQoM83agahy+PlW244XJSrmJRSBvGYELDhYXPacD4R/cauV5bzQ==} + '@shikijs/engine-oniguruma@3.23.0': + resolution: {integrity: sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g==} - '@shikijs/langs@3.20.0': - resolution: {integrity: sha512-le+bssCxcSHrygCWuOrYJHvjus6zhQ2K7q/0mgjiffRbkhM4o1EWu2m+29l0yEsHDbWaWPNnDUTRVVBvBBeKaA==} + '@shikijs/langs@3.23.0': + resolution: {integrity: sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg==} - '@shikijs/themes@3.20.0': - resolution: {integrity: sha512-U1NSU7Sl26Q7ErRvJUouArxfM2euWqq1xaSrbqMu2iqa+tSp0D1Yah8216sDYbdDHw4C8b75UpE65eWorm2erQ==} + '@shikijs/themes@3.23.0': + resolution: {integrity: sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA==} - '@shikijs/transformers@3.20.0': - resolution: {integrity: sha512-PrHHMRr3Q5W1qB/42kJW6laqFyWdhrPF2hNR9qjOm1xcSiAO3hAHo7HaVyHE6pMyevmy3i51O8kuGGXC78uK3g==} + '@shikijs/transformers@3.23.0': + resolution: {integrity: sha512-F9msZVxdF+krQNSdQ4V+Ja5QemeAoTQ2jxt7nJCwhDsdF1JWS3KxIQXA3lQbyKwS3J61oHRUSv4jYWv3CkaKTQ==} - '@shikijs/types@3.20.0': - resolution: {integrity: sha512-lhYAATn10nkZcBQ0BlzSbJA3wcmL5MXUUF8d2Zzon6saZDlToKaiRX60n2+ZaHJCmXEcZRWNzn+k9vplr8Jhsw==} + '@shikijs/types@3.23.0': + resolution: {integrity: sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ==} '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} @@ -1459,8 +1475,8 @@ packages: '@types/d3-selection@3.0.11': resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==} - '@types/d3-shape@3.1.7': - resolution: {integrity: sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==} + '@types/d3-shape@3.1.8': + resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==} '@types/d3-time-format@4.0.3': resolution: {integrity: sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==} @@ -1480,8 +1496,8 @@ packages: '@types/d3@7.4.3': resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==} - '@types/debug@4.1.12': - resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + '@types/debug@4.1.13': + resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} '@types/dompurify@3.2.0': resolution: {integrity: sha512-Fgg31wv9QbLDA0SpTOXO3MaxySc4DKGLi8sna4/Utjo4r3ZRPdCt4UQee8BWr+Q5z21yifghREPJGYaEOEIACg==} @@ -1505,8 +1521,8 @@ packages: '@types/js-cookie@3.0.6': resolution: {integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==} - '@types/katex@0.16.7': - resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==} + '@types/katex@0.16.8': + resolution: {integrity: sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==} '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} @@ -1605,6 +1621,9 @@ packages: '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + '@upsetjs/venn.js@2.0.0': + resolution: {integrity: sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==} + '@use-gesture/core@10.3.1': resolution: {integrity: sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==} @@ -1736,6 +1755,11 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + adler-32@1.3.1: resolution: {integrity: sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==} engines: {node: '>=0.8'} @@ -1744,8 +1768,8 @@ packages: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} - ahooks@3.9.6: - resolution: {integrity: sha512-Mr7f05swd5SmKlR9SZo5U6M0LsL4ErweLzpdgXjA1JPmnZ78Vr6wzx0jUtvoxrcqGKYnX0Yjc02iEASVxHFPjQ==} + ahooks@3.9.7: + resolution: {integrity: sha512-S0lvzhbdlhK36RFBkGv+RbOM/dbbweym+BIHM/bwwuWVSVN5TuVErHPMWo4w0t1NDYg5KPp2iEf7Y7E5LASYiw==} 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 @@ -1827,8 +1851,8 @@ packages: peerDependencies: postcss: ^8.1.0 - axios@1.13.5: - resolution: {integrity: sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==} + axios@1.15.0: + resolution: {integrity: sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==} babel-plugin-macros@3.1.0: resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} @@ -1924,13 +1948,14 @@ packages: resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} engines: {node: '>= 16'} - chevrotain-allstar@0.3.1: - resolution: {integrity: sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==} + chevrotain-allstar@0.4.1: + resolution: {integrity: sha512-PvVJm3oGqrveUVW2Vt/eZGeiAIsJszYweUcYwcskg9e+IubNYKKD+rHHem7A6XVO22eDAL+inxNIGAzZ/VIWlA==} peerDependencies: - chevrotain: ^11.0.0 + chevrotain: ^12.0.0 - chevrotain@11.0.3: - resolution: {integrity: sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==} + chevrotain@12.0.0: + resolution: {integrity: sha512-csJvb+6kEiQaqo1woTdSAuOWdN0WTLIydkKrBnS+V5gZz0oqBrp4kQ35519QgK6TpBThiG3V1vNSHlIkv4AglQ==} + engines: {node: '>=22.0.0'} chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} @@ -1952,10 +1977,6 @@ packages: cliui@6.0.0: resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} - clsx@1.2.1: - resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==} - engines: {node: '>=6'} - clsx@2.1.1: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} @@ -2056,8 +2077,8 @@ packages: peerDependencies: cytoscape: ^3.2.0 - cytoscape@3.33.1: - resolution: {integrity: sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==} + cytoscape@3.33.2: + resolution: {integrity: sha512-sj4HXd3DokGhzZAdjDejGvTPLqlt84vNFN8m7bGsOzDY5DyVcxIb2ejIXat2Iy7HxWhdT/N1oKyheJ5YdpsGuw==} engines: {node: '>=0.10'} d3-array@2.12.1: @@ -2116,8 +2137,8 @@ packages: resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==} engines: {node: '>=12'} - d3-format@3.1.0: - resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==} + d3-format@3.1.2: + resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==} engines: {node: '>=12'} d3-geo@3.1.1: @@ -2199,15 +2220,15 @@ packages: resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==} engines: {node: '>=12'} - dagre-d3-es@7.0.13: - resolution: {integrity: sha512-efEhnxpSuwpYOKRm/L5KbqoZmNNukHa/Flty4Wp62JRvgH2ojwVgPgdYyr4twpieZnyRDdIH7PY2mopX26+j2Q==} + dagre-d3-es@7.0.14: + resolution: {integrity: sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==} data-urls@5.0.0: resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} engines: {node: '>=18'} - dayjs@1.11.19: - resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==} + dayjs@1.11.20: + resolution: {integrity: sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==} de-indent@1.0.2: resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} @@ -2228,8 +2249,8 @@ packages: decimal.js@10.6.0: resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} - decode-named-character-reference@1.2.0: - resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==} + decode-named-character-reference@1.3.0: + resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} decode-uri-component@0.4.1: resolution: {integrity: sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ==} @@ -2242,8 +2263,8 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - delaunator@5.0.1: - resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==} + delaunator@5.1.0: + resolution: {integrity: sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==} delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} @@ -2276,6 +2297,9 @@ packages: dompurify@3.3.1: resolution: {integrity: sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==} + dompurify@3.3.3: + resolution: {integrity: sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==} + driver.js@1.4.0: resolution: {integrity: sha512-Gm64jm6PmcU+si21sQhBrTAM1JvUrR0QhNmjkprNLxohOBzul9+pNHXgQaT9lW84gwg9GMLB3NZGuGolsz5uew==} @@ -2336,8 +2360,8 @@ packages: resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} engines: {node: '>= 0.4'} - es-toolkit@1.43.0: - resolution: {integrity: sha512-SKCT8AsWvYzBBuUqMk4NPwFlSdqLpJwmy6AP322ERn8W2YLIB6JBXnwMI2Qsh2gfphT3q7EKAxKb23cvFHFwKA==} + es-toolkit@1.45.1: + resolution: {integrity: sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==} esast-util-from-estree@2.0.0: resolution: {integrity: sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==} @@ -2531,8 +2555,8 @@ packages: fraction.js@5.3.4: resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} - framer-motion@12.23.26: - resolution: {integrity: sha512-cPcIhgR42xBn1Uj+PzOyheMtZ73H927+uWPDVhUMqxy8UHt6Okavb6xIz9J/phFUHUj0OncR6UvMfJTXoc/LKA==} + framer-motion@12.38.0: + resolution: {integrity: sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==} peerDependencies: '@emotion/is-prop-valid': '*' react: ^18.0.0 || ^19.0.0 @@ -2560,8 +2584,8 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} - get-east-asian-width@1.4.0: - resolution: {integrity: sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==} + get-east-asian-width@1.5.0: + resolution: {integrity: sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==} engines: {node: '>=18'} get-intrinsic@1.3.0: @@ -2707,8 +2731,8 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} - immer@11.1.3: - resolution: {integrity: sha512-6jQTc5z0KJFtr1UgFpIL3N9XSC3saRaI9PwWtzM2pSqkNGtiNkYY2OSwkOGDK2XcTRcLb1pi/aNkKZz0nxVH4Q==} + immer@11.1.4: + resolution: {integrity: sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==} import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} @@ -2882,8 +2906,8 @@ packages: json2mq@0.2.0: resolution: {integrity: sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==} - katex@0.16.27: - resolution: {integrity: sha512-aeQoDkuRWSqQN6nSvVCEFvfXdqo1OQiCmmW1kc9xSdjutPv7BGO7pqY9sQRJpMOGrEdfDgF2TfRXe5eUAD2Waw==} + katex@0.16.45: + resolution: {integrity: sha512-pQpZbdBu7wCTmQUh7ufPmLr0pFoObnGUoL/yhtwJDgmmQpbkg/0HSVti25Fu4rmd1oCR6NGWe9vqTWuWv3GcNA==} hasBin: true keyv@4.5.4: @@ -2892,9 +2916,9 @@ packages: khroma@2.1.0: resolution: {integrity: sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==} - langium@3.3.1: - resolution: {integrity: sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w==} - engines: {node: '>=16.0.0'} + langium@4.2.2: + resolution: {integrity: sha512-JUshTRAfHI4/MF9dH2WupvjSXyn8JBuUEWazB8ZVJUtXutT0doDlAv1XKbZ1Pb5sMexa8FF4CFBc0iiul7gbUQ==} + engines: {node: '>=20.10.0', npm: '>=10.2.3'} layout-base@1.0.2: resolution: {integrity: sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==} @@ -2936,11 +2960,8 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} - lodash-es@4.17.21: - resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} - - lodash-es@4.17.22: - resolution: {integrity: sha512-XEawp1t0gxSi9x01glktRZ5HDy0HXqrM0x5pXQM98EaI0NxO6jVM7omDOxsuEo5UIASAnm2bRp1Jt/e0a2XU8Q==} + lodash-es@4.18.1: + resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==} lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} @@ -2948,6 +2969,9 @@ packages: lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} @@ -2998,6 +3022,11 @@ packages: engines: {node: '>= 20'} hasBin: true + marked@17.0.6: + resolution: {integrity: sha512-gB0gkNafnonOw0obSTEGZTT86IuhILt2Wfx0mWH/1Au83kybTayroZ/V6nS25mN7u8ASy+5fMhgB3XPNrOZdmA==} + engines: {node: '>= 20'} + hasBin: true + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -3005,8 +3034,8 @@ packages: mdast-util-find-and-replace@3.0.2: resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} - mdast-util-from-markdown@2.0.2: - resolution: {integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==} + mdast-util-from-markdown@2.0.3: + resolution: {integrity: sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==} mdast-util-gfm-autolink-literal@2.0.1: resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} @@ -3064,8 +3093,8 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} - mermaid@11.12.2: - resolution: {integrity: sha512-n34QPDPEKmaeCG4WDMGy0OT6PSyxKCfy2pJgShP+Qow2KLrvWjclwbc3yXfSIf4BanqWEhQEpngWwNp/XhZt6w==} + mermaid@11.14.0: + resolution: {integrity: sha512-GSGloRsBs+JINmmhl0JDwjpuezCsHB4WGI4NASHxL3fHo3o/BRXTxhDLKnln8/Q0lRFRyDdEjmk1/d5Sn1Xz8g==} micromark-core-commonmark@2.0.3: resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} @@ -3225,14 +3254,14 @@ packages: resolution: {integrity: sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==} engines: {node: '>=0.10.0'} - mlly@1.8.0: - resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} + mlly@1.8.2: + resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} - motion-dom@12.23.23: - resolution: {integrity: sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==} + motion-dom@12.38.0: + resolution: {integrity: sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==} - motion-utils@12.23.6: - resolution: {integrity: sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==} + motion-utils@12.36.0: + resolution: {integrity: sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==} motion@12.23.26: resolution: {integrity: sha512-Ll8XhVxY8LXMVYTCfme27WH2GjBrCIzY4+ndr5QKxsK+YwCtOi2B/oBi5jcIbik5doXuWT/4KKDOVAZJkeY5VQ==} @@ -3308,8 +3337,8 @@ packages: oniguruma-parser@0.12.1: resolution: {integrity: sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==} - oniguruma-to-es@4.3.4: - resolution: {integrity: sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA==} + oniguruma-to-es@4.3.5: + resolution: {integrity: sha512-Zjygswjpsewa0NLTsiizVuMQZbp0MDyM6lIt66OxsF21npUDlzpHi1Mgb/qhQdkb+dWFTzJmFbEWdvZgRho8eQ==} optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} @@ -3503,8 +3532,9 @@ packages: proto-list@1.2.4: resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} - proxy-from-env@1.1.0: - resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + proxy-from-env@2.1.0: + resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} + engines: {node: '>=10'} psl@1.15.0: resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} @@ -3617,8 +3647,8 @@ packages: peerDependencies: react: ^19.2.3 - react-draggable@4.4.6: - resolution: {integrity: sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw==} + react-draggable@4.5.0: + resolution: {integrity: sha512-VC+HBLEZ0XJxnOxVAZsdRi8rD04Iz3SiiKOoYzamjylUcju/hP9np/aZdLHf/7WOD268WMoNJMvYfB5yAK45cw==} peerDependencies: react: '>= 16.3.0' react-dom: '>= 16.3.0' @@ -3629,17 +3659,16 @@ packages: peerDependencies: react: '>= 16.8' - react-error-boundary@6.0.1: - resolution: {integrity: sha512-zArgQpjJUN1ZLMEKWtifxQweW3yfvwL5j2nh3Pesze1qG6r5oCDMy/TA97bUF01wy4xCeeL4/pd8GHmvEsP3Bg==} + react-error-boundary@6.1.1: + resolution: {integrity: sha512-BrYwPOdXi5mqkk5lw+Uvt0ThHx32rCt3BkukS4X23A2AIWDPSGX6iaWTc0y9TU/mHDA/6qOSGel+B2ERkOvD1w==} peerDependencies: react: ^18.0.0 || ^19.0.0 - react-dom: ^18.0.0 || ^19.0.0 react-fast-compare@3.2.2: resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} - react-hotkeys-hook@5.2.1: - resolution: {integrity: sha512-xbKh6zJxd/vJHT4Bw4+0pBD662Fk20V+VFhLqciCg+manTVO4qlqRqiwFOYelfHN9dBvWj9vxaPkSS26ZSIJGg==} + react-hotkeys-hook@5.2.4: + resolution: {integrity: sha512-BgKg+A1+TawkYluh5Bo4cTmcgMN5L29uhJbDUQdHwPX+qgXRjIPYU5kIDHyxnAwCkCBiu9V5OpB2mpyeluVF2A==} peerDependencies: react: '>=16.8.0' react-dom: '>=16.8.0' @@ -3664,8 +3693,8 @@ packages: react: optional: true - react-rnd@10.5.2: - resolution: {integrity: sha512-0Tm4x7k7pfHf2snewJA8x7Nwgt3LV+58MVEWOVsFjk51eYruFEa6Wy7BNdxt4/lH0wIRsu7Gm3KjSXY2w7YaNw==} + react-rnd@10.5.3: + resolution: {integrity: sha512-s/sIT3pGZnQ+57egijkTp9mizjIWrJz68Pq6yd+F/wniFY3IriML18dUXnQe/HP9uMiJ+9MAp44hljG99fZu6Q==} peerDependencies: react: '>=16.3.0' react-dom: '>=16.3.0' @@ -3795,8 +3824,8 @@ packages: deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true - robust-predicates@3.0.2: - resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==} + robust-predicates@3.0.3: + resolution: {integrity: sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==} rollup@4.54.0: resolution: {integrity: sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==} @@ -3858,19 +3887,22 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - shiki-stream@0.1.3: - resolution: {integrity: sha512-pDIqmaP/zJWHNV8bJKp0tD0CZ6OkF+lWTIvmNRLktlTjBjN3+durr19JarS657U1oSEf/WrSYmdzwr9CeD6m2Q==} + shiki-stream@0.1.4: + resolution: {integrity: sha512-4pz6JGSDmVTTkPJ/ueixHkFAXY4ySCc+unvCaDZV7hqq/sdJZirRxgIXSuNSKgiFlGTgRR97sdu2R8K55sPsrw==} peerDependencies: react: ^19.0.0 + solid-js: ^1.9.0 vue: ^3.2.0 peerDependenciesMeta: react: optional: true + solid-js: + optional: true vue: optional: true - shiki@3.20.0: - resolution: {integrity: sha512-kgCOlsnyWb+p0WU+01RjkCH+eBVsjL1jOwUYWv0YDWkM2/A46+LDKVs5yZCUXjJG6bj4ndFoAg5iLIIue6dulg==} + shiki@3.23.0: + resolution: {integrity: sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA==} siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -3967,8 +3999,8 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - swr@2.3.8: - resolution: {integrity: sha512-gaCPRVoMq8WGDcWj9p4YWzCMPHzE0WNl6W8ADIx9c3JBEIdMkJGMzW+uzXvxHMltwcYACr9jP+32H8/hgwMR7w==} + swr@2.4.1: + resolution: {integrity: sha512-2CC6CiKQtEwaEeNiqWTAw9PGykW8SR5zZX8MZk6TeAvEAnVS7Visz8WzphqgtQ8v2xz/4Q5K+j+SeMaKXeeQIA==} peerDependencies: react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -4010,8 +4042,8 @@ packages: tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} - tinyexec@1.0.2: - resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} + tinyexec@1.1.1: + resolution: {integrity: sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==} engines: {node: '>=18'} tinyglobby@0.2.15: @@ -4087,8 +4119,8 @@ packages: engines: {node: '>=14.17'} hasBin: true - ufo@1.6.1: - resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} + ufo@1.6.3: + resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} @@ -4121,8 +4153,8 @@ packages: unist-util-visit-parents@6.0.2: resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} - unist-util-visit@5.0.0: - resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + unist-util-visit@5.1.0: + resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} universalify@0.2.0: resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} @@ -4289,9 +4321,6 @@ packages: resolution: {integrity: sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==} hasBin: true - vscode-uri@3.0.8: - resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==} - vscode-uri@3.1.0: resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} @@ -4487,15 +4516,15 @@ snapshots: '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 - '@ant-design/colors@8.0.0': + '@ant-design/colors@8.0.1': dependencies: - '@ant-design/fast-color': 3.0.0 + '@ant-design/fast-color': 3.0.1 - '@ant-design/cssinjs-utils@2.0.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@ant-design/cssinjs-utils@2.1.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@ant-design/cssinjs': 2.0.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@babel/runtime': 7.28.4 - '@rc-component/util': 1.7.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@ant-design/cssinjs': 2.1.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@babel/runtime': 7.29.2 + '@rc-component/util': 1.10.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) @@ -4511,22 +4540,34 @@ snapshots: react-dom: 19.2.3(react@19.2.3) stylis: 4.3.6 - '@ant-design/fast-color@3.0.0': {} + '@ant-design/cssinjs@2.1.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@babel/runtime': 7.29.2 + '@emotion/hash': 0.8.0 + '@emotion/unitless': 0.7.5 + '@rc-component/util': 1.10.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + clsx: 2.1.1 + csstype: 3.2.3 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + stylis: 4.3.6 + + '@ant-design/fast-color@3.0.1': {} '@ant-design/icons-svg@4.4.2': {} - '@ant-design/icons@6.1.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@ant-design/icons@6.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@ant-design/colors': 8.0.0 + '@ant-design/colors': 8.0.1 '@ant-design/icons-svg': 4.4.2 - '@rc-component/util': 1.7.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@rc-component/util': 1.10.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) clsx: 2.1.1 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) '@ant-design/react-slick@2.0.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.29.2 clsx: 2.1.1 json2mq: 0.2.0 react: 19.2.3 @@ -4536,7 +4577,7 @@ snapshots: '@antfu/install-pkg@1.1.0': dependencies: package-manager-detector: 1.6.0 - tinyexec: 1.0.2 + tinyexec: 1.1.1 '@asamuzakjp/css-color@3.2.0': dependencies: @@ -4579,6 +4620,8 @@ snapshots: '@babel/runtime@7.28.4': {} + '@babel/runtime@7.29.2': {} + '@babel/template@7.27.2': dependencies: '@babel/code-frame': 7.27.1 @@ -4602,24 +4645,23 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 - '@base-ui/react@1.0.0(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@base-ui/react@1.3.0(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@babel/runtime': 7.28.4 - '@base-ui/utils': 0.2.3(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@floating-ui/react-dom': 2.1.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@floating-ui/utils': 0.2.10 + '@babel/runtime': 7.29.2 + '@base-ui/utils': 0.2.6(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@floating-ui/react-dom': 2.1.8(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@floating-ui/utils': 0.2.11 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - reselect: 5.1.1 tabbable: 6.4.0 use-sync-external-store: 1.6.0(react@19.2.3) optionalDependencies: '@types/react': 19.2.7 - '@base-ui/utils@0.2.3(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@base-ui/utils@0.2.6(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@babel/runtime': 7.28.4 - '@floating-ui/utils': 0.2.10 + '@babel/runtime': 7.29.2 + '@floating-ui/utils': 0.2.11 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) reselect: 5.1.1 @@ -4629,24 +4671,22 @@ snapshots: '@bcoe/v8-coverage@0.2.3': {} - '@braintree/sanitize-url@7.1.1': {} + '@braintree/sanitize-url@7.1.2': {} - '@chevrotain/cst-dts-gen@11.0.3': + '@chevrotain/cst-dts-gen@12.0.0': dependencies: - '@chevrotain/gast': 11.0.3 - '@chevrotain/types': 11.0.3 - lodash-es: 4.17.21 + '@chevrotain/gast': 12.0.0 + '@chevrotain/types': 12.0.0 - '@chevrotain/gast@11.0.3': + '@chevrotain/gast@12.0.0': dependencies: - '@chevrotain/types': 11.0.3 - lodash-es: 4.17.21 + '@chevrotain/types': 12.0.0 - '@chevrotain/regexp-to-ast@11.0.3': {} + '@chevrotain/regexp-to-ast@12.0.0': {} - '@chevrotain/types@11.0.3': {} + '@chevrotain/types@12.0.0': {} - '@chevrotain/utils@11.0.3': {} + '@chevrotain/utils@12.0.0': {} '@csstools/color-helpers@5.1.0': {} @@ -4881,30 +4921,30 @@ snapshots: '@eslint/js@8.57.1': {} - '@floating-ui/core@1.7.3': + '@floating-ui/core@1.7.5': dependencies: - '@floating-ui/utils': 0.2.10 + '@floating-ui/utils': 0.2.11 - '@floating-ui/dom@1.7.4': + '@floating-ui/dom@1.7.6': dependencies: - '@floating-ui/core': 1.7.3 - '@floating-ui/utils': 0.2.10 + '@floating-ui/core': 1.7.5 + '@floating-ui/utils': 0.2.11 - '@floating-ui/react-dom@2.1.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@floating-ui/react-dom@2.1.8(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@floating-ui/dom': 1.7.4 + '@floating-ui/dom': 1.7.6 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - '@floating-ui/react@0.27.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@floating-ui/react@0.27.19(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@floating-ui/react-dom': 2.1.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@floating-ui/utils': 0.2.10 + '@floating-ui/react-dom': 2.1.8(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@floating-ui/utils': 0.2.11 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) tabbable: 6.4.0 - '@floating-ui/utils@0.2.10': {} + '@floating-ui/utils@0.2.11': {} '@giscus/react@3.1.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: @@ -4930,7 +4970,7 @@ snapshots: dependencies: '@antfu/install-pkg': 1.1.0 '@iconify/types': 2.0.0 - mlly: 1.8.0 + mlly: 1.8.2 '@intlify/core-base@9.14.5': dependencies: @@ -4971,11 +5011,11 @@ snapshots: '@kurkle/color@0.3.4': {} - '@lit-labs/ssr-dom-shim@1.5.0': {} + '@lit-labs/ssr-dom-shim@1.5.1': {} '@lit/reactive-element@2.1.2': dependencies: - '@lit-labs/ssr-dom-shim': 1.5.0 + '@lit-labs/ssr-dom-shim': 1.5.1 '@lobehub/emojilib@1.0.0': {} @@ -4984,7 +5024,7 @@ snapshots: '@lobehub/emojilib': 1.0.0 antd-style: 4.1.0(@types/react@19.2.7)(antd@6.1.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) emoji-regex: 10.6.0 - es-toolkit: 1.43.0 + es-toolkit: 1.45.1 lucide-react: 0.562.0(react@19.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) @@ -5009,8 +5049,8 @@ snapshots: '@lobehub/ui@4.9.2(@lobehub/fluent-emoji@4.1.0(@types/react@19.2.7)(antd@6.1.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@lobehub/icons@4.0.2)(@types/mdast@4.0.4)(@types/react@19.2.7)(antd@6.1.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(micromark-util-types@2.0.2)(micromark@4.0.2)(motion@12.23.26(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vue@3.5.26(typescript@5.6.3))': dependencies: - '@ant-design/cssinjs': 2.0.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@base-ui/react': 1.0.0(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@ant-design/cssinjs': 2.1.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@base-ui/react': 1.3.0(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@dnd-kit/core': 6.3.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@dnd-kit/modifiers': 9.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) '@dnd-kit/sortable': 10.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) @@ -5018,32 +5058,32 @@ snapshots: '@emoji-mart/data': 1.2.1 '@emoji-mart/react': 1.1.1(emoji-mart@5.6.0)(react@19.2.3) '@emotion/is-prop-valid': 1.4.0 - '@floating-ui/react': 0.27.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@floating-ui/react': 0.27.19(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@giscus/react': 3.1.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@lobehub/fluent-emoji': 4.1.0(@types/react@19.2.7)(antd@6.1.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@lobehub/icons': 4.0.2(@lobehub/ui@4.9.2)(@types/react@19.2.7)(antd@6.1.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@mdx-js/mdx': 3.1.1 '@mdx-js/react': 3.1.1(@types/react@19.2.7)(react@19.2.3) '@radix-ui/react-slot': 1.2.4(@types/react@19.2.7)(react@19.2.3) - '@shikijs/core': 3.20.0 - '@shikijs/transformers': 3.20.0 + '@shikijs/core': 3.23.0 + '@shikijs/transformers': 3.23.0 '@splinetool/runtime': 0.9.526 - ahooks: 3.9.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + ahooks: 3.9.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3) antd: 6.1.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) antd-style: 4.1.0(@types/react@19.2.7)(antd@6.1.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) chroma-js: 3.2.0 class-variance-authority: 0.7.1 clsx: 2.1.1 - dayjs: 1.11.19 + dayjs: 1.11.20 emoji-mart: 5.6.0 - es-toolkit: 1.43.0 + es-toolkit: 1.45.1 fast-deep-equal: 3.1.3 - immer: 11.1.3 - katex: 0.16.27 + immer: 11.1.4 + katex: 0.16.45 leva: 0.10.1(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) lucide-react: 0.562.0(react@19.2.3) - marked: 17.0.1 - mermaid: 11.12.2 + marked: 17.0.6 + mermaid: 11.14.0 motion: 12.23.26(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) numeral: 2.0.6 polished: 4.3.1 @@ -5057,11 +5097,11 @@ snapshots: react: 19.2.3 react-avatar-editor: 14.0.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react-dom: 19.2.3(react@19.2.3) - react-error-boundary: 6.0.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - react-hotkeys-hook: 5.2.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react-error-boundary: 6.1.1(react@19.2.3) + react-hotkeys-hook: 5.2.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react-markdown: 10.1.0(@types/react@19.2.7)(react@19.2.3) react-merge-refs: 3.0.2(react@19.2.3) - react-rnd: 10.5.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react-rnd: 10.5.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react-zoom-pan-pinch: 3.7.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) rehype-github-alerts: 4.2.0 rehype-katex: 7.0.1 @@ -5071,9 +5111,9 @@ snapshots: remark-gfm: 4.0.1 remark-github: 12.0.0 remark-math: 6.0.0 - shiki: 3.20.0 - shiki-stream: 0.1.3(react@19.2.3)(vue@3.5.26(typescript@5.6.3)) - swr: 2.3.8(react@19.2.3) + shiki: 3.23.0 + shiki-stream: 0.1.4(react@19.2.3)(vue@3.5.26(typescript@5.6.3)) + swr: 2.4.1(react@19.2.3) ts-md5: 2.0.1 unified: 11.0.5 url-join: 5.0.0 @@ -5085,6 +5125,7 @@ snapshots: - '@types/react-dom' - micromark - micromark-util-types + - solid-js - supports-color - vue @@ -5094,7 +5135,7 @@ snapshots: '@types/estree-jsx': 1.0.5 '@types/hast': 3.0.4 '@types/mdx': 2.0.13 - acorn: 8.15.0 + acorn: 8.16.0 collapse-white-space: 2.1.0 devlop: 1.1.0 estree-util-is-identifier-name: 3.0.0 @@ -5103,7 +5144,7 @@ snapshots: hast-util-to-jsx-runtime: 2.3.6 markdown-extensions: 2.0.0 recma-build-jsx: 1.0.0 - recma-jsx: 1.0.1(acorn@8.15.0) + recma-jsx: 1.0.1(acorn@8.16.0) recma-stringify: 1.0.0 rehype-recma: 1.0.0 remark-mdx: 3.1.1 @@ -5113,7 +5154,7 @@ snapshots: unified: 11.0.5 unist-util-position-from-estree: 2.0.0 unist-util-stringify-position: 4.0.0 - unist-util-visit: 5.0.0 + unist-util-visit: 5.1.0 vfile: 6.0.3 transitivePeerDependencies: - supports-color @@ -5124,9 +5165,9 @@ snapshots: '@types/react': 19.2.7 react: 19.2.3 - '@mermaid-js/parser@0.6.3': + '@mermaid-js/parser@1.1.0': dependencies: - langium: 3.3.1 + langium: 4.2.2 '@nodelib/fs.scandir@2.1.5': dependencies: @@ -5145,7 +5186,7 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true - '@primer/octicons@19.21.1': + '@primer/octicons@19.23.1': dependencies: object-assign: 4.1.1 @@ -5192,7 +5233,7 @@ snapshots: '@radix-ui/react-popper@1.2.8(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@floating-ui/react-dom': 2.1.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@floating-ui/react-dom': 2.1.8(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@radix-ui/react-arrow': 1.1.7(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.3) '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.3) @@ -5341,46 +5382,46 @@ snapshots: '@radix-ui/rect@1.1.1': {} - '@rc-component/async-validator@5.0.4': + '@rc-component/async-validator@5.1.0': dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.29.2 '@rc-component/cascader@1.10.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@rc-component/select': 1.4.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@rc-component/tree': 1.1.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@rc-component/util': 1.7.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@rc-component/util': 1.10.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) clsx: 2.1.1 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) '@rc-component/checkbox@1.0.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@rc-component/util': 1.7.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@rc-component/util': 1.10.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) clsx: 2.1.1 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) '@rc-component/collapse@1.1.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.29.2 '@rc-component/motion': 1.1.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@rc-component/util': 1.7.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@rc-component/util': 1.10.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) clsx: 2.1.1 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) '@rc-component/color-picker@3.0.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@ant-design/fast-color': 3.0.0 - '@rc-component/util': 1.7.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@ant-design/fast-color': 3.0.1 + '@rc-component/util': 1.10.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) clsx: 2.1.1 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) '@rc-component/context@2.0.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@rc-component/util': 1.7.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@rc-component/util': 1.10.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) @@ -5388,7 +5429,7 @@ snapshots: dependencies: '@rc-component/motion': 1.1.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@rc-component/portal': 2.2.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@rc-component/util': 1.7.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@rc-component/util': 1.10.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) clsx: 2.1.1 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) @@ -5397,23 +5438,23 @@ snapshots: dependencies: '@rc-component/motion': 1.1.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@rc-component/portal': 2.2.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@rc-component/util': 1.7.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@rc-component/util': 1.10.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) clsx: 2.1.1 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) '@rc-component/dropdown@1.0.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@rc-component/trigger': 3.8.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@rc-component/util': 1.7.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@rc-component/trigger': 3.9.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@rc-component/util': 1.10.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) clsx: 2.1.1 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - '@rc-component/form@1.6.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@rc-component/form@1.6.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@rc-component/async-validator': 5.0.4 - '@rc-component/util': 1.7.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@rc-component/async-validator': 5.1.0 + '@rc-component/util': 1.10.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) clsx: 2.1.1 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) @@ -5422,22 +5463,22 @@ snapshots: dependencies: '@rc-component/motion': 1.1.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@rc-component/portal': 2.2.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@rc-component/util': 1.7.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@rc-component/util': 1.10.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) clsx: 2.1.1 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) '@rc-component/input-number@1.6.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@rc-component/mini-decimal': 1.1.0 - '@rc-component/util': 1.7.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@rc-component/mini-decimal': 1.1.3 + '@rc-component/util': 1.10.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) clsx: 2.1.1 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) '@rc-component/input@1.1.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@rc-component/util': 1.7.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@rc-component/util': 1.10.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) clsx: 2.1.1 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) @@ -5447,8 +5488,8 @@ snapshots: '@rc-component/input': 1.1.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@rc-component/menu': 1.2.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@rc-component/textarea': 1.1.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@rc-component/trigger': 3.8.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@rc-component/util': 1.7.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@rc-component/trigger': 3.9.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@rc-component/util': 1.10.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) clsx: 2.1.1 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) @@ -5457,68 +5498,68 @@ snapshots: dependencies: '@rc-component/motion': 1.1.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@rc-component/overflow': 1.0.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@rc-component/trigger': 3.8.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@rc-component/util': 1.7.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@rc-component/trigger': 3.9.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@rc-component/util': 1.10.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) clsx: 2.1.1 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - '@rc-component/mini-decimal@1.1.0': + '@rc-component/mini-decimal@1.1.3': dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.29.2 '@rc-component/motion@1.1.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@rc-component/util': 1.7.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@rc-component/util': 1.10.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) clsx: 2.1.1 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) '@rc-component/mutate-observer@2.0.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@rc-component/util': 1.7.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@rc-component/util': 1.10.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) '@rc-component/notification@1.2.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@rc-component/motion': 1.1.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@rc-component/util': 1.7.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@rc-component/util': 1.10.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) clsx: 2.1.1 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) '@rc-component/overflow@1.0.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@babel/runtime': 7.28.4 - '@rc-component/resize-observer': 1.0.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@rc-component/util': 1.7.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@babel/runtime': 7.29.2 + '@rc-component/resize-observer': 1.1.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@rc-component/util': 1.10.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) clsx: 2.1.1 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) '@rc-component/pagination@1.2.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@rc-component/util': 1.7.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@rc-component/util': 1.10.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) clsx: 2.1.1 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - '@rc-component/picker@1.9.0(dayjs@1.11.19)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@rc-component/picker@1.9.1(dayjs@1.11.20)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@rc-component/overflow': 1.0.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@rc-component/resize-observer': 1.0.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@rc-component/trigger': 3.8.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@rc-component/util': 1.7.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@rc-component/resize-observer': 1.1.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@rc-component/trigger': 3.9.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@rc-component/util': 1.10.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) clsx: 2.1.1 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) optionalDependencies: - dayjs: 1.11.19 + dayjs: 1.11.20 '@rc-component/portal@1.1.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.29.2 classnames: 2.5.1 rc-util: 5.44.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 @@ -5526,42 +5567,42 @@ snapshots: '@rc-component/portal@2.2.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@rc-component/util': 1.7.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@rc-component/util': 1.10.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) clsx: 2.1.1 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) '@rc-component/progress@1.0.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@rc-component/util': 1.7.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@rc-component/util': 1.10.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) clsx: 2.1.1 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) '@rc-component/qrcode@1.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.29.2 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) '@rc-component/rate@1.0.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@rc-component/util': 1.7.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@rc-component/util': 1.10.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) clsx: 2.1.1 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - '@rc-component/resize-observer@1.0.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@rc-component/resize-observer@1.1.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@rc-component/util': 1.7.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@rc-component/util': 1.10.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) '@rc-component/segmented@1.3.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.29.2 '@rc-component/motion': 1.1.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@rc-component/util': 1.7.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@rc-component/util': 1.10.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) clsx: 2.1.1 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) @@ -5569,8 +5610,8 @@ snapshots: '@rc-component/select@1.4.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@rc-component/overflow': 1.0.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@rc-component/trigger': 3.8.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@rc-component/util': 1.7.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@rc-component/trigger': 3.9.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@rc-component/util': 1.10.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@rc-component/virtual-list': 1.0.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) clsx: 2.1.1 react: 19.2.3 @@ -5578,21 +5619,21 @@ snapshots: '@rc-component/slider@1.0.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@rc-component/util': 1.7.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@rc-component/util': 1.10.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) clsx: 2.1.1 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) '@rc-component/steps@1.2.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@rc-component/util': 1.7.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@rc-component/util': 1.10.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) clsx: 2.1.1 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) '@rc-component/switch@1.0.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@rc-component/util': 1.7.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@rc-component/util': 1.10.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) clsx: 2.1.1 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) @@ -5600,8 +5641,8 @@ snapshots: '@rc-component/table@1.9.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@rc-component/context': 2.0.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@rc-component/resize-observer': 1.0.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@rc-component/util': 1.7.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@rc-component/resize-observer': 1.1.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@rc-component/util': 1.10.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@rc-component/virtual-list': 1.0.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) clsx: 2.1.1 react: 19.2.3 @@ -5612,8 +5653,8 @@ snapshots: '@rc-component/dropdown': 1.0.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@rc-component/menu': 1.2.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@rc-component/motion': 1.1.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@rc-component/resize-observer': 1.0.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@rc-component/util': 1.7.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@rc-component/resize-observer': 1.1.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@rc-component/util': 1.10.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) clsx: 2.1.1 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) @@ -5621,16 +5662,16 @@ snapshots: '@rc-component/textarea@1.1.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@rc-component/input': 1.1.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@rc-component/resize-observer': 1.0.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@rc-component/util': 1.7.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@rc-component/resize-observer': 1.1.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@rc-component/util': 1.10.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) clsx: 2.1.1 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) '@rc-component/tooltip@1.4.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@rc-component/trigger': 3.8.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@rc-component/util': 1.7.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@rc-component/trigger': 3.9.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@rc-component/util': 1.10.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) clsx: 2.1.1 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) @@ -5638,8 +5679,8 @@ snapshots: '@rc-component/tour@2.2.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@rc-component/portal': 2.2.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@rc-component/trigger': 3.8.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@rc-component/util': 1.7.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@rc-component/trigger': 3.9.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@rc-component/util': 1.10.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) clsx: 2.1.1 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) @@ -5648,7 +5689,7 @@ snapshots: dependencies: '@rc-component/select': 1.4.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@rc-component/tree': 1.1.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@rc-component/util': 1.7.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@rc-component/util': 1.10.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) clsx: 2.1.1 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) @@ -5656,15 +5697,15 @@ snapshots: '@rc-component/tree@1.1.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@rc-component/motion': 1.1.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@rc-component/util': 1.7.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@rc-component/util': 1.10.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@rc-component/virtual-list': 1.0.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) clsx: 2.1.1 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - '@rc-component/trigger@2.3.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@rc-component/trigger@2.3.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.29.2 '@rc-component/portal': 1.1.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) classnames: 2.5.1 rc-motion: 2.9.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -5673,23 +5714,30 @@ snapshots: react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - '@rc-component/trigger@3.8.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@rc-component/trigger@3.9.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@rc-component/motion': 1.1.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@rc-component/portal': 2.2.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@rc-component/resize-observer': 1.0.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@rc-component/util': 1.7.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@rc-component/resize-observer': 1.1.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@rc-component/util': 1.10.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) clsx: 2.1.1 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) '@rc-component/upload@1.1.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@rc-component/util': 1.7.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@rc-component/util': 1.10.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) clsx: 2.1.1 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) + '@rc-component/util@1.10.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + is-mobile: 5.0.0 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + react-is: 18.3.1 + '@rc-component/util@1.7.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: is-mobile: 5.0.0 @@ -5699,9 +5747,9 @@ snapshots: '@rc-component/virtual-list@1.0.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@babel/runtime': 7.28.4 - '@rc-component/resize-observer': 1.0.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@rc-component/util': 1.7.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@babel/runtime': 7.29.2 + '@rc-component/resize-observer': 1.1.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@rc-component/util': 1.10.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) clsx: 2.1.1 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) @@ -5772,38 +5820,38 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.54.0': optional: true - '@shikijs/core@3.20.0': + '@shikijs/core@3.23.0': dependencies: - '@shikijs/types': 3.20.0 + '@shikijs/types': 3.23.0 '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 hast-util-to-html: 9.0.5 - '@shikijs/engine-javascript@3.20.0': + '@shikijs/engine-javascript@3.23.0': dependencies: - '@shikijs/types': 3.20.0 + '@shikijs/types': 3.23.0 '@shikijs/vscode-textmate': 10.0.2 - oniguruma-to-es: 4.3.4 + oniguruma-to-es: 4.3.5 - '@shikijs/engine-oniguruma@3.20.0': + '@shikijs/engine-oniguruma@3.23.0': dependencies: - '@shikijs/types': 3.20.0 + '@shikijs/types': 3.23.0 '@shikijs/vscode-textmate': 10.0.2 - '@shikijs/langs@3.20.0': + '@shikijs/langs@3.23.0': dependencies: - '@shikijs/types': 3.20.0 + '@shikijs/types': 3.23.0 - '@shikijs/themes@3.20.0': + '@shikijs/themes@3.23.0': dependencies: - '@shikijs/types': 3.20.0 + '@shikijs/types': 3.23.0 - '@shikijs/transformers@3.20.0': + '@shikijs/transformers@3.23.0': dependencies: - '@shikijs/core': 3.20.0 - '@shikijs/types': 3.20.0 + '@shikijs/core': 3.23.0 + '@shikijs/types': 3.23.0 - '@shikijs/types@3.20.0': + '@shikijs/types@3.23.0': dependencies: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 @@ -5891,7 +5939,7 @@ snapshots: '@types/d3-selection@3.0.11': {} - '@types/d3-shape@3.1.7': + '@types/d3-shape@3.1.8': dependencies: '@types/d3-path': 3.1.1 @@ -5936,14 +5984,14 @@ snapshots: '@types/d3-scale': 4.0.9 '@types/d3-scale-chromatic': 3.1.0 '@types/d3-selection': 3.0.11 - '@types/d3-shape': 3.1.7 + '@types/d3-shape': 3.1.8 '@types/d3-time': 3.0.4 '@types/d3-time-format': 4.0.3 '@types/d3-timer': 3.0.2 '@types/d3-transition': 3.0.9 '@types/d3-zoom': 3.0.8 - '@types/debug@4.1.12': + '@types/debug@4.1.13': dependencies: '@types/ms': 2.1.0 @@ -5967,7 +6015,7 @@ snapshots: '@types/js-cookie@3.0.6': {} - '@types/katex@0.16.7': {} + '@types/katex@0.16.8': {} '@types/mdast@4.0.4': dependencies: @@ -6084,6 +6132,11 @@ snapshots: '@ungap/structured-clone@1.3.0': {} + '@upsetjs/venn.js@2.0.0': + optionalDependencies: + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + '@use-gesture/core@10.3.1': {} '@use-gesture/react@10.3.1(react@19.2.3)': @@ -6270,20 +6323,26 @@ snapshots: dependencies: acorn: 8.15.0 + acorn-jsx@5.3.2(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + acorn@8.15.0: {} + acorn@8.16.0: {} + adler-32@1.3.1: {} agent-base@7.1.4: {} - ahooks@3.9.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + ahooks@3.9.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.29.2 '@types/js-cookie': 3.0.6 - dayjs: 1.11.19 + dayjs: 1.11.20 intersection-observer: 0.12.2 js-cookie: 3.0.5 - lodash: 4.17.21 + lodash: 4.18.1 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) react-fast-compare: 3.2.2 @@ -6329,13 +6388,13 @@ snapshots: antd@6.1.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: - '@ant-design/colors': 8.0.0 - '@ant-design/cssinjs': 2.0.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@ant-design/cssinjs-utils': 2.0.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@ant-design/fast-color': 3.0.0 - '@ant-design/icons': 6.1.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@ant-design/colors': 8.0.1 + '@ant-design/cssinjs': 2.1.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@ant-design/cssinjs-utils': 2.1.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@ant-design/fast-color': 3.0.1 + '@ant-design/icons': 6.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@ant-design/react-slick': 2.0.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.29.2 '@rc-component/cascader': 1.10.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@rc-component/checkbox': 1.0.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@rc-component/collapse': 1.1.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -6343,7 +6402,7 @@ snapshots: '@rc-component/dialog': 1.5.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@rc-component/drawer': 1.3.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@rc-component/dropdown': 1.0.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@rc-component/form': 1.6.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@rc-component/form': 1.6.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@rc-component/image': 1.5.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@rc-component/input': 1.1.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@rc-component/input-number': 1.6.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -6353,11 +6412,11 @@ snapshots: '@rc-component/mutate-observer': 2.0.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@rc-component/notification': 1.2.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@rc-component/pagination': 1.2.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@rc-component/picker': 1.9.0(dayjs@1.11.19)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@rc-component/picker': 1.9.1(dayjs@1.11.20)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@rc-component/progress': 1.0.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@rc-component/qrcode': 1.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@rc-component/rate': 1.0.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@rc-component/resize-observer': 1.0.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@rc-component/resize-observer': 1.1.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@rc-component/segmented': 1.3.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@rc-component/select': 1.4.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@rc-component/slider': 1.0.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -6370,11 +6429,11 @@ snapshots: '@rc-component/tour': 2.2.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@rc-component/tree': 1.1.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@rc-component/tree-select': 1.5.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@rc-component/trigger': 3.8.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@rc-component/trigger': 3.9.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@rc-component/upload': 1.1.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@rc-component/util': 1.7.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@rc-component/util': 1.10.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) clsx: 2.1.1 - dayjs: 1.11.19 + dayjs: 1.11.20 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) scroll-into-view-if-needed: 3.1.0 @@ -6416,11 +6475,11 @@ snapshots: postcss: 8.5.6 postcss-value-parser: 4.2.0 - axios@1.13.5: + axios@1.15.0: dependencies: follow-redirects: 1.15.11 form-data: 4.0.5 - proxy-from-env: 1.1.0 + proxy-from-env: 2.1.0 transitivePeerDependencies: - debug @@ -6510,19 +6569,18 @@ snapshots: check-error@2.1.3: {} - chevrotain-allstar@0.3.1(chevrotain@11.0.3): + chevrotain-allstar@0.4.1(chevrotain@12.0.0): dependencies: - chevrotain: 11.0.3 - lodash-es: 4.17.22 + chevrotain: 12.0.0 + lodash-es: 4.18.1 - chevrotain@11.0.3: + chevrotain@12.0.0: dependencies: - '@chevrotain/cst-dts-gen': 11.0.3 - '@chevrotain/gast': 11.0.3 - '@chevrotain/regexp-to-ast': 11.0.3 - '@chevrotain/types': 11.0.3 - '@chevrotain/utils': 11.0.3 - lodash-es: 4.17.21 + '@chevrotain/cst-dts-gen': 12.0.0 + '@chevrotain/gast': 12.0.0 + '@chevrotain/regexp-to-ast': 12.0.0 + '@chevrotain/types': 12.0.0 + '@chevrotain/utils': 12.0.0 chokidar@3.6.0: dependencies: @@ -6554,8 +6612,6 @@ snapshots: strip-ansi: 6.0.1 wrap-ansi: 6.2.0 - clsx@1.2.1: {} - clsx@2.1.1: {} codepage@1.15.0: {} @@ -6630,17 +6686,17 @@ snapshots: csstype@3.2.3: {} - cytoscape-cose-bilkent@4.1.0(cytoscape@3.33.1): + cytoscape-cose-bilkent@4.1.0(cytoscape@3.33.2): dependencies: cose-base: 1.0.3 - cytoscape: 3.33.1 + cytoscape: 3.33.2 - cytoscape-fcose@2.2.0(cytoscape@3.33.1): + cytoscape-fcose@2.2.0(cytoscape@3.33.2): dependencies: cose-base: 2.2.0 - cytoscape: 3.33.1 + cytoscape: 3.33.2 - cytoscape@3.33.1: {} + cytoscape@3.33.2: {} d3-array@2.12.1: dependencies: @@ -6672,7 +6728,7 @@ snapshots: d3-delaunay@6.0.4: dependencies: - delaunator: 5.0.1 + delaunator: 5.1.0 d3-dispatch@3.0.1: {} @@ -6699,7 +6755,7 @@ snapshots: d3-quadtree: 3.0.1 d3-timer: 3.0.1 - d3-format@3.1.0: {} + d3-format@3.1.2: {} d3-geo@3.1.1: dependencies: @@ -6734,7 +6790,7 @@ snapshots: d3-scale@4.0.2: dependencies: d3-array: 3.2.4 - d3-format: 3.1.0 + d3-format: 3.1.2 d3-interpolate: 3.0.1 d3-time: 3.1.0 d3-time-format: 4.1.0 @@ -6791,7 +6847,7 @@ snapshots: d3-ease: 3.0.1 d3-fetch: 3.0.1 d3-force: 3.0.0 - d3-format: 3.1.0 + d3-format: 3.1.2 d3-geo: 3.1.1 d3-hierarchy: 3.1.2 d3-interpolate: 3.0.1 @@ -6809,17 +6865,17 @@ snapshots: d3-transition: 3.0.1(d3-selection@3.0.0) d3-zoom: 3.0.0 - dagre-d3-es@7.0.13: + dagre-d3-es@7.0.14: dependencies: d3: 7.9.0 - lodash-es: 4.17.22 + lodash-es: 4.18.1 data-urls@5.0.0: dependencies: whatwg-mimetype: 4.0.0 whatwg-url: 14.2.0 - dayjs@1.11.19: {} + dayjs@1.11.20: {} de-indent@1.0.2: {} @@ -6831,7 +6887,7 @@ snapshots: decimal.js@10.6.0: {} - decode-named-character-reference@1.2.0: + decode-named-character-reference@1.3.0: dependencies: character-entities: 2.0.2 @@ -6841,9 +6897,9 @@ snapshots: deep-is@0.1.4: {} - delaunator@5.0.1: + delaunator@5.1.0: dependencies: - robust-predicates: 3.0.2 + robust-predicates: 3.0.3 delayed-stream@1.0.0: {} @@ -6871,6 +6927,10 @@ snapshots: optionalDependencies: '@types/trusted-types': 2.0.7 + dompurify@3.3.3: + optionalDependencies: + '@types/trusted-types': 2.0.7 + driver.js@1.4.0: {} dunder-proto@1.0.1: @@ -6923,7 +6983,7 @@ snapshots: has-tostringtag: 1.0.2 hasown: 2.0.2 - es-toolkit@1.43.0: {} + es-toolkit@1.45.1: {} esast-util-from-estree@2.0.0: dependencies: @@ -6935,7 +6995,7 @@ snapshots: esast-util-from-js@2.0.1: dependencies: '@types/estree-jsx': 1.0.5 - acorn: 8.15.0 + acorn: 8.16.0 esast-util-from-estree: 2.0.0 vfile-message: 4.0.3 @@ -7180,10 +7240,10 @@ snapshots: fraction.js@5.3.4: {} - framer-motion@12.23.26(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + framer-motion@12.38.0(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: - motion-dom: 12.23.23 - motion-utils: 12.23.6 + motion-dom: 12.38.0 + motion-utils: 12.36.0 tslib: 2.8.1 optionalDependencies: '@emotion/is-prop-valid': 1.4.0 @@ -7199,7 +7259,7 @@ snapshots: get-caller-file@2.0.5: {} - get-east-asian-width@1.4.0: {} + get-east-asian-width@1.5.0: {} get-intrinsic@1.3.0: dependencies: @@ -7334,7 +7394,7 @@ snapshots: mdast-util-to-hast: 13.2.1 parse5: 7.3.0 unist-util-position: 5.0.0 - unist-util-visit: 5.0.0 + unist-util-visit: 5.1.0 vfile: 6.0.3 web-namespaces: 2.0.1 zwitch: 2.0.4 @@ -7459,7 +7519,7 @@ snapshots: ignore@5.3.2: {} - immer@11.1.3: {} + immer@11.1.4: {} import-fresh@3.3.1: dependencies: @@ -7625,7 +7685,7 @@ snapshots: dependencies: string-convert: 0.2.1 - katex@0.16.27: + katex@0.16.45: dependencies: commander: 8.3.0 @@ -7635,13 +7695,14 @@ snapshots: khroma@2.1.0: {} - langium@3.3.1: + langium@4.2.2: dependencies: - chevrotain: 11.0.3 - chevrotain-allstar: 0.3.1(chevrotain@11.0.3) + '@chevrotain/regexp-to-ast': 12.0.0 + chevrotain: 12.0.0 + chevrotain-allstar: 0.4.1(chevrotain@12.0.0) vscode-languageserver: 9.0.1 vscode-languageserver-textdocument: 1.0.12 - vscode-uri: 3.0.8 + vscode-uri: 3.1.0 layout-base@1.0.2: {} @@ -7677,7 +7738,7 @@ snapshots: lit-element@4.2.2: dependencies: - '@lit-labs/ssr-dom-shim': 1.5.0 + '@lit-labs/ssr-dom-shim': 1.5.1 '@lit/reactive-element': 2.1.2 lit-html: 3.3.2 @@ -7699,14 +7760,14 @@ snapshots: dependencies: p-locate: 5.0.0 - lodash-es@4.17.21: {} - - lodash-es@4.17.22: {} + lodash-es@4.18.1: {} lodash.merge@4.6.2: {} lodash@4.17.21: {} + lodash@4.18.1: {} + longest-streak@3.1.0: {} loose-envify@1.4.0: @@ -7747,6 +7808,8 @@ snapshots: marked@17.0.1: {} + marked@17.0.6: {} + math-intrinsics@1.1.0: {} mdast-util-find-and-replace@3.0.2: @@ -7756,11 +7819,11 @@ snapshots: unist-util-is: 6.0.1 unist-util-visit-parents: 6.0.2 - mdast-util-from-markdown@2.0.2: + mdast-util-from-markdown@2.0.3: dependencies: '@types/mdast': 4.0.4 '@types/unist': 3.0.3 - decode-named-character-reference: 1.2.0 + decode-named-character-reference: 1.3.0 devlop: 1.1.0 mdast-util-to-string: 4.0.0 micromark: 4.0.2 @@ -7785,7 +7848,7 @@ snapshots: dependencies: '@types/mdast': 4.0.4 devlop: 1.1.0 - mdast-util-from-markdown: 2.0.2 + mdast-util-from-markdown: 2.0.3 mdast-util-to-markdown: 2.1.2 micromark-util-normalize-identifier: 2.0.1 transitivePeerDependencies: @@ -7794,7 +7857,7 @@ snapshots: mdast-util-gfm-strikethrough@2.0.0: dependencies: '@types/mdast': 4.0.4 - mdast-util-from-markdown: 2.0.2 + mdast-util-from-markdown: 2.0.3 mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color @@ -7804,7 +7867,7 @@ snapshots: '@types/mdast': 4.0.4 devlop: 1.1.0 markdown-table: 3.0.4 - mdast-util-from-markdown: 2.0.2 + mdast-util-from-markdown: 2.0.3 mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color @@ -7813,14 +7876,14 @@ snapshots: dependencies: '@types/mdast': 4.0.4 devlop: 1.1.0 - mdast-util-from-markdown: 2.0.2 + mdast-util-from-markdown: 2.0.3 mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color mdast-util-gfm@3.1.0: dependencies: - mdast-util-from-markdown: 2.0.2 + mdast-util-from-markdown: 2.0.3 mdast-util-gfm-autolink-literal: 2.0.1 mdast-util-gfm-footnote: 2.1.0 mdast-util-gfm-strikethrough: 2.0.0 @@ -7836,7 +7899,7 @@ snapshots: '@types/mdast': 4.0.4 devlop: 1.1.0 longest-streak: 3.1.0 - mdast-util-from-markdown: 2.0.2 + mdast-util-from-markdown: 2.0.3 mdast-util-to-markdown: 2.1.2 unist-util-remove-position: 5.0.0 transitivePeerDependencies: @@ -7848,7 +7911,7 @@ snapshots: '@types/hast': 3.0.4 '@types/mdast': 4.0.4 devlop: 1.1.0 - mdast-util-from-markdown: 2.0.2 + mdast-util-from-markdown: 2.0.3 mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color @@ -7861,7 +7924,7 @@ snapshots: '@types/unist': 3.0.3 ccount: 2.0.1 devlop: 1.1.0 - mdast-util-from-markdown: 2.0.2 + mdast-util-from-markdown: 2.0.3 mdast-util-to-markdown: 2.1.2 parse-entities: 4.0.2 stringify-entities: 4.0.4 @@ -7872,7 +7935,7 @@ snapshots: mdast-util-mdx@3.0.0: dependencies: - mdast-util-from-markdown: 2.0.2 + mdast-util-from-markdown: 2.0.3 mdast-util-mdx-expression: 2.0.1 mdast-util-mdx-jsx: 3.2.0 mdast-util-mdxjs-esm: 2.0.1 @@ -7886,7 +7949,7 @@ snapshots: '@types/hast': 3.0.4 '@types/mdast': 4.0.4 devlop: 1.1.0 - mdast-util-from-markdown: 2.0.2 + mdast-util-from-markdown: 2.0.3 mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color @@ -7910,7 +7973,7 @@ snapshots: micromark-util-sanitize-uri: 2.0.1 trim-lines: 3.0.1 unist-util-position: 5.0.0 - unist-util-visit: 5.0.0 + unist-util-visit: 5.1.0 vfile: 6.0.3 mdast-util-to-markdown@2.1.2: @@ -7922,7 +7985,7 @@ snapshots: mdast-util-to-string: 4.0.0 micromark-util-classify-character: 2.0.1 micromark-util-decode-string: 2.0.1 - unist-util-visit: 5.0.0 + unist-util-visit: 5.1.0 zwitch: 2.0.4 mdast-util-to-string@4.0.0: @@ -7938,23 +8001,24 @@ snapshots: merge2@1.4.1: {} - mermaid@11.12.2: + mermaid@11.14.0: dependencies: - '@braintree/sanitize-url': 7.1.1 + '@braintree/sanitize-url': 7.1.2 '@iconify/utils': 3.1.0 - '@mermaid-js/parser': 0.6.3 + '@mermaid-js/parser': 1.1.0 '@types/d3': 7.4.3 - cytoscape: 3.33.1 - cytoscape-cose-bilkent: 4.1.0(cytoscape@3.33.1) - cytoscape-fcose: 2.2.0(cytoscape@3.33.1) + '@upsetjs/venn.js': 2.0.0 + cytoscape: 3.33.2 + cytoscape-cose-bilkent: 4.1.0(cytoscape@3.33.2) + cytoscape-fcose: 2.2.0(cytoscape@3.33.2) d3: 7.9.0 d3-sankey: 0.12.3 - dagre-d3-es: 7.0.13 - dayjs: 1.11.19 - dompurify: 3.3.1 - katex: 0.16.27 + dagre-d3-es: 7.0.14 + dayjs: 1.11.20 + dompurify: 3.3.3 + katex: 0.16.45 khroma: 2.1.0 - lodash-es: 4.17.22 + lodash-es: 4.18.1 marked: 16.4.2 roughjs: 4.6.6 stylis: 4.3.6 @@ -7963,7 +8027,7 @@ snapshots: micromark-core-commonmark@2.0.3: dependencies: - decode-named-character-reference: 1.2.0 + decode-named-character-reference: 1.3.0 devlop: 1.1.0 micromark-factory-destination: 2.0.1 micromark-factory-label: 2.0.1 @@ -7982,7 +8046,7 @@ snapshots: micromark-extension-cjk-friendly-util@2.1.1(micromark-util-types@2.0.2): dependencies: - get-east-asian-width: 1.4.0 + get-east-asian-width: 1.5.0 micromark-util-character: 2.1.1 micromark-util-symbol: 2.0.1 optionalDependencies: @@ -8059,9 +8123,9 @@ snapshots: micromark-extension-math@3.1.0: dependencies: - '@types/katex': 0.16.7 + '@types/katex': 0.16.8 devlop: 1.1.0 - katex: 0.16.27 + katex: 0.16.45 micromark-factory-space: 2.0.1 micromark-util-character: 2.1.1 micromark-util-symbol: 2.0.1 @@ -8109,8 +8173,8 @@ snapshots: micromark-extension-mdxjs@3.0.0: dependencies: - acorn: 8.15.0 - acorn-jsx: 5.3.2(acorn@8.15.0) + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) micromark-extension-mdx-expression: 3.0.1 micromark-extension-mdx-jsx: 3.0.2 micromark-extension-mdx-md: 2.0.0 @@ -8188,7 +8252,7 @@ snapshots: micromark-util-decode-string@2.0.1: dependencies: - decode-named-character-reference: 1.2.0 + decode-named-character-reference: 1.3.0 micromark-util-character: 2.1.1 micromark-util-decode-numeric-character-reference: 2.0.2 micromark-util-symbol: 2.0.1 @@ -8234,9 +8298,9 @@ snapshots: micromark@4.0.2: dependencies: - '@types/debug': 4.1.12 + '@types/debug': 4.1.13 debug: 4.4.3 - decode-named-character-reference: 1.2.0 + decode-named-character-reference: 1.3.0 devlop: 1.1.0 micromark-core-commonmark: 2.0.3 micromark-factory-space: 2.0.1 @@ -8284,22 +8348,22 @@ snapshots: for-in: 1.0.2 is-extendable: 1.0.1 - mlly@1.8.0: + mlly@1.8.2: dependencies: - acorn: 8.15.0 + acorn: 8.16.0 pathe: 2.0.3 pkg-types: 1.3.1 - ufo: 1.6.1 + ufo: 1.6.3 - motion-dom@12.23.23: + motion-dom@12.38.0: dependencies: - motion-utils: 12.23.6 + motion-utils: 12.36.0 - motion-utils@12.23.6: {} + motion-utils@12.36.0: {} motion@12.23.26(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: - framer-motion: 12.23.26(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + framer-motion: 12.38.0(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) tslib: 2.8.1 optionalDependencies: '@emotion/is-prop-valid': 1.4.0 @@ -8353,7 +8417,7 @@ snapshots: oniguruma-parser@0.12.1: {} - oniguruma-to-es@4.3.4: + oniguruma-to-es@4.3.5: dependencies: oniguruma-parser: 0.12.1 regex: 6.1.0 @@ -8399,7 +8463,7 @@ snapshots: '@types/unist': 2.0.11 character-entities-legacy: 3.0.0 character-reference-invalid: 2.0.1 - decode-named-character-reference: 1.2.0 + decode-named-character-reference: 1.3.0 is-alphanumerical: 2.0.1 is-decimal: 2.0.1 is-hexadecimal: 2.0.1 @@ -8465,7 +8529,7 @@ snapshots: pkg-types@1.3.1: dependencies: confbox: 0.1.8 - mlly: 1.8.0 + mlly: 1.8.2 pathe: 2.0.3 pngjs@5.0.0: {} @@ -8530,7 +8594,7 @@ snapshots: proto-list@1.2.4: {} - proxy-from-env@1.1.0: {} + proxy-from-env@2.1.0: {} psl@1.15.0: dependencies: @@ -8556,7 +8620,7 @@ snapshots: rc-collapse@4.0.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.29.2 classnames: 2.5.1 rc-motion: 2.9.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3) rc-util: 5.44.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -8565,7 +8629,7 @@ snapshots: rc-dialog@9.6.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.29.2 '@rc-component/portal': 1.1.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) classnames: 2.5.1 rc-motion: 2.9.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -8575,14 +8639,14 @@ snapshots: rc-footer@0.6.8(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.29.2 classnames: 2.5.1 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) rc-image@7.12.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.29.2 '@rc-component/portal': 1.1.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) classnames: 2.5.1 rc-dialog: 9.6.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -8593,8 +8657,8 @@ snapshots: rc-input-number@9.5.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: - '@babel/runtime': 7.28.4 - '@rc-component/mini-decimal': 1.1.0 + '@babel/runtime': 7.29.2 + '@rc-component/mini-decimal': 1.1.3 classnames: 2.5.1 rc-input: 1.8.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) rc-util: 5.44.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -8603,7 +8667,7 @@ snapshots: rc-input@1.8.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.29.2 classnames: 2.5.1 rc-util: 5.44.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 @@ -8611,8 +8675,8 @@ snapshots: rc-menu@9.16.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: - '@babel/runtime': 7.28.4 - '@rc-component/trigger': 2.3.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@babel/runtime': 7.29.2 + '@rc-component/trigger': 2.3.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) classnames: 2.5.1 rc-motion: 2.9.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3) rc-overflow: 1.5.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -8622,7 +8686,7 @@ snapshots: rc-motion@2.9.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.29.2 classnames: 2.5.1 rc-util: 5.44.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 @@ -8630,7 +8694,7 @@ snapshots: rc-overflow@1.5.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.29.2 classnames: 2.5.1 rc-resize-observer: 1.4.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) rc-util: 5.44.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -8639,7 +8703,7 @@ snapshots: rc-resize-observer@1.4.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.29.2 classnames: 2.5.1 rc-util: 5.44.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 @@ -8648,7 +8712,7 @@ snapshots: rc-util@5.44.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: - '@babel/runtime': 7.28.4 + '@babel/runtime': 7.29.2 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) react-is: 18.3.1 @@ -8673,9 +8737,9 @@ snapshots: react: 19.2.3 scheduler: 0.27.0 - react-draggable@4.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + react-draggable@4.5.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: - clsx: 1.2.1 + clsx: 2.1.1 prop-types: 15.8.1 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) @@ -8687,14 +8751,13 @@ snapshots: prop-types: 15.8.1 react: 19.2.3 - react-error-boundary@6.0.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + react-error-boundary@6.1.1(react@19.2.3): dependencies: react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) react-fast-compare@3.2.2: {} - react-hotkeys-hook@5.2.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + react-hotkeys-hook@5.2.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: react: 19.2.3 react-dom: 19.2.3(react@19.2.3) @@ -8716,7 +8779,7 @@ snapshots: remark-parse: 11.0.0 remark-rehype: 11.1.2 unified: 11.0.5 - unist-util-visit: 5.0.0 + unist-util-visit: 5.1.0 vfile: 6.0.3 transitivePeerDependencies: - supports-color @@ -8725,12 +8788,12 @@ snapshots: optionalDependencies: react: 19.2.3 - react-rnd@10.5.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + react-rnd@10.5.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: re-resizable: 6.11.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - react-draggable: 4.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react-draggable: 4.5.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) tslib: 2.6.2 react-zoom-pan-pinch@3.7.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): @@ -8756,10 +8819,10 @@ snapshots: estree-util-build-jsx: 3.0.1 vfile: 6.0.3 - recma-jsx@1.0.1(acorn@8.15.0): + recma-jsx@1.0.1(acorn@8.16.0): dependencies: - acorn: 8.15.0 - acorn-jsx: 5.3.2(acorn@8.15.0) + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) estree-util-to-js: 2.0.0 recma-parse: 1.0.0 recma-stringify: 1.0.0 @@ -8791,18 +8854,18 @@ snapshots: rehype-github-alerts@4.2.0: dependencies: - '@primer/octicons': 19.21.1 + '@primer/octicons': 19.23.1 hast-util-from-html: 2.0.3 hast-util-is-element: 3.0.0 - unist-util-visit: 5.0.0 + unist-util-visit: 5.1.0 rehype-katex@7.0.1: dependencies: '@types/hast': 3.0.4 - '@types/katex': 0.16.7 + '@types/katex': 0.16.8 hast-util-from-html-isomorphic: 2.0.0 hast-util-to-text: 4.0.2 - katex: 0.16.27 + katex: 0.16.45 unist-util-visit-parents: 6.0.2 vfile: 6.0.3 @@ -8853,7 +8916,7 @@ snapshots: mdast-util-find-and-replace: 3.0.2 mdast-util-to-string: 4.0.0 to-vfile: 8.0.0 - unist-util-visit: 5.0.0 + unist-util-visit: 5.1.0 vfile: 6.0.3 remark-math@6.0.0: @@ -8875,7 +8938,7 @@ snapshots: remark-parse@11.0.0: dependencies: '@types/mdast': 4.0.4 - mdast-util-from-markdown: 2.0.2 + mdast-util-from-markdown: 2.0.3 micromark-util-types: 2.0.2 unified: 11.0.5 transitivePeerDependencies: @@ -8919,7 +8982,7 @@ snapshots: dependencies: glob: 7.2.3 - robust-predicates@3.0.2: {} + robust-predicates@3.0.3: {} rollup@4.54.0: dependencies: @@ -8999,21 +9062,21 @@ snapshots: shebang-regex@3.0.0: {} - shiki-stream@0.1.3(react@19.2.3)(vue@3.5.26(typescript@5.6.3)): + shiki-stream@0.1.4(react@19.2.3)(vue@3.5.26(typescript@5.6.3)): dependencies: - '@shikijs/core': 3.20.0 + '@shikijs/core': 3.23.0 optionalDependencies: react: 19.2.3 vue: 3.5.26(typescript@5.6.3) - shiki@3.20.0: + shiki@3.23.0: dependencies: - '@shikijs/core': 3.20.0 - '@shikijs/engine-javascript': 3.20.0 - '@shikijs/engine-oniguruma': 3.20.0 - '@shikijs/langs': 3.20.0 - '@shikijs/themes': 3.20.0 - '@shikijs/types': 3.20.0 + '@shikijs/core': 3.23.0 + '@shikijs/engine-javascript': 3.23.0 + '@shikijs/engine-oniguruma': 3.23.0 + '@shikijs/langs': 3.23.0 + '@shikijs/themes': 3.23.0 + '@shikijs/types': 3.23.0 '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 @@ -9102,7 +9165,7 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - swr@2.3.8(react@19.2.3): + swr@2.4.1(react@19.2.3): dependencies: dequal: 2.0.3 react: 19.2.3 @@ -9164,7 +9227,7 @@ snapshots: tinyexec@0.3.2: {} - tinyexec@1.0.2: {} + tinyexec@1.1.1: {} tinyglobby@0.2.15: dependencies: @@ -9222,7 +9285,7 @@ snapshots: typescript@5.6.3: {} - ufo@1.6.1: {} + ufo@1.6.3: {} undici-types@6.21.0: {} @@ -9258,7 +9321,7 @@ snapshots: unist-util-remove-position@5.0.0: dependencies: '@types/unist': 3.0.3 - unist-util-visit: 5.0.0 + unist-util-visit: 5.1.0 unist-util-stringify-position@4.0.0: dependencies: @@ -9269,7 +9332,7 @@ snapshots: '@types/unist': 3.0.3 unist-util-is: 6.0.1 - unist-util-visit@5.0.0: + unist-util-visit@5.1.0: dependencies: '@types/unist': 3.0.3 unist-util-is: 6.0.1 @@ -9421,8 +9484,6 @@ snapshots: dependencies: vscode-languageserver-protocol: 3.17.5 - vscode-uri@3.0.8: {} - vscode-uri@3.1.0: {} vue-chartjs@5.3.3(chart.js@4.5.1)(vue@3.5.26(typescript@5.6.3)): diff --git a/frontend/src/api/admin/accounts.ts b/frontend/src/api/admin/accounts.ts index 8e40e18b..a146f1f7 100644 --- a/frontend/src/api/admin/accounts.ts +++ b/frontend/src/api/admin/accounts.ts @@ -38,6 +38,8 @@ export async function list( search?: string privacy_mode?: string lite?: string + sort_by?: string + sort_order?: 'asc' | 'desc' }, options?: { signal?: AbortSignal @@ -71,6 +73,8 @@ export async function listWithEtag( search?: string privacy_mode?: string lite?: string + sort_by?: string + sort_order?: 'asc' | 'desc' }, options?: { signal?: AbortSignal @@ -500,7 +504,11 @@ export async function exportData(options?: { platform?: string type?: string status?: string + group?: string + privacy_mode?: string search?: string + sort_by?: string + sort_order?: 'asc' | 'desc' } includeProxies?: boolean }): Promise { @@ -508,11 +516,15 @@ export async function exportData(options?: { if (options?.ids && options.ids.length > 0) { params.ids = options.ids.join(',') } else if (options?.filters) { - const { platform, type, status, search } = options.filters + const { platform, type, status, group, privacy_mode, search, sort_by, sort_order } = options.filters if (platform) params.platform = platform if (type) params.type = type if (status) params.status = status + if (group) params.group = group + if (privacy_mode) params.privacy_mode = privacy_mode if (search) params.search = search + if (sort_by) params.sort_by = sort_by + if (sort_order) params.sort_order = sort_order } if (options?.includeProxies === false) { params.include_proxies = 'false' diff --git a/frontend/src/api/admin/announcements.ts b/frontend/src/api/admin/announcements.ts index d02fdda7..92392a67 100644 --- a/frontend/src/api/admin/announcements.ts +++ b/frontend/src/api/admin/announcements.ts @@ -17,10 +17,16 @@ export async function list( filters?: { status?: string search?: string + sort_by?: string + sort_order?: 'asc' | 'desc' + }, + options?: { + signal?: AbortSignal } ): Promise> { const { data } = await apiClient.get>('/admin/announcements', { - params: { page, page_size: pageSize, ...filters } + params: { page, page_size: pageSize, ...filters }, + signal: options?.signal }) return data } @@ -49,11 +55,21 @@ export async function getReadStatus( id: number, page: number = 1, pageSize: number = 20, - search: string = '' + filters?: { + search?: string + sort_by?: string + sort_order?: 'asc' | 'desc' + }, + options?: { + signal?: AbortSignal + } ): Promise> { const { data } = await apiClient.get>( `/admin/announcements/${id}/read-status`, - { params: { page, page_size: pageSize, search } } + { + params: { page, page_size: pageSize, ...filters }, + signal: options?.signal + } ) return data } @@ -68,4 +84,3 @@ const announcementsAPI = { } export default announcementsAPI - diff --git a/frontend/src/api/admin/channels.ts b/frontend/src/api/admin/channels.ts index 5334dd47..b3455022 100644 --- a/frontend/src/api/admin/channels.ts +++ b/frontend/src/api/admin/channels.ts @@ -83,6 +83,8 @@ export async function list( filters?: { status?: string search?: string + sort_by?: string + sort_order?: 'asc' | 'desc' }, options?: { signal?: AbortSignal } ): Promise> { diff --git a/frontend/src/api/admin/groups.ts b/frontend/src/api/admin/groups.ts index 5885dc6a..8739d5cb 100644 --- a/frontend/src/api/admin/groups.ts +++ b/frontend/src/api/admin/groups.ts @@ -27,6 +27,8 @@ export async function list( status?: 'active' | 'inactive' is_exclusive?: boolean search?: string + sort_by?: string + sort_order?: 'asc' | 'desc' }, options?: { signal?: AbortSignal diff --git a/frontend/src/api/admin/promo.ts b/frontend/src/api/admin/promo.ts index 6a8c4559..b24dffc2 100644 --- a/frontend/src/api/admin/promo.ts +++ b/frontend/src/api/admin/promo.ts @@ -17,10 +17,16 @@ export async function list( filters?: { status?: string search?: string + sort_by?: string + sort_order?: 'asc' | 'desc' + }, + options?: { + signal?: AbortSignal } ): Promise> { const { data } = await apiClient.get>('/admin/promo-codes', { - params: { page, page_size: pageSize, ...filters } + params: { page, page_size: pageSize, ...filters }, + signal: options?.signal }) return data } diff --git a/frontend/src/api/admin/proxies.ts b/frontend/src/api/admin/proxies.ts index 5e31ae20..3e041ba9 100644 --- a/frontend/src/api/admin/proxies.ts +++ b/frontend/src/api/admin/proxies.ts @@ -29,6 +29,8 @@ export async function list( protocol?: string status?: 'active' | 'inactive' search?: string + sort_by?: string + sort_order?: 'asc' | 'desc' }, options?: { signal?: AbortSignal @@ -227,16 +229,20 @@ export async function exportData(options?: { protocol?: string status?: 'active' | 'inactive' search?: string + sort_by?: string + sort_order?: 'asc' | 'desc' } }): Promise { const params: Record = {} if (options?.ids && options.ids.length > 0) { params.ids = options.ids.join(',') } else if (options?.filters) { - const { protocol, status, search } = options.filters + const { protocol, status, search, sort_by, sort_order } = options.filters if (protocol) params.protocol = protocol if (status) params.status = status if (search) params.search = search + if (sort_by) params.sort_by = sort_by + if (sort_order) params.sort_order = sort_order } const { data } = await apiClient.get('/admin/proxies/data', { params }) return data diff --git a/frontend/src/api/admin/redeem.ts b/frontend/src/api/admin/redeem.ts index a53c3566..57626b1e 100644 --- a/frontend/src/api/admin/redeem.ts +++ b/frontend/src/api/admin/redeem.ts @@ -25,6 +25,8 @@ export async function list( type?: RedeemCodeType status?: 'active' | 'used' | 'expired' | 'unused' search?: string + sort_by?: string + sort_order?: 'asc' | 'desc' }, options?: { signal?: AbortSignal @@ -151,7 +153,10 @@ export async function getStats(): Promise<{ */ export async function exportCodes(filters?: { type?: RedeemCodeType - status?: 'active' | 'used' | 'expired' + status?: 'used' | 'expired' | 'unused' + search?: string + sort_by?: string + sort_order?: 'asc' | 'desc' }): Promise { const response = await apiClient.get('/admin/redeem-codes/export', { params: filters, diff --git a/frontend/src/api/admin/settings.ts b/frontend/src/api/admin/settings.ts index 9916f1ab..15a0219b 100644 --- a/frontend/src/api/admin/settings.ts +++ b/frontend/src/api/admin/settings.ts @@ -40,6 +40,8 @@ export interface SystemSettings { hide_ccs_import_button: boolean purchase_subscription_enabled: boolean purchase_subscription_url: string + table_default_page_size: number + table_page_size_options: number[] backend_mode_enabled: boolean custom_menu_items: CustomMenuItem[] custom_endpoints: CustomEndpoint[] @@ -138,6 +140,8 @@ export interface UpdateSettingsRequest { hide_ccs_import_button?: boolean purchase_subscription_enabled?: boolean purchase_subscription_url?: string + table_default_page_size?: number + table_page_size_options?: number[] backend_mode_enabled?: boolean custom_menu_items?: CustomMenuItem[] custom_endpoints?: CustomEndpoint[] diff --git a/frontend/src/api/admin/usage.ts b/frontend/src/api/admin/usage.ts index d21b28dc..37df7553 100644 --- a/frontend/src/api/admin/usage.ts +++ b/frontend/src/api/admin/usage.ts @@ -81,6 +81,8 @@ export interface AdminUsageQueryParams extends UsageQueryParams { user_id?: number exact_total?: boolean billing_mode?: string + sort_by?: string + sort_order?: 'asc' | 'desc' } // ==================== API Functions ==================== diff --git a/frontend/src/api/admin/users.ts b/frontend/src/api/admin/users.ts index bbf0ab51..39cb1dfa 100644 --- a/frontend/src/api/admin/users.ts +++ b/frontend/src/api/admin/users.ts @@ -24,6 +24,8 @@ export async function list( group_name?: string // fuzzy filter by allowed group name attributes?: Record // attributeId -> value include_subscriptions?: boolean + sort_by?: string + sort_order?: 'asc' | 'desc' }, options?: { signal?: AbortSignal @@ -37,7 +39,9 @@ export async function list( role: filters?.role, search: filters?.search, group_name: filters?.group_name, - include_subscriptions: filters?.include_subscriptions + include_subscriptions: filters?.include_subscriptions, + sort_by: filters?.sort_by, + sort_order: filters?.sort_order } // Add attribute filters as attr[id]=value diff --git a/frontend/src/api/keys.ts b/frontend/src/api/keys.ts index 137e10ba..34dd5b4b 100644 --- a/frontend/src/api/keys.ts +++ b/frontend/src/api/keys.ts @@ -17,7 +17,13 @@ import type { ApiKey, CreateApiKeyRequest, UpdateApiKeyRequest, PaginatedRespons export async function list( page: number = 1, pageSize: number = 10, - filters?: { search?: string; status?: string; group_id?: number | string }, + filters?: { + search?: string + status?: string + group_id?: number | string + sort_by?: string + sort_order?: 'asc' | 'desc' + }, options?: { signal?: AbortSignal } diff --git a/frontend/src/api/usage.ts b/frontend/src/api/usage.ts index 6efd7657..802c428f 100644 --- a/frontend/src/api/usage.ts +++ b/frontend/src/api/usage.ts @@ -91,7 +91,7 @@ export async function list( * @returns Paginated list of usage logs */ export async function query( - params: UsageQueryParams, + params: UsageQueryParams & { sort_by?: string; sort_order?: 'asc' | 'desc' }, config: { signal?: AbortSignal } = {} ): Promise> { const { data } = await apiClient.get>('/usage', { diff --git a/frontend/src/components/admin/account/AccountTableFilters.vue b/frontend/src/components/admin/account/AccountTableFilters.vue index 6b474183..b33dad84 100644 --- a/frontend/src/components/admin/account/AccountTableFilters.vue +++ b/frontend/src/components/admin/account/AccountTableFilters.vue @@ -27,7 +27,7 @@ const updatePrivacyMode = (value: string | number | boolean | null) => { emit('u const updateGroup = (value: string | number | boolean | null) => { emit('update:filters', { ...props.filters, group: value }) } const pOpts = computed(() => [{ value: '', label: t('admin.accounts.allPlatforms') }, { value: 'anthropic', label: 'Anthropic' }, { value: 'openai', label: 'OpenAI' }, { value: 'gemini', label: 'Gemini' }, { value: 'antigravity', label: 'Antigravity' }]) const tOpts = computed(() => [{ value: '', label: t('admin.accounts.allTypes') }, { value: 'oauth', label: t('admin.accounts.oauthType') }, { value: 'setup-token', label: t('admin.accounts.setupToken') }, { value: 'apikey', label: t('admin.accounts.apiKey') }, { value: 'bedrock', label: 'AWS Bedrock' }]) -const sOpts = computed(() => [{ value: '', label: t('admin.accounts.allStatus') }, { value: 'active', label: t('admin.accounts.status.active') }, { value: 'inactive', label: t('admin.accounts.status.inactive') }, { value: 'error', label: t('admin.accounts.status.error') }, { value: 'rate_limited', label: t('admin.accounts.status.rateLimited') }, { value: 'temp_unschedulable', label: t('admin.accounts.status.tempUnschedulable') }]) +const sOpts = computed(() => [{ value: '', label: t('admin.accounts.allStatus') }, { value: 'active', label: t('admin.accounts.status.active') }, { value: 'inactive', label: t('admin.accounts.status.inactive') }, { value: 'error', label: t('admin.accounts.status.error') }, { value: 'rate_limited', label: t('admin.accounts.status.rateLimited') }, { value: 'temp_unschedulable', label: t('admin.accounts.status.tempUnschedulable') }, { value: 'unschedulable', label: t('admin.accounts.status.unschedulable') }]) const privacyOpts = computed(() => [ { value: '', label: t('admin.accounts.allPrivacyModes') }, { value: '__unset__', label: t('admin.accounts.privacyUnset') }, diff --git a/frontend/src/components/admin/account/__tests__/AccountTableFilters.spec.ts b/frontend/src/components/admin/account/__tests__/AccountTableFilters.spec.ts deleted file mode 100644 index 5a0044e5..00000000 --- a/frontend/src/components/admin/account/__tests__/AccountTableFilters.spec.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { describe, expect, it, vi } from 'vitest' -import { mount } from '@vue/test-utils' - -import AccountTableFilters from '../AccountTableFilters.vue' - -vi.mock('vue-i18n', async () => { - const actual = await vi.importActual('vue-i18n') - return { - ...actual, - useI18n: () => ({ - t: (key: string) => key - }) - } -}) - -describe('AccountTableFilters', () => { - it('renders privacy mode options and emits privacy_mode updates', async () => { - const wrapper = mount(AccountTableFilters, { - props: { - searchQuery: '', - filters: { - platform: '', - type: '', - status: '', - group: '', - privacy_mode: '' - }, - groups: [] - }, - global: { - stubs: { - SearchInput: { - template: '
' - }, - Select: { - props: ['modelValue', 'options'], - emits: ['update:modelValue', 'change'], - template: '
' - } - } - } - }) - - const selects = wrapper.findAll('.select-stub') - expect(selects).toHaveLength(5) - - const privacyOptions = JSON.parse(selects[3].attributes('data-options')) - expect(privacyOptions).toEqual([ - { value: '', label: 'admin.accounts.allPrivacyModes' }, - { value: '__unset__', label: 'admin.accounts.privacyUnset' }, - { value: 'training_off', label: 'Privacy' }, - { value: 'training_set_cf_blocked', label: 'CF' }, - { value: 'training_set_failed', label: 'Fail' } - ]) - }) -}) diff --git a/frontend/src/components/admin/announcements/AnnouncementReadStatusDialog.vue b/frontend/src/components/admin/announcements/AnnouncementReadStatusDialog.vue index a0d9de3c..60c01c6d 100644 --- a/frontend/src/components/admin/announcements/AnnouncementReadStatusDialog.vue +++ b/frontend/src/components/admin/announcements/AnnouncementReadStatusDialog.vue @@ -21,7 +21,15 @@
- + @@ -62,7 +70,7 @@ diff --git a/frontend/src/components/admin/announcements/__tests__/AnnouncementReadStatusDialog.spec.ts b/frontend/src/components/admin/announcements/__tests__/AnnouncementReadStatusDialog.spec.ts new file mode 100644 index 00000000..26c87d73 --- /dev/null +++ b/frontend/src/components/admin/announcements/__tests__/AnnouncementReadStatusDialog.spec.ts @@ -0,0 +1,95 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { flushPromises, mount } from '@vue/test-utils' + +import AnnouncementReadStatusDialog from '../AnnouncementReadStatusDialog.vue' + +const { getReadStatus, showError } = vi.hoisted(() => ({ + getReadStatus: vi.fn(), + showError: vi.fn(), +})) + +vi.mock('@/api/admin', () => ({ + adminAPI: { + announcements: { + getReadStatus, + }, + }, +})) + +vi.mock('@/stores/app', () => ({ + useAppStore: () => ({ + showError, + }), +})) + +vi.mock('vue-i18n', async () => { + const actual = await vi.importActual('vue-i18n') + return { + ...actual, + useI18n: () => ({ + t: (key: string) => key, + }), + } +}) + +vi.mock('@/composables/usePersistedPageSize', () => ({ + getPersistedPageSize: () => 20, +})) + +const BaseDialogStub = { + props: ['show', 'title', 'width'], + emits: ['close'], + template: '
', +} + +describe('AnnouncementReadStatusDialog', () => { + beforeEach(() => { + getReadStatus.mockReset() + showError.mockReset() + vi.useFakeTimers() + }) + + it('closes by aborting active requests and clearing debounced reloads', async () => { + let activeSignal: AbortSignal | undefined + getReadStatus.mockImplementation(async (...args: any[]) => { + activeSignal = args[4]?.signal + return new Promise(() => {}) + }) + + const wrapper = mount(AnnouncementReadStatusDialog, { + props: { + show: false, + announcementId: 1, + }, + global: { + stubs: { + BaseDialog: BaseDialogStub, + DataTable: true, + Pagination: true, + Icon: true, + }, + }, + }) + + await wrapper.setProps({ show: true }) + await flushPromises() + + expect(getReadStatus).toHaveBeenCalledTimes(1) + expect(activeSignal?.aborted).toBe(false) + + const setupState = (wrapper.vm as any).$?.setupState + setupState.search = 'alice' + setupState.handleSearch() + + setupState.handleClose() + await flushPromises() + + expect(activeSignal?.aborted).toBe(true) + expect(wrapper.emitted('close')).toHaveLength(1) + + vi.advanceTimersByTime(350) + await flushPromises() + + expect(getReadStatus).toHaveBeenCalledTimes(1) + }) +}) diff --git a/frontend/src/components/admin/group/GroupRateMultipliersModal.vue b/frontend/src/components/admin/group/GroupRateMultipliersModal.vue index cbd18af6..bf79bea2 100644 --- a/frontend/src/components/admin/group/GroupRateMultipliersModal.vue +++ b/frontend/src/components/admin/group/GroupRateMultipliersModal.vue @@ -196,7 +196,6 @@ :total="localEntries.length" :page="currentPage" :page-size="pageSize" - :page-size-options="[10, 20, 50]" @update:page="currentPage = $event" @update:pageSize="handlePageSizeChange" /> diff --git a/frontend/src/components/admin/usage/UsageTable.vue b/frontend/src/components/admin/usage/UsageTable.vue index 9bbdb380..f4494e69 100644 --- a/frontend/src/components/admin/usage/UsageTable.vue +++ b/frontend/src/components/admin/usage/UsageTable.vue @@ -1,7 +1,15 @@