feat: 分组终端节点
This commit is contained in:
@@ -2,6 +2,7 @@ package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"gpt-load/internal/db"
|
||||
"gpt-load/internal/models"
|
||||
"reflect"
|
||||
@@ -18,6 +19,7 @@ import (
|
||||
// 使用结构体标签作为唯一事实来源
|
||||
type SystemSettings struct {
|
||||
// 基础参数
|
||||
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",
|
||||
|
@@ -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,
|
||||
|
@@ -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"`
|
||||
|
Reference in New Issue
Block a user