init
This commit is contained in:
251
internal/config/config.go
Normal file
251
internal/config/config.go
Normal file
@@ -0,0 +1,251 @@
|
||||
// Package config 配置管理模块
|
||||
// @author OpenAI Proxy Team
|
||||
// @version 2.0.0
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Constants 配置常量
|
||||
type Constants struct {
|
||||
MinPort int
|
||||
MaxPort int
|
||||
MinTimeout int
|
||||
DefaultTimeout int
|
||||
DefaultMaxSockets int
|
||||
DefaultMaxFreeSockets int
|
||||
}
|
||||
|
||||
// DefaultConstants 默认常量
|
||||
var DefaultConstants = Constants{
|
||||
MinPort: 1,
|
||||
MaxPort: 65535,
|
||||
MinTimeout: 1000,
|
||||
DefaultTimeout: 30000,
|
||||
DefaultMaxSockets: 50,
|
||||
DefaultMaxFreeSockets: 10,
|
||||
}
|
||||
|
||||
// ServerConfig 服务器配置
|
||||
type ServerConfig struct {
|
||||
Port int `json:"port"`
|
||||
Host string `json:"host"`
|
||||
}
|
||||
|
||||
// KeysConfig 密钥管理配置
|
||||
type KeysConfig struct {
|
||||
FilePath string `json:"filePath"`
|
||||
StartIndex int `json:"startIndex"`
|
||||
BlacklistThreshold int `json:"blacklistThreshold"`
|
||||
}
|
||||
|
||||
// OpenAIConfig OpenAI API 配置
|
||||
type OpenAIConfig struct {
|
||||
BaseURL string `json:"baseURL"`
|
||||
Timeout int `json:"timeout"`
|
||||
}
|
||||
|
||||
// AuthConfig 认证配置
|
||||
type AuthConfig struct {
|
||||
Key string `json:"key"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
// CORSConfig CORS 配置
|
||||
type CORSConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
AllowedOrigins []string `json:"allowedOrigins"`
|
||||
}
|
||||
|
||||
// PerformanceConfig 性能配置
|
||||
type PerformanceConfig struct {
|
||||
MaxSockets int `json:"maxSockets"`
|
||||
MaxFreeSockets int `json:"maxFreeSockets"`
|
||||
}
|
||||
|
||||
// Config 应用配置
|
||||
type Config struct {
|
||||
Server ServerConfig `json:"server"`
|
||||
Keys KeysConfig `json:"keys"`
|
||||
OpenAI OpenAIConfig `json:"openai"`
|
||||
Auth AuthConfig `json:"auth"`
|
||||
CORS CORSConfig `json:"cors"`
|
||||
Performance PerformanceConfig `json:"performance"`
|
||||
}
|
||||
|
||||
// Global config instance
|
||||
var AppConfig *Config
|
||||
|
||||
// LoadConfig 加载配置
|
||||
func LoadConfig() (*Config, error) {
|
||||
// 尝试加载 .env 文件
|
||||
if err := godotenv.Load(); err != nil {
|
||||
logrus.Info("💡 提示: 创建 .env 文件以支持环境变量配置")
|
||||
}
|
||||
|
||||
config := &Config{
|
||||
Server: ServerConfig{
|
||||
Port: parseInteger(os.Getenv("PORT"), 3000),
|
||||
Host: getEnvOrDefault("HOST", "0.0.0.0"),
|
||||
},
|
||||
Keys: KeysConfig{
|
||||
FilePath: getEnvOrDefault("KEYS_FILE", "keys.txt"),
|
||||
StartIndex: parseInteger(os.Getenv("START_INDEX"), 0),
|
||||
BlacklistThreshold: parseInteger(os.Getenv("BLACKLIST_THRESHOLD"), 1),
|
||||
},
|
||||
OpenAI: OpenAIConfig{
|
||||
BaseURL: getEnvOrDefault("OPENAI_BASE_URL", "https://api.openai.com"),
|
||||
Timeout: parseInteger(os.Getenv("REQUEST_TIMEOUT"), DefaultConstants.DefaultTimeout),
|
||||
},
|
||||
Auth: AuthConfig{
|
||||
Key: os.Getenv("AUTH_KEY"),
|
||||
Enabled: os.Getenv("AUTH_KEY") != "",
|
||||
},
|
||||
CORS: CORSConfig{
|
||||
Enabled: parseBoolean(os.Getenv("ENABLE_CORS"), true),
|
||||
AllowedOrigins: parseArray(os.Getenv("ALLOWED_ORIGINS"), []string{"*"}),
|
||||
},
|
||||
Performance: PerformanceConfig{
|
||||
MaxSockets: parseInteger(os.Getenv("MAX_SOCKETS"), DefaultConstants.DefaultMaxSockets),
|
||||
MaxFreeSockets: parseInteger(os.Getenv("MAX_FREE_SOCKETS"), DefaultConstants.DefaultMaxFreeSockets),
|
||||
},
|
||||
}
|
||||
|
||||
// 验证配置
|
||||
if err := validateConfig(config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
AppConfig = config
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// validateConfig 验证配置有效性
|
||||
func validateConfig(config *Config) error {
|
||||
var errors []string
|
||||
|
||||
// 验证端口
|
||||
if config.Server.Port < DefaultConstants.MinPort || config.Server.Port > DefaultConstants.MaxPort {
|
||||
errors = append(errors, fmt.Sprintf("端口号必须在 %d-%d 之间", DefaultConstants.MinPort, DefaultConstants.MaxPort))
|
||||
}
|
||||
|
||||
// 验证起始索引
|
||||
if config.Keys.StartIndex < 0 {
|
||||
errors = append(errors, "起始索引不能小于 0")
|
||||
}
|
||||
|
||||
// 验证黑名单阈值
|
||||
if config.Keys.BlacklistThreshold < 1 {
|
||||
errors = append(errors, "黑名单阈值不能小于 1")
|
||||
}
|
||||
|
||||
// 验证超时时间
|
||||
if config.OpenAI.Timeout < DefaultConstants.MinTimeout {
|
||||
errors = append(errors, fmt.Sprintf("请求超时时间不能小于 %dms", DefaultConstants.MinTimeout))
|
||||
}
|
||||
|
||||
// 验证上游URL格式
|
||||
if _, err := url.Parse(config.OpenAI.BaseURL); err != nil {
|
||||
errors = append(errors, "上游API地址格式无效")
|
||||
}
|
||||
|
||||
// 验证性能配置
|
||||
if config.Performance.MaxSockets < 1 {
|
||||
errors = append(errors, "最大连接数不能小于 1")
|
||||
}
|
||||
|
||||
if config.Performance.MaxFreeSockets < 0 {
|
||||
errors = append(errors, "最大空闲连接数不能小于 0")
|
||||
}
|
||||
|
||||
if len(errors) > 0 {
|
||||
logrus.Error("❌ 配置验证失败:")
|
||||
for _, err := range errors {
|
||||
logrus.Errorf(" - %s", err)
|
||||
}
|
||||
return fmt.Errorf("配置验证失败")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DisplayConfig 显示当前配置信息
|
||||
func DisplayConfig(config *Config) {
|
||||
logrus.Info("⚙️ 当前配置:")
|
||||
logrus.Infof(" 服务器: %s:%d", config.Server.Host, config.Server.Port)
|
||||
logrus.Infof(" 密钥文件: %s", config.Keys.FilePath)
|
||||
logrus.Infof(" 起始索引: %d", config.Keys.StartIndex)
|
||||
logrus.Infof(" 黑名单阈值: %d 次错误", config.Keys.BlacklistThreshold)
|
||||
logrus.Infof(" 上游地址: %s", config.OpenAI.BaseURL)
|
||||
logrus.Infof(" 请求超时: %dms", config.OpenAI.Timeout)
|
||||
|
||||
authStatus := "未启用"
|
||||
if config.Auth.Enabled {
|
||||
authStatus = "已启用"
|
||||
}
|
||||
logrus.Infof(" 认证: %s", authStatus)
|
||||
|
||||
corsStatus := "已禁用"
|
||||
if config.CORS.Enabled {
|
||||
corsStatus = "已启用"
|
||||
}
|
||||
logrus.Infof(" CORS: %s", corsStatus)
|
||||
logrus.Infof(" 连接池: %d/%d", config.Performance.MaxSockets, config.Performance.MaxFreeSockets)
|
||||
}
|
||||
|
||||
// 辅助函数
|
||||
|
||||
// parseInteger 解析整数环境变量
|
||||
func parseInteger(value string, defaultValue int) int {
|
||||
if value == "" {
|
||||
return defaultValue
|
||||
}
|
||||
if parsed, err := strconv.Atoi(value); err == nil {
|
||||
return parsed
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// parseBoolean 解析布尔值环境变量
|
||||
func parseBoolean(value string, defaultValue bool) bool {
|
||||
if value == "" {
|
||||
return defaultValue
|
||||
}
|
||||
return strings.ToLower(value) == "true"
|
||||
}
|
||||
|
||||
// parseArray 解析数组环境变量(逗号分隔)
|
||||
func parseArray(value string, defaultValue []string) []string {
|
||||
if value == "" {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
parts := strings.Split(value, ",")
|
||||
result := make([]string, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
if trimmed := strings.TrimSpace(part); trimmed != "" {
|
||||
result = append(result, trimmed)
|
||||
}
|
||||
}
|
||||
|
||||
if len(result) == 0 {
|
||||
return defaultValue
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// getEnvOrDefault 获取环境变量或默认值
|
||||
func getEnvOrDefault(key, defaultValue string) string {
|
||||
if value := os.Getenv(key); value != "" {
|
||||
return value
|
||||
}
|
||||
return defaultValue
|
||||
}
|
397
internal/keymanager/keymanager.go
Normal file
397
internal/keymanager/keymanager.go
Normal file
@@ -0,0 +1,397 @@
|
||||
// Package keymanager 高性能密钥管理器
|
||||
// @author OpenAI Proxy Team
|
||||
// @version 2.0.0
|
||||
package keymanager
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"openai-multi-key-proxy/internal/config"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// KeyInfo 密钥信息
|
||||
type KeyInfo struct {
|
||||
Key string `json:"key"`
|
||||
Index int `json:"index"`
|
||||
Preview string `json:"preview"`
|
||||
}
|
||||
|
||||
// Stats 统计信息
|
||||
type Stats struct {
|
||||
CurrentIndex int64 `json:"currentIndex"`
|
||||
TotalKeys int `json:"totalKeys"`
|
||||
HealthyKeys int `json:"healthyKeys"`
|
||||
BlacklistedKeys int `json:"blacklistedKeys"`
|
||||
SuccessCount int64 `json:"successCount"`
|
||||
FailureCount int64 `json:"failureCount"`
|
||||
MemoryUsage MemoryUsage `json:"memoryUsage"`
|
||||
}
|
||||
|
||||
// MemoryUsage 内存使用情况
|
||||
type MemoryUsage struct {
|
||||
FailureCountsSize int `json:"failureCountsSize"`
|
||||
BlacklistSize int `json:"blacklistSize"`
|
||||
}
|
||||
|
||||
// BlacklistDetail 黑名单详情
|
||||
type BlacklistDetail struct {
|
||||
Index int `json:"index"`
|
||||
LineNumber int `json:"lineNumber"`
|
||||
KeyPreview string `json:"keyPreview"`
|
||||
FullKey string `json:"fullKey"`
|
||||
}
|
||||
|
||||
// BlacklistInfo 黑名单信息
|
||||
type BlacklistInfo struct {
|
||||
TotalBlacklisted int `json:"totalBlacklisted"`
|
||||
TotalKeys int `json:"totalKeys"`
|
||||
HealthyKeys int `json:"healthyKeys"`
|
||||
BlacklistedKeys []BlacklistDetail `json:"blacklistedKeys"`
|
||||
}
|
||||
|
||||
// KeyManager 密钥管理器
|
||||
type KeyManager struct {
|
||||
keysFilePath string
|
||||
keys []string
|
||||
keyPreviews []string
|
||||
currentIndex int64
|
||||
blacklistedKeys sync.Map
|
||||
successCount int64
|
||||
failureCount int64
|
||||
keyFailureCounts sync.Map
|
||||
|
||||
// 性能优化:预编译正则表达式
|
||||
permanentErrorPatterns []*regexp.Regexp
|
||||
|
||||
// 内存管理
|
||||
cleanupTicker *time.Ticker
|
||||
stopCleanup chan bool
|
||||
|
||||
// 读写锁保护密钥列表
|
||||
keysMutex sync.RWMutex
|
||||
}
|
||||
|
||||
// NewKeyManager 创建新的密钥管理器
|
||||
func NewKeyManager(keysFilePath string) *KeyManager {
|
||||
if keysFilePath == "" {
|
||||
keysFilePath = config.AppConfig.Keys.FilePath
|
||||
}
|
||||
|
||||
km := &KeyManager{
|
||||
keysFilePath: keysFilePath,
|
||||
currentIndex: int64(config.AppConfig.Keys.StartIndex),
|
||||
stopCleanup: make(chan bool),
|
||||
|
||||
// 预编译正则表达式
|
||||
permanentErrorPatterns: []*regexp.Regexp{
|
||||
regexp.MustCompile(`(?i)invalid api key`),
|
||||
regexp.MustCompile(`(?i)incorrect api key`),
|
||||
regexp.MustCompile(`(?i)api key not found`),
|
||||
regexp.MustCompile(`(?i)unauthorized`),
|
||||
regexp.MustCompile(`(?i)account deactivated`),
|
||||
regexp.MustCompile(`(?i)billing`),
|
||||
},
|
||||
}
|
||||
|
||||
// 启动内存清理
|
||||
km.setupMemoryCleanup()
|
||||
|
||||
return km
|
||||
}
|
||||
|
||||
// LoadKeys 加载密钥文件
|
||||
func (km *KeyManager) LoadKeys() error {
|
||||
file, err := os.Open(km.keysFilePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("无法打开密钥文件: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var keys []string
|
||||
scanner := bufio.NewScanner(file)
|
||||
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if line != "" && strings.HasPrefix(line, "sk-") {
|
||||
keys = append(keys, line)
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return fmt.Errorf("读取密钥文件失败: %w", err)
|
||||
}
|
||||
|
||||
if len(keys) == 0 {
|
||||
return fmt.Errorf("密钥文件中没有有效的API密钥")
|
||||
}
|
||||
|
||||
km.keysMutex.Lock()
|
||||
km.keys = keys
|
||||
// 预生成密钥预览,避免运行时重复计算
|
||||
km.keyPreviews = make([]string, len(keys))
|
||||
for i, key := range keys {
|
||||
if len(key) > 20 {
|
||||
km.keyPreviews[i] = key[:20] + "..."
|
||||
} else {
|
||||
km.keyPreviews[i] = key
|
||||
}
|
||||
}
|
||||
km.keysMutex.Unlock()
|
||||
|
||||
logrus.Infof("✅ 成功加载 %d 个 API 密钥", len(keys))
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetNextKey 获取下一个可用的密钥(高性能版本)
|
||||
func (km *KeyManager) GetNextKey() (*KeyInfo, error) {
|
||||
km.keysMutex.RLock()
|
||||
keysLen := len(km.keys)
|
||||
km.keysMutex.RUnlock()
|
||||
|
||||
if keysLen == 0 {
|
||||
return nil, fmt.Errorf("没有可用的 API 密钥")
|
||||
}
|
||||
|
||||
// 检查是否所有密钥都被拉黑
|
||||
blacklistedCount := 0
|
||||
km.blacklistedKeys.Range(func(key, value interface{}) bool {
|
||||
blacklistedCount++
|
||||
return true
|
||||
})
|
||||
|
||||
if blacklistedCount >= keysLen {
|
||||
logrus.Warn("⚠️ 所有密钥都被拉黑,重置黑名单")
|
||||
km.blacklistedKeys = sync.Map{}
|
||||
km.keyFailureCounts = sync.Map{}
|
||||
}
|
||||
|
||||
// 使用原子操作避免竞态条件
|
||||
attempts := 0
|
||||
for attempts < keysLen {
|
||||
currentIdx := atomic.AddInt64(&km.currentIndex, 1) - 1
|
||||
keyIndex := int(currentIdx) % keysLen
|
||||
|
||||
km.keysMutex.RLock()
|
||||
selectedKey := km.keys[keyIndex]
|
||||
keyPreview := km.keyPreviews[keyIndex]
|
||||
km.keysMutex.RUnlock()
|
||||
|
||||
if _, blacklisted := km.blacklistedKeys.Load(selectedKey); !blacklisted {
|
||||
return &KeyInfo{
|
||||
Key: selectedKey,
|
||||
Index: keyIndex,
|
||||
Preview: keyPreview,
|
||||
}, nil
|
||||
}
|
||||
|
||||
attempts++
|
||||
}
|
||||
|
||||
// 兜底:返回第一个密钥
|
||||
km.keysMutex.RLock()
|
||||
firstKey := km.keys[0]
|
||||
firstPreview := km.keyPreviews[0]
|
||||
km.keysMutex.RUnlock()
|
||||
|
||||
return &KeyInfo{
|
||||
Key: firstKey,
|
||||
Index: 0,
|
||||
Preview: firstPreview,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// RecordSuccess 记录密钥使用成功
|
||||
func (km *KeyManager) RecordSuccess(key string) {
|
||||
atomic.AddInt64(&km.successCount, 1)
|
||||
// 成功时重置该密钥的失败计数
|
||||
km.keyFailureCounts.Delete(key)
|
||||
}
|
||||
|
||||
// RecordFailure 记录密钥使用失败
|
||||
func (km *KeyManager) RecordFailure(key string, err error) {
|
||||
atomic.AddInt64(&km.failureCount, 1)
|
||||
|
||||
// 检查是否是永久性错误
|
||||
if km.isPermanentError(err) {
|
||||
km.blacklistedKeys.Store(key, true)
|
||||
km.keyFailureCounts.Delete(key) // 清理计数
|
||||
logrus.Warnf("🚫 密钥已被拉黑(永久性错误): %s (%s)", key[:20]+"...", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 临时性错误:增加失败计数
|
||||
currentFailures := 0
|
||||
if val, exists := km.keyFailureCounts.Load(key); exists {
|
||||
currentFailures = val.(int)
|
||||
}
|
||||
newFailures := currentFailures + 1
|
||||
km.keyFailureCounts.Store(key, newFailures)
|
||||
|
||||
threshold := config.AppConfig.Keys.BlacklistThreshold
|
||||
if newFailures >= threshold {
|
||||
km.blacklistedKeys.Store(key, true)
|
||||
km.keyFailureCounts.Delete(key) // 清理计数
|
||||
logrus.Warnf("🚫 密钥已被拉黑(达到阈值): %s (失败 %d 次: %s)", key[:20]+"...", newFailures, err.Error())
|
||||
} else {
|
||||
logrus.Warnf("⚠️ 密钥失败: %s (%d/%d 次: %s)", key[:20]+"...", newFailures, threshold, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// isPermanentError 判断是否是永久性错误
|
||||
func (km *KeyManager) isPermanentError(err error) bool {
|
||||
errorMessage := err.Error()
|
||||
for _, pattern := range km.permanentErrorPatterns {
|
||||
if pattern.MatchString(errorMessage) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GetStats 获取密钥统计信息
|
||||
func (km *KeyManager) GetStats() *Stats {
|
||||
km.keysMutex.RLock()
|
||||
totalKeys := len(km.keys)
|
||||
km.keysMutex.RUnlock()
|
||||
|
||||
blacklistedCount := 0
|
||||
km.blacklistedKeys.Range(func(key, value interface{}) bool {
|
||||
blacklistedCount++
|
||||
return true
|
||||
})
|
||||
|
||||
failureCountsSize := 0
|
||||
km.keyFailureCounts.Range(func(key, value interface{}) bool {
|
||||
failureCountsSize++
|
||||
return true
|
||||
})
|
||||
|
||||
return &Stats{
|
||||
CurrentIndex: atomic.LoadInt64(&km.currentIndex),
|
||||
TotalKeys: totalKeys,
|
||||
HealthyKeys: totalKeys - blacklistedCount,
|
||||
BlacklistedKeys: blacklistedCount,
|
||||
SuccessCount: atomic.LoadInt64(&km.successCount),
|
||||
FailureCount: atomic.LoadInt64(&km.failureCount),
|
||||
MemoryUsage: MemoryUsage{
|
||||
FailureCountsSize: failureCountsSize,
|
||||
BlacklistSize: blacklistedCount,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ResetKeys 重置密钥状态
|
||||
func (km *KeyManager) ResetKeys() map[string]interface{} {
|
||||
beforeCount := 0
|
||||
km.blacklistedKeys.Range(func(key, value interface{}) bool {
|
||||
beforeCount++
|
||||
return true
|
||||
})
|
||||
|
||||
km.blacklistedKeys = sync.Map{}
|
||||
km.keyFailureCounts = sync.Map{}
|
||||
|
||||
logrus.Infof("🔄 密钥状态已重置,清除了 %d 个黑名单密钥", beforeCount)
|
||||
|
||||
km.keysMutex.RLock()
|
||||
totalKeys := len(km.keys)
|
||||
km.keysMutex.RUnlock()
|
||||
|
||||
return map[string]interface{}{
|
||||
"success": true,
|
||||
"message": fmt.Sprintf("已清除 %d 个黑名单密钥", beforeCount),
|
||||
"clearedCount": beforeCount,
|
||||
"totalKeys": totalKeys,
|
||||
}
|
||||
}
|
||||
|
||||
// GetBlacklistDetails 获取黑名单详情
|
||||
func (km *KeyManager) GetBlacklistDetails() *BlacklistInfo {
|
||||
var blacklistDetails []BlacklistDetail
|
||||
|
||||
km.keysMutex.RLock()
|
||||
keys := km.keys
|
||||
keyPreviews := km.keyPreviews
|
||||
km.keysMutex.RUnlock()
|
||||
|
||||
for i, key := range keys {
|
||||
if _, blacklisted := km.blacklistedKeys.Load(key); blacklisted {
|
||||
blacklistDetails = append(blacklistDetails, BlacklistDetail{
|
||||
Index: i,
|
||||
LineNumber: i + 1,
|
||||
KeyPreview: keyPreviews[i],
|
||||
FullKey: key,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return &BlacklistInfo{
|
||||
TotalBlacklisted: len(blacklistDetails),
|
||||
TotalKeys: len(keys),
|
||||
HealthyKeys: len(keys) - len(blacklistDetails),
|
||||
BlacklistedKeys: blacklistDetails,
|
||||
}
|
||||
}
|
||||
|
||||
// setupMemoryCleanup 设置内存清理机制
|
||||
func (km *KeyManager) setupMemoryCleanup() {
|
||||
km.cleanupTicker = time.NewTicker(10 * time.Minute)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-km.cleanupTicker.C:
|
||||
km.performMemoryCleanup()
|
||||
case <-km.stopCleanup:
|
||||
km.cleanupTicker.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// performMemoryCleanup 执行内存清理
|
||||
func (km *KeyManager) performMemoryCleanup() {
|
||||
km.keysMutex.RLock()
|
||||
maxSize := len(km.keys) * 2
|
||||
if maxSize < 1000 {
|
||||
maxSize = 1000
|
||||
}
|
||||
km.keysMutex.RUnlock()
|
||||
|
||||
currentSize := 0
|
||||
km.keyFailureCounts.Range(func(key, value interface{}) bool {
|
||||
currentSize++
|
||||
return true
|
||||
})
|
||||
|
||||
if currentSize > maxSize {
|
||||
logrus.Infof("🧹 清理失败计数缓存 (%d -> %d)", currentSize, maxSize)
|
||||
|
||||
// 简单策略:清理一半的失败计数
|
||||
cleared := 0
|
||||
target := currentSize - maxSize
|
||||
|
||||
km.keyFailureCounts.Range(func(key, value interface{}) bool {
|
||||
if cleared < target {
|
||||
km.keyFailureCounts.Delete(key)
|
||||
cleared++
|
||||
}
|
||||
return cleared < target
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Close 关闭密钥管理器
|
||||
func (km *KeyManager) Close() {
|
||||
close(km.stopCleanup)
|
||||
}
|
423
internal/proxy/proxy.go
Normal file
423
internal/proxy/proxy.go
Normal file
@@ -0,0 +1,423 @@
|
||||
// Package proxy 高性能OpenAI多密钥代理服务器
|
||||
// @author OpenAI Proxy Team
|
||||
// @version 2.0.0
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"openai-multi-key-proxy/internal/config"
|
||||
"openai-multi-key-proxy/internal/keymanager"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// ProxyServer 代理服务器
|
||||
type ProxyServer struct {
|
||||
keyManager *keymanager.KeyManager
|
||||
httpClient *http.Client
|
||||
upstreamURL *url.URL
|
||||
requestCount int64
|
||||
startTime time.Time
|
||||
}
|
||||
|
||||
// NewProxyServer 创建新的代理服务器
|
||||
func NewProxyServer() (*ProxyServer, error) {
|
||||
// 解析上游URL
|
||||
upstreamURL, err := url.Parse(config.AppConfig.OpenAI.BaseURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解析上游URL失败: %w", err)
|
||||
}
|
||||
|
||||
// 创建密钥管理器
|
||||
keyManager := keymanager.NewKeyManager(config.AppConfig.Keys.FilePath)
|
||||
if err := keyManager.LoadKeys(); err != nil {
|
||||
return nil, fmt.Errorf("加载密钥失败: %w", err)
|
||||
}
|
||||
|
||||
// 创建高性能HTTP客户端
|
||||
transport := &http.Transport{
|
||||
MaxIdleConns: config.AppConfig.Performance.MaxSockets,
|
||||
MaxIdleConnsPerHost: config.AppConfig.Performance.MaxFreeSockets,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
DisableCompression: false,
|
||||
ForceAttemptHTTP2: true,
|
||||
}
|
||||
|
||||
httpClient := &http.Client{
|
||||
Transport: transport,
|
||||
Timeout: time.Duration(config.AppConfig.OpenAI.Timeout) * time.Millisecond,
|
||||
}
|
||||
|
||||
return &ProxyServer{
|
||||
keyManager: keyManager,
|
||||
httpClient: httpClient,
|
||||
upstreamURL: upstreamURL,
|
||||
startTime: time.Now(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SetupRoutes 设置路由
|
||||
func (ps *ProxyServer) SetupRoutes() *gin.Engine {
|
||||
// 设置Gin模式
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
|
||||
router := gin.New()
|
||||
|
||||
// 自定义日志中间件
|
||||
router.Use(ps.loggerMiddleware())
|
||||
|
||||
// 恢复中间件
|
||||
router.Use(gin.Recovery())
|
||||
|
||||
// CORS中间件
|
||||
if config.AppConfig.CORS.Enabled {
|
||||
router.Use(ps.corsMiddleware())
|
||||
}
|
||||
|
||||
// 认证中间件(如果启用)
|
||||
if config.AppConfig.Auth.Enabled {
|
||||
router.Use(ps.authMiddleware())
|
||||
}
|
||||
|
||||
// 管理端点
|
||||
router.GET("/health", ps.handleHealth)
|
||||
router.GET("/stats", ps.handleStats)
|
||||
router.GET("/blacklist", ps.handleBlacklist)
|
||||
router.GET("/reset-keys", ps.handleResetKeys)
|
||||
|
||||
// 代理所有其他请求
|
||||
router.NoRoute(ps.handleProxy)
|
||||
|
||||
return router
|
||||
}
|
||||
|
||||
// corsMiddleware CORS中间件
|
||||
func (ps *ProxyServer) corsMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
origin := "*"
|
||||
if len(config.AppConfig.CORS.AllowedOrigins) > 0 && config.AppConfig.CORS.AllowedOrigins[0] != "*" {
|
||||
origin = strings.Join(config.AppConfig.CORS.AllowedOrigins, ",")
|
||||
}
|
||||
|
||||
c.Header("Access-Control-Allow-Origin", origin)
|
||||
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
||||
c.Header("Access-Control-Max-Age", "86400")
|
||||
|
||||
if c.Request.Method == "OPTIONS" {
|
||||
c.AbortWithStatus(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// authMiddleware 认证中间件
|
||||
func (ps *ProxyServer) authMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// 管理端点不需要认证
|
||||
if strings.HasPrefix(c.Request.URL.Path, "/health") ||
|
||||
strings.HasPrefix(c.Request.URL.Path, "/stats") ||
|
||||
strings.HasPrefix(c.Request.URL.Path, "/blacklist") ||
|
||||
strings.HasPrefix(c.Request.URL.Path, "/reset-keys") {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"error": gin.H{
|
||||
"message": "未提供认证信息",
|
||||
"type": "authentication_error",
|
||||
"code": "missing_authorization",
|
||||
"timestamp": time.Now().Format(time.RFC3339),
|
||||
},
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(authHeader, "Bearer ") {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"error": gin.H{
|
||||
"message": "认证格式错误",
|
||||
"type": "authentication_error",
|
||||
"code": "invalid_authorization_format",
|
||||
"timestamp": time.Now().Format(time.RFC3339),
|
||||
},
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
token := authHeader[7:] // 移除 "Bearer " 前缀
|
||||
if token != config.AppConfig.Auth.Key {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"error": gin.H{
|
||||
"message": "认证失败",
|
||||
"type": "authentication_error",
|
||||
"code": "invalid_authorization",
|
||||
"timestamp": time.Now().Format(time.RFC3339),
|
||||
},
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// loggerMiddleware 自定义日志中间件
|
||||
func (ps *ProxyServer) loggerMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
start := time.Now()
|
||||
path := c.Request.URL.Path
|
||||
raw := c.Request.URL.RawQuery
|
||||
|
||||
// 处理请求
|
||||
c.Next()
|
||||
|
||||
// 计算响应时间
|
||||
latency := time.Since(start)
|
||||
|
||||
// 获取客户端IP
|
||||
clientIP := c.ClientIP()
|
||||
|
||||
// 获取方法和状态码
|
||||
method := c.Request.Method
|
||||
statusCode := c.Writer.Status()
|
||||
|
||||
// 构建完整路径
|
||||
if raw != "" {
|
||||
path = path + "?" + raw
|
||||
}
|
||||
|
||||
// 获取密钥信息(如果存在)
|
||||
keyInfo := ""
|
||||
if keyIndex, exists := c.Get("keyIndex"); exists {
|
||||
if keyPreview, exists := c.Get("keyPreview"); exists {
|
||||
keyInfo = fmt.Sprintf(" - Key[%v] %v", keyIndex, keyPreview)
|
||||
}
|
||||
}
|
||||
|
||||
// 根据状态码选择颜色
|
||||
var statusColor string
|
||||
if statusCode >= 200 && statusCode < 300 {
|
||||
statusColor = "\033[32m" // 绿色
|
||||
} else {
|
||||
statusColor = "\033[31m" // 红色
|
||||
}
|
||||
resetColor := "\033[0m"
|
||||
keyColor := "\033[36m" // 青色
|
||||
|
||||
// 输出日志
|
||||
logrus.Infof("%s[%s] %s %s%s%s%s - %s%d%s - %v - %s",
|
||||
statusColor, time.Now().Format(time.RFC3339), method, path, resetColor,
|
||||
keyColor, keyInfo, resetColor,
|
||||
statusColor, statusCode, resetColor,
|
||||
latency, clientIP)
|
||||
}
|
||||
}
|
||||
|
||||
// handleHealth 健康检查处理器
|
||||
func (ps *ProxyServer) handleHealth(c *gin.Context) {
|
||||
uptime := time.Since(ps.startTime)
|
||||
stats := ps.keyManager.GetStats()
|
||||
requestCount := atomic.LoadInt64(&ps.requestCount)
|
||||
|
||||
response := gin.H{
|
||||
"status": "healthy",
|
||||
"uptime": fmt.Sprintf("%.0fs", uptime.Seconds()),
|
||||
"requestCount": requestCount,
|
||||
"keysStatus": gin.H{
|
||||
"total": stats.TotalKeys,
|
||||
"healthy": stats.HealthyKeys,
|
||||
"blacklisted": stats.BlacklistedKeys,
|
||||
},
|
||||
"timestamp": time.Now().Format(time.RFC3339),
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// handleStats 统计信息处理器
|
||||
func (ps *ProxyServer) handleStats(c *gin.Context) {
|
||||
uptime := time.Since(ps.startTime)
|
||||
stats := ps.keyManager.GetStats()
|
||||
requestCount := atomic.LoadInt64(&ps.requestCount)
|
||||
|
||||
response := gin.H{
|
||||
"server": gin.H{
|
||||
"uptime": fmt.Sprintf("%.0fs", uptime.Seconds()),
|
||||
"requestCount": requestCount,
|
||||
"startTime": ps.startTime.Format(time.RFC3339),
|
||||
"version": "2.0.0",
|
||||
},
|
||||
"keys": stats,
|
||||
"timestamp": time.Now().Format(time.RFC3339),
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// handleBlacklist 黑名单处理器
|
||||
func (ps *ProxyServer) handleBlacklist(c *gin.Context) {
|
||||
blacklistInfo := ps.keyManager.GetBlacklistDetails()
|
||||
c.JSON(http.StatusOK, blacklistInfo)
|
||||
}
|
||||
|
||||
// handleResetKeys 重置密钥处理器
|
||||
func (ps *ProxyServer) handleResetKeys(c *gin.Context) {
|
||||
result := ps.keyManager.ResetKeys()
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// handleProxy 代理请求处理器
|
||||
func (ps *ProxyServer) handleProxy(c *gin.Context) {
|
||||
startTime := time.Now()
|
||||
|
||||
// 增加请求计数
|
||||
atomic.AddInt64(&ps.requestCount, 1)
|
||||
|
||||
// 获取密钥信息
|
||||
keyInfo, err := ps.keyManager.GetNextKey()
|
||||
if err != nil {
|
||||
logrus.Errorf("获取密钥失败: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": gin.H{
|
||||
"message": "服务器内部错误: " + err.Error(),
|
||||
"type": "server_error",
|
||||
"code": "no_keys_available",
|
||||
"timestamp": time.Now().Format(time.RFC3339),
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 设置密钥信息到上下文(用于日志)
|
||||
c.Set("keyIndex", keyInfo.Index)
|
||||
c.Set("keyPreview", keyInfo.Preview)
|
||||
|
||||
// 读取请求体
|
||||
var bodyBytes []byte
|
||||
if c.Request.Body != nil {
|
||||
bodyBytes, err = io.ReadAll(c.Request.Body)
|
||||
if err != nil {
|
||||
logrus.Errorf("读取请求体失败: %v", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": gin.H{
|
||||
"message": "读取请求体失败",
|
||||
"type": "request_error",
|
||||
"code": "invalid_request_body",
|
||||
"timestamp": time.Now().Format(time.RFC3339),
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 构建上游请求URL
|
||||
targetURL := *ps.upstreamURL
|
||||
targetURL.Path = c.Request.URL.Path
|
||||
targetURL.RawQuery = c.Request.URL.RawQuery
|
||||
|
||||
// 创建上游请求
|
||||
req, err := http.NewRequestWithContext(
|
||||
context.Background(),
|
||||
c.Request.Method,
|
||||
targetURL.String(),
|
||||
bytes.NewReader(bodyBytes),
|
||||
)
|
||||
if err != nil {
|
||||
logrus.Errorf("创建上游请求失败: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": gin.H{
|
||||
"message": "创建上游请求失败",
|
||||
"type": "proxy_error",
|
||||
"code": "request_creation_failed",
|
||||
"timestamp": time.Now().Format(time.RFC3339),
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 复制请求头
|
||||
for key, values := range c.Request.Header {
|
||||
if key != "Host" {
|
||||
for _, value := range values {
|
||||
req.Header.Add(key, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 设置认证头
|
||||
req.Header.Set("Authorization", "Bearer "+keyInfo.Key)
|
||||
|
||||
// 发送请求
|
||||
resp, err := ps.httpClient.Do(req)
|
||||
if err != nil {
|
||||
responseTime := time.Since(startTime)
|
||||
logrus.Errorf("代理请求失败: %v (响应时间: %v)", err, responseTime)
|
||||
|
||||
// 异步记录失败
|
||||
go ps.keyManager.RecordFailure(keyInfo.Key, err)
|
||||
|
||||
c.JSON(http.StatusBadGateway, gin.H{
|
||||
"error": gin.H{
|
||||
"message": "代理请求失败: " + err.Error(),
|
||||
"type": "proxy_error",
|
||||
"code": "request_failed",
|
||||
"timestamp": time.Now().Format(time.RFC3339),
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
responseTime := time.Since(startTime)
|
||||
|
||||
// 异步记录统计信息(不阻塞响应)
|
||||
go func() {
|
||||
if resp.StatusCode >= 200 && resp.StatusCode < 400 {
|
||||
ps.keyManager.RecordSuccess(keyInfo.Key)
|
||||
} else if resp.StatusCode >= 400 {
|
||||
ps.keyManager.RecordFailure(keyInfo.Key, fmt.Errorf("HTTP %d", resp.StatusCode))
|
||||
}
|
||||
}()
|
||||
|
||||
// 复制响应头
|
||||
for key, values := range resp.Header {
|
||||
for _, value := range values {
|
||||
c.Header(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
// 设置状态码
|
||||
c.Status(resp.StatusCode)
|
||||
|
||||
// 流式复制响应体(零拷贝)
|
||||
_, err = io.Copy(c.Writer, resp.Body)
|
||||
if err != nil {
|
||||
logrus.Errorf("复制响应体失败: %v (响应时间: %v)", err, responseTime)
|
||||
}
|
||||
}
|
||||
|
||||
// Close 关闭代理服务器
|
||||
func (ps *ProxyServer) Close() {
|
||||
if ps.keyManager != nil {
|
||||
ps.keyManager.Close()
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user