diff --git a/internal/config/types.go b/internal/config/types.go index d027127..8d91145 100644 --- a/internal/config/types.go +++ b/internal/config/types.go @@ -1,13 +1,13 @@ package config type Config struct { - Server ServerConfig `yaml:"server"` - Endpoints []EndpointConfig `yaml:"endpoints"` - Logging LoggingConfig `yaml:"logging"` - Validation ValidationConfig `yaml:"validation"` - Tagging TaggingConfig `yaml:"tagging"` // 标签系统配置(永远启用) - Timeouts TimeoutConfig `yaml:"timeouts"` // 超时配置 - I18n I18nConfig `yaml:"i18n"` // 国际化配置 + Server ServerConfig `yaml:"server"` + Endpoints []EndpointConfig `yaml:"endpoints"` + Logging LoggingConfig `yaml:"logging"` + Validation ValidationConfig `yaml:"validation"` + Tagging TaggingConfig `yaml:"tagging"` // 标签系统配置(永远启用) + Timeouts TimeoutConfig `yaml:"timeouts"` // 超时配置 + I18n I18nConfig `yaml:"i18n"` // 国际化配置 } // I18nConfig 国际化配置 @@ -23,43 +23,44 @@ type ServerConfig struct { } type EndpointConfig struct { - Name string `yaml:"name"` - URL string `yaml:"url"` - EndpointType string `yaml:"endpoint_type"` // "anthropic" | "openai" 等 - PathPrefix string `yaml:"path_prefix,omitempty"` // OpenAI端点的路径前缀,如 "/v1/chat/completions" - AuthType string `yaml:"auth_type"` - AuthValue string `yaml:"auth_value"` - Enabled bool `yaml:"enabled"` - Priority int `yaml:"priority"` - Tags []string `yaml:"tags"` // 新增:支持的tag列表 - ModelRewrite *ModelRewriteConfig `yaml:"model_rewrite,omitempty"` // 新增:模型重写配置 - Proxy *ProxyConfig `yaml:"proxy,omitempty"` // 新增:代理配置 - OAuthConfig *OAuthConfig `yaml:"oauth_config,omitempty"` // 新增:OAuth配置 - HeaderOverrides map[string]string `yaml:"header_overrides,omitempty" json:"header_overrides,omitempty"` // 新增:HTTP Header覆盖配置 - ParameterOverrides map[string]string `yaml:"parameter_overrides,omitempty" json:"parameter_overrides,omitempty"` // 新增:Request Parameters覆盖配置 - MaxTokensFieldName string `yaml:"max_tokens_field_name,omitempty" json:"max_tokens_field_name,omitempty"` // max_tokens 参数名转换选项 - RateLimitReset *int64 `yaml:"rate_limit_reset,omitempty" json:"rate_limit_reset,omitempty"` // Anthropic-Ratelimit-Unified-Reset - RateLimitStatus *string `yaml:"rate_limit_status,omitempty" json:"rate_limit_status,omitempty"` // Anthropic-Ratelimit-Unified-Status - EnhancedProtection bool `yaml:"enhanced_protection,omitempty" json:"enhanced_protection,omitempty"` // 官方帐号增强保护:allowed_warning时即禁用端点 + Name string `yaml:"name"` + URL string `yaml:"url"` + EndpointType string `yaml:"endpoint_type"` // "anthropic" | "openai" 等 + PathPrefix string `yaml:"path_prefix,omitempty"` // OpenAI端点的路径前缀,如 "/v1/chat/completions" + AuthType string `yaml:"auth_type"` + AuthValue string `yaml:"auth_value"` + Enabled bool `yaml:"enabled"` + Priority int `yaml:"priority"` + Tags []string `yaml:"tags"` // 新增:支持的tag列表 + ModelAlias string `yaml:"model_alias,omitempty" json:"model_alias,omitempty"` + ModelRewrite *ModelRewriteConfig `yaml:"model_rewrite,omitempty"` // 新增:模型重写配置 + Proxy *ProxyConfig `yaml:"proxy,omitempty"` // 新增:代理配置 + OAuthConfig *OAuthConfig `yaml:"oauth_config,omitempty"` // 新增:OAuth配置 + HeaderOverrides map[string]string `yaml:"header_overrides,omitempty" json:"header_overrides,omitempty"` // 新增:HTTP Header覆盖配置 + ParameterOverrides map[string]string `yaml:"parameter_overrides,omitempty" json:"parameter_overrides,omitempty"` // 新增:Request Parameters覆盖配置 + MaxTokensFieldName string `yaml:"max_tokens_field_name,omitempty" json:"max_tokens_field_name,omitempty"` // max_tokens 参数名转换选项 + RateLimitReset *int64 `yaml:"rate_limit_reset,omitempty" json:"rate_limit_reset,omitempty"` // Anthropic-Ratelimit-Unified-Reset + RateLimitStatus *string `yaml:"rate_limit_status,omitempty" json:"rate_limit_status,omitempty"` // Anthropic-Ratelimit-Unified-Status + EnhancedProtection bool `yaml:"enhanced_protection,omitempty" json:"enhanced_protection,omitempty"` // 官方帐号增强保护:allowed_warning时即禁用端点 } // 新增:代理配置结构 type ProxyConfig struct { - Type string `yaml:"type" json:"type"` // "http" | "socks5" - Address string `yaml:"address" json:"address"` // 代理服务器地址,如 "127.0.0.1:1080" + Type string `yaml:"type" json:"type"` // "http" | "socks5" + Address string `yaml:"address" json:"address"` // 代理服务器地址,如 "127.0.0.1:1080" Username string `yaml:"username,omitempty" json:"username,omitempty"` // 代理认证用户名(可选) Password string `yaml:"password,omitempty" json:"password,omitempty"` // 代理认证密码(可选) } // 新增:OAuth 配置结构 type OAuthConfig struct { - AccessToken string `yaml:"access_token" json:"access_token"` // 访问令牌 - RefreshToken string `yaml:"refresh_token" json:"refresh_token"` // 刷新令牌 - ExpiresAt int64 `yaml:"expires_at" json:"expires_at"` // 过期时间戳(毫秒) - TokenURL string `yaml:"token_url" json:"token_url"` // Token刷新URL(必填) - ClientID string `yaml:"client_id,omitempty" json:"client_id,omitempty"` // 客户端ID - Scopes []string `yaml:"scopes,omitempty" json:"scopes,omitempty"` // 权限范围 - AutoRefresh bool `yaml:"auto_refresh" json:"auto_refresh"` // 是否自动刷新 + AccessToken string `yaml:"access_token" json:"access_token"` // 访问令牌 + RefreshToken string `yaml:"refresh_token" json:"refresh_token"` // 刷新令牌 + ExpiresAt int64 `yaml:"expires_at" json:"expires_at"` // 过期时间戳(毫秒) + TokenURL string `yaml:"token_url" json:"token_url"` // Token刷新URL(必填) + ClientID string `yaml:"client_id,omitempty" json:"client_id,omitempty"` // 客户端ID + Scopes []string `yaml:"scopes,omitempty" json:"scopes,omitempty"` // 权限范围 + AutoRefresh bool `yaml:"auto_refresh" json:"auto_refresh"` // 是否自动刷新 } // 新增:模型重写配置结构 @@ -83,23 +84,23 @@ type LoggingConfig struct { } type ValidationConfig struct { - PythonJSONFixing PythonJSONFixingConfig `yaml:"python_json_fixing"` + PythonJSONFixing PythonJSONFixingConfig `yaml:"python_json_fixing"` } // PythonJSONFixing 配置结构 type PythonJSONFixingConfig struct { - Enabled bool `yaml:"enabled" json:"enabled"` // 是否启用 Python JSON 修复 - TargetTools []string `yaml:"target_tools" json:"target_tools"` // 需要修复的工具列表 - DebugLogging bool `yaml:"debug_logging" json:"debug_logging"` // 是否启用调试日志 - MaxAttempts int `yaml:"max_attempts" json:"max_attempts"` // 最大修复尝试次数 + Enabled bool `yaml:"enabled" json:"enabled"` // 是否启用 Python JSON 修复 + TargetTools []string `yaml:"target_tools" json:"target_tools"` // 需要修复的工具列表 + DebugLogging bool `yaml:"debug_logging" json:"debug_logging"` // 是否启用调试日志 + MaxAttempts int `yaml:"max_attempts" json:"max_attempts"` // 最大修复尝试次数 } // 新增:超时配置结构 type TimeoutConfig struct { // 网络超时设置(代理和健康检查共用) - TLSHandshake string `yaml:"tls_handshake" json:"tls_handshake"` // TLS握手超时,默认10s - ResponseHeader string `yaml:"response_header" json:"response_header"` // 响应头超时,默认60s - IdleConnection string `yaml:"idle_connection" json:"idle_connection"` // 空闲连接超时,默认90s + TLSHandshake string `yaml:"tls_handshake" json:"tls_handshake"` // TLS握手超时,默认10s + ResponseHeader string `yaml:"response_header" json:"response_header"` // 响应头超时,默认60s + IdleConnection string `yaml:"idle_connection" json:"idle_connection"` // 空闲连接超时,默认90s // 健康检查特有配置 HealthCheckTimeout string `yaml:"health_check_timeout" json:"health_check_timeout"` // 健康检查整体响应超时,默认30s CheckInterval string `yaml:"check_interval" json:"check_interval"` // 健康检查间隔,默认30s @@ -108,19 +109,19 @@ type TimeoutConfig struct { // 代理客户端超时配置(内部使用,从TimeoutConfig转换) type ProxyTimeoutConfig struct { - TLSHandshake string `yaml:"tls_handshake" json:"tls_handshake"` - ResponseHeader string `yaml:"response_header" json:"response_header"` - IdleConnection string `yaml:"idle_connection" json:"idle_connection"` - OverallRequest string `yaml:"overall_request" json:"overall_request"` // 保持为空,无限制 + TLSHandshake string `yaml:"tls_handshake" json:"tls_handshake"` + ResponseHeader string `yaml:"response_header" json:"response_header"` + IdleConnection string `yaml:"idle_connection" json:"idle_connection"` + OverallRequest string `yaml:"overall_request" json:"overall_request"` // 保持为空,无限制 } // 健康检查超时配置(内部使用,从TimeoutConfig转换) type HealthCheckTimeoutConfig struct { - TLSHandshake string `yaml:"tls_handshake" json:"tls_handshake"` - ResponseHeader string `yaml:"response_header" json:"response_header"` - IdleConnection string `yaml:"idle_connection" json:"idle_connection"` - OverallRequest string `yaml:"overall_request" json:"overall_request"` - CheckInterval string `yaml:"check_interval" json:"check_interval"` + TLSHandshake string `yaml:"tls_handshake" json:"tls_handshake"` + ResponseHeader string `yaml:"response_header" json:"response_header"` + IdleConnection string `yaml:"idle_connection" json:"idle_connection"` + OverallRequest string `yaml:"overall_request" json:"overall_request"` + CheckInterval string `yaml:"check_interval" json:"check_interval"` RecoveryThreshold int `yaml:"recovery_threshold" json:"recovery_threshold"` } @@ -148,8 +149,8 @@ func (tc *TimeoutConfig) ToHealthCheckTimeoutConfig() HealthCheckTimeoutConfig { // Tag系统配置结构 (永远启用) type TaggingConfig struct { - PipelineTimeout string `yaml:"pipeline_timeout"` - Taggers []TaggerConfig `yaml:"taggers"` + PipelineTimeout string `yaml:"pipeline_timeout"` + Taggers []TaggerConfig `yaml:"taggers"` } type TaggerConfig struct { @@ -158,6 +159,6 @@ type TaggerConfig struct { BuiltinType string `yaml:"builtin_type"` // 内置类型: "path" | "header" | "body-json" | "method" | "query" Tag string `yaml:"tag"` // 标记的tag名称 Enabled bool `yaml:"enabled"` - Priority int `yaml:"priority"` // 执行优先级(未使用,因为并发执行) - Config map[string]interface{} `yaml:"config"` // tagger特定配置 -} \ No newline at end of file + Priority int `yaml:"priority"` // 执行优先级(未使用,因为并发执行) + Config map[string]interface{} `yaml:"config"` // tagger特定配置 +} diff --git a/internal/endpoint/endpoint.go b/internal/endpoint/endpoint.go index d88105f..d6c0acd 100644 --- a/internal/endpoint/endpoint.go +++ b/internal/endpoint/endpoint.go @@ -27,10 +27,10 @@ const ( type BlacklistReason struct { // 导致失效的请求ID列表 CausingRequestIDs []string `json:"causing_request_ids"` - + // 失效时间 BlacklistedAt time.Time `json:"blacklisted_at"` - + // 失效时的错误信息摘要 ErrorSummary string `json:"error_summary"` } @@ -38,73 +38,75 @@ type BlacklistReason struct { // 删除不再需要的 RequestRecord 定义,因为已经移到 utils 包 type Endpoint struct { - ID string `json:"id"` - Name string `json:"name"` - URL string `json:"url"` - EndpointType string `json:"endpoint_type"` // "anthropic" | "openai" 等 - PathPrefix string `json:"path_prefix,omitempty"` // OpenAI端点的路径前缀 - AuthType string `json:"auth_type"` - AuthValue string `json:"auth_value"` - Enabled bool `json:"enabled"` - Priority int `json:"priority"` - Tags []string `json:"tags"` // 新增:支持的tag列表 - ModelRewrite *config.ModelRewriteConfig `json:"model_rewrite,omitempty"` // 新增:模型重写配置 - Proxy *config.ProxyConfig `json:"proxy,omitempty"` // 新增:代理配置 - OAuthConfig *config.OAuthConfig `json:"oauth_config,omitempty"` // 新增:OAuth配置 - HeaderOverrides map[string]string `json:"header_overrides,omitempty"` // 新增:HTTP Header覆盖配置 - ParameterOverrides map[string]string `json:"parameter_overrides,omitempty"` // 新增:Request Parameters覆盖配置 - MaxTokensFieldName string `json:"max_tokens_field_name,omitempty"` // max_tokens 参数名转换选项 - RateLimitReset *int64 `json:"rate_limit_reset,omitempty"` // Anthropic-Ratelimit-Unified-Reset - RateLimitStatus *string `json:"rate_limit_status,omitempty"` // Anthropic-Ratelimit-Unified-Status - EnhancedProtection bool `json:"enhanced_protection,omitempty"` // 官方帐号增强保护:allowed_warning时即禁用端点 - Status Status `json:"status"` - LastCheck time.Time `json:"last_check"` - FailureCount int `json:"failure_count"` - TotalRequests int `json:"total_requests"` - SuccessRequests int `json:"success_requests"` - LastFailure time.Time `json:"last_failure"` - SuccessiveSuccesses int `json:"successive_successes"` // 连续成功次数 - RequestHistory *utils.CircularBuffer `json:"-"` // 使用环形缓冲区,不导出到JSON - + ID string `json:"id"` + Name string `json:"name"` + URL string `json:"url"` + EndpointType string `json:"endpoint_type"` // "anthropic" | "openai" 等 + PathPrefix string `json:"path_prefix,omitempty"` // OpenAI端点的路径前缀 + AuthType string `json:"auth_type"` + AuthValue string `json:"auth_value"` + Enabled bool `json:"enabled"` + Priority int `json:"priority"` + Tags []string `json:"tags"` // 新增:支持的tag列表 + ModelAlias string `json:"model_alias,omitempty"` + ModelRewrite *config.ModelRewriteConfig `json:"model_rewrite,omitempty"` // 新增:模型重写配置 + Proxy *config.ProxyConfig `json:"proxy,omitempty"` // 新增:代理配置 + OAuthConfig *config.OAuthConfig `json:"oauth_config,omitempty"` // 新增:OAuth配置 + HeaderOverrides map[string]string `json:"header_overrides,omitempty"` // 新增:HTTP Header覆盖配置 + ParameterOverrides map[string]string `json:"parameter_overrides,omitempty"` // 新增:Request Parameters覆盖配置 + MaxTokensFieldName string `json:"max_tokens_field_name,omitempty"` // max_tokens 参数名转换选项 + RateLimitReset *int64 `json:"rate_limit_reset,omitempty"` // Anthropic-Ratelimit-Unified-Reset + RateLimitStatus *string `json:"rate_limit_status,omitempty"` // Anthropic-Ratelimit-Unified-Status + EnhancedProtection bool `json:"enhanced_protection,omitempty"` // 官方帐号增强保护:allowed_warning时即禁用端点 + Status Status `json:"status"` + LastCheck time.Time `json:"last_check"` + FailureCount int `json:"failure_count"` + TotalRequests int `json:"total_requests"` + SuccessRequests int `json:"success_requests"` + LastFailure time.Time `json:"last_failure"` + SuccessiveSuccesses int `json:"successive_successes"` // 连续成功次数 + RequestHistory *utils.CircularBuffer `json:"-"` // 使用环形缓冲区,不导出到JSON + // 新增:被拉黑的原因(内存中,不持久化) BlacklistReason *BlacklistReason `json:"-"` - + // 新增:保护 BlacklistReason 的互斥锁 blacklistMutex sync.RWMutex - + // 新增:上次记录跳过健康检查日志的时间(用于减少日志频率) lastSkipLogTime time.Time `json:"-"` - - mutex sync.RWMutex + + mutex sync.RWMutex } func NewEndpoint(cfg config.EndpointConfig) *Endpoint { // 如果没有指定 endpoint_type,使用统一默认值 endpointType := config.GetStringWithDefault(cfg.EndpointType, config.Default.Endpoint.Type) - + return &Endpoint{ - ID: generateID(cfg.Name), - Name: cfg.Name, - URL: cfg.URL, - EndpointType: endpointType, - PathPrefix: cfg.PathPrefix, // 新增:复制PathPrefix - AuthType: cfg.AuthType, - AuthValue: cfg.AuthValue, - Enabled: config.GetBoolWithDefault(cfg.Enabled, true, config.Default.Endpoint.Enabled), - Priority: config.GetIntWithDefault(cfg.Priority, config.Default.Endpoint.Priority), - Tags: cfg.Tags, // 新增:从配置中复制tags - ModelRewrite: cfg.ModelRewrite, // 新增:从配置中复制模型重写配置 - Proxy: cfg.Proxy, // 新增:从配置中复制代理配置 - OAuthConfig: cfg.OAuthConfig, // 新增:从配置中复制OAuth配置 - HeaderOverrides: cfg.HeaderOverrides, // 新增:从配置中复制HTTP Header覆盖配置 - ParameterOverrides: cfg.ParameterOverrides, // 新增:从配置中复制Request Parameters覆盖配置 - MaxTokensFieldName: cfg.MaxTokensFieldName, // 新增:从配置中复制max_tokens参数名转换选项 - RateLimitReset: cfg.RateLimitReset, // 新增:从配置加载rate limit reset状态 - RateLimitStatus: cfg.RateLimitStatus, // 新增:从配置加载rate limit status状态 - EnhancedProtection: cfg.EnhancedProtection, // 新增:从配置加载官方帐号增强保护设置 - Status: StatusActive, - LastCheck: time.Now(), - RequestHistory: utils.NewCircularBuffer(100, 140*time.Second), // 100个记录,140秒窗口 + ID: generateID(cfg.Name), + Name: cfg.Name, + URL: cfg.URL, + EndpointType: endpointType, + PathPrefix: cfg.PathPrefix, // 新增:复制PathPrefix + AuthType: cfg.AuthType, + AuthValue: cfg.AuthValue, + Enabled: config.GetBoolWithDefault(cfg.Enabled, true, config.Default.Endpoint.Enabled), + Priority: config.GetIntWithDefault(cfg.Priority, config.Default.Endpoint.Priority), + Tags: cfg.Tags, // 新增:从配置中复制tags + ModelAlias: cfg.ModelAlias, + ModelRewrite: cfg.ModelRewrite, // 新增:从配置中复制模型重写配置 + Proxy: cfg.Proxy, // 新增:从配置中复制代理配置 + OAuthConfig: cfg.OAuthConfig, // 新增:从配置中复制OAuth配置 + HeaderOverrides: cfg.HeaderOverrides, // 新增:从配置中复制HTTP Header覆盖配置 + ParameterOverrides: cfg.ParameterOverrides, // 新增:从配置中复制Request Parameters覆盖配置 + MaxTokensFieldName: cfg.MaxTokensFieldName, // 新增:从配置中复制max_tokens参数名转换选项 + RateLimitReset: cfg.RateLimitReset, // 新增:从配置加载rate limit reset状态 + RateLimitStatus: cfg.RateLimitStatus, // 新增:从配置加载rate limit status状态 + EnhancedProtection: cfg.EnhancedProtection, // 新增:从配置加载官方帐号增强保护设置 + Status: StatusActive, + LastCheck: time.Now(), + RequestHistory: utils.NewCircularBuffer(100, 140*time.Second), // 100个记录,140秒窗口 } } @@ -134,12 +136,12 @@ func (e *Endpoint) GetAuthHeader() (string, error) { if e.OAuthConfig == nil { return "", fmt.Errorf("oauth config is required for oauth auth_type") } - + // 检查 token 是否需要刷新 if oauth.IsTokenExpired(e.OAuthConfig) { return "", fmt.Errorf("oauth token expired, refresh required") } - + return oauth.GetAuthorizationHeader(e.OAuthConfig), nil default: return e.AuthValue, nil @@ -149,7 +151,7 @@ func (e *Endpoint) GetAuthHeader() (string, error) { func (e *Endpoint) GetTags() []string { e.mutex.RLock() defer e.mutex.RUnlock() - + // 返回tags的副本以避免并发修改 tags := make([]string, len(e.Tags)) copy(tags, e.Tags) @@ -160,11 +162,11 @@ func (e *Endpoint) GetTags() []string { func (e *Endpoint) GetHeaderOverrides() map[string]string { e.mutex.RLock() defer e.mutex.RUnlock() - + if e.HeaderOverrides == nil { return nil } - + // 返回HeaderOverrides的副本以避免并发修改 overrides := make(map[string]string, len(e.HeaderOverrides)) for k, v := range e.HeaderOverrides { @@ -177,11 +179,11 @@ func (e *Endpoint) GetHeaderOverrides() map[string]string { func (e *Endpoint) GetParameterOverrides() map[string]string { e.mutex.RLock() defer e.mutex.RUnlock() - + if e.ParameterOverrides == nil { return nil } - + // 返回ParameterOverrides的副本以避免并发修改 overrides := make(map[string]string, len(e.ParameterOverrides)) for k, v := range e.ParameterOverrides { @@ -194,10 +196,10 @@ func (e *Endpoint) GetParameterOverrides() map[string]string { func (e *Endpoint) ToTaggedEndpoint() interfaces.TaggedEndpoint { e.mutex.RLock() defer e.mutex.RUnlock() - + tags := make([]string, len(e.Tags)) copy(tags, e.Tags) - + return interfaces.TaggedEndpoint{ Name: e.Name, URL: e.URL, @@ -210,10 +212,10 @@ func (e *Endpoint) ToTaggedEndpoint() interfaces.TaggedEndpoint { func (e *Endpoint) GetFullURL(path string) string { e.mutex.RLock() defer e.mutex.RUnlock() - + // 直接使用端点的URL作为基础URL baseURL := e.URL - + // 根据端点类型自动添加正确的路径前缀 switch e.EndpointType { case "anthropic": @@ -234,7 +236,7 @@ func (e *Endpoint) IsAvailable() bool { enabled := e.Enabled status := e.Status e.mutex.RUnlock() - + return enabled && status == StatusActive } @@ -243,7 +245,7 @@ func (e *Endpoint) RecordRequest(success bool, requestID string) { defer e.mutex.Unlock() now := time.Now() - + // 添加到环形缓冲区(包含请求ID) record := utils.RequestRecord{ Timestamp: now, @@ -251,11 +253,11 @@ func (e *Endpoint) RecordRequest(success bool, requestID string) { RequestID: requestID, } e.RequestHistory.Add(record) - + e.TotalRequests++ if success { e.SuccessRequests++ - e.FailureCount = 0 // 重置失败计数 + e.FailureCount = 0 // 重置失败计数 e.SuccessiveSuccesses++ // 增加连续成功次数 // 如果成功且之前是不可用状态,恢复为可用 if e.Status == StatusInactive { @@ -268,7 +270,7 @@ func (e *Endpoint) RecordRequest(success bool, requestID string) { e.FailureCount++ e.LastFailure = now e.SuccessiveSuccesses = 0 // 重置连续成功次数 - + // 使用环形缓冲区检查是否应该标记为不可用 if e.Status == StatusActive && e.RequestHistory.ShouldMarkInactive(now) { // 释放 mutex 以避免死锁,因为 MarkInactiveWithReason 需要获取 mutex @@ -289,13 +291,13 @@ func (e *Endpoint) MarkInactive() { func (e *Endpoint) MarkInactiveWithReason() { e.mutex.Lock() defer e.mutex.Unlock() - + if e.Status == StatusActive { e.Status = StatusInactive - + // 从循环缓冲区获取导致失效的请求ID failedRequestIDs := e.RequestHistory.GetRecentFailureRequestIDs(time.Now()) - + // 构建失效原因记录 e.blacklistMutex.Lock() e.BlacklistReason = &BlacklistReason{ @@ -313,15 +315,15 @@ func (e *Endpoint) MarkActive() { e.Status = StatusActive e.FailureCount = 0 e.SuccessiveSuccesses = 0 // 重置连续成功次数 - + // 清除失效原因记录 e.blacklistMutex.Lock() e.BlacklistReason = nil e.blacklistMutex.Unlock() - + // 重置跳过健康检查日志时间,确保下次rate limit时能立即记录 e.lastSkipLogTime = time.Time{} - + // 清理历史记录 e.RequestHistory.Clear() } @@ -332,7 +334,6 @@ func (e *Endpoint) GetSuccessiveSuccesses() int { return e.SuccessiveSuccesses } - func generateID(name string) string { // Use stable ID based on endpoint name hash for statistics persistence return statistics.GenerateEndpointID(name) @@ -354,7 +355,7 @@ func (e *Endpoint) CreateProxyClient(timeoutConfig config.ProxyTimeoutConfig) (* e.mutex.RLock() proxyConfig := e.Proxy e.mutex.RUnlock() - + factory := httpclient.NewFactory() clientConfig := httpclient.ClientConfig{ Type: httpclient.ClientTypeEndpoint, @@ -366,7 +367,7 @@ func (e *Endpoint) CreateProxyClient(timeoutConfig config.ProxyTimeoutConfig) (* }, ProxyConfig: proxyConfig, } - + return factory.CreateClient(clientConfig) } @@ -375,7 +376,7 @@ func (e *Endpoint) CreateHealthClient(timeoutConfig config.HealthCheckTimeoutCon e.mutex.RLock() proxyConfig := e.Proxy e.mutex.RUnlock() - + factory := httpclient.NewFactory() clientConfig := httpclient.ClientConfig{ Type: httpclient.ClientTypeHealth, @@ -387,7 +388,7 @@ func (e *Endpoint) CreateHealthClient(timeoutConfig config.HealthCheckTimeoutCon }, ProxyConfig: proxyConfig, } - + return factory.CreateClient(clientConfig) } @@ -400,15 +401,15 @@ func (e *Endpoint) RefreshOAuthToken(timeoutConfig config.ProxyTimeoutConfig) er func (e *Endpoint) RefreshOAuthTokenWithCallback(timeoutConfig config.ProxyTimeoutConfig, onTokenRefreshed func(*Endpoint) error) error { e.mutex.Lock() defer e.mutex.Unlock() - + if e.AuthType != "oauth" { return fmt.Errorf("endpoint is not configured for oauth authentication") } - + if e.OAuthConfig == nil { return fmt.Errorf("oauth config is nil") } - + // 创建HTTP客户端用于刷新请求 factory := httpclient.NewFactory() clientConfig := httpclient.ClientConfig{ @@ -421,21 +422,21 @@ func (e *Endpoint) RefreshOAuthTokenWithCallback(timeoutConfig config.ProxyTimeo }, ProxyConfig: e.Proxy, } - + client, err := factory.CreateClient(clientConfig) if err != nil { return fmt.Errorf("failed to create http client for token refresh: %v", err) } - + // 刷新token newOAuthConfig, err := oauth.RefreshToken(e.OAuthConfig, client) if err != nil { return fmt.Errorf("failed to refresh oauth token: %v", err) } - + // 更新配置 e.OAuthConfig = newOAuthConfig - + // 如果提供了回调函数,调用它来处理配置持久化 if onTokenRefreshed != nil { if err := onTokenRefreshed(e); err != nil { @@ -443,7 +444,7 @@ func (e *Endpoint) RefreshOAuthTokenWithCallback(timeoutConfig config.ProxyTimeo return fmt.Errorf("oauth token refreshed successfully but failed to persist to config file: %v", err) } } - + return nil } @@ -456,7 +457,7 @@ func (e *Endpoint) GetAuthHeaderWithRefresh(timeoutConfig config.ProxyTimeoutCon func (e *Endpoint) GetAuthHeaderWithRefreshCallback(timeoutConfig config.ProxyTimeoutConfig, onTokenRefreshed func(*Endpoint) error) (string, error) { // 首先尝试获取认证头部 authHeader, err := e.GetAuthHeader() - + if e.AuthType == "oauth" { if err != nil { // 如果获取失败且token确实过期,尝试刷新 @@ -470,7 +471,7 @@ func (e *Endpoint) GetAuthHeaderWithRefreshCallback(timeoutConfig config.ProxyTi // 如果不是因为过期导致的错误,直接返回错误 return "", err } - + // 即使获取成功,也检查是否应该主动刷新 if oauth.ShouldRefreshToken(e.OAuthConfig) { // 主动刷新,但如果失败不影响当前请求 @@ -485,7 +486,7 @@ func (e *Endpoint) GetAuthHeaderWithRefreshCallback(timeoutConfig config.ProxyTi } } } - + return authHeader, err } @@ -493,11 +494,11 @@ func (e *Endpoint) GetAuthHeaderWithRefreshCallback(timeoutConfig config.ProxyTi func (e *Endpoint) GetBlacklistReason() *BlacklistReason { e.blacklistMutex.RLock() defer e.blacklistMutex.RUnlock() - + if e.BlacklistReason == nil { return nil } - + // 返回深度拷贝以避免并发修改 return &BlacklistReason{ CausingRequestIDs: append([]string{}, e.BlacklistReason.CausingRequestIDs...), @@ -510,30 +511,30 @@ func (e *Endpoint) GetBlacklistReason() *BlacklistReason { func (e *Endpoint) UpdateRateLimitState(reset *int64, status *string) (bool, error) { e.mutex.Lock() defer e.mutex.Unlock() - + // 检查是否有变化 changed := false - + // 比较reset值 if (e.RateLimitReset == nil) != (reset == nil) { changed = true } else if e.RateLimitReset != nil && reset != nil && *e.RateLimitReset != *reset { changed = true } - + // 比较status值 if (e.RateLimitStatus == nil) != (status == nil) { changed = true } else if e.RateLimitStatus != nil && status != nil && *e.RateLimitStatus != *status { changed = true } - + // 如果有变化,更新状态 if changed { e.RateLimitReset = reset e.RateLimitStatus = status } - + return changed, nil } @@ -541,20 +542,20 @@ func (e *Endpoint) UpdateRateLimitState(reset *int64, status *string) (bool, err func (e *Endpoint) GetRateLimitState() (*int64, *string) { e.mutex.RLock() defer e.mutex.RUnlock() - + var reset *int64 var status *string - + if e.RateLimitReset != nil { resetCopy := *e.RateLimitReset reset = &resetCopy } - + if e.RateLimitStatus != nil { statusCopy := *e.RateLimitStatus status = &statusCopy } - + return reset, status } @@ -574,17 +575,17 @@ func (e *Endpoint) ShouldMonitorRateLimit() bool { func (e *Endpoint) ShouldSkipHealthCheckUntilReset() bool { e.mutex.RLock() defer e.mutex.RUnlock() - + // 1. 必须是Anthropic官方端点 if !strings.Contains(strings.ToLower(e.URL), "api.anthropic.com") { return false } - + // 2. 必须有rate limit reset信息 if e.RateLimitReset == nil { return false } - + // 3. 当前时间必须小于reset时间 currentTime := time.Now().Unix() return currentTime < *e.RateLimitReset @@ -594,11 +595,11 @@ func (e *Endpoint) ShouldSkipHealthCheckUntilReset() bool { func (e *Endpoint) GetRateLimitResetTimeRemaining() int64 { e.mutex.RLock() defer e.mutex.RUnlock() - + if e.RateLimitReset == nil { return 0 } - + currentTime := time.Now().Unix() remaining := *e.RateLimitReset - currentTime if remaining < 0 { @@ -612,7 +613,7 @@ func (e *Endpoint) GetRateLimitResetTimeRemaining() int64 { func (e *Endpoint) ShouldLogSkipHealthCheck() bool { e.mutex.Lock() defer e.mutex.Unlock() - + now := time.Now() // 如果从未记录过,或者距离上次记录超过5分钟,则应该记录 if e.lastSkipLogTime.IsZero() || now.Sub(e.lastSkipLogTime) >= 5*time.Minute { @@ -630,21 +631,21 @@ func (e *Endpoint) ShouldLogSkipHealthCheck() bool { func (e *Endpoint) ShouldDisableOnAllowedWarning() bool { e.mutex.RLock() defer e.mutex.RUnlock() - + // 必须启用增强保护 if !e.EnhancedProtection { return false } - + // 必须是Anthropic官方端点 if !strings.Contains(strings.ToLower(e.URL), "api.anthropic.com") { return false } - + // 必须有rate limit status信息且为allowed_warning if e.RateLimitStatus == nil || *e.RateLimitStatus != "allowed_warning" { return false } - + return true -} \ No newline at end of file +} diff --git a/internal/web/endpoint_crud.go b/internal/web/endpoint_crud.go index 1172a43..fdf816c 100644 --- a/internal/web/endpoint_crud.go +++ b/internal/web/endpoint_crud.go @@ -6,8 +6,8 @@ import ( "net/url" "claude-code-companion/internal/config" - "claude-code-companion/internal/security" "claude-code-companion/internal/i18n" + "claude-code-companion/internal/security" "github.com/gin-gonic/gin" ) @@ -54,18 +54,19 @@ func (s *AdminServer) handleUpdateEndpoints(c *gin.Context) { // handleCreateEndpoint 创建新端点 func (s *AdminServer) handleCreateEndpoint(c *gin.Context) { var request struct { - Name string `json:"name" binding:"required"` - URL string `json:"url" binding:"required"` - EndpointType string `json:"endpoint_type"` // "anthropic" | "openai" - PathPrefix string `json:"path_prefix"` // OpenAI 端点的路径前缀 - AuthType string `json:"auth_type" binding:"required"` - AuthValue string `json:"auth_value"` // OAuth时不需要 - Enabled bool `json:"enabled"` - Tags []string `json:"tags"` - Proxy *config.ProxyConfig `json:"proxy,omitempty"` // 新增:代理配置 - OAuthConfig *config.OAuthConfig `json:"oauth_config,omitempty"` // 新增:OAuth配置 - HeaderOverrides map[string]string `json:"header_overrides,omitempty"` // 新增:HTTP Header覆盖配置 - ParameterOverrides map[string]string `json:"parameter_overrides,omitempty"` // 新增:Request Parameter覆盖配置 + Name string `json:"name" binding:"required"` + URL string `json:"url" binding:"required"` + EndpointType string `json:"endpoint_type"` // "anthropic" | "openai" + PathPrefix string `json:"path_prefix"` // OpenAI 端点的路径前缀 + AuthType string `json:"auth_type" binding:"required"` + AuthValue string `json:"auth_value"` // OAuth时不需要 + Enabled bool `json:"enabled"` + Tags []string `json:"tags"` + ModelAlias string `json:"model_alias"` + Proxy *config.ProxyConfig `json:"proxy,omitempty"` // 新增:代理配置 + OAuthConfig *config.OAuthConfig `json:"oauth_config,omitempty"` // 新增:OAuth配置 + HeaderOverrides map[string]string `json:"header_overrides,omitempty"` // 新增:HTTP Header覆盖配置 + ParameterOverrides map[string]string `json:"parameter_overrides,omitempty"` // 新增:Request Parameter覆盖配置 } if err := c.ShouldBindJSON(&request); err != nil { @@ -89,6 +90,13 @@ func (s *AdminServer) handleCreateEndpoint(c *gin.Context) { return } + if request.ModelAlias != "" { + if err := security.ValidateGenericText(request.ModelAlias, 100, i18n.TCtx(c, "model_alias", "模型简称")); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + } + if request.AuthValue != "" { if err := security.ValidateAuthToken(request.AuthValue); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": i18n.TCtx(c, "auth_token_validation_failed", "认证令牌验证失败: ") + err.Error()}) @@ -108,7 +116,7 @@ func (s *AdminServer) handleCreateEndpoint(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": "auth_type must be 'api_key', 'auth_token', or 'oauth'"}) return } - + // 验证 OAuth 或传统认证配置 if request.AuthType == "oauth" { if request.OAuthConfig == nil { @@ -150,10 +158,24 @@ func (s *AdminServer) handleCreateEndpoint(c *gin.Context) { } // 创建新端点配置 + if request.ModelAlias != "" { + // 自动添加同名标签 + exists := false + for _, t := range request.Tags { + if t == request.ModelAlias { + exists = true + break + } + } + if !exists { + request.Tags = append(request.Tags, request.ModelAlias) + } + } + newEndpoint := createEndpointConfigFromRequest( request.Name, request.URL, request.EndpointType, request.PathPrefix, - request.AuthType, request.AuthValue, - request.Enabled, maxPriority+1, request.Tags, request.Proxy, request.OAuthConfig, request.HeaderOverrides, request.ParameterOverrides) + request.AuthType, request.AuthValue, + request.Enabled, maxPriority+1, request.Tags, request.Proxy, request.OAuthConfig, request.HeaderOverrides, request.ParameterOverrides, request.ModelAlias) currentEndpoints = append(currentEndpoints, newEndpoint) // 使用热更新机制 @@ -180,18 +202,19 @@ func (s *AdminServer) handleUpdateEndpoint(c *gin.Context) { } var request struct { - Name string `json:"name"` - URL string `json:"url"` - EndpointType string `json:"endpoint_type"` - PathPrefix string `json:"path_prefix"` // OpenAI 端点的路径前缀 - AuthType string `json:"auth_type"` - AuthValue string `json:"auth_value"` - Enabled bool `json:"enabled"` - Tags []string `json:"tags"` - Proxy *config.ProxyConfig `json:"proxy,omitempty"` // 新增:代理配置 - OAuthConfig *config.OAuthConfig `json:"oauth_config,omitempty"` // 新增:OAuth配置 - HeaderOverrides map[string]string `json:"header_overrides,omitempty"` // 新增:HTTP Header覆盖配置 - ParameterOverrides map[string]string `json:"parameter_overrides,omitempty"` // 新增:Request Parameter覆盖配置 + Name string `json:"name"` + URL string `json:"url"` + EndpointType string `json:"endpoint_type"` + PathPrefix string `json:"path_prefix"` // OpenAI 端点的路径前缀 + AuthType string `json:"auth_type"` + AuthValue string `json:"auth_value"` + Enabled bool `json:"enabled"` + Tags []string `json:"tags"` + ModelAlias string `json:"model_alias"` + Proxy *config.ProxyConfig `json:"proxy,omitempty"` // 新增:代理配置 + OAuthConfig *config.OAuthConfig `json:"oauth_config,omitempty"` // 新增:OAuth配置 + HeaderOverrides map[string]string `json:"header_overrides,omitempty"` // 新增:HTTP Header覆盖配置 + ParameterOverrides map[string]string `json:"parameter_overrides,omitempty"` // 新增:Request Parameter覆盖配置 } if err := c.ShouldBindJSON(&request); err != nil { @@ -221,6 +244,13 @@ func (s *AdminServer) handleUpdateEndpoint(c *gin.Context) { } } + if request.ModelAlias != "" { + if err := security.ValidateGenericText(request.ModelAlias, 100, i18n.TCtx(c, "model_alias", "模型简称")); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + } + if request.AuthValue != "" { if err := security.ValidateAuthToken(request.AuthValue); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": i18n.TCtx(c, "auth_token_validation_failed", "认证令牌验证失败: ") + err.Error()}) @@ -266,7 +296,7 @@ func (s *AdminServer) handleUpdateEndpoint(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": "auth_type must be 'api_key', 'auth_token', or 'oauth'"}) return } - + // 验证 OAuth 或传统认证配置 if request.AuthType == "oauth" { if request.OAuthConfig == nil { @@ -278,23 +308,23 @@ func (s *AdminServer) handleUpdateEndpoint(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid oauth config: " + err.Error()}) return } - + // 检查内存中是否已有更新的 OAuth token(防止覆盖已刷新的token) if currentEndpoints[i].AuthType == "oauth" && currentEndpoints[i].OAuthConfig != nil { currentExpiresAt := currentEndpoints[i].OAuthConfig.ExpiresAt requestExpiresAt := request.OAuthConfig.ExpiresAt - + // 如果内存中的过期时间比 WebUI 发送的更大,说明后台已刷新token,拒绝更新 if currentExpiresAt > requestExpiresAt && requestExpiresAt > 0 { c.JSON(http.StatusConflict, gin.H{ - "error": "Cannot update OAuth config: token has been refreshed in background. Please reload the page to get the latest configuration.", + "error": "Cannot update OAuth config: token has been refreshed in background. Please reload the page to get the latest configuration.", "current_expires_at": currentExpiresAt, "request_expires_at": requestExpiresAt, }) return } } - + // 设置OAuth配置,清空auth_value currentEndpoints[i].OAuthConfig = request.OAuthConfig currentEndpoints[i].AuthValue = "" @@ -308,20 +338,36 @@ func (s *AdminServer) handleUpdateEndpoint(c *gin.Context) { currentEndpoints[i].AuthType = request.AuthType } currentEndpoints[i].Enabled = request.Enabled - + // 更新tags字段 currentEndpoints[i].Tags = request.Tags - + + // 自动添加同名标签 + if request.ModelAlias != "" { + exists := false + for _, t := range currentEndpoints[i].Tags { + if t == request.ModelAlias { + exists = true + break + } + } + if !exists { + currentEndpoints[i].Tags = append(currentEndpoints[i].Tags, request.ModelAlias) + } + } + + // 更新模型简称 + currentEndpoints[i].ModelAlias = request.ModelAlias + // 更新代理配置 currentEndpoints[i].Proxy = request.Proxy - - + // 更新HTTP Header覆盖配置 currentEndpoints[i].HeaderOverrides = request.HeaderOverrides - + // 更新Request Parameter覆盖配置 currentEndpoints[i].ParameterOverrides = request.ParameterOverrides - + found = true break } @@ -384,4 +430,4 @@ func (s *AdminServer) handleDeleteEndpoint(c *gin.Context) { } c.JSON(http.StatusOK, gin.H{"message": "Endpoint deleted successfully"}) -} \ No newline at end of file +} diff --git a/internal/web/endpoint_management.go b/internal/web/endpoint_management.go index a86bd17..ceec309 100644 --- a/internal/web/endpoint_management.go +++ b/internal/web/endpoint_management.go @@ -103,15 +103,16 @@ func (s *AdminServer) handleCopyEndpoint(c *gin.Context) { // 创建新端点(复制所有属性,除了名称和优先级) newEndpoint := config.EndpointConfig{ - Name: newName, - URL: sourceEndpoint.URL, - EndpointType: sourceEndpoint.EndpointType, - PathPrefix: sourceEndpoint.PathPrefix, - AuthType: sourceEndpoint.AuthType, - AuthValue: sourceEndpoint.AuthValue, - Enabled: sourceEndpoint.Enabled, - Priority: maxPriority + 1, - Tags: make([]string, len(sourceEndpoint.Tags)), // 复制tags + Name: newName, + URL: sourceEndpoint.URL, + EndpointType: sourceEndpoint.EndpointType, + PathPrefix: sourceEndpoint.PathPrefix, + AuthType: sourceEndpoint.AuthType, + AuthValue: sourceEndpoint.AuthValue, + Enabled: sourceEndpoint.Enabled, + Priority: maxPriority + 1, + Tags: make([]string, len(sourceEndpoint.Tags)), // 复制tags + ModelAlias: sourceEndpoint.ModelAlias, } // 深度复制Tags切片 @@ -202,7 +203,7 @@ func (s *AdminServer) handleReorderEndpoints(c *gin.Context) { // 获取当前所有端点 currentEndpoints := s.config.Endpoints - + // 创建按名称索引的map endpointMap := make(map[string]config.EndpointConfig) for _, ep := range currentEndpoints { @@ -233,4 +234,4 @@ func (s *AdminServer) handleReorderEndpoints(c *gin.Context) { } c.JSON(http.StatusOK, gin.H{"message": "Endpoints reordered successfully"}) -} \ No newline at end of file +} diff --git a/internal/web/endpoint_utils.go b/internal/web/endpoint_utils.go index 3661e3b..287e801 100644 --- a/internal/web/endpoint_utils.go +++ b/internal/web/endpoint_utils.go @@ -10,31 +10,32 @@ import ( func (s *AdminServer) saveEndpointsToConfig(endpointConfigs []config.EndpointConfig) error { // 更新配置 s.config.Endpoints = endpointConfigs - + // 保存到文件 return config.SaveConfig(s.config, s.configFilePath) } // createEndpointConfigFromRequest 从请求创建端点配置,自动设置优先级 -func createEndpointConfigFromRequest(name, url, endpointType, pathPrefix, authType, authValue string, enabled bool, priority int, tags []string, proxy *config.ProxyConfig, oauthConfig *config.OAuthConfig, headerOverrides map[string]string, parameterOverrides map[string]string) config.EndpointConfig { +func createEndpointConfigFromRequest(name, url, endpointType, pathPrefix, authType, authValue string, enabled bool, priority int, tags []string, proxy *config.ProxyConfig, oauthConfig *config.OAuthConfig, headerOverrides map[string]string, parameterOverrides map[string]string, modelAlias string) config.EndpointConfig { // 如果没有指定endpoint_type,默认为anthropic(向后兼容) if endpointType == "" { endpointType = "anthropic" } - + return config.EndpointConfig{ - Name: name, - URL: url, - EndpointType: endpointType, - PathPrefix: pathPrefix, // 新增:支持路径前缀 - AuthType: authType, - AuthValue: authValue, - Enabled: enabled, - Priority: priority, - Tags: tags, - Proxy: proxy, // 新增:支持代理配置 - OAuthConfig: oauthConfig, // 新增:支持OAuth配置 - HeaderOverrides: headerOverrides, // 新增:支持HTTP Header覆盖配置 + Name: name, + URL: url, + EndpointType: endpointType, + PathPrefix: pathPrefix, // 新增:支持路径前缀 + AuthType: authType, + AuthValue: authValue, + Enabled: enabled, + Priority: priority, + Tags: tags, + ModelAlias: modelAlias, + Proxy: proxy, // 新增:支持代理配置 + OAuthConfig: oauthConfig, // 新增:支持OAuth配置 + HeaderOverrides: headerOverrides, // 新增:支持HTTP Header覆盖配置 ParameterOverrides: parameterOverrides, // 新增:支持Request Parameter覆盖配置 } } @@ -42,7 +43,7 @@ func createEndpointConfigFromRequest(name, url, endpointType, pathPrefix, authTy // generateUniqueEndpointName 生成唯一的端点名称,如果存在重名则添加数字后缀 func (s *AdminServer) generateUniqueEndpointName(baseName string) string { currentEndpoints := s.config.Endpoints - + // 检查基础名称是否已存在 nameExists := func(name string) bool { for _, ep := range currentEndpoints { @@ -52,12 +53,12 @@ func (s *AdminServer) generateUniqueEndpointName(baseName string) string { } return false } - + // 如果基础名称不存在,直接返回 if !nameExists(baseName) { return baseName } - + // 如果存在,添加数字后缀 counter := 1 for { @@ -77,4 +78,4 @@ func generateEndpointNameWithSuffix(baseName string, counter int) string { // generateEndpointNameFormat 格式化端点名称 func generateEndpointNameFormat(baseName string, counter int) string { return fmt.Sprintf("%s (%d)", baseName, counter) -} \ No newline at end of file +} diff --git a/internal/web/endpoint_wizard.go b/internal/web/endpoint_wizard.go index ab8ea83..0224688 100644 --- a/internal/web/endpoint_wizard.go +++ b/internal/web/endpoint_wizard.go @@ -4,8 +4,8 @@ import ( "net/http" "claude-code-companion/internal/config" - "claude-code-companion/internal/security" "claude-code-companion/internal/i18n" + "claude-code-companion/internal/security" "github.com/gin-gonic/gin" ) @@ -33,6 +33,7 @@ type CreateFromWizardRequest struct { AuthValue string `json:"auth_value" binding:"required"` URL string `json:"url" binding:"required"` DefaultModel string `json:"default_model,omitempty"` + ModelAlias string `json:"model_alias,omitempty"` } // handleCreateEndpointFromWizard 从向导创建端点 @@ -87,6 +88,13 @@ func (s *AdminServer) handleCreateEndpointFromWizard(c *gin.Context) { } } + if request.ModelAlias != "" { + if err := security.ValidateGenericText(request.ModelAlias, 100, i18n.TCtx(c, "model_alias", "模型简称")); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + } + // 检查默认模型要求 if profile.RequireDefaultModel && request.DefaultModel == "" { c.JSON(http.StatusBadRequest, gin.H{ @@ -118,6 +126,10 @@ func (s *AdminServer) handleCreateEndpointFromWizard(c *gin.Context) { // 使用预设配置创建端点配置 newEndpoint := profile.ToEndpointConfig(request.Name, request.AuthValue, request.DefaultModel, request.URL) newEndpoint.Priority = maxPriority + 1 + newEndpoint.ModelAlias = request.ModelAlias + if request.ModelAlias != "" { + newEndpoint.Tags = append(newEndpoint.Tags, request.ModelAlias) + } // 添加到端点列表 updatedEndpoints := append(currentEndpoints, newEndpoint) @@ -182,10 +194,10 @@ func (s *AdminServer) endpointNameExists(name string, endpoints []config.Endpoin func (s *AdminServer) registerEndpointWizardRoutes(api *gin.RouterGroup) { // 获取端点预设配置列表 api.GET("/endpoint-profiles", s.handleGetEndpointProfiles) - + // 从向导创建端点 api.POST("/endpoints/from-wizard", s.handleCreateEndpointFromWizard) - + // 生成唯一端点名称 api.POST("/endpoints/generate-name", s.handleGenerateEndpointName) -} \ No newline at end of file +} diff --git a/web/static/endpoint-wizard.js b/web/static/endpoint-wizard.js index 5d22800..da193f7 100644 --- a/web/static/endpoint-wizard.js +++ b/web/static/endpoint-wizard.js @@ -137,9 +137,11 @@ class EndpointWizard { document.getElementById('auth-value-wizard').value = ''; document.getElementById('url-wizard').value = ''; document.getElementById('default-model-input-wizard').value = ''; - + document.getElementById('model-alias-wizard').value = ''; + // 隐藏默认模型组 document.getElementById('default-model-group-wizard').classList.add('d-none'); + document.getElementById('model-alias-group-wizard').classList.add('d-none'); } clearAlerts() { @@ -239,13 +241,24 @@ class EndpointWizard { const modelGroup = document.getElementById('default-model-group-wizard'); const modelInput = document.getElementById('default-model-input-wizard'); const datalist = document.getElementById('model-options'); + const aliasGroup = document.getElementById('model-alias-group-wizard'); + const aliasInput = document.getElementById('model-alias-wizard'); if (!this.selectedProfile.require_default_model) { modelGroup.classList.add('d-none'); + aliasGroup.classList.add('d-none'); + aliasInput.value = ''; return; } modelGroup.classList.remove('d-none'); + if (this.selectedProfile.endpoint_type === 'openai') { + aliasGroup.classList.remove('d-none'); + aliasInput.disabled = false; + } else { + aliasGroup.classList.add('d-none'); + aliasInput.value = ''; + } // 清空datalist datalist.innerHTML = ''; @@ -262,6 +275,7 @@ class EndpointWizard { // 清空输入框 modelInput.value = ''; + aliasInput.value = ''; } renderStep3() { @@ -284,6 +298,16 @@ class EndpointWizard { modelRow.classList.add('d-none'); } + // 处理模型简称行 + const modelAlias = document.getElementById('model-alias-wizard').value; + const aliasRow = document.getElementById('confirm-alias-row'); + if (modelAlias) { + document.getElementById('confirm-alias').textContent = modelAlias; + aliasRow.classList.remove('d-none'); + } else { + aliasRow.classList.add('d-none'); + } + // 处理路径前缀行 const pathRow = document.getElementById('confirm-path-row'); if (this.selectedProfile.path_prefix) { @@ -426,7 +450,8 @@ class EndpointWizard { name: document.getElementById('endpoint-name-wizard').value.trim(), auth_value: document.getElementById('auth-value-wizard').value.trim(), url: document.getElementById('url-wizard').value.trim(), - default_model: document.getElementById('default-model-input-wizard').value.trim() + default_model: document.getElementById('default-model-input-wizard').value.trim(), + model_alias: document.getElementById('model-alias-wizard').value.trim() }; const response = await apiRequest('/admin/api/endpoints/from-wizard', { diff --git a/web/static/endpoints-config.js b/web/static/endpoints-config.js index bb59fb0..f530113 100644 --- a/web/static/endpoints-config.js +++ b/web/static/endpoints-config.js @@ -89,6 +89,7 @@ function resetAuthVisibility() { function onEndpointTypeChange() { togglePathPrefixField(); toggleAuthTypeForEndpointType(); + toggleModelAliasField(); } function togglePathPrefixField() { @@ -152,6 +153,19 @@ function toggleAuthTypeForEndpointType() { onAuthTypeChange(); } +function toggleModelAliasField() { + const endpointType = document.getElementById('endpoint-type').value; + const aliasInput = document.getElementById('endpoint-model-alias'); + if (!aliasInput) return; + + if (endpointType === 'openai') { + aliasInput.disabled = false; + } else { + aliasInput.value = ''; + aliasInput.disabled = true; + } +} + function onAuthTypeChange() { const authType = document.getElementById('endpoint-auth-type').value; const authValueGroup = document.getElementById('auth-value-group'); diff --git a/web/static/endpoints-modal.js b/web/static/endpoints-modal.js index 62d3cfb..da88d3c 100644 --- a/web/static/endpoints-modal.js +++ b/web/static/endpoints-modal.js @@ -25,6 +25,9 @@ function showAddEndpointModal() { // Clear default model document.getElementById('endpoint-default-model').value = ''; + + // Clear model alias + document.getElementById('endpoint-model-alias').value = ''; // Clear header override configuration @@ -101,6 +104,9 @@ function showEditEndpointModal(endpointName) { // Load default model after loading model rewrite config loadDefaultModel(endpoint.model_rewrite); + + // Load model alias + document.getElementById('endpoint-model-alias').value = endpoint.model_alias || ''; // Load header override configuration @@ -193,9 +199,15 @@ function saveEndpoint() { } } + // Model alias + const modelAlias = document.getElementById('endpoint-model-alias').value.trim(); + // Parse tags field const tagsInput = document.getElementById('endpoint-tags').value.trim(); const tags = tagsInput ? tagsInput.split(',').map(tag => tag.trim()).filter(tag => tag) : []; + if (modelAlias && !tags.includes(modelAlias)) { + tags.push(modelAlias); + } const data = { name: document.getElementById('endpoint-name').value, @@ -206,6 +218,7 @@ function saveEndpoint() { auth_value: authValue, enabled: document.getElementById('endpoint-enabled').checked, tags: tags, + model_alias: modelAlias, max_tokens_field_name: document.getElementById('max-tokens-field-name').value || '', // New: max tokens field name proxy: collectProxyData(), // New: collect proxy configuration header_overrides: collectHeaderOverrideData(), // New: collect header override configuration diff --git a/web/templates/endpoint-modal.html b/web/templates/endpoint-modal.html index 646fe1a..9b07b6c 100644 --- a/web/templates/endpoint-modal.html +++ b/web/templates/endpoint-modal.html @@ -36,9 +36,9 @@
- +