From ca431ce3a39e9e2b6dbe5bc50596a33a5facf152 Mon Sep 17 00:00:00 2001 From: tbphp Date: Thu, 10 Jul 2025 23:20:52 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E5=88=86=E7=BB=84?= =?UTF-8?q?=E7=BC=93=E5=AD=98=E9=85=8D=E7=BD=AE=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/app/app.go | 4 +- internal/config/system_settings.go | 91 +++++++++++++++--------------- internal/models/types.go | 32 ++++++----- internal/services/group_manager.go | 26 +++++++-- internal/syncer/cache_syncer.go | 8 +++ internal/types/types.go | 70 ++++++++++++----------- 6 files changed, 131 insertions(+), 100 deletions(-) diff --git a/internal/app/app.go b/internal/app/app.go index a003652..ed0a34c 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -118,7 +118,7 @@ func (a *App) Start() error { } logrus.Info("System settings initialized in DB.") - a.settingsManager.Initialize(a.storage) + a.settingsManager.Initialize(a.storage, a.groupManager) // 从数据库加载密钥到 Redis if err := a.keyPoolProvider.LoadKeysFromDB(); err != nil { @@ -131,7 +131,7 @@ func (a *App) Start() error { if err := a.leaderService.WaitForInitializationToComplete(); err != nil { return fmt.Errorf("follower failed to start: %w", err) } - a.settingsManager.Initialize(a.storage) + a.settingsManager.Initialize(a.storage, a.groupManager) } a.groupManager.Initialize() diff --git a/internal/config/system_settings.go b/internal/config/system_settings.go index a09e2ed..dffad5e 100644 --- a/internal/config/system_settings.go +++ b/internal/config/system_settings.go @@ -1,11 +1,13 @@ package config import ( + "encoding/json" "fmt" "gpt-load/internal/db" "gpt-load/internal/models" "gpt-load/internal/store" "gpt-load/internal/syncer" + "gpt-load/internal/types" "os" "reflect" "strconv" @@ -16,38 +18,15 @@ import ( "gorm.io/gorm/clause" ) -// SystemSettings 定义所有系统配置项 -// 使用结构体标签作为唯一事实来源 -type SystemSettings struct { - // 基础参数 - AppUrl string `json:"app_url" default:"" name:"项目地址" category:"基础参数" desc:"项目的基础 URL,用于拼接分组终端节点地址。系统配置优先于环境变量 APP_URL。"` - RequestLogRetentionDays int `json:"request_log_retention_days" default:"30" name:"日志保留天数" category:"基础参数" desc:"请求日志在数据库中的保留天数" validate:"min=1"` - - // 服务超时 - ServerReadTimeout int `json:"server_read_timeout" default:"120" name:"读取超时" category:"服务超时" desc:"HTTP 服务器读取超时时间(秒)" validate:"min=1"` - ServerWriteTimeout int `json:"server_write_timeout" default:"1800" name:"写入超时" category:"服务超时" desc:"HTTP 服务器写入超时时间(秒)" validate:"min=1"` - ServerIdleTimeout int `json:"server_idle_timeout" default:"120" name:"空闲超时" category:"服务超时" desc:"HTTP 服务器空闲超时时间(秒)" validate:"min=1"` - ServerGracefulShutdownTimeout int `json:"server_graceful_shutdown_timeout" default:"60" name:"优雅关闭超时" category:"服务超时" desc:"服务优雅关闭的等待超时时间(秒)" validate:"min=1"` - - // 请求超时 - RequestTimeout int `json:"request_timeout" default:"30" name:"请求超时" category:"请求超时" desc:"请求处理的总体超时时间(秒)" validate:"min=1"` - ResponseTimeout int `json:"response_timeout" default:"30" name:"响应超时" category:"请求超时" desc:"TLS 握手和响应头的超时时间(秒)" validate:"min=1"` - IdleConnTimeout int `json:"idle_conn_timeout" default:"120" name:"空闲连接超时" category:"请求超时" desc:"空闲连接的超时时间(秒)" validate:"min=1"` - - // 密钥配置 - MaxRetries int `json:"max_retries" default:"3" name:"最大重试次数" category:"密钥配置" desc:"单个请求使用不同 Key 的最大重试次数" validate:"min=0"` - BlacklistThreshold int `json:"blacklist_threshold" default:"1" name:"黑名单阈值" category:"密钥配置" desc:"一个 Key 连续失败多少次后进入黑名单" validate:"min=0"` - KeyValidationIntervalMinutes int `json:"key_validation_interval_minutes" default:"60" name:"定时验证周期" category:"密钥配置" desc:"后台定时验证密钥的默认周期(分钟)" validate:"min=5"` - KeyValidationTaskTimeoutMinutes int `json:"key_validation_task_timeout_minutes" default:"60" name:"手动验证超时" category:"密钥配置" desc:"手动触发的全量验证任务的超时时间(分钟)" validate:"min=10"` -} +const SettingsUpdateChannel = "system_settings:updated" // GenerateSettingsMetadata 使用反射从 SystemSettings 结构体动态生成元数据 -func GenerateSettingsMetadata(s *SystemSettings) []models.SystemSettingInfo { +func GenerateSettingsMetadata(s *types.SystemSettings) []models.SystemSettingInfo { var settingsInfo []models.SystemSettingInfo v := reflect.ValueOf(s).Elem() t := v.Type() - for i := 0; i < t.NumField(); i++ { + for i := range t.NumField() { field := t.Field(i) fieldValue := v.Field(i) @@ -86,12 +65,12 @@ func GenerateSettingsMetadata(s *SystemSettings) []models.SystemSettingInfo { } // DefaultSystemSettings 返回默认的系统配置 -func DefaultSystemSettings() SystemSettings { - s := SystemSettings{} +func DefaultSystemSettings() types.SystemSettings { + s := types.SystemSettings{} v := reflect.ValueOf(&s).Elem() t := v.Type() - for i := 0; i < t.NumField(); i++ { + for i := range t.NumField() { field := t.Field(i) defaultTag := field.Tag.Get("default") if defaultTag == "" { @@ -110,22 +89,24 @@ func DefaultSystemSettings() SystemSettings { // SystemSettingsManager 管理系统配置 type SystemSettingsManager struct { - syncer *syncer.CacheSyncer[SystemSettings] + syncer *syncer.CacheSyncer[types.SystemSettings] } -const SettingsUpdateChannel = "system_settings:updated" - // NewSystemSettingsManager creates a new, uninitialized SystemSettingsManager. -func NewSystemSettingsManager() (*SystemSettingsManager, error) { - return &SystemSettingsManager{}, nil +func NewSystemSettingsManager() *SystemSettingsManager { + return &SystemSettingsManager{} +} + +type gm interface { + Invalidate() error } // Initialize initializes the SystemSettingsManager with database and store dependencies. -func (sm *SystemSettingsManager) Initialize(store store.Store) error { - settingsLoader := func() (SystemSettings, error) { +func (sm *SystemSettingsManager) Initialize(store store.Store, gm gm) error { + settingsLoader := func() (types.SystemSettings, error) { var dbSettings []models.SystemSetting if err := db.DB.Find(&dbSettings).Error; err != nil { - return SystemSettings{}, fmt.Errorf("failed to load system settings from db: %w", err) + return types.SystemSettings{}, fmt.Errorf("failed to load system settings from db: %w", err) } settingsMap := make(map[string]string) @@ -138,7 +119,7 @@ func (sm *SystemSettingsManager) Initialize(store store.Store) error { v := reflect.ValueOf(&settings).Elem() t := v.Type() jsonToField := make(map[string]string) - for i := 0; i < t.NumField(); i++ { + for i := range t.NumField() { field := t.Field(i) jsonTag := strings.Split(field.Tag.Get("json"), ",")[0] if jsonTag != "" { @@ -162,11 +143,18 @@ func (sm *SystemSettingsManager) Initialize(store store.Store) error { return settings, nil } + afterLoader := func(newData types.SystemSettings) { + if err := gm.Invalidate(); err != nil { + logrus.Debugf("Failed to invalidate group manager cache after settings update: %v", err) + } + } + syncer, err := syncer.NewCacheSyncer( settingsLoader, store, SettingsUpdateChannel, logrus.WithField("syncer", "system_settings"), + afterLoader, ) if err != nil { return fmt.Errorf("failed to create system settings syncer: %w", err) @@ -227,7 +215,7 @@ func (sm *SystemSettingsManager) EnsureSettingsInitialized() error { // GetSettings 获取当前系统配置 // If the syncer is not initialized, it returns default settings. -func (sm *SystemSettingsManager) GetSettings() SystemSettings { +func (sm *SystemSettingsManager) GetSettings() types.SystemSettings { if sm.syncer == nil { logrus.Warn("SystemSettingsManager is not initialized, returning default settings.") return DefaultSystemSettings() @@ -278,7 +266,7 @@ func (sm *SystemSettingsManager) UpdateSettings(settingsMap map[string]any) erro } // GetEffectiveConfig 获取有效配置 (系统配置 + 分组覆盖) -func (sm *SystemSettingsManager) GetEffectiveConfig(groupConfig datatypes.JSONMap) SystemSettings { +func (sm *SystemSettingsManager) GetEffectiveConfig(groupConfig datatypes.JSONMap) types.SystemSettings { // 从系统配置开始 effectiveConfig := sm.GetSettings() v := reflect.ValueOf(&effectiveConfig).Elem() @@ -286,7 +274,7 @@ func (sm *SystemSettingsManager) GetEffectiveConfig(groupConfig datatypes.JSONMa // 创建一个从 json 标签到字段名的映射 jsonToField := make(map[string]string) - for i := 0; i < t.NumField(); i++ { + for i := range t.NumField() { field := t.Field(i) jsonTag := strings.Split(field.Tag.Get("json"), ",")[0] if jsonTag != "" { @@ -378,7 +366,7 @@ func (sm *SystemSettingsManager) ValidateSettings(settingsMap map[string]any) er } // DisplayCurrentSettings 显示当前系统配置信息 -func (sm *SystemSettingsManager) DisplayCurrentSettings(settings SystemSettings) { +func (sm *SystemSettingsManager) DisplayCurrentSettings(settings types.SystemSettings) { logrus.Info("Current System Settings:") logrus.Infof(" App URL: %s", settings.AppUrl) logrus.Infof(" Blacklist threshold: %d", settings.BlacklistThreshold) @@ -386,8 +374,10 @@ func (sm *SystemSettingsManager) DisplayCurrentSettings(settings SystemSettings) logrus.Infof(" Server timeouts: read=%ds, write=%ds, idle=%ds, shutdown=%ds", settings.ServerReadTimeout, settings.ServerWriteTimeout, settings.ServerIdleTimeout, settings.ServerGracefulShutdownTimeout) - logrus.Infof(" Request timeouts: request=%ds, response=%ds, idle_conn=%ds", - settings.RequestTimeout, settings.ResponseTimeout, settings.IdleConnTimeout) + logrus.Infof(" Request timeouts: request=%ds, connect=%ds, idle_conn=%ds", + settings.RequestTimeout, settings.ConnectTimeout, settings.IdleConnTimeout) + logrus.Infof(" HTTP Client Pool: max_idle_conns=%d, max_idle_conns_per_host=%d", + settings.MaxIdleConns, settings.MaxIdleConnsPerHost) logrus.Infof(" Request log retention: %d days", settings.RequestLogRetentionDays) logrus.Infof(" Key validation: interval=%dmin, task_timeout=%dmin", settings.KeyValidationIntervalMinutes, settings.KeyValidationTaskTimeoutMinutes) @@ -424,10 +414,15 @@ func setFieldFromString(fieldValue reflect.Value, value string) error { func interfaceToInt(val any) (int, error) { switch v := val.(type) { + case json.Number: + i64, err := v.Int64() + if err != nil { + return 0, err + } + return int(i64), nil case int: return v, nil case float64: - // JSON unmarshals numbers into float64 if v != float64(int(v)) { return 0, fmt.Errorf("value is a float, not an integer: %v", v) } @@ -448,6 +443,12 @@ func interfaceToString(val any) (string, bool) { // interfaceToBool is kept for GetEffectiveConfig func interfaceToBool(val any) (bool, bool) { switch v := val.(type) { + case json.Number: + if s := v.String(); s == "1" { + return true, true + } else if s == "0" { + return false, true + } case bool: return v, true case string: diff --git a/internal/models/types.go b/internal/models/types.go index 9cfea91..25eef6b 100644 --- a/internal/models/types.go +++ b/internal/models/types.go @@ -1,6 +1,7 @@ package models import ( + "gpt-load/internal/types" "time" "gorm.io/datatypes" @@ -38,21 +39,22 @@ type GroupConfig struct { // Group 对应 groups 表 type Group struct { - ID uint `gorm:"primaryKey;autoIncrement" json:"id"` - Name string `gorm:"type:varchar(255);not null;unique" json:"name"` - Endpoint string `gorm:"-" json:"endpoint"` - DisplayName string `gorm:"type:varchar(255)" json:"display_name"` - Description string `gorm:"type:varchar(512)" json:"description"` - Upstreams datatypes.JSON `gorm:"type:json;not null" json:"upstreams"` - ChannelType string `gorm:"type:varchar(50);not null" json:"channel_type"` - Sort int `gorm:"default:0" json:"sort"` - TestModel string `gorm:"type:varchar(255);not null" json:"test_model"` - ParamOverrides datatypes.JSONMap `gorm:"type:json" json:"param_overrides"` - Config datatypes.JSONMap `gorm:"type:json" json:"config"` - APIKeys []APIKey `gorm:"foreignKey:GroupID" json:"api_keys"` - LastValidatedAt *time.Time `json:"last_validated_at"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uint `gorm:"primaryKey;autoIncrement" json:"id"` + EffectiveConfig types.SystemSettings `gorm:"-" json:"effective_config,omitempty"` + Name string `gorm:"type:varchar(255);not null;unique" json:"name"` + Endpoint string `gorm:"-" json:"endpoint"` + DisplayName string `gorm:"type:varchar(255)" json:"display_name"` + Description string `gorm:"type:varchar(512)" json:"description"` + Upstreams datatypes.JSON `gorm:"type:json;not null" json:"upstreams"` + ChannelType string `gorm:"type:varchar(50);not null" json:"channel_type"` + Sort int `gorm:"default:0" json:"sort"` + TestModel string `gorm:"type:varchar(255);not null" json:"test_model"` + ParamOverrides datatypes.JSONMap `gorm:"type:json" json:"param_overrides"` + Config datatypes.JSONMap `gorm:"type:json" json:"config"` + APIKeys []APIKey `gorm:"foreignKey:GroupID" json:"api_keys"` + LastValidatedAt *time.Time `json:"last_validated_at"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } // APIKey 对应 api_keys 表 diff --git a/internal/services/group_manager.go b/internal/services/group_manager.go index 8f85433..cf0de8d 100644 --- a/internal/services/group_manager.go +++ b/internal/services/group_manager.go @@ -2,6 +2,7 @@ package services import ( "fmt" + "gpt-load/internal/config" "gpt-load/internal/models" "gpt-load/internal/store" "gpt-load/internal/syncer" @@ -14,16 +15,22 @@ const GroupUpdateChannel = "groups:updated" // GroupManager manages the caching of group data. type GroupManager struct { - syncer *syncer.CacheSyncer[map[string]*models.Group] - db *gorm.DB - store store.Store + syncer *syncer.CacheSyncer[map[string]*models.Group] + db *gorm.DB + store store.Store + settingsManager *config.SystemSettingsManager } // NewGroupManager creates a new, uninitialized GroupManager. -func NewGroupManager(db *gorm.DB, store store.Store) *GroupManager { +func NewGroupManager( + db *gorm.DB, + store store.Store, + settingsManager *config.SystemSettingsManager, +) *GroupManager { return &GroupManager{ - db: db, - store: store, + db: db, + store: store, + settingsManager: settingsManager, } } @@ -38,7 +45,13 @@ func (gm *GroupManager) Initialize() error { groupMap := make(map[string]*models.Group, len(groups)) for _, group := range groups { g := *group + g.EffectiveConfig = gm.settingsManager.GetEffectiveConfig(g.Config) groupMap[g.Name] = &g + logrus.WithFields(logrus.Fields{ + "group_name": g.Name, + "group_config": g.Config, + "effective_config": g.EffectiveConfig, + }).Debug("Loaded group with effective config") } return groupMap, nil } @@ -48,6 +61,7 @@ func (gm *GroupManager) Initialize() error { gm.store, GroupUpdateChannel, logrus.WithField("syncer", "groups"), + nil, ) if err != nil { return fmt.Errorf("failed to create group syncer: %w", err) diff --git a/internal/syncer/cache_syncer.go b/internal/syncer/cache_syncer.go index 1afa173..28cee7b 100644 --- a/internal/syncer/cache_syncer.go +++ b/internal/syncer/cache_syncer.go @@ -23,6 +23,7 @@ type CacheSyncer[T any] struct { logger *logrus.Entry stopChan chan struct{} wg sync.WaitGroup + afterReload func(newValue T) } // NewCacheSyncer creates and initializes a new CacheSyncer. @@ -31,6 +32,7 @@ func NewCacheSyncer[T any]( store store.Store, channelName string, logger *logrus.Entry, + afterReload func(newValue T), ) (*CacheSyncer[T], error) { s := &CacheSyncer[T]{ loader: loader, @@ -38,6 +40,7 @@ func NewCacheSyncer[T any]( channelName: channelName, logger: logger, stopChan: make(chan struct{}), + afterReload: afterReload, } if err := s.reload(); err != nil { @@ -85,6 +88,11 @@ func (s *CacheSyncer[T]) reload() error { s.mu.Unlock() s.logger.Info("cache reloaded successfully") + // After successfully reloading and updating the cache, trigger the hook. + if s.afterReload != nil { + s.logger.Debug("triggering afterReload hook") + s.afterReload(newData) + } return nil } diff --git a/internal/types/types.go b/internal/types/types.go index c06798e..91048c0 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -1,10 +1,5 @@ -// Package types defines common interfaces and types used across the application package types -import ( - "github.com/gin-gonic/gin" -) - // ConfigManager defines the interface for configuration management type ConfigManager interface { GetAuthConfig() AuthConfig @@ -19,29 +14,40 @@ type ConfigManager interface { ReloadConfig() error } -// ProxyServer defines the interface for proxy server -type ProxyServer interface { - HandleProxy(c *gin.Context) - Close() +// SystemSettings 定义所有系统配置项 +type SystemSettings struct { + // 基础参数 + AppUrl string `json:"app_url" default:"" name:"项目地址" category:"基础参数" desc:"项目的基础 URL,用于拼接分组终端节点地址。系统配置优先于环境变量 APP_URL。"` + RequestLogRetentionDays int `json:"request_log_retention_days" default:"30" name:"日志保留天数" category:"基础参数" desc:"请求日志在数据库中的保留天数" validate:"min=1"` + + // 服务超时 + ServerReadTimeout int `json:"server_read_timeout" default:"120" name:"读取超时" category:"服务超时" desc:"HTTP 服务器读取超时时间(秒)" validate:"min=1"` + ServerWriteTimeout int `json:"server_write_timeout" default:"1800" name:"写入超时" category:"服务超时" desc:"HTTP 服务器写入超时时间(秒)" validate:"min=1"` + ServerIdleTimeout int `json:"server_idle_timeout" default:"120" name:"空闲超时" category:"服务超时" desc:"HTTP 服务器空闲超时时间(秒)" validate:"min=1"` + ServerGracefulShutdownTimeout int `json:"server_graceful_shutdown_timeout" default:"60" name:"优雅关闭超时" category:"服务超时" desc:"服务优雅关闭的等待超时时间(秒)" validate:"min=1"` + + // 请求超时 + RequestTimeout int `json:"request_timeout" default:"600" name:"请求超时" category:"请求超时" desc:"转发请求的完整生命周期超时(秒),包括连接、重试等。" validate:"min=1"` + ConnectTimeout int `json:"connect_timeout" default:"5" name:"连接超时" category:"请求超时" desc:"与上游服务建立新连接的超时时间(秒)。" validate:"min=1"` + IdleConnTimeout int `json:"idle_conn_timeout" default:"120" name:"空闲连接超时" category:"请求超时" desc:"HTTP 客户端中空闲连接的超时时间(秒)。" validate:"min=1"` + MaxIdleConns int `json:"max_idle_conns" default:"100" name:"最大空闲连接数" category:"请求超时" desc:"HTTP 客户端连接池中允许的最大空闲连接总数。" validate:"min=1"` + MaxIdleConnsPerHost int `json:"max_idle_conns_per_host" default:"10" name:"每主机最大空闲连接数" category:"请求超时" desc:"HTTP 客户端连接池对每个上游主机允许的最大空闲连接数。" validate:"min=1"` + + // 密钥配置 + MaxRetries int `json:"max_retries" default:"3" name:"最大重试次数" category:"密钥配置" desc:"单个请求使用不同 Key 的最大重试次数" validate:"min=0"` + BlacklistThreshold int `json:"blacklist_threshold" default:"1" name:"黑名单阈值" category:"密钥配置" desc:"一个 Key 连续失败多少次后进入黑名单" validate:"min=0"` + KeyValidationIntervalMinutes int `json:"key_validation_interval_minutes" default:"60" name:"定时验证周期" category:"密钥配置" desc:"后台定时验证密钥的默认周期(分钟)" validate:"min=5"` + KeyValidationTaskTimeoutMinutes int `json:"key_validation_task_timeout_minutes" default:"60" name:"手动验证超时" category:"密钥配置" desc:"手动触发的全量验证任务的超时时间(分钟)" validate:"min=10"` } // ServerConfig represents server configuration type ServerConfig struct { Port int `json:"port"` Host string `json:"host"` - ReadTimeout int `json:"readTimeout"` - WriteTimeout int `json:"writeTimeout"` - IdleTimeout int `json:"idleTimeout"` - GracefulShutdownTimeout int `json:"gracefulShutdownTimeout"` -} - -// OpenAIConfig represents OpenAI API configuration -type OpenAIConfig struct { - BaseURL string `json:"baseUrl"` - BaseURLs []string `json:"baseUrls"` - RequestTimeout int `json:"requestTimeout"` - ResponseTimeout int `json:"responseTimeout"` - IdleConnTimeout int `json:"idleConnTimeout"` + ReadTimeout int `json:"read_timeout"` + WriteTimeout int `json:"write_timeout"` + IdleTimeout int `json:"idle_timeout"` + GracefulShutdownTimeout int `json:"graceful_shutdown_timeout"` } // AuthConfig represents authentication configuration @@ -53,26 +59,26 @@ type AuthConfig struct { // CORSConfig represents CORS configuration type CORSConfig struct { Enabled bool `json:"enabled"` - AllowedOrigins []string `json:"allowedOrigins"` - AllowedMethods []string `json:"allowedMethods"` - AllowedHeaders []string `json:"allowedHeaders"` - AllowCredentials bool `json:"allowCredentials"` + AllowedOrigins []string `json:"allowed_origins"` + AllowedMethods []string `json:"allowed_methods"` + AllowedHeaders []string `json:"allowed_headers"` + AllowCredentials bool `json:"allow_credentials"` } // PerformanceConfig represents performance configuration type PerformanceConfig struct { - MaxConcurrentRequests int `json:"maxConcurrentRequests"` - KeyValidationPoolSize int `json:"KeyValidationPoolSize"` - EnableGzip bool `json:"enableGzip"` + MaxConcurrentRequests int `json:"max_concurrent_requests"` + KeyValidationPoolSize int `json:"key_validation_pool_size"` + EnableGzip bool `json:"enable_gzip"` } // LogConfig represents logging configuration type LogConfig struct { Level string `json:"level"` Format string `json:"format"` - EnableFile bool `json:"enableFile"` - FilePath string `json:"filePath"` - EnableRequest bool `json:"enableRequest"` + EnableFile bool `json:"enable_file"` + FilePath string `json:"file_path"` + EnableRequest bool `json:"enable_request"` } // DatabaseConfig represents database configuration