diff --git a/internal/config/system_settings.go b/internal/config/system_settings.go index 10e73af..673b053 100644 --- a/internal/config/system_settings.go +++ b/internal/config/system_settings.go @@ -2,6 +2,7 @@ package config import ( "fmt" + "os" "gpt-load/internal/db" "gpt-load/internal/models" "reflect" @@ -18,7 +19,8 @@ import ( // 使用结构体标签作为唯一事实来源 type SystemSettings struct { // 基础参数 - BlacklistThreshold int `json:"blacklist_threshold" default:"1" name:"黑名单阈值" category:"基础参数" desc:"一个 Key 连续失败多少次后进入黑名单" validate:"min=0"` + AppUrl string `json:"app_url" default:"" name:"项目地址" category:"基础参数" desc:"项目的基础 URL,用于拼接分组终端节点地址。系统配置优先于环境变量 APP_URL。"` + BlacklistThreshold int `json:"blacklist_threshold" default:"1" name:"黑名单阈值" category:"基础参数" desc:"一个 Key 连续失败多少次后进入黑名单" validate:"min=0"` MaxRetries int `json:"max_retries" default:"3" name:"最大重试次数" category:"基础参数" desc:"单个请求使用不同 Key 的最大重试次数" validate:"min=0"` RequestLogRetentionDays int `json:"request_log_retention_days" default:"30" name:"日志保留天数" category:"基础参数" desc:"请求日志在数据库中的保留天数" validate:"min=1"` @@ -137,9 +139,26 @@ func (sm *SystemSettingsManager) InitializeSystemSettings() error { var existing models.SystemSetting err := db.DB.Where("setting_key = ?", meta.Key).First(&existing).Error if err != nil { // Not found + value := fmt.Sprintf("%v", meta.DefaultValue) + if meta.Key == "app_url" { + // Special handling for app_url initialization + if appURL := os.Getenv("APP_URL"); appURL != "" { + value = appURL + } else { + host := os.Getenv("HOST") + if host == "" || host == "0.0.0.0" { + host = "localhost" + } + port := os.Getenv("PORT") + if port == "" { + port = "3000" + } + value = fmt.Sprintf("http://%s:%s", host, port) + } + } setting := models.SystemSetting{ SettingKey: meta.Key, - SettingValue: fmt.Sprintf("%v", meta.DefaultValue), + SettingValue: value, Description: meta.Description, } if err := db.DB.Create(&setting).Error; err != nil { @@ -190,6 +209,21 @@ func (sm *SystemSettingsManager) GetSettings() SystemSettings { return sm.settings } +// GetAppUrl returns the effective App URL. +// It prioritizes the value from system settings (database) over the APP_URL environment variable. +func (sm *SystemSettingsManager) GetAppUrl() string { + sm.mu.RLock() + defer sm.mu.RUnlock() + + // 1. 优先级: 数据库中的系统配置 + if sm.settings.AppUrl != "" { + return sm.settings.AppUrl + } + + // 2. 回退: 环境变量 + return os.Getenv("APP_URL") +} + // UpdateSettings 更新系统配置 func (sm *SystemSettingsManager) UpdateSettings(settingsMap map[string]any) error { if db.DB == nil { @@ -332,6 +366,7 @@ func (sm *SystemSettingsManager) DisplayCurrentSettings() { defer sm.mu.RUnlock() logrus.Info("Current System Settings:") + logrus.Infof(" App URL: %s", sm.settings.AppUrl) logrus.Infof(" Blacklist threshold: %d", sm.settings.BlacklistThreshold) logrus.Infof(" Max retries: %d", sm.settings.MaxRetries) logrus.Infof(" Server timeouts: read=%ds, write=%ds, idle=%ds, shutdown=%ds", diff --git a/internal/handler/group_handler.go b/internal/handler/group_handler.go index 373a50a..e02278f 100644 --- a/internal/handler/group_handler.go +++ b/internal/handler/group_handler.go @@ -4,6 +4,7 @@ package handler import ( "encoding/json" "fmt" + "net/url" "gpt-load/internal/config" app_errors "gpt-load/internal/errors" @@ -208,7 +209,7 @@ func (s *Server) CreateGroup(c *gin.Context) { return } - response.Success(c, newGroupResponse(&group)) + response.Success(c, s.newGroupResponse(&group)) } // ListGroups handles listing all groups. @@ -220,8 +221,8 @@ func (s *Server) ListGroups(c *gin.Context) { } var groupResponses []GroupResponse - for _, group := range groups { - groupResponses = append(groupResponses, *newGroupResponse(&group)) + for i := range groups { + groupResponses = append(groupResponses, *s.newGroupResponse(&groups[i])) } response.Success(c, groupResponses) @@ -339,13 +340,14 @@ func (s *Server) UpdateGroup(c *gin.Context) { return } - response.Success(c, newGroupResponse(&group)) + response.Success(c, s.newGroupResponse(&group)) } // GroupResponse defines the structure for a group response, excluding sensitive or large fields. type GroupResponse struct { ID uint `json:"id"` Name string `json:"name"` + Endpoint string `json:"endpoint"` DisplayName string `json:"display_name"` Description string `json:"description"` Upstreams datatypes.JSON `json:"upstreams"` @@ -360,10 +362,21 @@ type GroupResponse struct { } // newGroupResponse creates a new GroupResponse from a models.Group. -func newGroupResponse(group *models.Group) *GroupResponse { +func (s *Server) newGroupResponse(group *models.Group) *GroupResponse { + appURL := s.SettingsManager.GetAppUrl() + endpoint := "" + if appURL != "" { + u, err := url.Parse(appURL) + if err == nil { + u.Path = strings.TrimRight(u.Path, "/") + "/proxy/" + group.Name + endpoint = u.String() + } + } + return &GroupResponse{ ID: group.ID, Name: group.Name, + Endpoint: endpoint, DisplayName: group.DisplayName, Description: group.Description, Upstreams: group.Upstreams, diff --git a/internal/models/types.go b/internal/models/types.go index 6193f91..550afc0 100644 --- a/internal/models/types.go +++ b/internal/models/types.go @@ -34,6 +34,7 @@ type GroupConfig struct { 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"`