feat: 分组终端节点

This commit is contained in:
tbphp
2025-07-05 10:36:19 +08:00
parent 94a9b0c00f
commit fc442516ec
3 changed files with 56 additions and 7 deletions

View File

@@ -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",

View File

@@ -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,

View File

@@ -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"`