feat: 分组终端节点
This commit is contained in:
@@ -2,6 +2,7 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"gpt-load/internal/db"
|
"gpt-load/internal/db"
|
||||||
"gpt-load/internal/models"
|
"gpt-load/internal/models"
|
||||||
"reflect"
|
"reflect"
|
||||||
@@ -18,6 +19,7 @@ import (
|
|||||||
// 使用结构体标签作为唯一事实来源
|
// 使用结构体标签作为唯一事实来源
|
||||||
type SystemSettings struct {
|
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"`
|
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"`
|
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"`
|
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
|
var existing models.SystemSetting
|
||||||
err := db.DB.Where("setting_key = ?", meta.Key).First(&existing).Error
|
err := db.DB.Where("setting_key = ?", meta.Key).First(&existing).Error
|
||||||
if err != nil { // Not found
|
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{
|
setting := models.SystemSetting{
|
||||||
SettingKey: meta.Key,
|
SettingKey: meta.Key,
|
||||||
SettingValue: fmt.Sprintf("%v", meta.DefaultValue),
|
SettingValue: value,
|
||||||
Description: meta.Description,
|
Description: meta.Description,
|
||||||
}
|
}
|
||||||
if err := db.DB.Create(&setting).Error; err != nil {
|
if err := db.DB.Create(&setting).Error; err != nil {
|
||||||
@@ -190,6 +209,21 @@ func (sm *SystemSettingsManager) GetSettings() SystemSettings {
|
|||||||
return sm.settings
|
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 更新系统配置
|
// UpdateSettings 更新系统配置
|
||||||
func (sm *SystemSettingsManager) UpdateSettings(settingsMap map[string]any) error {
|
func (sm *SystemSettingsManager) UpdateSettings(settingsMap map[string]any) error {
|
||||||
if db.DB == nil {
|
if db.DB == nil {
|
||||||
@@ -332,6 +366,7 @@ func (sm *SystemSettingsManager) DisplayCurrentSettings() {
|
|||||||
defer sm.mu.RUnlock()
|
defer sm.mu.RUnlock()
|
||||||
|
|
||||||
logrus.Info("Current System Settings:")
|
logrus.Info("Current System Settings:")
|
||||||
|
logrus.Infof(" App URL: %s", sm.settings.AppUrl)
|
||||||
logrus.Infof(" Blacklist threshold: %d", sm.settings.BlacklistThreshold)
|
logrus.Infof(" Blacklist threshold: %d", sm.settings.BlacklistThreshold)
|
||||||
logrus.Infof(" Max retries: %d", sm.settings.MaxRetries)
|
logrus.Infof(" Max retries: %d", sm.settings.MaxRetries)
|
||||||
logrus.Infof(" Server timeouts: read=%ds, write=%ds, idle=%ds, shutdown=%ds",
|
logrus.Infof(" Server timeouts: read=%ds, write=%ds, idle=%ds, shutdown=%ds",
|
||||||
|
@@ -4,6 +4,7 @@ package handler
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
"gpt-load/internal/config"
|
"gpt-load/internal/config"
|
||||||
app_errors "gpt-load/internal/errors"
|
app_errors "gpt-load/internal/errors"
|
||||||
@@ -208,7 +209,7 @@ func (s *Server) CreateGroup(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
response.Success(c, newGroupResponse(&group))
|
response.Success(c, s.newGroupResponse(&group))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListGroups handles listing all groups.
|
// ListGroups handles listing all groups.
|
||||||
@@ -220,8 +221,8 @@ func (s *Server) ListGroups(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var groupResponses []GroupResponse
|
var groupResponses []GroupResponse
|
||||||
for _, group := range groups {
|
for i := range groups {
|
||||||
groupResponses = append(groupResponses, *newGroupResponse(&group))
|
groupResponses = append(groupResponses, *s.newGroupResponse(&groups[i]))
|
||||||
}
|
}
|
||||||
|
|
||||||
response.Success(c, groupResponses)
|
response.Success(c, groupResponses)
|
||||||
@@ -339,13 +340,14 @@ func (s *Server) UpdateGroup(c *gin.Context) {
|
|||||||
return
|
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.
|
// GroupResponse defines the structure for a group response, excluding sensitive or large fields.
|
||||||
type GroupResponse struct {
|
type GroupResponse struct {
|
||||||
ID uint `json:"id"`
|
ID uint `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
Endpoint string `json:"endpoint"`
|
||||||
DisplayName string `json:"display_name"`
|
DisplayName string `json:"display_name"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Upstreams datatypes.JSON `json:"upstreams"`
|
Upstreams datatypes.JSON `json:"upstreams"`
|
||||||
@@ -360,10 +362,21 @@ type GroupResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// newGroupResponse creates a new GroupResponse from a models.Group.
|
// 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{
|
return &GroupResponse{
|
||||||
ID: group.ID,
|
ID: group.ID,
|
||||||
Name: group.Name,
|
Name: group.Name,
|
||||||
|
Endpoint: endpoint,
|
||||||
DisplayName: group.DisplayName,
|
DisplayName: group.DisplayName,
|
||||||
Description: group.Description,
|
Description: group.Description,
|
||||||
Upstreams: group.Upstreams,
|
Upstreams: group.Upstreams,
|
||||||
|
@@ -34,6 +34,7 @@ type GroupConfig struct {
|
|||||||
type Group struct {
|
type Group struct {
|
||||||
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
|
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||||
Name string `gorm:"type:varchar(255);not null;unique" json:"name"`
|
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"`
|
DisplayName string `gorm:"type:varchar(255)" json:"display_name"`
|
||||||
Description string `gorm:"type:varchar(512)" json:"description"`
|
Description string `gorm:"type:varchar(512)" json:"description"`
|
||||||
Upstreams datatypes.JSON `gorm:"type:json;not null" json:"upstreams"`
|
Upstreams datatypes.JSON `gorm:"type:json;not null" json:"upstreams"`
|
||||||
|
Reference in New Issue
Block a user