* feat: Create Aggregate Group * feat: Subgroup List * refactor: Optimize code * refactor: Frontend Optimization * feat: ValidationEndpoint * feat: Subgroup modification verification * fix: Aggregate Grouping Editing Issue * feat: Show Aggregate References * feat: Filter duplicate prompts * feat: Subgroup manager * feat: Log aggregate * feat: Group statistics * fix: up url * fix: Aggregate group config * feat: Log add aggregate group * fix: Cron filter group
468 lines
15 KiB
Go
468 lines
15 KiB
Go
// Package handler provides HTTP handlers for the application
|
|
package handler
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
app_errors "gpt-load/internal/errors"
|
|
"gpt-load/internal/i18n"
|
|
"gpt-load/internal/models"
|
|
"gpt-load/internal/response"
|
|
"gpt-load/internal/services"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/sirupsen/logrus"
|
|
"gorm.io/datatypes"
|
|
)
|
|
|
|
func (s *Server) handleGroupError(c *gin.Context, err error) bool {
|
|
if err == nil {
|
|
return false
|
|
}
|
|
|
|
if svcErr, ok := err.(*services.I18nError); ok {
|
|
if svcErr.Template != nil {
|
|
response.ErrorI18nFromAPIError(c, svcErr.APIError, svcErr.MessageID, svcErr.Template)
|
|
} else {
|
|
response.ErrorI18nFromAPIError(c, svcErr.APIError, svcErr.MessageID)
|
|
}
|
|
return true
|
|
}
|
|
|
|
if apiErr, ok := err.(*app_errors.APIError); ok {
|
|
response.Error(c, apiErr)
|
|
return true
|
|
}
|
|
|
|
logrus.WithContext(c.Request.Context()).WithError(err).Error("unexpected group service error")
|
|
response.Error(c, app_errors.ErrInternalServer)
|
|
return true
|
|
}
|
|
|
|
// GroupCreateRequest defines the payload for creating a group.
|
|
type GroupCreateRequest struct {
|
|
Name string `json:"name"`
|
|
DisplayName string `json:"display_name"`
|
|
Description string `json:"description"`
|
|
GroupType string `json:"group_type"` // 'standard' or 'aggregate'
|
|
Upstreams json.RawMessage `json:"upstreams"`
|
|
ChannelType string `json:"channel_type"`
|
|
Sort int `json:"sort"`
|
|
TestModel string `json:"test_model"`
|
|
ValidationEndpoint string `json:"validation_endpoint"`
|
|
ParamOverrides map[string]any `json:"param_overrides"`
|
|
Config map[string]any `json:"config"`
|
|
HeaderRules []models.HeaderRule `json:"header_rules"`
|
|
ProxyKeys string `json:"proxy_keys"`
|
|
}
|
|
|
|
// CreateGroup handles the creation of a new group.
|
|
func (s *Server) CreateGroup(c *gin.Context) {
|
|
var req GroupCreateRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.Error(c, app_errors.NewAPIError(app_errors.ErrInvalidJSON, err.Error()))
|
|
return
|
|
}
|
|
|
|
params := services.GroupCreateParams{
|
|
Name: req.Name,
|
|
DisplayName: req.DisplayName,
|
|
Description: req.Description,
|
|
GroupType: req.GroupType,
|
|
Upstreams: req.Upstreams,
|
|
ChannelType: req.ChannelType,
|
|
Sort: req.Sort,
|
|
TestModel: req.TestModel,
|
|
ValidationEndpoint: req.ValidationEndpoint,
|
|
ParamOverrides: req.ParamOverrides,
|
|
Config: req.Config,
|
|
HeaderRules: req.HeaderRules,
|
|
ProxyKeys: req.ProxyKeys,
|
|
}
|
|
|
|
group, err := s.GroupService.CreateGroup(c.Request.Context(), params)
|
|
if s.handleGroupError(c, err) {
|
|
return
|
|
}
|
|
|
|
response.Success(c, s.newGroupResponse(group))
|
|
}
|
|
|
|
// ListGroups handles listing all groups.
|
|
func (s *Server) ListGroups(c *gin.Context) {
|
|
groups, err := s.GroupService.ListGroups(c.Request.Context())
|
|
if s.handleGroupError(c, err) {
|
|
return
|
|
}
|
|
|
|
groupResponses := make([]GroupResponse, 0, len(groups))
|
|
for i := range groups {
|
|
groupResponses = append(groupResponses, *s.newGroupResponse(&groups[i]))
|
|
}
|
|
|
|
response.Success(c, groupResponses)
|
|
}
|
|
|
|
// GroupUpdateRequest defines the payload for updating a group.
|
|
// Using a dedicated struct avoids issues with zero values being ignored by GORM's Update.
|
|
type GroupUpdateRequest struct {
|
|
Name *string `json:"name,omitempty"`
|
|
DisplayName *string `json:"display_name,omitempty"`
|
|
Description *string `json:"description,omitempty"`
|
|
GroupType *string `json:"group_type,omitempty"`
|
|
Upstreams json.RawMessage `json:"upstreams"`
|
|
ChannelType *string `json:"channel_type,omitempty"`
|
|
Sort *int `json:"sort"`
|
|
TestModel string `json:"test_model"`
|
|
ValidationEndpoint *string `json:"validation_endpoint,omitempty"`
|
|
ParamOverrides map[string]any `json:"param_overrides"`
|
|
Config map[string]any `json:"config"`
|
|
HeaderRules []models.HeaderRule `json:"header_rules"`
|
|
ProxyKeys *string `json:"proxy_keys,omitempty"`
|
|
}
|
|
|
|
// UpdateGroup handles updating an existing group.
|
|
func (s *Server) UpdateGroup(c *gin.Context) {
|
|
id, err := strconv.Atoi(c.Param("id"))
|
|
if err != nil {
|
|
response.ErrorI18nFromAPIError(c, app_errors.ErrBadRequest, "validation.invalid_group_id")
|
|
return
|
|
}
|
|
|
|
var req GroupUpdateRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.Error(c, app_errors.NewAPIError(app_errors.ErrInvalidJSON, err.Error()))
|
|
return
|
|
}
|
|
|
|
params := services.GroupUpdateParams{
|
|
Name: req.Name,
|
|
DisplayName: req.DisplayName,
|
|
Description: req.Description,
|
|
GroupType: req.GroupType,
|
|
ChannelType: req.ChannelType,
|
|
Sort: req.Sort,
|
|
ValidationEndpoint: req.ValidationEndpoint,
|
|
ParamOverrides: req.ParamOverrides,
|
|
Config: req.Config,
|
|
ProxyKeys: req.ProxyKeys,
|
|
}
|
|
|
|
if req.Upstreams != nil {
|
|
params.Upstreams = req.Upstreams
|
|
params.HasUpstreams = true
|
|
}
|
|
|
|
if req.TestModel != "" {
|
|
params.TestModel = req.TestModel
|
|
params.HasTestModel = true
|
|
}
|
|
|
|
if req.HeaderRules != nil {
|
|
rules := req.HeaderRules
|
|
params.HeaderRules = &rules
|
|
}
|
|
|
|
group, err := s.GroupService.UpdateGroup(c.Request.Context(), uint(id), params)
|
|
if s.handleGroupError(c, err) {
|
|
return
|
|
}
|
|
|
|
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"`
|
|
GroupType string `json:"group_type"`
|
|
Upstreams datatypes.JSON `json:"upstreams"`
|
|
ChannelType string `json:"channel_type"`
|
|
Sort int `json:"sort"`
|
|
TestModel string `json:"test_model"`
|
|
ValidationEndpoint string `json:"validation_endpoint"`
|
|
ParamOverrides datatypes.JSONMap `json:"param_overrides"`
|
|
Config datatypes.JSONMap `json:"config"`
|
|
HeaderRules []models.HeaderRule `json:"header_rules"`
|
|
ProxyKeys string `json:"proxy_keys"`
|
|
LastValidatedAt *time.Time `json:"last_validated_at"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
// newGroupResponse creates a new GroupResponse from a models.Group.
|
|
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()
|
|
}
|
|
}
|
|
|
|
// Parse header rules from JSON
|
|
var headerRules []models.HeaderRule
|
|
if len(group.HeaderRules) > 0 {
|
|
if err := json.Unmarshal(group.HeaderRules, &headerRules); err != nil {
|
|
logrus.WithError(err).Error("Failed to unmarshal header rules")
|
|
headerRules = make([]models.HeaderRule, 0)
|
|
}
|
|
}
|
|
|
|
return &GroupResponse{
|
|
ID: group.ID,
|
|
Name: group.Name,
|
|
Endpoint: endpoint,
|
|
DisplayName: group.DisplayName,
|
|
Description: group.Description,
|
|
GroupType: group.GroupType,
|
|
Upstreams: group.Upstreams,
|
|
ChannelType: group.ChannelType,
|
|
Sort: group.Sort,
|
|
TestModel: group.TestModel,
|
|
ValidationEndpoint: group.ValidationEndpoint,
|
|
ParamOverrides: group.ParamOverrides,
|
|
Config: group.Config,
|
|
HeaderRules: headerRules,
|
|
ProxyKeys: group.ProxyKeys,
|
|
LastValidatedAt: group.LastValidatedAt,
|
|
CreatedAt: group.CreatedAt,
|
|
UpdatedAt: group.UpdatedAt,
|
|
}
|
|
}
|
|
|
|
// DeleteGroup handles deleting a group.
|
|
func (s *Server) DeleteGroup(c *gin.Context) {
|
|
id, err := strconv.Atoi(c.Param("id"))
|
|
if err != nil {
|
|
response.ErrorI18nFromAPIError(c, app_errors.ErrBadRequest, "validation.invalid_group_id")
|
|
return
|
|
}
|
|
|
|
if s.handleGroupError(c, s.GroupService.DeleteGroup(c.Request.Context(), uint(id))) {
|
|
return
|
|
}
|
|
response.SuccessI18n(c, "success.group_deleted", nil)
|
|
}
|
|
|
|
// ConfigOption represents a single configurable option for a group.
|
|
type ConfigOption struct {
|
|
Key string `json:"key"`
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
DefaultValue any `json:"default_value"`
|
|
}
|
|
|
|
// GetGroupConfigOptions returns a list of available configuration options for groups.
|
|
func (s *Server) GetGroupConfigOptions(c *gin.Context) {
|
|
options, err := s.GroupService.GetGroupConfigOptions()
|
|
if s.handleGroupError(c, err) {
|
|
return
|
|
}
|
|
|
|
translated := make([]ConfigOption, 0, len(options))
|
|
for _, option := range options {
|
|
name := option.Name
|
|
if strings.HasPrefix(name, "config.") {
|
|
name = i18n.Message(c, name)
|
|
}
|
|
description := option.Description
|
|
if strings.HasPrefix(description, "config.") {
|
|
description = i18n.Message(c, description)
|
|
}
|
|
|
|
translated = append(translated, ConfigOption{
|
|
Key: option.Key,
|
|
Name: name,
|
|
Description: description,
|
|
DefaultValue: option.DefaultValue,
|
|
})
|
|
}
|
|
|
|
response.Success(c, translated)
|
|
}
|
|
|
|
// calculateRequestStats is a helper to compute request statistics.
|
|
func (s *Server) GetGroupStats(c *gin.Context) {
|
|
id, err := strconv.Atoi(c.Param("id"))
|
|
if err != nil {
|
|
response.ErrorI18nFromAPIError(c, app_errors.ErrBadRequest, "validation.invalid_group_id")
|
|
return
|
|
}
|
|
|
|
stats, err := s.GroupService.GetGroupStats(c.Request.Context(), uint(id))
|
|
if s.handleGroupError(c, err) {
|
|
return
|
|
}
|
|
|
|
response.Success(c, stats)
|
|
}
|
|
|
|
// GroupCopyRequest defines the payload for copying a group.
|
|
type GroupCopyRequest struct {
|
|
CopyKeys string `json:"copy_keys"` // "none"|"valid_only"|"all"
|
|
}
|
|
|
|
// GroupCopyResponse defines the response for group copy operation.
|
|
type GroupCopyResponse struct {
|
|
Group *GroupResponse `json:"group"`
|
|
}
|
|
|
|
// CopyGroup handles copying a group with optional content.
|
|
|
|
func (s *Server) CopyGroup(c *gin.Context) {
|
|
id, err := strconv.Atoi(c.Param("id"))
|
|
if err != nil {
|
|
response.ErrorI18nFromAPIError(c, app_errors.ErrBadRequest, "validation.invalid_group_id")
|
|
return
|
|
}
|
|
|
|
var req GroupCopyRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.Error(c, app_errors.NewAPIError(app_errors.ErrInvalidJSON, err.Error()))
|
|
return
|
|
}
|
|
|
|
newGroup, err := s.GroupService.CopyGroup(c.Request.Context(), uint(id), req.CopyKeys)
|
|
if s.handleGroupError(c, err) {
|
|
return
|
|
}
|
|
|
|
groupResponse := s.newGroupResponse(newGroup)
|
|
copyResponse := &GroupCopyResponse{
|
|
Group: groupResponse,
|
|
}
|
|
|
|
response.Success(c, copyResponse)
|
|
}
|
|
|
|
// List godoc
|
|
func (s *Server) List(c *gin.Context) {
|
|
var groups []models.Group
|
|
if err := s.DB.Select("id, name,display_name").Find(&groups).Error; err != nil {
|
|
response.ErrorI18nFromAPIError(c, app_errors.ErrDatabase, "database.cannot_get_groups")
|
|
return
|
|
}
|
|
response.Success(c, groups)
|
|
}
|
|
|
|
// AddSubGroupsRequest defines the payload for adding sub groups to an aggregate group
|
|
type AddSubGroupsRequest struct {
|
|
SubGroups []services.SubGroupInput `json:"sub_groups"`
|
|
}
|
|
|
|
// UpdateSubGroupWeightRequest defines the payload for updating a sub group weight
|
|
type UpdateSubGroupWeightRequest struct {
|
|
Weight int `json:"weight"`
|
|
}
|
|
|
|
// GetSubGroups handles getting sub groups of an aggregate group
|
|
func (s *Server) GetSubGroups(c *gin.Context) {
|
|
id, err := strconv.Atoi(c.Param("id"))
|
|
if err != nil {
|
|
response.ErrorI18nFromAPIError(c, app_errors.ErrBadRequest, "validation.invalid_group_id")
|
|
return
|
|
}
|
|
|
|
subGroups, err := s.AggregateGroupService.GetSubGroups(c.Request.Context(), uint(id))
|
|
if s.handleGroupError(c, err) {
|
|
return
|
|
}
|
|
|
|
response.Success(c, subGroups)
|
|
}
|
|
|
|
// AddSubGroups handles adding sub groups to an aggregate group
|
|
func (s *Server) AddSubGroups(c *gin.Context) {
|
|
id, err := strconv.Atoi(c.Param("id"))
|
|
if err != nil {
|
|
response.ErrorI18nFromAPIError(c, app_errors.ErrBadRequest, "validation.invalid_group_id")
|
|
return
|
|
}
|
|
|
|
var req AddSubGroupsRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.Error(c, app_errors.NewAPIError(app_errors.ErrInvalidJSON, err.Error()))
|
|
return
|
|
}
|
|
|
|
if err := s.AggregateGroupService.AddSubGroups(c.Request.Context(), uint(id), req.SubGroups); s.handleGroupError(c, err) {
|
|
return
|
|
}
|
|
|
|
response.SuccessI18n(c, "success.sub_groups_added", nil)
|
|
}
|
|
|
|
// UpdateSubGroupWeight handles updating the weight of a sub group
|
|
func (s *Server) UpdateSubGroupWeight(c *gin.Context) {
|
|
id, err := strconv.Atoi(c.Param("id"))
|
|
if err != nil {
|
|
response.ErrorI18nFromAPIError(c, app_errors.ErrBadRequest, "validation.invalid_group_id")
|
|
return
|
|
}
|
|
|
|
subGroupID, err := strconv.Atoi(c.Param("subGroupId"))
|
|
if err != nil {
|
|
response.ErrorI18nFromAPIError(c, app_errors.ErrBadRequest, "validation.invalid_sub_group_id")
|
|
return
|
|
}
|
|
|
|
var req UpdateSubGroupWeightRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
response.Error(c, app_errors.NewAPIError(app_errors.ErrInvalidJSON, err.Error()))
|
|
return
|
|
}
|
|
|
|
if err := s.AggregateGroupService.UpdateSubGroupWeight(c.Request.Context(), uint(id), uint(subGroupID), req.Weight); s.handleGroupError(c, err) {
|
|
return
|
|
}
|
|
|
|
response.SuccessI18n(c, "success.sub_group_weight_updated", nil)
|
|
}
|
|
|
|
// DeleteSubGroup handles deleting a sub group from an aggregate group
|
|
func (s *Server) DeleteSubGroup(c *gin.Context) {
|
|
id, err := strconv.Atoi(c.Param("id"))
|
|
if err != nil {
|
|
response.ErrorI18nFromAPIError(c, app_errors.ErrBadRequest, "validation.invalid_group_id")
|
|
return
|
|
}
|
|
|
|
subGroupID, err := strconv.Atoi(c.Param("subGroupId"))
|
|
if err != nil {
|
|
response.ErrorI18nFromAPIError(c, app_errors.ErrBadRequest, "validation.invalid_sub_group_id")
|
|
return
|
|
}
|
|
|
|
if err := s.AggregateGroupService.DeleteSubGroup(c.Request.Context(), uint(id), uint(subGroupID)); s.handleGroupError(c, err) {
|
|
return
|
|
}
|
|
|
|
response.SuccessI18n(c, "success.sub_group_deleted", nil)
|
|
}
|
|
|
|
// GetParentAggregateGroups handles getting parent aggregate groups that reference a group
|
|
func (s *Server) GetParentAggregateGroups(c *gin.Context) {
|
|
id, err := strconv.Atoi(c.Param("id"))
|
|
if err != nil {
|
|
response.ErrorI18nFromAPIError(c, app_errors.ErrBadRequest, "validation.invalid_group_id")
|
|
return
|
|
}
|
|
|
|
parentGroups, err := s.AggregateGroupService.GetParentAggregateGroups(c.Request.Context(), uint(id))
|
|
if s.handleGroupError(c, err) {
|
|
return
|
|
}
|
|
|
|
response.Success(c, parentGroups)
|
|
}
|