diff --git a/backend/cmd/server/wire_gen.go b/backend/cmd/server/wire_gen.go index 9028210c..46e996c5 100644 --- a/backend/cmd/server/wire_gen.go +++ b/backend/cmd/server/wire_gen.go @@ -234,7 +234,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) { settingHandler := admin.NewSettingHandler(settingService, emailService, turnstileService, opsService, paymentConfigService, paymentService) paymentOrderExpiryService := service.ProvidePaymentOrderExpiryService(paymentService) paymentHandler := admin.NewPaymentHandler(paymentService, paymentConfigService) - availableChannelUserHandler := handler.NewAvailableChannelHandler(channelService, apiKeyService) + availableChannelUserHandler := handler.NewAvailableChannelHandler(channelService, apiKeyService, settingService) adminHandlers := handler.ProvideAdminHandlers(dashboardHandler, adminUserHandler, groupHandler, accountHandler, adminAnnouncementHandler, dataManagementHandler, backupHandler, oAuthHandler, openAIOAuthHandler, geminiOAuthHandler, antigravityOAuthHandler, proxyHandler, adminRedeemHandler, promoHandler, settingHandler, opsHandler, systemHandler, adminSubscriptionHandler, adminUsageHandler, userAttributeHandler, errorPassthroughHandler, tlsFingerprintProfileHandler, adminAPIKeyHandler, scheduledTestHandler, channelHandler, channelMonitorHandler, channelMonitorRequestTemplateHandler, paymentHandler) usageRecordWorkerPool := service.NewUsageRecordWorkerPool(configConfig) userMsgQueueCache := repository.NewUserMsgQueueCache(redisClient) diff --git a/backend/internal/handler/admin/setting_handler.go b/backend/internal/handler/admin/setting_handler.go index 40c944eb..09dc8251 100644 --- a/backend/internal/handler/admin/setting_handler.go +++ b/backend/internal/handler/admin/setting_handler.go @@ -238,6 +238,8 @@ func (h *SettingHandler) GetSettings(c *gin.Context) { ChannelMonitorEnabled: settings.ChannelMonitorEnabled, ChannelMonitorDefaultIntervalSeconds: settings.ChannelMonitorDefaultIntervalSeconds, + + AvailableChannelsEnabled: settings.AvailableChannelsEnabled, } response.Success(c, systemSettingsResponseData(payload, authSourceDefaults)) } @@ -432,6 +434,9 @@ type UpdateSettingsRequest struct { // Channel Monitor feature switch ChannelMonitorEnabled *bool `json:"channel_monitor_enabled"` ChannelMonitorDefaultIntervalSeconds *int `json:"channel_monitor_default_interval_seconds"` + + // Available Channels feature switch (user-facing) + AvailableChannelsEnabled *bool `json:"available_channels_enabled"` } // UpdateSettings 更新系统设置 @@ -1238,6 +1243,12 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) { } return previousSettings.ChannelMonitorDefaultIntervalSeconds }(), + AvailableChannelsEnabled: func() bool { + if req.AvailableChannelsEnabled != nil { + return *req.AvailableChannelsEnabled + } + return previousSettings.AvailableChannelsEnabled + }(), } authSourceDefaults := &service.AuthSourceDefaultSettings{ @@ -1471,6 +1482,8 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) { ChannelMonitorEnabled: updatedSettings.ChannelMonitorEnabled, ChannelMonitorDefaultIntervalSeconds: updatedSettings.ChannelMonitorDefaultIntervalSeconds, + + AvailableChannelsEnabled: updatedSettings.AvailableChannelsEnabled, } response.Success(c, systemSettingsResponseData(payload, updatedAuthSourceDefaults)) } @@ -1833,6 +1846,9 @@ func diffSettings(before *service.SystemSettings, after *service.SystemSettings, if before.ChannelMonitorDefaultIntervalSeconds != after.ChannelMonitorDefaultIntervalSeconds { changed = append(changed, "channel_monitor_default_interval_seconds") } + if before.AvailableChannelsEnabled != after.AvailableChannelsEnabled { + changed = append(changed, "available_channels_enabled") + } changed = appendAuthSourceDefaultChanges(changed, beforeAuthSourceDefaults, afterAuthSourceDefaults) return changed } diff --git a/backend/internal/handler/available_channel_handler.go b/backend/internal/handler/available_channel_handler.go index d19fa9b6..8b489388 100644 --- a/backend/internal/handler/available_channel_handler.go +++ b/backend/internal/handler/available_channel_handler.go @@ -21,19 +21,30 @@ import ( type AvailableChannelHandler struct { channelService *service.ChannelService apiKeyService *service.APIKeyService + settingService *service.SettingService } // NewAvailableChannelHandler 创建用户侧可用渠道 handler。 func NewAvailableChannelHandler( channelService *service.ChannelService, apiKeyService *service.APIKeyService, + settingService *service.SettingService, ) *AvailableChannelHandler { return &AvailableChannelHandler{ channelService: channelService, apiKeyService: apiKeyService, + settingService: settingService, } } +// featureEnabled 返回 available-channels 开关是否启用。默认关闭(opt-in)。 +func (h *AvailableChannelHandler) featureEnabled(c *gin.Context) bool { + if h.settingService == nil { + return false + } + return h.settingService.GetAvailableChannelsRuntime(c.Request.Context()).Enabled +} + // userAvailableGroup 用户可见的分组概要(白名单字段)。 type userAvailableGroup struct { ID int64 `json:"id"` @@ -89,6 +100,13 @@ func (h *AvailableChannelHandler) List(c *gin.Context) { return } + // Feature 未启用时返回空数组(不暴露渠道信息)。检查放在认证之后, + // 保持与未开关前的 401 行为一致:未登录先 401,登录后再按开关决定。 + if !h.featureEnabled(c) { + response.Success(c, []userAvailableChannel{}) + return + } + userGroups, err := h.apiKeyService.GetAvailableGroups(c.Request.Context(), subject.UserID) if err != nil { response.ErrorFrom(c, err) diff --git a/backend/internal/handler/dto/settings.go b/backend/internal/handler/dto/settings.go index 9d9bb6c5..193dc940 100644 --- a/backend/internal/handler/dto/settings.go +++ b/backend/internal/handler/dto/settings.go @@ -187,6 +187,9 @@ type SystemSettings struct { // Channel Monitor feature switch ChannelMonitorEnabled bool `json:"channel_monitor_enabled"` ChannelMonitorDefaultIntervalSeconds int `json:"channel_monitor_default_interval_seconds"` + + // Available Channels feature switch (user-facing aggregate view) + AvailableChannelsEnabled bool `json:"available_channels_enabled"` } type DefaultSubscriptionSetting struct { @@ -237,6 +240,8 @@ type PublicSettings struct { ChannelMonitorEnabled bool `json:"channel_monitor_enabled"` ChannelMonitorDefaultIntervalSeconds int `json:"channel_monitor_default_interval_seconds"` + + AvailableChannelsEnabled bool `json:"available_channels_enabled"` } // OverloadCooldownSettings 529过载冷却配置 DTO diff --git a/backend/internal/handler/setting_handler.go b/backend/internal/handler/setting_handler.go index 8d72206f..96964de4 100644 --- a/backend/internal/handler/setting_handler.go +++ b/backend/internal/handler/setting_handler.go @@ -73,5 +73,7 @@ func (h *SettingHandler) GetPublicSettings(c *gin.Context) { ChannelMonitorEnabled: settings.ChannelMonitorEnabled, ChannelMonitorDefaultIntervalSeconds: settings.ChannelMonitorDefaultIntervalSeconds, + + AvailableChannelsEnabled: settings.AvailableChannelsEnabled, }) } diff --git a/backend/internal/service/domain_constants.go b/backend/internal/service/domain_constants.go index ef2259ed..f31255b6 100644 --- a/backend/internal/service/domain_constants.go +++ b/backend/internal/service/domain_constants.go @@ -254,6 +254,11 @@ const ( // pre-filled when creating a new channel monitor from the admin UI. Range: [15, 3600]. SettingKeyChannelMonitorDefaultIntervalSeconds = "channel_monitor_default_interval_seconds" + // SettingKeyAvailableChannelsEnabled is a DB-backed soft switch for the "Available Channels" + // user-facing aggregate view. When false: user endpoint returns an empty list and the + // sidebar entry is hidden. Defaults to false (opt-in feature). + SettingKeyAvailableChannelsEnabled = "available_channels_enabled" + // ========================= // Overload Cooldown (529) // ========================= diff --git a/backend/internal/service/setting_service.go b/backend/internal/service/setting_service.go index c901be84..5340ff18 100644 --- a/backend/internal/service/setting_service.go +++ b/backend/internal/service/setting_service.go @@ -452,6 +452,7 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings SettingKeyAccountQuotaNotifyEnabled, SettingKeyChannelMonitorEnabled, SettingKeyChannelMonitorDefaultIntervalSeconds, + SettingKeyAvailableChannelsEnabled, } settings, err := s.settingRepo.GetMultiple(ctx, keys) @@ -537,6 +538,8 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings ChannelMonitorEnabled: !isFalseSettingValue(settings[SettingKeyChannelMonitorEnabled]), ChannelMonitorDefaultIntervalSeconds: parseChannelMonitorInterval(settings[SettingKeyChannelMonitorDefaultIntervalSeconds]), + + AvailableChannelsEnabled: settings[SettingKeyAvailableChannelsEnabled] == "true", }, nil } @@ -595,6 +598,25 @@ func (s *SettingService) GetChannelMonitorRuntime(ctx context.Context) ChannelMo } } +// AvailableChannelsRuntime is the lightweight view of the available-channels feature +// switch consumed by the user-facing handler. +type AvailableChannelsRuntime struct { + Enabled bool +} + +// GetAvailableChannelsRuntime reads the available-channels feature switch directly +// from the settings store. Fail-closed: on error returns Enabled=false, matching +// the opt-in default (unknown ↔ disabled). +func (s *SettingService) GetAvailableChannelsRuntime(ctx context.Context) AvailableChannelsRuntime { + vals, err := s.settingRepo.GetMultiple(ctx, []string{SettingKeyAvailableChannelsEnabled}) + if err != nil { + return AvailableChannelsRuntime{Enabled: false} + } + return AvailableChannelsRuntime{ + Enabled: vals[SettingKeyAvailableChannelsEnabled] == "true", + } +} + // SetOnUpdateCallback sets a callback function to be called when settings are updated // This is used for cache invalidation (e.g., HTML cache in frontend server) func (s *SettingService) SetOnUpdateCallback(callback func()) { @@ -1151,6 +1173,9 @@ func (s *SettingService) buildSystemSettingsUpdates(ctx context.Context, setting updates[SettingKeyChannelMonitorDefaultIntervalSeconds] = strconv.Itoa(v) } + // Available channels feature switch + updates[SettingKeyAvailableChannelsEnabled] = strconv.FormatBool(settings.AvailableChannelsEnabled) + // Claude Code version check updates[SettingKeyMinClaudeCodeVersion] = settings.MinClaudeCodeVersion updates[SettingKeyMaxClaudeCodeVersion] = settings.MaxClaudeCodeVersion @@ -1700,6 +1725,9 @@ func (s *SettingService) InitializeDefaultSettings(ctx context.Context) error { SettingKeyChannelMonitorEnabled: "true", SettingKeyChannelMonitorDefaultIntervalSeconds: "60", + // Available channels feature (default disabled; opt-in) + SettingKeyAvailableChannelsEnabled: "false", + // Claude Code version check (default: empty = disabled) SettingKeyMinClaudeCodeVersion: "", SettingKeyMaxClaudeCodeVersion: "", @@ -2008,6 +2036,9 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin settings[SettingKeyChannelMonitorDefaultIntervalSeconds], ) + // Available channels feature (default: disabled; strict true) + result.AvailableChannelsEnabled = settings[SettingKeyAvailableChannelsEnabled] == "true" + // Claude Code version check result.MinClaudeCodeVersion = settings[SettingKeyMinClaudeCodeVersion] result.MaxClaudeCodeVersion = settings[SettingKeyMaxClaudeCodeVersion] diff --git a/backend/internal/service/settings_view.go b/backend/internal/service/settings_view.go index 972faf80..7902ff5b 100644 --- a/backend/internal/service/settings_view.go +++ b/backend/internal/service/settings_view.go @@ -129,6 +129,9 @@ type SystemSettings struct { ChannelMonitorEnabled bool `json:"channel_monitor_enabled"` ChannelMonitorDefaultIntervalSeconds int `json:"channel_monitor_default_interval_seconds"` + // Available Channels feature (user-facing aggregate view) + AvailableChannelsEnabled bool `json:"available_channels_enabled"` + // Claude Code version check MinClaudeCodeVersion string MaxClaudeCodeVersion string @@ -217,6 +220,9 @@ type PublicSettings struct { // Channel Monitor feature ChannelMonitorEnabled bool `json:"channel_monitor_enabled"` ChannelMonitorDefaultIntervalSeconds int `json:"channel_monitor_default_interval_seconds"` + + // Available Channels feature (user-facing aggregate view) + AvailableChannelsEnabled bool `json:"available_channels_enabled"` } type WeChatConnectOAuthConfig struct {