diff --git a/backend/cmd/server/VERSION b/backend/cmd/server/VERSION index 2317587346..9e3db2aa12 100644 --- a/backend/cmd/server/VERSION +++ b/backend/cmd/server/VERSION @@ -1 +1 @@ -0.1.105 +0.1.106 diff --git a/backend/internal/handler/admin/setting_handler.go b/backend/internal/handler/admin/setting_handler.go index 397526a7ea..af265d0ad0 100644 --- a/backend/internal/handler/admin/setting_handler.go +++ b/backend/internal/handler/admin/setting_handler.go @@ -1207,6 +1207,106 @@ func (h *SettingHandler) GetStreamTimeoutSettings(c *gin.Context) { }) } +func toExtremePerformanceSettingsDTO(settings *service.ExtremePerformanceSettings) dto.ExtremePerformanceSettings { + if settings == nil { + settings = service.DefaultExtremePerformanceSettings() + } + return dto.ExtremePerformanceSettings{ + Enabled: settings.Enabled, + Admin: dto.ExtremePerformanceAdminSettings{ + DisableAutoUsageFetch: settings.Admin.DisableAutoUsageFetch, + DisableAutoTodayStatsFetch: settings.Admin.DisableAutoTodayStatsFetch, + AllowManualUsageFetch: settings.Admin.AllowManualUsageFetch, + }, + Pool: dto.ExtremePerformancePoolSettings{ + PlatformLimits: dto.ExtremePerformancePlatformLimits{ + OpenAI: settings.Pool.PlatformLimits.OpenAI, + Gemini: settings.Pool.PlatformLimits.Gemini, + Anthropic: settings.Pool.PlatformLimits.Anthropic, + }, + RefillTriggerGap: settings.Pool.RefillTriggerGap, + RefillBatchSize: settings.Pool.RefillBatchSize, + SelectionOrder: settings.Pool.SelectionOrder, + }, + AccountPolicy: dto.ExtremePerformanceAccountPolicySettings{ + DeleteOnAnyUpstream401: settings.AccountPolicy.DeleteOnAnyUpstream401, + CooldownOn429Minutes: settings.AccountPolicy.CooldownOn429Minutes, + CooldownOn5xxMinutes: settings.AccountPolicy.CooldownOn5xxMinutes, + RemoveFromHotPoolOnOverload: settings.AccountPolicy.RemoveFromHotPoolOnOverload, + RemoveFromHotPoolOnTempUnschedulable: settings.AccountPolicy.RemoveFromHotPoolOnTempUnschedulable, + }, + } +} + +func toExtremePerformanceSettingsModel(settings dto.ExtremePerformanceSettings) *service.ExtremePerformanceSettings { + return &service.ExtremePerformanceSettings{ + Enabled: settings.Enabled, + Admin: service.ExtremePerformanceAdminSettings{ + DisableAutoUsageFetch: settings.Admin.DisableAutoUsageFetch, + DisableAutoTodayStatsFetch: settings.Admin.DisableAutoTodayStatsFetch, + AllowManualUsageFetch: settings.Admin.AllowManualUsageFetch, + }, + Pool: service.ExtremePerformancePoolSettings{ + PlatformLimits: service.ExtremePerformancePlatformLimits{ + OpenAI: settings.Pool.PlatformLimits.OpenAI, + Gemini: settings.Pool.PlatformLimits.Gemini, + Anthropic: settings.Pool.PlatformLimits.Anthropic, + }, + RefillTriggerGap: settings.Pool.RefillTriggerGap, + RefillBatchSize: settings.Pool.RefillBatchSize, + SelectionOrder: strings.TrimSpace(settings.Pool.SelectionOrder), + }, + AccountPolicy: service.ExtremePerformanceAccountPolicySettings{ + DeleteOnAnyUpstream401: settings.AccountPolicy.DeleteOnAnyUpstream401, + CooldownOn429Minutes: settings.AccountPolicy.CooldownOn429Minutes, + CooldownOn5xxMinutes: settings.AccountPolicy.CooldownOn5xxMinutes, + RemoveFromHotPoolOnOverload: settings.AccountPolicy.RemoveFromHotPoolOnOverload, + RemoveFromHotPoolOnTempUnschedulable: settings.AccountPolicy.RemoveFromHotPoolOnTempUnschedulable, + }, + } +} + +// GetExtremePerformanceSettings 获取极致性能模式配置 +// GET /api/v1/admin/settings/extreme-performance +func (h *SettingHandler) GetExtremePerformanceSettings(c *gin.Context) { + settings, err := h.settingService.GetExtremePerformanceSettings(c.Request.Context()) + if err != nil { + response.ErrorFrom(c, err) + return + } + + response.Success(c, toExtremePerformanceSettingsDTO(settings)) +} + +// UpdateExtremePerformanceSettings 更新极致性能模式配置 +// PUT /api/v1/admin/settings/extreme-performance +func (h *SettingHandler) UpdateExtremePerformanceSettings(c *gin.Context) { + var req dto.ExtremePerformanceSettings + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "Invalid request: "+err.Error()) + return + } + + settings := toExtremePerformanceSettingsModel(req) + if err := service.ValidateExtremePerformanceSettings(settings); err != nil { + response.BadRequest(c, err.Error()) + return + } + + if err := h.settingService.SetExtremePerformanceSettings(c.Request.Context(), settings); err != nil { + response.BadRequest(c, err.Error()) + return + } + + updatedSettings, err := h.settingService.GetExtremePerformanceSettings(c.Request.Context()) + if err != nil { + response.ErrorFrom(c, err) + return + } + + response.Success(c, toExtremePerformanceSettingsDTO(updatedSettings)) +} + func toSoraS3SettingsDTO(settings *service.SoraS3Settings) dto.SoraS3Settings { if settings == nil { return dto.SoraS3Settings{} diff --git a/backend/internal/handler/admin/setting_handler_extreme_performance_test.go b/backend/internal/handler/admin/setting_handler_extreme_performance_test.go new file mode 100644 index 0000000000..84810cc0fc --- /dev/null +++ b/backend/internal/handler/admin/setting_handler_extreme_performance_test.go @@ -0,0 +1,182 @@ +package admin + +import ( + "bytes" + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/Wei-Shaw/sub2api/internal/config" + "github.com/Wei-Shaw/sub2api/internal/pkg/response" + "github.com/Wei-Shaw/sub2api/internal/service" + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/require" +) + +type extremePerformanceHandlerRepoStub struct { + values map[string]string +} + +func (s *extremePerformanceHandlerRepoStub) Get(ctx context.Context, key string) (*service.Setting, error) { + panic("unexpected Get call") +} + +func (s *extremePerformanceHandlerRepoStub) GetValue(ctx context.Context, key string) (string, error) { + if s.values == nil { + return "", service.ErrSettingNotFound + } + value, ok := s.values[key] + if !ok { + return "", service.ErrSettingNotFound + } + return value, nil +} + +func (s *extremePerformanceHandlerRepoStub) Set(ctx context.Context, key, value string) error { + if s.values == nil { + s.values = make(map[string]string) + } + s.values[key] = value + return nil +} + +func (s *extremePerformanceHandlerRepoStub) GetMultiple(ctx context.Context, keys []string) (map[string]string, error) { + panic("unexpected GetMultiple call") +} + +func (s *extremePerformanceHandlerRepoStub) SetMultiple(ctx context.Context, settings map[string]string) error { + panic("unexpected SetMultiple call") +} + +func (s *extremePerformanceHandlerRepoStub) GetAll(ctx context.Context) (map[string]string, error) { + panic("unexpected GetAll call") +} + +func (s *extremePerformanceHandlerRepoStub) Delete(ctx context.Context, key string) error { + panic("unexpected Delete call") +} + +type extremePerformanceResponseEnvelope struct { + Code int `json:"code"` + Message string `json:"message"` + Data json.RawMessage `json:"data"` +} + +func newExtremePerformanceTestHandler(t *testing.T) (*SettingHandler, *extremePerformanceHandlerRepoStub) { + t.Helper() + repo := &extremePerformanceHandlerRepoStub{} + svc := service.NewSettingService(repo, &config.Config{}) + return NewSettingHandler(svc, nil, nil, nil, nil), repo +} + +func TestSettingHandler_GetExtremePerformanceSettings(t *testing.T) { + gin.SetMode(gin.TestMode) + h, _ := newExtremePerformanceTestHandler(t) + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Request = httptest.NewRequest(http.MethodGet, "/api/v1/admin/settings/extreme-performance", nil) + + h.GetExtremePerformanceSettings(c) + + require.Equal(t, http.StatusOK, w.Code) + var envelope extremePerformanceResponseEnvelope + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &envelope)) + require.Equal(t, 0, envelope.Code) + + var data map[string]any + require.NoError(t, json.Unmarshal(envelope.Data, &data)) + require.Equal(t, false, data["enabled"]) + admin, ok := data["admin"].(map[string]any) + require.True(t, ok) + require.Equal(t, true, admin["disable_auto_usage_fetch"]) +} + +func TestSettingHandler_UpdateExtremePerformanceSettings(t *testing.T) { + gin.SetMode(gin.TestMode) + h, repo := newExtremePerformanceTestHandler(t) + body := []byte(`{ + "enabled": true, + "admin": { + "disable_auto_usage_fetch": true, + "disable_auto_today_stats_fetch": true, + "allow_manual_usage_fetch": false + }, + "pool": { + "platform_limits": { + "openai": 1200, + "gemini": 240, + "anthropic": 80 + }, + "refill_trigger_gap": 25, + "refill_batch_size": 40, + "selection_order": "imported_first" + }, + "account_policy": { + "delete_on_any_upstream_401": true, + "cooldown_on_429_minutes": 5, + "cooldown_on_5xx_minutes": 1, + "remove_from_hot_pool_on_overload": true, + "remove_from_hot_pool_on_temp_unschedulable": true + } + }`) + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Request = httptest.NewRequest(http.MethodPut, "/api/v1/admin/settings/extreme-performance", bytes.NewReader(body)) + c.Request.Header.Set("Content-Type", "application/json") + + h.UpdateExtremePerformanceSettings(c) + + require.Equal(t, http.StatusOK, w.Code) + require.Contains(t, repo.values, service.SettingKeyExtremePerformanceSettings) + + var envelope extremePerformanceResponseEnvelope + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &envelope)) + require.Equal(t, 0, envelope.Code) + + var data map[string]any + require.NoError(t, json.Unmarshal(envelope.Data, &data)) + require.Equal(t, true, data["enabled"]) +} + +func TestSettingHandler_UpdateExtremePerformanceSettings_RejectsInvalidSelectionOrder(t *testing.T) { + gin.SetMode(gin.TestMode) + h, _ := newExtremePerformanceTestHandler(t) + body := []byte(`{ + "enabled": true, + "admin": { + "disable_auto_usage_fetch": true, + "disable_auto_today_stats_fetch": true, + "allow_manual_usage_fetch": true + }, + "pool": { + "platform_limits": { + "openai": 1200, + "gemini": 240, + "anthropic": 80 + }, + "refill_trigger_gap": 25, + "refill_batch_size": 40, + "selection_order": "random" + }, + "account_policy": { + "delete_on_any_upstream_401": true, + "cooldown_on_429_minutes": 5, + "cooldown_on_5xx_minutes": 1, + "remove_from_hot_pool_on_overload": true, + "remove_from_hot_pool_on_temp_unschedulable": true + } + }`) + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Request = httptest.NewRequest(http.MethodPut, "/api/v1/admin/settings/extreme-performance", bytes.NewReader(body)) + c.Request.Header.Set("Content-Type", "application/json") + + h.UpdateExtremePerformanceSettings(c) + + require.Equal(t, http.StatusBadRequest, w.Code) + var envelope response.Response + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &envelope)) + require.Contains(t, envelope.Message, "selection_order") +} diff --git a/backend/internal/handler/dto/settings.go b/backend/internal/handler/dto/settings.go index 47bab091d7..3bc0213636 100644 --- a/backend/internal/handler/dto/settings.go +++ b/backend/internal/handler/dto/settings.go @@ -186,7 +186,63 @@ type StreamTimeoutSettings struct { ThresholdWindowMinutes int `json:"threshold_window_minutes"` } +// ExtremePerformanceAdminSettings 极致性能模式后台轻量化配置 DTO +type ExtremePerformanceAdminSettings struct { + DisableAutoUsageFetch bool `json:"disable_auto_usage_fetch"` + DisableAutoTodayStatsFetch bool `json:"disable_auto_today_stats_fetch"` + AllowManualUsageFetch bool `json:"allow_manual_usage_fetch"` +} + +// ExtremePerformancePlatformLimits 极致性能模式热池平台上限 DTO +type ExtremePerformancePlatformLimits struct { + OpenAI int `json:"openai"` + Gemini int `json:"gemini"` + Anthropic int `json:"anthropic"` +} + +// ExtremePerformancePoolSettings 极致性能模式热池配置 DTO +type ExtremePerformancePoolSettings struct { + PlatformLimits ExtremePerformancePlatformLimits `json:"platform_limits"` + RefillTriggerGap int `json:"refill_trigger_gap"` + RefillBatchSize int `json:"refill_batch_size"` + SelectionOrder string `json:"selection_order"` +} + +// ExtremePerformanceAccountPolicySettings 极致性能模式账号策略配置 DTO +type ExtremePerformanceAccountPolicySettings struct { + DeleteOnAnyUpstream401 bool `json:"delete_on_any_upstream_401"` + CooldownOn429Minutes int `json:"cooldown_on_429_minutes"` + CooldownOn5xxMinutes int `json:"cooldown_on_5xx_minutes"` + RemoveFromHotPoolOnOverload bool `json:"remove_from_hot_pool_on_overload"` + RemoveFromHotPoolOnTempUnschedulable bool `json:"remove_from_hot_pool_on_temp_unschedulable"` +} + +// ExtremePerformanceSettings 极致性能模式配置 DTO +type ExtremePerformanceSettings struct { + Enabled bool `json:"enabled"` + Admin ExtremePerformanceAdminSettings `json:"admin"` + Pool ExtremePerformancePoolSettings `json:"pool"` + AccountPolicy ExtremePerformanceAccountPolicySettings `json:"account_policy"` +} + +// ExtremePerformancePlatformStatus 极致性能模式平台运行态 DTO +type ExtremePerformancePlatformStatus struct { + Platform string `json:"platform"` + TargetSize int `json:"target_size"` + CurrentSize int `json:"current_size"` + ColdCandidates int `json:"cold_candidates,omitempty"` + LastRefillAt string `json:"last_refill_at,omitempty"` + LastRebuildAt string `json:"last_rebuild_at,omitempty"` + Version int64 `json:"version"` +} + +// ExtremePerformanceStatus 极致性能模式运行态 DTO +type ExtremePerformanceStatus struct { + Platforms []ExtremePerformancePlatformStatus `json:"platforms"` +} + // RectifierSettings 请求整流器配置 DTO + type RectifierSettings struct { Enabled bool `json:"enabled"` ThinkingSignatureEnabled bool `json:"thinking_signature_enabled"` diff --git a/backend/internal/server/routes/admin.go b/backend/internal/server/routes/admin.go index e04dae8521..0bcf2e4677 100644 --- a/backend/internal/server/routes/admin.go +++ b/backend/internal/server/routes/admin.go @@ -413,6 +413,9 @@ func registerSettingsRoutes(admin *gin.RouterGroup, h *handler.Handlers) { // 流超时处理配置 adminSettings.GET("/stream-timeout", h.Admin.Setting.GetStreamTimeoutSettings) adminSettings.PUT("/stream-timeout", h.Admin.Setting.UpdateStreamTimeoutSettings) + // 极致性能模式配置 + adminSettings.GET("/extreme-performance", h.Admin.Setting.GetExtremePerformanceSettings) + adminSettings.PUT("/extreme-performance", h.Admin.Setting.UpdateExtremePerformanceSettings) // 请求整流器配置 adminSettings.GET("/rectifier", h.Admin.Setting.GetRectifierSettings) adminSettings.PUT("/rectifier", h.Admin.Setting.UpdateRectifierSettings) diff --git a/backend/internal/service/domain_constants.go b/backend/internal/service/domain_constants.go index ecac0db0cf..e988d23871 100644 --- a/backend/internal/service/domain_constants.go +++ b/backend/internal/service/domain_constants.go @@ -185,6 +185,13 @@ const ( // SettingKeyStreamTimeoutSettings stores JSON config for stream timeout handling. SettingKeyStreamTimeoutSettings = "stream_timeout_settings" + // ========================= + // Extreme Performance Mode + // ========================= + + // SettingKeyExtremePerformanceSettings stores JSON config for extreme performance mode. + SettingKeyExtremePerformanceSettings = "extreme_performance_settings" + // ========================= // Request Rectifier (请求整流器) // ========================= diff --git a/backend/internal/service/extreme_performance_settings.go b/backend/internal/service/extreme_performance_settings.go new file mode 100644 index 0000000000..e425a6fa3b --- /dev/null +++ b/backend/internal/service/extreme_performance_settings.go @@ -0,0 +1,182 @@ +package service + +import ( + "fmt" + "strings" +) + +const ( + // ExtremePerformanceSelectionOrderImportedFirst 表示热池补位按先导入优先(created_at ASC, id ASC)。 + ExtremePerformanceSelectionOrderImportedFirst = "imported_first" + + extremePerformanceMinPlatformLimit = 1 + extremePerformanceMaxPlatformLimit = 100000 + extremePerformanceMinRefillTriggerGap = 1 + extremePerformanceMaxRefillTriggerGap = 10000 + extremePerformanceMinRefillBatchSize = 1 + extremePerformanceMaxRefillBatchSize = 10000 + extremePerformanceMinCooldownMinutes = 0 + extremePerformanceMaxCooldownMinutes = 1440 +) + +// ExtremePerformanceSettings 极致性能模式设置。 +type ExtremePerformanceSettings struct { + Enabled bool `json:"enabled"` + Admin ExtremePerformanceAdminSettings `json:"admin"` + Pool ExtremePerformancePoolSettings `json:"pool"` + AccountPolicy ExtremePerformanceAccountPolicySettings `json:"account_policy"` +} + +// ExtremePerformanceAdminSettings 后台轻量化相关设置。 +type ExtremePerformanceAdminSettings struct { + DisableAutoUsageFetch bool `json:"disable_auto_usage_fetch"` + DisableAutoTodayStatsFetch bool `json:"disable_auto_today_stats_fetch"` + AllowManualUsageFetch bool `json:"allow_manual_usage_fetch"` +} + +// ExtremePerformancePoolSettings 热池相关设置。 +type ExtremePerformancePoolSettings struct { + PlatformLimits ExtremePerformancePlatformLimits `json:"platform_limits"` + RefillTriggerGap int `json:"refill_trigger_gap"` + RefillBatchSize int `json:"refill_batch_size"` + SelectionOrder string `json:"selection_order"` +} + +// ExtremePerformancePlatformLimits 各平台热池目标上限。 +type ExtremePerformancePlatformLimits struct { + OpenAI int `json:"openai"` + Gemini int `json:"gemini"` + Anthropic int `json:"anthropic"` +} + +// ExtremePerformanceAccountPolicySettings 账号级策略设置。 +type ExtremePerformanceAccountPolicySettings struct { + DeleteOnAnyUpstream401 bool `json:"delete_on_any_upstream_401"` + CooldownOn429Minutes int `json:"cooldown_on_429_minutes"` + CooldownOn5xxMinutes int `json:"cooldown_on_5xx_minutes"` + RemoveFromHotPoolOnOverload bool `json:"remove_from_hot_pool_on_overload"` + RemoveFromHotPoolOnTempUnschedulable bool `json:"remove_from_hot_pool_on_temp_unschedulable"` +} + +// DefaultExtremePerformanceSettings 返回极致性能模式默认配置。 +func DefaultExtremePerformanceSettings() *ExtremePerformanceSettings { + return &ExtremePerformanceSettings{ + Enabled: false, + Admin: ExtremePerformanceAdminSettings{ + DisableAutoUsageFetch: true, + DisableAutoTodayStatsFetch: true, + AllowManualUsageFetch: true, + }, + Pool: ExtremePerformancePoolSettings{ + PlatformLimits: ExtremePerformancePlatformLimits{ + OpenAI: 2000, + Gemini: 300, + Anthropic: 100, + }, + RefillTriggerGap: 50, + RefillBatchSize: 100, + SelectionOrder: ExtremePerformanceSelectionOrderImportedFirst, + }, + AccountPolicy: ExtremePerformanceAccountPolicySettings{ + DeleteOnAnyUpstream401: true, + CooldownOn429Minutes: 5, + CooldownOn5xxMinutes: 1, + RemoveFromHotPoolOnOverload: true, + RemoveFromHotPoolOnTempUnschedulable: true, + }, + } +} + +// ValidateExtremePerformanceSettings 校验极致性能模式设置。 +func ValidateExtremePerformanceSettings(settings *ExtremePerformanceSettings) error { + if settings == nil { + return fmt.Errorf("settings cannot be nil") + } + + if err := validateExtremePerformancePlatformLimit("pool.platform_limits.openai", settings.Pool.PlatformLimits.OpenAI); err != nil { + return err + } + if err := validateExtremePerformancePlatformLimit("pool.platform_limits.gemini", settings.Pool.PlatformLimits.Gemini); err != nil { + return err + } + if err := validateExtremePerformancePlatformLimit("pool.platform_limits.anthropic", settings.Pool.PlatformLimits.Anthropic); err != nil { + return err + } + if settings.Pool.RefillTriggerGap < extremePerformanceMinRefillTriggerGap || settings.Pool.RefillTriggerGap > extremePerformanceMaxRefillTriggerGap { + return fmt.Errorf("pool.refill_trigger_gap must be between %d-%d", extremePerformanceMinRefillTriggerGap, extremePerformanceMaxRefillTriggerGap) + } + if settings.Pool.RefillBatchSize < extremePerformanceMinRefillBatchSize || settings.Pool.RefillBatchSize > extremePerformanceMaxRefillBatchSize { + return fmt.Errorf("pool.refill_batch_size must be between %d-%d", extremePerformanceMinRefillBatchSize, extremePerformanceMaxRefillBatchSize) + } + + selectionOrder := strings.ToLower(strings.TrimSpace(settings.Pool.SelectionOrder)) + switch selectionOrder { + case ExtremePerformanceSelectionOrderImportedFirst: + // valid + default: + return fmt.Errorf("pool.selection_order must be %q", ExtremePerformanceSelectionOrderImportedFirst) + } + + if err := validateExtremePerformanceCooldown("account_policy.cooldown_on_429_minutes", settings.AccountPolicy.CooldownOn429Minutes); err != nil { + return err + } + if err := validateExtremePerformanceCooldown("account_policy.cooldown_on_5xx_minutes", settings.AccountPolicy.CooldownOn5xxMinutes); err != nil { + return err + } + + return nil +} + +func validateExtremePerformancePlatformLimit(field string, value int) error { + if value < extremePerformanceMinPlatformLimit || value > extremePerformanceMaxPlatformLimit { + return fmt.Errorf("%s must be between %d-%d", field, extremePerformanceMinPlatformLimit, extremePerformanceMaxPlatformLimit) + } + return nil +} + +func validateExtremePerformanceCooldown(field string, value int) error { + if value < extremePerformanceMinCooldownMinutes || value > extremePerformanceMaxCooldownMinutes { + return fmt.Errorf("%s must be between %d-%d", field, extremePerformanceMinCooldownMinutes, extremePerformanceMaxCooldownMinutes) + } + return nil +} + +func normalizeExtremePerformanceSettings(settings *ExtremePerformanceSettings) *ExtremePerformanceSettings { + defaults := DefaultExtremePerformanceSettings() + if settings == nil { + return defaults + } + + normalized := *settings + normalized.Pool.SelectionOrder = strings.ToLower(strings.TrimSpace(normalized.Pool.SelectionOrder)) + + if normalized.Pool.PlatformLimits.OpenAI < extremePerformanceMinPlatformLimit || normalized.Pool.PlatformLimits.OpenAI > extremePerformanceMaxPlatformLimit { + normalized.Pool.PlatformLimits.OpenAI = defaults.Pool.PlatformLimits.OpenAI + } + if normalized.Pool.PlatformLimits.Gemini < extremePerformanceMinPlatformLimit || normalized.Pool.PlatformLimits.Gemini > extremePerformanceMaxPlatformLimit { + normalized.Pool.PlatformLimits.Gemini = defaults.Pool.PlatformLimits.Gemini + } + if normalized.Pool.PlatformLimits.Anthropic < extremePerformanceMinPlatformLimit || normalized.Pool.PlatformLimits.Anthropic > extremePerformanceMaxPlatformLimit { + normalized.Pool.PlatformLimits.Anthropic = defaults.Pool.PlatformLimits.Anthropic + } + if normalized.Pool.RefillTriggerGap < extremePerformanceMinRefillTriggerGap || normalized.Pool.RefillTriggerGap > extremePerformanceMaxRefillTriggerGap { + normalized.Pool.RefillTriggerGap = defaults.Pool.RefillTriggerGap + } + if normalized.Pool.RefillBatchSize < extremePerformanceMinRefillBatchSize || normalized.Pool.RefillBatchSize > extremePerformanceMaxRefillBatchSize { + normalized.Pool.RefillBatchSize = defaults.Pool.RefillBatchSize + } + if normalized.Pool.SelectionOrder == "" { + normalized.Pool.SelectionOrder = defaults.Pool.SelectionOrder + } + if normalized.Pool.SelectionOrder != ExtremePerformanceSelectionOrderImportedFirst { + normalized.Pool.SelectionOrder = defaults.Pool.SelectionOrder + } + if normalized.AccountPolicy.CooldownOn429Minutes < extremePerformanceMinCooldownMinutes || normalized.AccountPolicy.CooldownOn429Minutes > extremePerformanceMaxCooldownMinutes { + normalized.AccountPolicy.CooldownOn429Minutes = defaults.AccountPolicy.CooldownOn429Minutes + } + if normalized.AccountPolicy.CooldownOn5xxMinutes < extremePerformanceMinCooldownMinutes || normalized.AccountPolicy.CooldownOn5xxMinutes > extremePerformanceMaxCooldownMinutes { + normalized.AccountPolicy.CooldownOn5xxMinutes = defaults.AccountPolicy.CooldownOn5xxMinutes + } + + return &normalized +} diff --git a/backend/internal/service/extreme_performance_settings_test.go b/backend/internal/service/extreme_performance_settings_test.go new file mode 100644 index 0000000000..7ee3251250 --- /dev/null +++ b/backend/internal/service/extreme_performance_settings_test.go @@ -0,0 +1,125 @@ +//go:build unit + +package service + +import ( + "context" + "testing" + + "github.com/Wei-Shaw/sub2api/internal/config" + "github.com/stretchr/testify/require" +) + +type extremePerformanceSettingRepoStub struct { + values map[string]string +} + +func (s *extremePerformanceSettingRepoStub) Get(ctx context.Context, key string) (*Setting, error) { + panic("unexpected Get call") +} + +func (s *extremePerformanceSettingRepoStub) GetValue(ctx context.Context, key string) (string, error) { + if s.values == nil { + return "", ErrSettingNotFound + } + value, ok := s.values[key] + if !ok { + return "", ErrSettingNotFound + } + return value, nil +} + +func (s *extremePerformanceSettingRepoStub) Set(ctx context.Context, key, value string) error { + if s.values == nil { + s.values = make(map[string]string) + } + s.values[key] = value + return nil +} + +func (s *extremePerformanceSettingRepoStub) GetMultiple(ctx context.Context, keys []string) (map[string]string, error) { + panic("unexpected GetMultiple call") +} + +func (s *extremePerformanceSettingRepoStub) SetMultiple(ctx context.Context, settings map[string]string) error { + panic("unexpected SetMultiple call") +} + +func (s *extremePerformanceSettingRepoStub) GetAll(ctx context.Context) (map[string]string, error) { + panic("unexpected GetAll call") +} + +func (s *extremePerformanceSettingRepoStub) Delete(ctx context.Context, key string) error { + panic("unexpected Delete call") +} + +func TestGetExtremePerformanceSettings_DefaultsWhenMissing(t *testing.T) { + repo := &extremePerformanceSettingRepoStub{} + svc := NewSettingService(repo, &config.Config{}) + + got, err := svc.GetExtremePerformanceSettings(context.Background()) + require.NoError(t, err) + require.Equal(t, DefaultExtremePerformanceSettings(), got) +} + +func TestSetAndGetExtremePerformanceSettings_RoundTrip(t *testing.T) { + repo := &extremePerformanceSettingRepoStub{} + svc := NewSettingService(repo, &config.Config{}) + + updated := false + svc.SetOnUpdateCallback(func() { + updated = true + }) + + input := &ExtremePerformanceSettings{ + Enabled: true, + Admin: ExtremePerformanceAdminSettings{ + DisableAutoUsageFetch: true, + DisableAutoTodayStatsFetch: true, + AllowManualUsageFetch: false, + }, + Pool: ExtremePerformancePoolSettings{ + PlatformLimits: ExtremePerformancePlatformLimits{ + OpenAI: 1500, + Gemini: 250, + Anthropic: 90, + }, + RefillTriggerGap: 25, + RefillBatchSize: 50, + SelectionOrder: "IMPORTED_FIRST", + }, + AccountPolicy: ExtremePerformanceAccountPolicySettings{ + DeleteOnAnyUpstream401: true, + CooldownOn429Minutes: 7, + CooldownOn5xxMinutes: 2, + RemoveFromHotPoolOnOverload: true, + RemoveFromHotPoolOnTempUnschedulable: false, + }, + } + + err := svc.SetExtremePerformanceSettings(context.Background(), input) + require.NoError(t, err) + require.True(t, updated) + require.Contains(t, repo.values, SettingKeyExtremePerformanceSettings) + + got, err := svc.GetExtremePerformanceSettings(context.Background()) + require.NoError(t, err) + require.True(t, got.Enabled) + require.Equal(t, 1500, got.Pool.PlatformLimits.OpenAI) + require.Equal(t, 250, got.Pool.PlatformLimits.Gemini) + require.Equal(t, 90, got.Pool.PlatformLimits.Anthropic) + require.Equal(t, 25, got.Pool.RefillTriggerGap) + require.Equal(t, 50, got.Pool.RefillBatchSize) + require.Equal(t, ExtremePerformanceSelectionOrderImportedFirst, got.Pool.SelectionOrder) + require.False(t, got.Admin.AllowManualUsageFetch) + require.Equal(t, 7, got.AccountPolicy.CooldownOn429Minutes) +} + +func TestValidateExtremePerformanceSettings_RejectsInvalidSelectionOrder(t *testing.T) { + settings := DefaultExtremePerformanceSettings() + settings.Pool.SelectionOrder = "random" + + err := ValidateExtremePerformanceSettings(settings) + require.Error(t, err) + require.Contains(t, err.Error(), "pool.selection_order") +} diff --git a/backend/internal/service/setting_service.go b/backend/internal/service/setting_service.go index 1a24bad149..ad9be13d70 100644 --- a/backend/internal/service/setting_service.go +++ b/backend/internal/service/setting_service.go @@ -1584,6 +1584,53 @@ func (s *SettingService) SetStreamTimeoutSettings(ctx context.Context, settings return s.settingRepo.Set(ctx, SettingKeyStreamTimeoutSettings, string(data)) } +// GetExtremePerformanceSettings 获取极致性能模式配置 +func (s *SettingService) GetExtremePerformanceSettings(ctx context.Context) (*ExtremePerformanceSettings, error) { + value, err := s.settingRepo.GetValue(ctx, SettingKeyExtremePerformanceSettings) + if err != nil { + if errors.Is(err, ErrSettingNotFound) { + return DefaultExtremePerformanceSettings(), nil + } + return nil, fmt.Errorf("get extreme performance settings: %w", err) + } + if strings.TrimSpace(value) == "" { + return DefaultExtremePerformanceSettings(), nil + } + + settings := *DefaultExtremePerformanceSettings() + if err := json.Unmarshal([]byte(value), &settings); err != nil { + return DefaultExtremePerformanceSettings(), nil + } + + return normalizeExtremePerformanceSettings(&settings), nil +} + +// SetExtremePerformanceSettings 设置极致性能模式配置 +func (s *SettingService) SetExtremePerformanceSettings(ctx context.Context, settings *ExtremePerformanceSettings) error { + if settings == nil { + return fmt.Errorf("settings cannot be nil") + } + + normalized := *settings + normalized.Pool.SelectionOrder = strings.ToLower(strings.TrimSpace(normalized.Pool.SelectionOrder)) + if err := ValidateExtremePerformanceSettings(&normalized); err != nil { + return err + } + + data, err := json.Marshal(&normalized) + if err != nil { + return fmt.Errorf("marshal extreme performance settings: %w", err) + } + + if err := s.settingRepo.Set(ctx, SettingKeyExtremePerformanceSettings, string(data)); err != nil { + return err + } + if s.onUpdate != nil { + s.onUpdate() + } + return nil +} + type soraS3ProfilesStore struct { ActiveProfileID string `json:"active_profile_id"` Items []soraS3ProfileStoreItem `json:"items"` diff --git a/frontend/src/api/admin/settings.ts b/frontend/src/api/admin/settings.ts index cabdd5aac9..1fbba15540 100644 --- a/frontend/src/api/admin/settings.ts +++ b/frontend/src/api/admin/settings.ts @@ -254,6 +254,52 @@ export async function deleteAdminApiKey(): Promise<{ message: string }> { // ==================== Overload Cooldown Settings ==================== +/** + * Overload cooldown settings interface (529 handling) + */ +export interface ExtremePerformanceSettings { + enabled: boolean + admin: { + disable_auto_usage_fetch: boolean + disable_auto_today_stats_fetch: boolean + allow_manual_usage_fetch: boolean + } + pool: { + platform_limits: { + openai: number + gemini: number + anthropic: number + } + refill_trigger_gap: number + refill_batch_size: number + selection_order: 'imported_first' | string + } + account_policy: { + delete_on_any_upstream_401: boolean + cooldown_on_429_minutes: number + cooldown_on_5xx_minutes: number + remove_from_hot_pool_on_overload: boolean + remove_from_hot_pool_on_temp_unschedulable: boolean + } +} + +export async function getExtremePerformanceSettings(): Promise { + const { data } = await apiClient.get('/admin/settings/extreme-performance') + return data +} + +export async function updateExtremePerformanceSettings( + settings: ExtremePerformanceSettings +): Promise { + const { data } = await apiClient.put( + '/admin/settings/extreme-performance', + settings + ) + return data +} + +// ==================== Overload Cooldown Settings ==================== + /** * Overload cooldown settings interface (529 handling) */ @@ -538,6 +584,8 @@ export const settingsAPI = { getAdminApiKey, regenerateAdminApiKey, deleteAdminApiKey, + getExtremePerformanceSettings, + updateExtremePerformanceSettings, getOverloadCooldownSettings, updateOverloadCooldownSettings, getStreamTimeoutSettings, diff --git a/frontend/src/components/account/AccountUsageCell.vue b/frontend/src/components/account/AccountUsageCell.vue index 37e18c35df..7d2a325323 100644 --- a/frontend/src/components/account/AccountUsageCell.vue +++ b/frontend/src/components/account/AccountUsageCell.vue @@ -454,11 +454,13 @@ const props = withDefaults( todayStats?: WindowStats | null todayStatsLoading?: boolean manualRefreshToken?: number + autoFetchEnabled?: boolean }>(), { todayStats: null, todayStatsLoading: false, - manualRefreshToken: 0 + manualRefreshToken: 0, + autoFetchEnabled: true } ) @@ -511,7 +513,7 @@ const hasOpenAIUsageFallback = computed(() => { const openAIUsageRefreshKey = computed(() => buildOpenAIUsageRefreshKey(props.account)) const shouldAutoLoadUsageOnMount = computed(() => { - return shouldFetchUsage.value + return shouldFetchUsage.value && props.autoFetchEnabled }) // Antigravity quota types (用于 API 返回的数据) @@ -1046,6 +1048,7 @@ onMounted(() => { }) watch(openAIUsageRefreshKey, (nextKey, prevKey) => { + if (!props.autoFetchEnabled) return if (!prevKey || nextKey === prevKey) return if (props.account.platform !== 'openai' || props.account.type !== 'oauth') return @@ -1057,6 +1060,7 @@ watch(openAIUsageRefreshKey, (nextKey, prevKey) => { watch( () => props.manualRefreshToken, (nextToken, prevToken) => { + if (!props.autoFetchEnabled) return if (nextToken === prevToken) return if (!shouldFetchUsage.value) return diff --git a/frontend/src/components/account/__tests__/AccountUsageCell.spec.ts b/frontend/src/components/account/__tests__/AccountUsageCell.spec.ts index 9158da64ed..5a82b6d78c 100644 --- a/frontend/src/components/account/__tests__/AccountUsageCell.spec.ts +++ b/frontend/src/components/account/__tests__/AccountUsageCell.spec.ts @@ -59,6 +59,59 @@ describe('AccountUsageCell', () => { getUsage.mockReset() }) + it('禁用 auto fetch 时挂载不会自动请求 usage', async () => { + getUsage.mockResolvedValue({}) + + mount(AccountUsageCell, { + props: { + account: makeAccount({ + id: 9001, + platform: 'openai', + type: 'oauth', + extra: {} + }), + autoFetchEnabled: false + }, + global: { + stubs: { + UsageProgressBar: true, + AccountQuotaInfo: true + } + } + }) + + await flushPromises() + expect(getUsage).not.toHaveBeenCalled() + }) + + it('禁用 auto fetch 时不会响应 manualRefreshToken 变化', async () => { + getUsage.mockResolvedValue({}) + + const wrapper = mount(AccountUsageCell, { + props: { + account: makeAccount({ + id: 9002, + platform: 'openai', + type: 'oauth', + extra: {} + }), + autoFetchEnabled: false, + manualRefreshToken: 0 + }, + global: { + stubs: { + UsageProgressBar: true, + AccountQuotaInfo: true + } + } + }) + + await flushPromises() + await wrapper.setProps({ manualRefreshToken: 1 }) + await flushPromises() + expect(getUsage).not.toHaveBeenCalled() + }) + it('Antigravity 图片用量会聚合新旧 image 模型', async () => { getUsage.mockResolvedValue({ antigravity_quota: { @@ -193,7 +246,7 @@ describe('AccountUsageCell', () => { await flushPromises() - expect(getUsage).toHaveBeenCalledWith(2000) + expect(getUsage).toHaveBeenCalledWith(2000, undefined) expect(wrapper.text()).toContain('5h|15|300') expect(wrapper.text()).toContain('7d|77|300') }) @@ -254,7 +307,7 @@ describe('AccountUsageCell', () => { await flushPromises() - expect(getUsage).toHaveBeenCalledWith(2001) + expect(getUsage).toHaveBeenCalledWith(2001, undefined) // 单一数据源:始终使用 /usage API 返回值,忽略 codex 快照 expect(wrapper.text()).toContain('5h|18|900') expect(wrapper.text()).toContain('7d|36|900') @@ -325,7 +378,7 @@ describe('AccountUsageCell', () => { // 手动刷新再拉一次 expect(getUsage).toHaveBeenCalledTimes(2) - expect(getUsage).toHaveBeenCalledWith(2010) + expect(getUsage).toHaveBeenCalledWith(2010, undefined) // 单一数据源:始终使用 /usage API 值 expect(wrapper.text()).toContain('5h|18|900') }) @@ -380,7 +433,7 @@ describe('AccountUsageCell', () => { await flushPromises() - expect(getUsage).toHaveBeenCalledWith(2002) + expect(getUsage).toHaveBeenCalledWith(2002, undefined) expect(wrapper.text()).toContain('5h|0|27700') expect(wrapper.text()).toContain('7d|0|27700') }) @@ -512,7 +565,7 @@ describe('AccountUsageCell', () => { await flushPromises() - expect(getUsage).toHaveBeenCalledWith(2004) + expect(getUsage).toHaveBeenCalledWith(2004, undefined) expect(wrapper.text()).toContain('5h|100|106540000') expect(wrapper.text()).toContain('7d|100|106540000') }) diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index f5267d6a8a..9aaeec3135 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -4441,16 +4441,48 @@ export default { testFailed: 'Google Drive storage test failed' } }, + extremePerformance: { + title: 'Extreme Performance Mode', + description: 'Keep original features while disabling automatic admin usage requests and preparing the system for hot-pool scheduling.', + enabled: 'Enable extreme performance mode', + enabledHint: 'When enabled, admin pages and scheduling will gradually switch to lightweight / hot-pool behavior.', + disableAutoUsageFetch: 'Disable automatic usage refresh', + disableAutoUsageFetchHint: 'The accounts list will stop auto-requesting /usage and rely on manual actions.', + disableAutoTodayStatsFetch: 'Disable automatic today stats refresh', + disableAutoTodayStatsFetchHint: 'The accounts list will stop auto-requesting /today-stats/batch.', + allowManualUsageFetch: 'Allow manual usage inspection', + allowManualUsageFetchHint: 'Keep manual single-account usage fetch available so original functionality is preserved.', + openaiHotPool: 'OpenAI hot pool limit', + geminiHotPool: 'Gemini hot pool limit', + anthropicHotPool: 'Anthropic hot pool limit', + refillTriggerGap: 'Refill trigger gap', + refillBatchSize: 'Refill batch size', + deleteOn401: 'Delete account on account-level upstream 401', + deleteOn401Hint: 'Only applies to upstream authentication failures that can be mapped to a concrete account.', + runtimeStatus: 'Runtime status', + targetSize: 'Target hot pool', + currentSize: 'Current hot pool', + coldCandidates: 'Cold candidates', + version: 'Version', + triggerRefill: 'Trigger refill', + triggerRebuild: 'Trigger rebuild', + refillTriggered: 'Hot pool refill triggered', + rebuildTriggered: 'Hot pool rebuild triggered', + actionFailed: 'Hot pool action failed', + saved: 'Extreme performance settings saved', + saveFailed: 'Failed to save extreme performance settings' + }, overloadCooldown: { title: '529 Overload Cooldown', description: 'Configure account scheduling pause strategy when upstream returns 529 (overloaded)', - enabled: 'Enable Overload Cooldown', - enabledHint: 'Pause account scheduling on 529 errors, auto-recover after cooldown', + enabled: 'Enable 529 cooldown', + enabledHint: 'Automatically pause scheduling for this account when 529 is received', cooldownMinutes: 'Cooldown Duration (minutes)', - cooldownMinutesHint: 'Duration to pause account scheduling (1-120 minutes)', + cooldownMinutesHint: 'Recommended 1-30 minutes, default 10 minutes', saved: 'Overload cooldown settings saved', saveFailed: 'Failed to save overload cooldown settings' }, + streamTimeout: { title: 'Stream Timeout Handling', description: 'Configure account handling strategy when upstream response times out', diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts index 9581206ecc..db2a059af2 100644 --- a/frontend/src/i18n/locales/zh.ts +++ b/frontend/src/i18n/locales/zh.ts @@ -4605,16 +4605,48 @@ export default { testFailed: 'Google Drive 存储测试失败' } }, + extremePerformance: { + title: '极致性能模式', + description: '在保留原功能的前提下,关闭后台账号列表的自动用量请求,并为后续热池调度提供统一配置入口。', + enabled: '启用极致性能模式', + enabledHint: '开启后,后台管理页和调度层会逐步切换到轻量化/热池模式。', + disableAutoUsageFetch: '禁止自动刷新用量窗口', + disableAutoUsageFetchHint: '账号列表页不再自动请求 /usage,由手动操作按需触发。', + disableAutoTodayStatsFetch: '禁止自动刷新今日统计', + disableAutoTodayStatsFetchHint: '账号列表页不再自动请求 /today-stats/batch。', + allowManualUsageFetch: '允许手动查看用量', + allowManualUsageFetchHint: '保留单账号手动查看 usage 的能力,不阉割原功能。', + openaiHotPool: 'OpenAI 热池上限', + geminiHotPool: 'Gemini 热池上限', + anthropicHotPool: 'Anthropic 热池上限', + refillTriggerGap: '补位触发缺口', + refillBatchSize: '单次补位批量大小', + deleteOn401: '账号级上游 401 直接删除账号', + deleteOn401Hint: '仅针对可确认映射到具体账号的上游认证失效,不包含后台自身 401。', + runtimeStatus: '运行态状态', + targetSize: '目标热池', + currentSize: '当前热池', + coldCandidates: '冷池候选', + version: '版本', + triggerRefill: '立即补位', + triggerRebuild: '立即重建', + refillTriggered: '已触发热池补位', + rebuildTriggered: '已触发热池重建', + actionFailed: '热池操作失败', + saved: '极致性能模式设置已保存', + saveFailed: '保存极致性能模式设置失败' + }, overloadCooldown: { title: '529 过载冷却', description: '配置上游返回 529(过载)时的账号调度暂停策略', - enabled: '启用过载冷却', - enabledHint: '收到 529 错误时暂停该账号的调度,冷却后自动恢复', + enabled: '启用 529 冷却', + enabledHint: '收到 529 时自动暂停该账号调度,避免问题账号持续被选中', cooldownMinutes: '冷却时长(分钟)', - cooldownMinutesHint: '账号暂停调度的持续时间(1-120 分钟)', - saved: '过载冷却设置保存成功', + cooldownMinutesHint: '建议 1-30 分钟,默认 10 分钟', + saved: '过载冷却设置已保存', saveFailed: '保存过载冷却设置失败' }, + streamTimeout: { title: '流超时处理', description: '配置上游响应超时时的账户处理策略,避免问题账户持续被选中', diff --git a/frontend/src/stores/__tests__/adminSettings.spec.ts b/frontend/src/stores/__tests__/adminSettings.spec.ts new file mode 100644 index 0000000000..a0e39ae00a --- /dev/null +++ b/frontend/src/stores/__tests__/adminSettings.spec.ts @@ -0,0 +1,81 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { createPinia, setActivePinia } from 'pinia' +import { useAdminSettingsStore } from '@/stores/adminSettings' + +const mockGetSettings = vi.fn() +const mockGetExtremePerformanceSettings = vi.fn() + +vi.mock('@/api', () => ({ + adminAPI: { + settings: { + getSettings: (...args: any[]) => mockGetSettings(...args), + getExtremePerformanceSettings: (...args: any[]) => mockGetExtremePerformanceSettings(...args) + } + } +})) + +describe('useAdminSettingsStore', () => { + beforeEach(() => { + setActivePinia(createPinia()) + localStorage.clear() + vi.clearAllMocks() + }) + + it('fetch caches extreme performance settings for admin pages', async () => { + mockGetSettings.mockResolvedValue({ + ops_monitoring_enabled: true, + ops_realtime_monitoring_enabled: true, + ops_query_mode_default: 'auto', + custom_menu_items: [] + }) + mockGetExtremePerformanceSettings.mockResolvedValue({ + enabled: true, + admin: { + disable_auto_usage_fetch: true, + disable_auto_today_stats_fetch: false, + allow_manual_usage_fetch: true + }, + pool: { + platform_limits: { openai: 2000, gemini: 300, anthropic: 100 }, + refill_trigger_gap: 50, + refill_batch_size: 100, + selection_order: 'imported_first' + }, + account_policy: { + delete_on_any_upstream_401: true, + cooldown_on_429_minutes: 5, + cooldown_on_5xx_minutes: 1, + remove_from_hot_pool_on_overload: true, + remove_from_hot_pool_on_temp_unschedulable: true + } + }) + + const store = useAdminSettingsStore() + await store.fetch(true) + + expect(store.extremeModeEnabled).toBe(true) + expect(store.disableAutoUsageFetch).toBe(true) + expect(store.disableAutoTodayStatsFetch).toBe(false) + expect(store.allowManualUsageFetch).toBe(true) + expect(localStorage.getItem('extreme_mode_enabled_cached')).toBe('true') + expect(localStorage.getItem('extreme_disable_auto_today_stats_fetch_cached')).toBe('false') + }) + + it('falls back to cached extreme performance values when fetch fails', async () => { + localStorage.setItem('extreme_mode_enabled_cached', 'true') + localStorage.setItem('extreme_disable_auto_usage_fetch_cached', 'false') + localStorage.setItem('extreme_disable_auto_today_stats_fetch_cached', 'true') + localStorage.setItem('extreme_allow_manual_usage_fetch_cached', 'false') + + mockGetSettings.mockRejectedValue(new Error('network')) + mockGetExtremePerformanceSettings.mockRejectedValue(new Error('network')) + + const store = useAdminSettingsStore() + await store.fetch(true) + + expect(store.extremeModeEnabled).toBe(true) + expect(store.disableAutoUsageFetch).toBe(false) + expect(store.disableAutoTodayStatsFetch).toBe(true) + expect(store.allowManualUsageFetch).toBe(false) + }) +}) diff --git a/frontend/src/stores/adminSettings.ts b/frontend/src/stores/adminSettings.ts index 76010c5ef9..4cdb594832 100644 --- a/frontend/src/stores/adminSettings.ts +++ b/frontend/src/stores/adminSettings.ts @@ -48,6 +48,10 @@ export const useAdminSettingsStore = defineStore('adminSettings', () => { const opsMonitoringEnabled = ref(readCachedBool('ops_monitoring_enabled_cached', true)) const opsRealtimeMonitoringEnabled = ref(readCachedBool('ops_realtime_monitoring_enabled_cached', true)) const opsQueryModeDefault = ref(readCachedString('ops_query_mode_default_cached', 'auto')) + const extremeModeEnabled = ref(readCachedBool('extreme_mode_enabled_cached', false)) + const disableAutoUsageFetch = ref(readCachedBool('extreme_disable_auto_usage_fetch_cached', true)) + const disableAutoTodayStatsFetch = ref(readCachedBool('extreme_disable_auto_today_stats_fetch_cached', true)) + const allowManualUsageFetch = ref(readCachedBool('extreme_allow_manual_usage_fetch_cached', true)) const customMenuItems = ref([]) async function fetch(force = false): Promise { @@ -56,7 +60,10 @@ export const useAdminSettingsStore = defineStore('adminSettings', () => { loading.value = true try { - const settings = await adminAPI.settings.getSettings() + const [settings, extreme] = await Promise.all([ + adminAPI.settings.getSettings(), + adminAPI.settings.getExtremePerformanceSettings() + ]) opsMonitoringEnabled.value = settings.ops_monitoring_enabled ?? true writeCachedBool('ops_monitoring_enabled_cached', opsMonitoringEnabled.value) @@ -66,6 +73,18 @@ export const useAdminSettingsStore = defineStore('adminSettings', () => { opsQueryModeDefault.value = settings.ops_query_mode_default || 'auto' writeCachedString('ops_query_mode_default_cached', opsQueryModeDefault.value) + extremeModeEnabled.value = extreme.enabled ?? false + writeCachedBool('extreme_mode_enabled_cached', extremeModeEnabled.value) + + disableAutoUsageFetch.value = extreme.admin?.disable_auto_usage_fetch ?? true + writeCachedBool('extreme_disable_auto_usage_fetch_cached', disableAutoUsageFetch.value) + + disableAutoTodayStatsFetch.value = extreme.admin?.disable_auto_today_stats_fetch ?? true + writeCachedBool('extreme_disable_auto_today_stats_fetch_cached', disableAutoTodayStatsFetch.value) + + allowManualUsageFetch.value = extreme.admin?.allow_manual_usage_fetch ?? true + writeCachedBool('extreme_allow_manual_usage_fetch_cached', allowManualUsageFetch.value) + customMenuItems.value = Array.isArray(settings.custom_menu_items) ? settings.custom_menu_items : [] loaded.value = true @@ -126,6 +145,10 @@ export const useAdminSettingsStore = defineStore('adminSettings', () => { opsMonitoringEnabled, opsRealtimeMonitoringEnabled, opsQueryModeDefault, + extremeModeEnabled, + disableAutoUsageFetch, + disableAutoTodayStatsFetch, + allowManualUsageFetch, customMenuItems, fetch, setOpsMonitoringEnabledLocal, diff --git a/frontend/src/views/admin/AccountsView.vue b/frontend/src/views/admin/AccountsView.vue index 35e0fcec48..8efc522bae 100644 --- a/frontend/src/views/admin/AccountsView.vue +++ b/frontend/src/views/admin/AccountsView.vue @@ -18,6 +18,12 @@ @create="showCreate = true" >