Skip to content

Commit d3dd4fe

Browse files
authored
feat: HTTP代理功能增强 (#141)
* feat: http代理功能增强 * fix: 字段类型验证 * fix: 前端动态类型错误
1 parent a97765a commit d3dd4fe

File tree

13 files changed

+183
-75
lines changed

13 files changed

+183
-75
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ GPT-Load 采用双层配置架构:
152152

153153
- **系统设置**:存储在数据库中,为整个应用提供统一的行为基准
154154
- **分组配置**:为特定分组定制的行为参数,可覆盖系统设置
155-
- **配置优先级**:分组配置 > 系统设置
155+
- **配置优先级**:分组配置 > 系统设置 > 环境配置
156156
- **特点**:支持热重载,修改后立即生效,无需重启应用
157157

158158
<details>
@@ -238,6 +238,7 @@ GPT-Load 会自动从环境变量中读取代理设置,用于向上游 AI 服
238238
| 响应头超时 | `response_header_timeout` | 600 || 等待上游响应头超时(秒) |
239239
| 最大空闲连接数 | `max_idle_conns` | 100 || 连接池最大空闲连接总数 |
240240
| 每主机最大空闲连接数 | `max_idle_conns_per_host` | 50 || 每个上游主机最大空闲连接数 |
241+
| 代理服务器地址 | `proxy_url` | - || 用于转发请求的 HTTP/HTTPS 代理,为空则使用环境配置 |
241242

242243
**密钥配置:**
243244

README_EN.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ GPT-Load adopts a dual-layer configuration architecture:
152152

153153
- **System Settings**: Stored in database, providing unified behavioral standards for the entire application
154154
- **Group Configuration**: Behavior parameters customized for specific groups, can override system settings
155-
- **Configuration Priority**: Group Configuration > System Settings
155+
- **Configuration Priority**: Group Configuration > System Settings > Environment Configuration
156156
- **Characteristics**: Supports hot-reload, takes effect immediately after modification without application restart
157157

158158
<details>
@@ -238,6 +238,7 @@ Supported Proxy Protocol Formats:
238238
| Response Header Timeout | `response_header_timeout` | 600 || Timeout for waiting upstream response headers (seconds) |
239239
| Max Idle Connections | `max_idle_conns` | 100 || Connection pool maximum total idle connections |
240240
| Max Idle Connections Per Host | `max_idle_conns_per_host` | 50 || Maximum idle connections per upstream host |
241+
| Proxy URL | `proxy_url` | - || HTTP/HTTPS proxy for forwarding requests, uses environment if empty |
241242

242243
**Key Configuration:**
243244

internal/channel/factory.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ func (f *Factory) newBaseChannel(name string, group *models.Group) (*BaseChannel
117117
MaxIdleConns: group.EffectiveConfig.MaxIdleConns,
118118
MaxIdleConnsPerHost: group.EffectiveConfig.MaxIdleConnsPerHost,
119119
ResponseHeaderTimeout: time.Duration(group.EffectiveConfig.ResponseHeaderTimeout) * time.Second,
120+
ProxyURL: group.EffectiveConfig.ProxyURL,
120121
DisableCompression: false,
121122
WriteBufferSize: 32 * 1024,
122123
ReadBufferSize: 32 * 1024,

internal/config/system_settings.go

Lines changed: 58 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ func (sm *SystemSettingsManager) ValidateSettings(settingsMap map[string]any) er
253253
jsonToField := make(map[string]reflect.StructField)
254254
for i := range t.NumField() {
255255
field := t.Field(i)
256-
jsonTag := field.Tag.Get("json")
256+
jsonTag := strings.Split(field.Tag.Get("json"), ",")[0]
257257
if jsonTag != "" {
258258
jsonToField[jsonTag] = field
259259
}
@@ -266,6 +266,7 @@ func (sm *SystemSettingsManager) ValidateSettings(settingsMap map[string]any) er
266266
}
267267

268268
validateTag := field.Tag.Get("validate")
269+
rules := strings.Split(validateTag, ",")
269270

270271
switch field.Type.Kind() {
271272
case reflect.Int:
@@ -278,21 +279,34 @@ func (sm *SystemSettingsManager) ValidateSettings(settingsMap map[string]any) er
278279
return fmt.Errorf("invalid value for %s: must be an integer", key)
279280
}
280281

281-
if strings.HasPrefix(validateTag, "min=") {
282-
minValStr := strings.TrimPrefix(validateTag, "min=")
283-
minVal, _ := strconv.Atoi(minValStr)
284-
if intVal < minVal {
285-
return fmt.Errorf("value for %s (%d) is below minimum value (%d)", key, intVal, minVal)
282+
// The 'required' check is implicitly handled by the type assertion above.
283+
for _, rule := range rules {
284+
trimmedRule := strings.TrimSpace(rule)
285+
if strings.HasPrefix(trimmedRule, "min=") {
286+
minValStr := strings.TrimPrefix(trimmedRule, "min=")
287+
minVal, _ := strconv.Atoi(minValStr)
288+
if intVal < minVal {
289+
return fmt.Errorf("value for %s (%d) is below minimum value (%d)", key, intVal, minVal)
290+
}
286291
}
287292
}
288293
case reflect.Bool:
289294
if _, ok := value.(bool); !ok {
290295
return fmt.Errorf("invalid type for %s: expected a boolean, got %T", key, value)
291296
}
292297
case reflect.String:
293-
if _, ok := value.(string); !ok {
298+
strVal, ok := value.(string)
299+
if !ok {
294300
return fmt.Errorf("invalid type for %s: expected a string, got %T", key, value)
295301
}
302+
for _, rule := range rules {
303+
trimmedRule := strings.TrimSpace(rule)
304+
if trimmedRule == "required" {
305+
if strVal == "" {
306+
return fmt.Errorf("value for %s is required", key)
307+
}
308+
}
309+
}
296310
default:
297311
return fmt.Errorf("unsupported type for setting key validation: %s", key)
298312
}
@@ -309,7 +323,7 @@ func (sm *SystemSettingsManager) ValidateGroupConfigOverrides(configMap map[stri
309323
jsonToField := make(map[string]reflect.StructField)
310324
for i := range t.NumField() {
311325
field := t.Field(i)
312-
jsonTag := field.Tag.Get("json")
326+
jsonTag := strings.Split(field.Tag.Get("json"), ",")[0]
313327
if jsonTag != "" {
314328
jsonToField[jsonTag] = field
315329
}
@@ -326,22 +340,45 @@ func (sm *SystemSettingsManager) ValidateGroupConfigOverrides(configMap map[stri
326340
}
327341

328342
validateTag := field.Tag.Get("validate")
343+
rules := strings.Split(validateTag, ",")
329344

330-
floatVal, isFloat := value.(float64)
331-
if !isFloat {
332-
continue
333-
}
334-
intVal := int(floatVal)
335-
if floatVal != float64(intVal) {
336-
return fmt.Errorf("invalid value for %s: must be an integer", key)
337-
}
345+
switch field.Type.Kind() {
346+
case reflect.Int:
347+
floatVal, ok := value.(float64)
348+
if !ok {
349+
continue
350+
}
351+
intVal := int(floatVal)
352+
if floatVal != float64(intVal) {
353+
return fmt.Errorf("invalid value for %s: must be an integer", key)
354+
}
338355

339-
if strings.HasPrefix(validateTag, "min=") {
340-
minValStr := strings.TrimPrefix(validateTag, "min=")
341-
minVal, _ := strconv.Atoi(minValStr)
342-
if intVal < minVal {
343-
return fmt.Errorf("value for %s (%d) is below minimum value (%d)", key, intVal, minVal)
356+
// The 'required' check is implicitly handled by the type assertion above.
357+
for _, rule := range rules {
358+
trimmedRule := strings.TrimSpace(rule)
359+
if strings.HasPrefix(trimmedRule, "min=") {
360+
minValStr := strings.TrimPrefix(trimmedRule, "min=")
361+
minVal, _ := strconv.Atoi(minValStr)
362+
if intVal < minVal {
363+
return fmt.Errorf("value for %s (%d) is below minimum value (%d)", key, intVal, minVal)
364+
}
365+
}
344366
}
367+
case reflect.String:
368+
strVal, ok := value.(string)
369+
if !ok {
370+
continue
371+
}
372+
for _, rule := range rules {
373+
trimmedRule := strings.TrimSpace(rule)
374+
if trimmedRule == "required" {
375+
if strVal == "" {
376+
return fmt.Errorf("value for %s is required", key)
377+
}
378+
}
379+
}
380+
default:
381+
// Do not validate other types for group overrides
345382
}
346383
}
347384

internal/httpclient/manager.go

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@ import (
44
"fmt"
55
"net"
66
"net/http"
7+
"net/url"
78
"sync"
89
"time"
10+
11+
"github.com/sirupsen/logrus"
912
)
1013

1114
// Config defines the parameters for creating an HTTP client.
@@ -23,6 +26,7 @@ type Config struct {
2326
ForceAttemptHTTP2 bool
2427
TLSHandshakeTimeout time.Duration
2528
ExpectContinueTimeout time.Duration
29+
ProxyURL string
2630
}
2731

2832
// HTTPClientManager manages the lifecycle of HTTP clients.
@@ -65,7 +69,6 @@ func (m *HTTPClientManager) GetClient(config *Config) *http.Client {
6569

6670
// Create a new transport and client with the specified configuration.
6771
transport := &http.Transport{
68-
Proxy: http.ProxyFromEnvironment,
6972
DialContext: (&net.Dialer{
7073
Timeout: config.ConnectTimeout,
7174
KeepAlive: 30 * time.Second,
@@ -82,6 +85,19 @@ func (m *HTTPClientManager) GetClient(config *Config) *http.Client {
8285
ReadBufferSize: config.ReadBufferSize,
8386
}
8487

88+
// Set http proxy.
89+
if config.ProxyURL != "" {
90+
proxyURL, err := url.Parse(config.ProxyURL)
91+
if err != nil {
92+
logrus.Warnf("Invalid proxy URL '%s' provided, falling back to environment settings: %v", config.ProxyURL, err)
93+
transport.Proxy = http.ProxyFromEnvironment
94+
} else {
95+
transport.Proxy = http.ProxyURL(proxyURL)
96+
}
97+
} else {
98+
transport.Proxy = http.ProxyFromEnvironment
99+
}
100+
85101
newClient := &http.Client{
86102
Transport: transport,
87103
Timeout: config.RequestTimeout,
@@ -94,7 +110,7 @@ func (m *HTTPClientManager) GetClient(config *Config) *http.Client {
94110
// getFingerprint generates a unique string representation of the client configuration.
95111
func (c *Config) getFingerprint() string {
96112
return fmt.Sprintf(
97-
"ct:%.0fs|rt:%.0fs|it:%.0fs|mic:%d|mich:%d|rht:%.0fs|dc:%t|wbs:%d|rbs:%d|fh2:%t|tlst:%.0fs|ect:%.0fs",
113+
"ct:%.0fs|rt:%.0fs|it:%.0fs|mic:%d|mich:%d|rht:%.0fs|dc:%t|wbs:%d|rbs:%d|fh2:%t|tlst:%.0fs|ect:%.0fs|proxy:%s",
98114
c.ConnectTimeout.Seconds(),
99115
c.RequestTimeout.Seconds(),
100116
c.IdleConnTimeout.Seconds(),
@@ -107,5 +123,6 @@ func (c *Config) getFingerprint() string {
107123
c.ForceAttemptHTTP2,
108124
c.TLSHandshakeTimeout.Seconds(),
109125
c.ExpectContinueTimeout.Seconds(),
126+
c.ProxyURL,
110127
)
111128
}

internal/models/setting_info.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ type SystemSettingInfo struct {
1010
Description string `json:"description"`
1111
Category string `json:"category"`
1212
MinValue *int `json:"min_value,omitempty"`
13+
Required bool `json:"required"`
1314
}
1415

1516
// CategorizedSettings a list of settings grouped by category

internal/models/types.go

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,18 @@ type SystemSetting struct {
2525

2626
// GroupConfig 存储特定于分组的配置
2727
type GroupConfig struct {
28-
RequestTimeout *int `json:"request_timeout,omitempty"`
29-
IdleConnTimeout *int `json:"idle_conn_timeout,omitempty"`
30-
ConnectTimeout *int `json:"connect_timeout,omitempty"`
31-
MaxIdleConns *int `json:"max_idle_conns,omitempty"`
32-
MaxIdleConnsPerHost *int `json:"max_idle_conns_per_host,omitempty"`
33-
ResponseHeaderTimeout *int `json:"response_header_timeout,omitempty"`
34-
MaxRetries *int `json:"max_retries,omitempty"`
35-
BlacklistThreshold *int `json:"blacklist_threshold,omitempty"`
36-
KeyValidationIntervalMinutes *int `json:"key_validation_interval_minutes,omitempty"`
37-
KeyValidationConcurrency *int `json:"key_validation_concurrency,omitempty"`
38-
KeyValidationTimeoutSeconds *int `json:"key_validation_timeout_seconds,omitempty"`
28+
RequestTimeout *int `json:"request_timeout,omitempty"`
29+
IdleConnTimeout *int `json:"idle_conn_timeout,omitempty"`
30+
ConnectTimeout *int `json:"connect_timeout,omitempty"`
31+
MaxIdleConns *int `json:"max_idle_conns,omitempty"`
32+
MaxIdleConnsPerHost *int `json:"max_idle_conns_per_host,omitempty"`
33+
ResponseHeaderTimeout *int `json:"response_header_timeout,omitempty"`
34+
ProxyURL *string `json:"proxy_url,omitempty"`
35+
MaxRetries *int `json:"max_retries,omitempty"`
36+
BlacklistThreshold *int `json:"blacklist_threshold,omitempty"`
37+
KeyValidationIntervalMinutes *int `json:"key_validation_interval_minutes,omitempty"`
38+
KeyValidationConcurrency *int `json:"key_validation_concurrency,omitempty"`
39+
KeyValidationTimeoutSeconds *int `json:"key_validation_timeout_seconds,omitempty"`
3940
}
4041

4142
// Group 对应 groups 表

internal/types/types.go

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,25 +18,26 @@ type ConfigManager interface {
1818
// SystemSettings 定义所有系统配置项
1919
type SystemSettings struct {
2020
// 基础参数
21-
AppUrl string `json:"app_url" default:"http://localhost:3001" name:"项目地址" category:"基础参数" desc:"项目的基础 URL,用于拼接分组终端节点地址。系统配置优先于环境变量 APP_URL。"`
22-
RequestLogRetentionDays int `json:"request_log_retention_days" default:"7" name:"日志保留时长(天)" category:"基础参数" desc:"请求日志在数据库中的保留天数,0为不清理日志。" validate:"min=0"`
23-
RequestLogWriteIntervalMinutes int `json:"request_log_write_interval_minutes" default:"1" name:"日志延迟写入周期(分钟)" category:"基础参数" desc:"请求日志从缓存写入数据库的周期(分钟),0为实时写入数据。" validate:"min=0"`
24-
ProxyKeys string `json:"proxy_keys" name:"全局代理密钥" category:"基础参数" desc:"全局代理密钥,用于访问所有分组的代理端点。多个密钥请用逗号分隔。"`
21+
AppUrl string `json:"app_url" default:"http://localhost:3001" name:"项目地址" category:"基础参数" desc:"项目的基础 URL,用于拼接分组终端节点地址。系统配置优先于环境变量 APP_URL。" validate:"required"`
22+
RequestLogRetentionDays int `json:"request_log_retention_days" default:"7" name:"日志保留时长(天)" category:"基础参数" desc:"请求日志在数据库中的保留天数,0为不清理日志。" validate:"required,min=0"`
23+
RequestLogWriteIntervalMinutes int `json:"request_log_write_interval_minutes" default:"1" name:"日志延迟写入周期(分钟)" category:"基础参数" desc:"请求日志从缓存写入数据库的周期(分钟),0为实时写入数据。" validate:"required,min=0"`
24+
ProxyKeys string `json:"proxy_keys" name:"全局代理密钥" category:"基础参数" desc:"全局代理密钥,用于访问所有分组的代理端点。多个密钥请用逗号分隔。" validate:"required"`
2525

2626
// 请求设置
27-
RequestTimeout int `json:"request_timeout" default:"600" name:"请求超时(秒)" category:"请求设置" desc:"转发请求的完整生命周期超时(秒)等。" validate:"min=1"`
28-
ConnectTimeout int `json:"connect_timeout" default:"15" name:"连接超时(秒)" category:"请求设置" desc:"与上游服务建立新连接的超时时间(秒)。" validate:"min=1"`
29-
IdleConnTimeout int `json:"idle_conn_timeout" default:"120" name:"空闲连接超时(秒)" category:"请求设置" desc:"HTTP 客户端中空闲连接的超时时间(秒)。" validate:"min=1"`
30-
ResponseHeaderTimeout int `json:"response_header_timeout" default:"600" name:"响应头超时(秒)" category:"请求设置" desc:"等待上游服务响应头的最长时间(秒)。" validate:"min=1"`
31-
MaxIdleConns int `json:"max_idle_conns" default:"100" name:"最大空闲连接数" category:"请求设置" desc:"HTTP 客户端连接池中允许的最大空闲连接总数。" validate:"min=1"`
32-
MaxIdleConnsPerHost int `json:"max_idle_conns_per_host" default:"50" name:"每主机最大空闲连接数" category:"请求设置" desc:"HTTP 客户端连接池对每个上游主机允许的最大空闲连接数。" validate:"min=1"`
27+
RequestTimeout int `json:"request_timeout" default:"600" name:"请求超时(秒)" category:"请求设置" desc:"转发请求的完整生命周期超时(秒)等。" validate:"required,min=1"`
28+
ConnectTimeout int `json:"connect_timeout" default:"15" name:"连接超时(秒)" category:"请求设置" desc:"与上游服务建立新连接的超时时间(秒)。" validate:"required,min=1"`
29+
IdleConnTimeout int `json:"idle_conn_timeout" default:"120" name:"空闲连接超时(秒)" category:"请求设置" desc:"HTTP 客户端中空闲连接的超时时间(秒)。" validate:"required,min=1"`
30+
ResponseHeaderTimeout int `json:"response_header_timeout" default:"600" name:"响应头超时(秒)" category:"请求设置" desc:"等待上游服务响应头的最长时间(秒)。" validate:"required,min=1"`
31+
MaxIdleConns int `json:"max_idle_conns" default:"100" name:"最大空闲连接数" category:"请求设置" desc:"HTTP 客户端连接池中允许的最大空闲连接总数。" validate:"required,min=1"`
32+
MaxIdleConnsPerHost int `json:"max_idle_conns_per_host" default:"50" name:"每主机最大空闲连接数" category:"请求设置" desc:"HTTP 客户端连接池对每个上游主机允许的最大空闲连接数。" validate:"required,min=1"`
33+
ProxyURL string `json:"proxy_url" name:"代理服务器地址" category:"请求设置" desc:"全局 HTTP/HTTPS 代理服务器地址,例如:http://user:pass@host:port。如果为空,则使用环境变量配置。"`
3334

3435
// 密钥配置
35-
MaxRetries int `json:"max_retries" default:"3" name:"最大重试次数" category:"密钥配置" desc:"单个请求使用不同 Key 的最大重试次数,0为不重试。" validate:"min=0"`
36-
BlacklistThreshold int `json:"blacklist_threshold" default:"3" name:"黑名单阈值" category:"密钥配置" desc:"一个 Key 连续失败多少次后进入黑名单,0为不拉黑。" validate:"min=0"`
37-
KeyValidationIntervalMinutes int `json:"key_validation_interval_minutes" default:"60" name:"密钥验证间隔(分钟)" category:"密钥配置" desc:"后台验证密钥的默认间隔(分钟)。" validate:"min=30"`
38-
KeyValidationConcurrency int `json:"key_validation_concurrency" default:"10" name:"密钥验证并发数" category:"密钥配置" desc:"后台定时验证无效 Key 时的并发数,如果使用SQLite或者运行环境性能不佳,请尽量保证20以下,避免过高的并发导致数据不一致问题。" validate:"min=1"`
39-
KeyValidationTimeoutSeconds int `json:"key_validation_timeout_seconds" default:"20" name:"密钥验证超时(秒)" category:"密钥配置" desc:"后台定时验证单个 Key 时的 API 请求超时时间(秒)。" validate:"min=5"`
36+
MaxRetries int `json:"max_retries" default:"3" name:"最大重试次数" category:"密钥配置" desc:"单个请求使用不同 Key 的最大重试次数,0为不重试。" validate:"required,min=0"`
37+
BlacklistThreshold int `json:"blacklist_threshold" default:"3" name:"黑名单阈值" category:"密钥配置" desc:"一个 Key 连续失败多少次后进入黑名单,0为不拉黑。" validate:"required,min=0"`
38+
KeyValidationIntervalMinutes int `json:"key_validation_interval_minutes" default:"60" name:"密钥验证间隔(分钟)" category:"密钥配置" desc:"后台验证密钥的默认间隔(分钟)。" validate:"required,min=1"`
39+
KeyValidationConcurrency int `json:"key_validation_concurrency" default:"10" name:"密钥验证并发数" category:"密钥配置" desc:"后台定时验证无效 Key 时的并发数,如果使用SQLite或者运行环境性能不佳,请尽量保证20以下,避免过高的并发导致数据不一致问题。" validate:"required,min=1"`
40+
KeyValidationTimeoutSeconds int `json:"key_validation_timeout_seconds" default:"20" name:"密钥验证超时(秒)" category:"密钥配置" desc:"后台定时验证单个 Key 时的 API 请求超时时间(秒)。" validate:"required,min=1"`
4041

4142
// For cache
4243
ProxyKeysMap map[string]struct{} `json:"-"`

internal/utils/config_utils.go

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,18 @@ func GenerateSettingsMetadata(s *types.SystemSettings) []models.SystemSettingInf
3434
categoryTag := field.Tag.Get("category")
3535

3636
var minValue *int
37-
if strings.HasPrefix(validateTag, "min=") {
38-
valStr := strings.TrimPrefix(validateTag, "min=")
39-
if val, err := strconv.Atoi(valStr); err == nil {
40-
minValue = &val
37+
var required bool
38+
39+
rules := strings.Split(validateTag, ",")
40+
for _, rule := range rules {
41+
rule = strings.TrimSpace(rule)
42+
if rule == "required" {
43+
required = true
44+
} else if strings.HasPrefix(rule, "min=") {
45+
valStr := strings.TrimPrefix(rule, "min=")
46+
if val, err := strconv.Atoi(valStr); err == nil {
47+
minValue = &val
48+
}
4149
}
4250
}
4351

@@ -50,6 +58,7 @@ func GenerateSettingsMetadata(s *types.SystemSettings) []models.SystemSettingInf
5058
Description: descTag,
5159
Category: categoryTag,
5260
MinValue: minValue,
61+
Required: required,
5362
}
5463
settingsInfo = append(settingsInfo, info)
5564
}

web/src/api/settings.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export interface Setting {
77
type: "int" | "string";
88
min_value?: number;
99
description: string;
10+
required: boolean;
1011
}
1112

1213
export interface SettingCategory {

0 commit comments

Comments
 (0)