Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 69 additions & 50 deletions internal/handler/group_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,21 +48,22 @@ func (s *Server) handleGroupError(c *gin.Context, err error) bool {

// 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"`
ModelRedirectRules map[string]string `json:"model_redirect_rules"`
ModelRedirectStrict bool `json:"model_redirect_strict"`
Config map[string]any `json:"config"`
HeaderRules []models.HeaderRule `json:"header_rules"`
ProxyKeys string `json:"proxy_keys"`
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"`
ModelRedirectRules map[string]string `json:"model_redirect_rules"`
ModelRedirectStrict bool `json:"model_redirect_strict"`
Config map[string]any `json:"config"`
HeaderRules []models.HeaderRule `json:"header_rules"`
QueryParamRules []models.QueryParamRule `json:"query_param_rules"`
ProxyKeys string `json:"proxy_keys"`
}

// CreateGroup handles the creation of a new group.
Expand All @@ -88,6 +89,7 @@ func (s *Server) CreateGroup(c *gin.Context) {
ModelRedirectStrict: req.ModelRedirectStrict,
Config: req.Config,
HeaderRules: req.HeaderRules,
QueryParamRules: req.QueryParamRules,
ProxyKeys: req.ProxyKeys,
}

Expand Down Expand Up @@ -117,21 +119,22 @@ func (s *Server) ListGroups(c *gin.Context) {
// 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"`
ModelRedirectRules map[string]string `json:"model_redirect_rules"`
ModelRedirectStrict *bool `json:"model_redirect_strict"`
Config map[string]any `json:"config"`
HeaderRules []models.HeaderRule `json:"header_rules"`
ProxyKeys *string `json:"proxy_keys,omitempty"`
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"`
ModelRedirectRules map[string]string `json:"model_redirect_rules"`
ModelRedirectStrict *bool `json:"model_redirect_strict"`
Config map[string]any `json:"config"`
HeaderRules []models.HeaderRule `json:"header_rules"`
QueryParamRules []models.QueryParamRule `json:"query_param_rules"`
ProxyKeys *string `json:"proxy_keys,omitempty"`
}

type GroupReorderItemRequest struct {
Expand Down Expand Up @@ -209,6 +212,11 @@ func (s *Server) UpdateGroup(c *gin.Context) {
params.HeaderRules = &rules
}

if req.QueryParamRules != nil {
rules := req.QueryParamRules
params.QueryParamRules = &rules
}

group, err := s.GroupService.UpdateGroup(c.Request.Context(), uint(id), params)
if s.handleGroupError(c, err) {
return
Expand Down Expand Up @@ -246,26 +254,27 @@ func (s *Server) ReorderGroups(c *gin.Context) {

// 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"`
ModelRedirectRules datatypes.JSONMap `json:"model_redirect_rules"`
ModelRedirectStrict bool `json:"model_redirect_strict"`
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"`
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"`
ModelRedirectRules datatypes.JSONMap `json:"model_redirect_rules"`
ModelRedirectStrict bool `json:"model_redirect_strict"`
Config datatypes.JSONMap `json:"config"`
HeaderRules []models.HeaderRule `json:"header_rules"`
QueryParamRules []models.QueryParamRule `json:"query_param_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.
Expand All @@ -289,6 +298,15 @@ func (s *Server) newGroupResponse(group *models.Group) *GroupResponse {
}
}

// Parse query param rules from JSON
var queryParamRules []models.QueryParamRule
if len(group.QueryParamRules) > 0 {
if err := json.Unmarshal(group.QueryParamRules, &queryParamRules); err != nil {
logrus.WithError(err).Error("Failed to unmarshal query param rules")
queryParamRules = make([]models.QueryParamRule, 0)
}
}

return &GroupResponse{
ID: group.ID,
Name: group.Name,
Expand All @@ -306,6 +324,7 @@ func (s *Server) newGroupResponse(group *models.Group) *GroupResponse {
ModelRedirectStrict: group.ModelRedirectStrict,
Config: group.Config,
HeaderRules: headerRules,
QueryParamRules: queryParamRules,
ProxyKeys: group.ProxyKeys,
LastValidatedAt: group.LastValidatedAt,
CreatedAt: group.CreatedAt,
Expand Down
5 changes: 4 additions & 1 deletion internal/i18n/locales/en-US.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ var MessagesEnUS = map[string]string{
"validation.invalid_group_name": "Invalid group name. Can only contain lowercase letters, numbers, hyphens or underscores, 1-100 characters",
"validation.invalid_test_path": "Invalid test path. If provided, must be a valid path starting with / and not a full URL.",
"validation.duplicate_header": "Duplicate header: {{.key}}",
"validation.duplicate_query_param": "Duplicate query parameter: {{.key}}",
"validation.invalid_query_param_action": "Invalid query parameter action: {{.action}}. Must be 'set' or 'remove'",
"validation.group_not_found": "Group not found",
"validation.invalid_status_filter": "Invalid status filter",
"validation.invalid_group_id": "Invalid group ID format",
Expand Down Expand Up @@ -185,7 +187,8 @@ var MessagesEnUS = map[string]string{
"error.upstream_weight_positive": "upstream weight must be a positive integer",
"error.marshal_upstreams_failed": "failed to marshal cleaned upstreams",
"error.invalid_config_format": "Invalid config format: {{.error}}",
"error.process_header_rules": "Failed to process header rules: {{.error}}",
"error.process_header_rules": "Failed to process header rules: {{.error}}",
"error.process_query_param_rules": "Failed to process query param rules: {{.error}}",
"error.invalidate_group_cache": "failed to invalidate group cache",
"error.unmarshal_header_rules": "Failed to unmarshal header rules",
"error.delete_group_cache": "Failed to delete group: unable to clean up cache",
Expand Down
5 changes: 4 additions & 1 deletion internal/i18n/locales/ja-JP.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ var MessagesJaJP = map[string]string{
"validation.invalid_group_name": "無効なグループ名。小文字、数字、ハイフン、アンダースコアのみ使用可能、1-100文字",
"validation.invalid_test_path": "無効なテストパス。指定する場合は / で始まる有効なパスであり、完全なURLではない必要があります。",
"validation.duplicate_header": "重複ヘッダー: {{.key}}",
"validation.duplicate_query_param": "重複クエリパラメータ: {{.key}}",
"validation.invalid_query_param_action": "無効なクエリパラメータアクション: {{.action}}。'set' または 'remove' である必要があります",
"validation.group_not_found": "グループが見つかりません",
"validation.invalid_status_filter": "無効なステータスフィルター",
"validation.invalid_group_id": "無効なグループID形式",
Expand Down Expand Up @@ -185,7 +187,8 @@ var MessagesJaJP = map[string]string{
"error.upstream_weight_positive": "upstreamの重みは正の整数である必要があります",
"error.marshal_upstreams_failed": "クリーンアップされたupstreamsのシリアル化に失敗しました",
"error.invalid_config_format": "無効な設定形式: {{.error}}",
"error.process_header_rules": "ヘッダールールの処理に失敗しました: {{.error}}",
"error.process_header_rules": "ヘッダールールの処理に失敗しました: {{.error}}",
"error.process_query_param_rules": "クエリパラメータルールの処理に失敗しました: {{.error}}",
"error.invalidate_group_cache": "グループキャッシュの無効化に失敗しました",
"error.unmarshal_header_rules": "ヘッダールールのアンマーシャルに失敗しました",
"error.delete_group_cache": "グループの削除に失敗: キャッシュをクリーンアップできません",
Expand Down
5 changes: 4 additions & 1 deletion internal/i18n/locales/zh-CN.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ var MessagesZhCN = map[string]string{
"validation.invalid_group_name": "无效的分组名称。只能包含小写字母、数字、中划线或下划线,长度1-100位",
"validation.invalid_test_path": "无效的测试路径。如果提供,必须是以 / 开头的有效路径,且不能是完整的URL。",
"validation.duplicate_header": "重复的请求头: {{.key}}",
"validation.duplicate_query_param": "重复的查询参数: {{.key}}",
"validation.invalid_query_param_action": "无效的查询参数操作: {{.action}},必须为 'set' 或 'remove'",
"validation.group_not_found": "分组不存在",
"validation.invalid_status_filter": "无效的状态过滤器",
"validation.invalid_group_id": "无效的分组ID格式",
Expand Down Expand Up @@ -185,7 +187,8 @@ var MessagesZhCN = map[string]string{
"error.upstream_weight_positive": "upstream权重必须是正整数",
"error.marshal_upstreams_failed": "序列化清理后的upstreams失败",
"error.invalid_config_format": "无效的配置格式: {{.error}}",
"error.process_header_rules": "处理请求头规则失败: {{.error}}",
"error.process_header_rules": "处理请求头规则失败: {{.error}}",
"error.process_query_param_rules": "处理查询参数规则失败: {{.error}}",
"error.invalidate_group_cache": "刷新分组缓存失败",
"error.unmarshal_header_rules": "解析请求头规则失败",
"error.delete_group_cache": "删除分组失败: 无法清理缓存",
Expand Down
15 changes: 12 additions & 3 deletions internal/models/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,13 @@ type HeaderRule struct {
Action string `json:"action"` // "set" or "remove"
}

// QueryParamRule defines a single rule for URL query parameter manipulation.
type QueryParamRule struct {
Key string `json:"key"`
Value string `json:"value"`
Action string `json:"action"` // "set" or "remove"
}

// GroupSubGroup 聚合分组和子分组的关联表
type GroupSubGroup struct {
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
Expand Down Expand Up @@ -95,6 +102,7 @@ type Group struct {
ParamOverrides datatypes.JSONMap `gorm:"type:json" json:"param_overrides"`
Config datatypes.JSONMap `gorm:"type:json" json:"config"`
HeaderRules datatypes.JSON `gorm:"type:json" json:"header_rules"`
QueryParamRules datatypes.JSON `gorm:"type:json" json:"query_param_rules"`
ModelRedirectRules datatypes.JSONMap `gorm:"type:json" json:"model_redirect_rules"`
ModelRedirectStrict bool `gorm:"default:false" json:"model_redirect_strict"`
APIKeys []APIKey `gorm:"foreignKey:GroupID" json:"api_keys"`
Expand All @@ -104,9 +112,10 @@ type Group struct {
UpdatedAt time.Time `json:"updated_at"`

// For cache
ProxyKeysMap map[string]struct{} `gorm:"-" json:"-"`
HeaderRuleList []HeaderRule `gorm:"-" json:"-"`
ModelRedirectMap map[string]string `gorm:"-" json:"-"`
ProxyKeysMap map[string]struct{} `gorm:"-" json:"-"`
HeaderRuleList []HeaderRule `gorm:"-" json:"-"`
QueryParamRuleList []QueryParamRule `gorm:"-" json:"-"`
ModelRedirectMap map[string]string `gorm:"-" json:"-"`
}

// APIKey 对应 api_keys 表
Expand Down
6 changes: 6 additions & 0 deletions internal/proxy/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,12 @@ func (ps *ProxyServer) executeRequestWithRetry(
utils.ApplyHeaderRules(req, group.HeaderRuleList, headerCtx)
}

// Apply custom query parameter rules
if len(group.QueryParamRuleList) > 0 {
queryCtx := utils.NewHeaderVariableContextFromGin(c, group, apiKey)
utils.ApplyQueryParamRules(req, group.QueryParamRuleList, queryCtx)
}

var client *http.Client
if isStream {
client = channelHandler.GetStreamClient()
Expand Down
21 changes: 16 additions & 5 deletions internal/services/group_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,16 @@ func (gm *GroupManager) Initialize() error {
g.HeaderRuleList = []models.HeaderRule{}
}

// Parse query param rules with error handling
if len(group.QueryParamRules) > 0 {
if err := json.Unmarshal(group.QueryParamRules, &g.QueryParamRuleList); err != nil {
logrus.WithError(err).WithField("group_name", g.Name).Warn("Failed to parse query param rules for group")
g.QueryParamRuleList = []models.QueryParamRule{}
}
} else {
g.QueryParamRuleList = []models.QueryParamRule{}
}

// Parse model redirect rules with error handling
g.ModelRedirectMap = make(map[string]string)
if len(group.ModelRedirectRules) > 0 {
Expand Down Expand Up @@ -119,12 +129,13 @@ func (gm *GroupManager) Initialize() error {

groupMap[g.Name] = &g
logrus.WithFields(logrus.Fields{
"group_name": g.Name,
"effective_config": g.EffectiveConfig,
"header_rules_count": len(g.HeaderRuleList),
"group_name": g.Name,
"effective_config": g.EffectiveConfig,
"header_rules_count": len(g.HeaderRuleList),
"query_param_rules_count": len(g.QueryParamRuleList),
"model_redirect_rules_count": len(g.ModelRedirectMap),
"model_redirect_strict": g.ModelRedirectStrict,
"sub_group_count": len(g.SubGroups),
"model_redirect_strict": g.ModelRedirectStrict,
"sub_group_count": len(g.SubGroups),
}).Debug("Loaded group with effective config")
}

Expand Down
Loading