diff --git a/internal/handler/group_handler.go b/internal/handler/group_handler.go index 8c42bf1a3..0ad3fff7e 100644 --- a/internal/handler/group_handler.go +++ b/internal/handler/group_handler.go @@ -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. @@ -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, } @@ -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 { @@ -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 @@ -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. @@ -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, @@ -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, diff --git a/internal/i18n/locales/en-US.go b/internal/i18n/locales/en-US.go index 1c6a94f78..7870c5c23 100644 --- a/internal/i18n/locales/en-US.go +++ b/internal/i18n/locales/en-US.go @@ -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", @@ -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", diff --git a/internal/i18n/locales/ja-JP.go b/internal/i18n/locales/ja-JP.go index f0b33fd67..28c213a9f 100644 --- a/internal/i18n/locales/ja-JP.go +++ b/internal/i18n/locales/ja-JP.go @@ -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形式", @@ -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": "グループの削除に失敗: キャッシュをクリーンアップできません", diff --git a/internal/i18n/locales/zh-CN.go b/internal/i18n/locales/zh-CN.go index 188f1a791..3618f5939 100644 --- a/internal/i18n/locales/zh-CN.go +++ b/internal/i18n/locales/zh-CN.go @@ -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格式", @@ -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": "删除分组失败: 无法清理缓存", diff --git a/internal/models/types.go b/internal/models/types.go index b064f472d..15bc7acd6 100644 --- a/internal/models/types.go +++ b/internal/models/types.go @@ -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"` @@ -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"` @@ -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 表 diff --git a/internal/proxy/server.go b/internal/proxy/server.go index 0499de49e..b29410c8b 100644 --- a/internal/proxy/server.go +++ b/internal/proxy/server.go @@ -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() diff --git a/internal/services/group_manager.go b/internal/services/group_manager.go index 17b3ea8a5..56ed40f23 100644 --- a/internal/services/group_manager.go +++ b/internal/services/group_manager.go @@ -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 { @@ -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") } diff --git a/internal/services/group_service.go b/internal/services/group_service.go index db73aac81..60e804734 100644 --- a/internal/services/group_service.go +++ b/internal/services/group_service.go @@ -99,6 +99,7 @@ type GroupCreateParams struct { ModelRedirectStrict bool Config map[string]any HeaderRules []models.HeaderRule + QueryParamRules []models.QueryParamRule ProxyKeys string SubGroups []SubGroupInput } @@ -121,6 +122,7 @@ type GroupUpdateParams struct { ModelRedirectStrict *bool Config map[string]any HeaderRules *[]models.HeaderRule + QueryParamRules *[]models.QueryParamRule ProxyKeys *string SubGroups *[]SubGroupInput } @@ -221,6 +223,14 @@ func (s *GroupService) CreateGroup(ctx context.Context, params GroupCreateParams headerRulesJSON = datatypes.JSON("[]") } + queryParamRulesJSON, err := s.normalizeQueryParamRules(params.QueryParamRules) + if err != nil { + return nil, err + } + if queryParamRulesJSON == nil { + queryParamRulesJSON = datatypes.JSON("[]") + } + // Validate model redirect rules for aggregate groups if groupType == "aggregate" && len(params.ModelRedirectRules) > 0 { return nil, NewI18nError(app_errors.ErrValidation, "validation.aggregate_no_model_redirect", nil) @@ -246,6 +256,7 @@ func (s *GroupService) CreateGroup(ctx context.Context, params GroupCreateParams ModelRedirectStrict: params.ModelRedirectStrict, Config: cleanedConfig, HeaderRules: headerRulesJSON, + QueryParamRules: queryParamRulesJSON, ProxyKeys: strings.TrimSpace(params.ProxyKeys), } @@ -484,6 +495,17 @@ func (s *GroupService) UpdateGroup(ctx context.Context, id uint, params GroupUpd group.HeaderRules = headerRulesJSON } + if params.QueryParamRules != nil { + queryParamRulesJSON, err := s.normalizeQueryParamRules(*params.QueryParamRules) + if err != nil { + return nil, err + } + if queryParamRulesJSON == nil { + queryParamRulesJSON = datatypes.JSON("[]") + } + group.QueryParamRules = queryParamRulesJSON + } + if err := tx.Save(&group).Error; err != nil { return nil, app_errors.ParseDBError(err) } @@ -933,6 +955,43 @@ func (s *GroupService) normalizeHeaderRules(rules []models.HeaderRule) (datatype return datatypes.JSON(headerRulesBytes), nil } +// normalizeQueryParamRules deduplicates and normalises query parameter rules. +func (s *GroupService) normalizeQueryParamRules(rules []models.QueryParamRule) (datatypes.JSON, error) { + if len(rules) == 0 { + return nil, nil + } + + normalized := make([]models.QueryParamRule, 0, len(rules)) + seenKeys := make(map[string]bool) + + for _, rule := range rules { + key := strings.TrimSpace(rule.Key) + if key == "" { + continue + } + action := strings.TrimSpace(rule.Action) + if action != "set" && action != "remove" { + return nil, NewI18nError(app_errors.ErrValidation, "validation.invalid_query_param_action", map[string]any{"action": action}) + } + if seenKeys[key] { + return nil, NewI18nError(app_errors.ErrValidation, "validation.duplicate_query_param", map[string]any{"key": key}) + } + seenKeys[key] = true + normalized = append(normalized, models.QueryParamRule{Key: key, Value: rule.Value, Action: action}) + } + + if len(normalized) == 0 { + return nil, nil + } + + queryParamRulesBytes, err := json.Marshal(normalized) + if err != nil { + return nil, NewI18nError(app_errors.ErrInternalServer, "error.process_query_param_rules", map[string]any{"error": err.Error()}) + } + + return datatypes.JSON(queryParamRulesBytes), nil +} + // validateAndCleanUpstreams validates upstream definitions. func (s *GroupService) validateAndCleanUpstreams(upstreams json.RawMessage) (datatypes.JSON, error) { if len(upstreams) == 0 { diff --git a/internal/utils/query_param_utils.go b/internal/utils/query_param_utils.go new file mode 100644 index 000000000..533e5a6bf --- /dev/null +++ b/internal/utils/query_param_utils.go @@ -0,0 +1,27 @@ +package utils + +import ( + "gpt-load/internal/models" + "net/http" +) + +// ApplyQueryParamRules applies query parameter rules to the HTTP request URL. +func ApplyQueryParamRules(req *http.Request, rules []models.QueryParamRule, ctx *HeaderVariableContext) { + if req == nil || len(rules) == 0 { + return + } + + q := req.URL.Query() + + for _, rule := range rules { + switch rule.Action { + case "remove": + q.Del(rule.Key) + case "set": + resolvedValue := ResolveHeaderVariables(rule.Value, ctx) + q.Set(rule.Key, resolvedValue) + } + } + + req.URL.RawQuery = q.Encode() +} diff --git a/web/src/components/keys/GroupFormModal.vue b/web/src/components/keys/GroupFormModal.vue index 5eea0426f..434fe3aa2 100644 --- a/web/src/components/keys/GroupFormModal.vue +++ b/web/src/components/keys/GroupFormModal.vue @@ -46,6 +46,13 @@ interface HeaderRuleItem { action: "set" | "remove"; } +// Query Param规则类型 +interface QueryParamRuleItem { + key: string; + value: string; + action: "set" | "remove"; +} + const props = withDefaults(defineProps(), { group: null, }); @@ -77,6 +84,7 @@ interface GroupFormData { config: Record; configItems: ConfigItem[]; header_rules: HeaderRuleItem[]; + query_param_rules: QueryParamRuleItem[]; proxy_keys: string; group_type?: string; } @@ -102,6 +110,7 @@ const formData = reactive({ config: {}, configItems: [] as ConfigItem[], header_rules: [] as HeaderRuleItem[], + query_param_rules: [] as QueryParamRuleItem[], proxy_keys: "", group_type: "standard", }); @@ -303,6 +312,7 @@ function resetForm() { config: {}, configItems: [], header_rules: [], + query_param_rules: [], proxy_keys: "", group_type: "standard", }); @@ -349,6 +359,11 @@ function loadGroupData() { value: rule.value || "", action: (rule.action as "set" | "remove") || "set", })), + query_param_rules: (props.group.query_param_rules || []).map((rule: QueryParamRuleItem) => ({ + key: rule.key || "", + value: rule.value || "", + action: (rule.action as "set" | "remove") || "set", + })), proxy_keys: props.group.proxy_keys || "", group_type: props.group.group_type || "standard", }); @@ -414,6 +429,35 @@ function removeHeaderRule(index: number) { formData.header_rules.splice(index, 1); } +// 添加Query Param规则 +function addQueryParamRule() { + formData.query_param_rules.push({ + key: "", + value: "", + action: "remove", + }); +} + +// 删除Query Param规则 +function removeQueryParamRule(index: number) { + formData.query_param_rules.splice(index, 1); +} + +// 验证Query Param Key唯一性 +function validateQueryParamKeyUniqueness( + rules: QueryParamRuleItem[], + currentIndex: number, + key: string +): boolean { + if (!key.trim()) { + return true; + } + const trimmedKey = key.trim(); + return !rules.some( + (rule, index) => index !== currentIndex && rule.key.trim() === trimmedKey + ); +} + // 规范化Header Key到Canonical格式(模拟HTTP标准) function canonicalHeaderKey(key: string): string { if (!key) { @@ -538,6 +582,13 @@ async function handleSubmit() { value: rule.value, action: rule.action, })), + query_param_rules: formData.query_param_rules + .filter((rule: QueryParamRuleItem) => rule.key.trim()) + .map((rule: QueryParamRuleItem) => ({ + key: rule.key.trim(), + value: rule.value, + action: rule.action, + })), proxy_keys: formData.proxy_keys, }; @@ -1083,6 +1134,128 @@ async function handleSubmit() { +
+
+ {{ t("keys.customQueryParams") }} + + +
+ {{ t("keys.queryParamRulesTooltip1") }} +
+ {{ t("keys.supportedVariables") }}: +
+ • ${CLIENT_IP} - {{ t("keys.clientIpVar") }} +
+ • ${GROUP_NAME} - {{ t("keys.groupNameVar") }} +
+ • ${API_KEY} - {{ t("keys.apiKeyVar") }} +
+ • ${TIMESTAMP_MS} - {{ t("keys.timestampMsVar") }} +
+ • ${TIMESTAMP_S} - {{ t("keys.timestampSVar") }} +
+
+
+ +
+ + +
+
+ +
+ {{ t("keys.duplicateQueryParam") }} +
+
+
+ +
+
+ {{ + t("keys.queryParamWillRemoveFromRequest") + }} +
+
+ + + {{ t("keys.queryParamRemoveToggleTooltip") }} + +
+
+ + + +
+
+
+
+ +
+ + + {{ t("keys.addQueryParam") }} + +
+
+
diff --git a/web/src/locales/en-US.ts b/web/src/locales/en-US.ts index 3942c7844..a31f43d2c 100644 --- a/web/src/locales/en-US.ts +++ b/web/src/locales/en-US.ts @@ -358,6 +358,19 @@ export default { removeToggleTooltip: "Enable remove switch to delete this header, disable to add or override this header", addHeader: "Add Header", + customQueryParams: "Custom Query Parameters", + queryParamRulesTooltip1: + "Add, override or remove URL query parameters before forwarding proxy requests to upstream services. For example, remove ?beta=true appended by the client.", + queryParam: "Param", + queryParamTooltip: + "Configure URL query parameter name, value and operation type. Remove operation will strip the specified parameter from the URL", + queryParamName: "Parameter name", + duplicateQueryParam: "Duplicate parameter name", + queryParamValuePlaceholder: "Supports variables, e.g.: ${GROUP_NAME}", + queryParamWillRemoveFromRequest: "Will be removed from URL", + queryParamRemoveToggleTooltip: + "Enable remove switch to delete this parameter, disable to add or override this parameter", + addQueryParam: "Add Query Parameter", paramOverridesTooltip: "Define the API request parameters to be overridden using JSON format. These parameters will be merged with the original parameters when sending the request.", modelRedirectPolicy: "Unconfigured Model Policy", diff --git a/web/src/locales/ja-JP.ts b/web/src/locales/ja-JP.ts index 4d9321714..8ec5443b9 100644 --- a/web/src/locales/ja-JP.ts +++ b/web/src/locales/ja-JP.ts @@ -357,6 +357,19 @@ export default { removeToggleTooltip: "削除スイッチを有効にするとこのヘッダーを削除、無効にするとこのヘッダーを追加または上書き", addHeader: "ヘッダー追加", + customQueryParams: "カスタムクエリパラメータ", + queryParamRulesTooltip1: + "プロキシリクエストをアップストリームサービスに転送する前に、URLクエリパラメータの追加、上書き、または削除を行います。例: クライアントが付加する ?beta=true を削除。", + queryParam: "パラメータ", + queryParamTooltip: + "URLクエリパラメータの名前、値、操作タイプを設定します。削除操作はURLから指定のパラメータを削除します", + queryParamName: "パラメータ名", + duplicateQueryParam: "パラメータ名が重複しています", + queryParamValuePlaceholder: "変数をサポート、例:${GROUP_NAME}", + queryParamWillRemoveFromRequest: "URLから削除されます", + queryParamRemoveToggleTooltip: + "削除スイッチを有効にするとこのパラメータを削除、無効にするとこのパラメータを追加または上書き", + addQueryParam: "クエリパラメータ追加", paramOverridesTooltip: "JSON形式を使用して、上書きするAPIリクエストパラメータを定義します。これらのパラメータは、リクエスト送信時に元のパラメータにマージされます。", modelRedirectPolicy: "未設定モデルポリシー", diff --git a/web/src/locales/zh-CN.ts b/web/src/locales/zh-CN.ts index ca1a64a52..4fcd73ed3 100644 --- a/web/src/locales/zh-CN.ts +++ b/web/src/locales/zh-CN.ts @@ -345,6 +345,17 @@ export default { willRemoveFromRequest: "将从请求中移除", removeToggleTooltip: "开启移除开关将删除此请求头,关闭则添加或覆盖此请求头", addHeader: "添加请求头", + customQueryParams: "自定义 URL 参数", + queryParamRulesTooltip1: + "在代理请求转发至上游服务前,对 URL 查询参数进行添加、覆盖或移除操作。例如移除客户端附加的 ?beta=true。", + queryParam: "参数", + queryParamTooltip: "配置URL查询参数的名称、值和操作类型。移除操作会从URL中删除指定的参数", + queryParamName: "参数名称", + duplicateQueryParam: "参数名称重复", + queryParamValuePlaceholder: "支持变量,例如:${GROUP_NAME}", + queryParamWillRemoveFromRequest: "将从URL中移除", + queryParamRemoveToggleTooltip: "开启移除开关将删除此参数,关闭则添加或覆盖此参数", + addQueryParam: "添加 URL 参数", paramOverridesTooltip: "使用JSON格式定义要覆盖的API请求参数。这些参数会在发送请求时合并到原始参数中", modelRedirectPolicy: "未配置模型策略", diff --git a/web/src/types/models.ts b/web/src/types/models.ts index bfdc91163..fe8ba2342 100644 --- a/web/src/types/models.ts +++ b/web/src/types/models.ts @@ -39,6 +39,12 @@ export interface HeaderRule { action: "set" | "remove"; } +export interface QueryParamRule { + key: string; + value: string; + action: "set" | "remove"; +} + // 子分组配置(创建/更新时使用) export interface SubGroupConfig { group_id: number; @@ -79,6 +85,7 @@ export interface Group { model_redirect_rules: Record; model_redirect_strict: boolean; header_rules?: HeaderRule[]; + query_param_rules?: QueryParamRule[]; proxy_keys: string; group_type?: GroupType; sub_groups?: SubGroupInfo[]; // 子分组列表(仅聚合分组)