From 16be3f0a127aa73bbb80b2cc507054b533db5f2d Mon Sep 17 00:00:00 2001 From: hkfires <10558748+hkfires@users.noreply.github.com> Date: Fri, 31 Oct 2025 13:20:10 +0800 Subject: [PATCH] fix(config): dedupe and normalize Gemini keys and headers --- .../api/handlers/management/config_basic.go | 8 +- .../api/handlers/management/config_lists.go | 65 ++++++++++++-- internal/config/config.go | 85 ++++++++++--------- 3 files changed, 107 insertions(+), 51 deletions(-) diff --git a/internal/api/handlers/management/config_basic.go b/internal/api/handlers/management/config_basic.go index 0f052fbd..9a8c2923 100644 --- a/internal/api/handlers/management/config_basic.go +++ b/internal/api/handlers/management/config_basic.go @@ -12,7 +12,13 @@ import ( ) func (h *Handler) GetConfig(c *gin.Context) { - c.JSON(200, h.cfg) + if h == nil || h.cfg == nil { + c.JSON(200, gin.H{}) + return + } + cfgCopy := *h.cfg + cfgCopy.GlAPIKey = geminiKeyStringsFromConfig(h.cfg) + c.JSON(200, &cfgCopy) } func (h *Handler) GetConfigYAML(c *gin.Context) { diff --git a/internal/api/handlers/management/config_lists.go b/internal/api/handlers/management/config_lists.go index 530adacc..af48b14f 100644 --- a/internal/api/handlers/management/config_lists.go +++ b/internal/api/handlers/management/config_lists.go @@ -87,10 +87,10 @@ func (h *Handler) deleteFromStringList(c *gin.Context, target *[]string, after f return } } - if val := c.Query("value"); val != "" { + if val := strings.TrimSpace(c.Query("value")); val != "" { out := make([]string, 0, len(*target)) for _, v := range *target { - if v != val { + if strings.TrimSpace(v) != val { out = append(out, v) } } @@ -104,6 +104,53 @@ func (h *Handler) deleteFromStringList(c *gin.Context, target *[]string, after f c.JSON(400, gin.H{"error": "missing index or value"}) } +func sanitizeStringSlice(in []string) []string { + out := make([]string, 0, len(in)) + for i := range in { + if trimmed := strings.TrimSpace(in[i]); trimmed != "" { + out = append(out, trimmed) + } + } + return out +} + +func geminiKeyStringsFromConfig(cfg *config.Config) []string { + if cfg == nil || len(cfg.GeminiKey) == 0 { + return nil + } + out := make([]string, 0, len(cfg.GeminiKey)) + for i := range cfg.GeminiKey { + if key := strings.TrimSpace(cfg.GeminiKey[i].APIKey); key != "" { + out = append(out, key) + } + } + return out +} + +func (h *Handler) applyLegacyKeys(keys []string) { + if h == nil || h.cfg == nil { + return + } + sanitized := sanitizeStringSlice(keys) + existing := make(map[string]config.GeminiKey, len(h.cfg.GeminiKey)) + for _, entry := range h.cfg.GeminiKey { + if key := strings.TrimSpace(entry.APIKey); key != "" { + existing[key] = entry + } + } + newList := make([]config.GeminiKey, 0, len(sanitized)) + for _, key := range sanitized { + if entry, ok := existing[key]; ok { + newList = append(newList, entry) + } else { + newList = append(newList, config.GeminiKey{APIKey: key}) + } + } + h.cfg.GeminiKey = newList + h.cfg.GlAPIKey = sanitized + h.cfg.SyncGeminiKeys() +} + // api-keys func (h *Handler) GetAPIKeys(c *gin.Context) { c.JSON(200, gin.H{"api-keys": h.cfg.APIKeys}) } func (h *Handler) PutAPIKeys(c *gin.Context) { @@ -121,20 +168,20 @@ func (h *Handler) DeleteAPIKeys(c *gin.Context) { // generative-language-api-key func (h *Handler) GetGlKeys(c *gin.Context) { - c.JSON(200, gin.H{"generative-language-api-key": h.cfg.GlAPIKey}) + c.JSON(200, gin.H{"generative-language-api-key": geminiKeyStringsFromConfig(h.cfg)}) } func (h *Handler) PutGlKeys(c *gin.Context) { h.putStringList(c, func(v []string) { - h.cfg.GlAPIKey = append([]string(nil), v...) - }, func() { - h.cfg.SyncGeminiKeys() - }) + h.applyLegacyKeys(v) + }, nil) } func (h *Handler) PatchGlKeys(c *gin.Context) { - h.patchStringList(c, &h.cfg.GlAPIKey, func() { h.cfg.SyncGeminiKeys() }) + target := append([]string(nil), geminiKeyStringsFromConfig(h.cfg)...) + h.patchStringList(c, &target, func() { h.applyLegacyKeys(target) }) } func (h *Handler) DeleteGlKeys(c *gin.Context) { - h.deleteFromStringList(c, &h.cfg.GlAPIKey, func() { h.cfg.SyncGeminiKeys() }) + target := append([]string(nil), geminiKeyStringsFromConfig(h.cfg)...) + h.deleteFromStringList(c, &target, func() { h.applyLegacyKeys(target) }) } // gemini-api-key: []GeminiKey diff --git a/internal/config/config.go b/internal/config/config.go index 1424788f..ee01c117 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -303,56 +303,40 @@ func (cfg *Config) SyncGeminiKeys() { return } - if len(cfg.GeminiKey) > 0 { - out := make([]GeminiKey, 0, len(cfg.GeminiKey)) - for i := range cfg.GeminiKey { - entry := cfg.GeminiKey[i] - entry.APIKey = strings.TrimSpace(entry.APIKey) - entry.BaseURL = strings.TrimSpace(entry.BaseURL) - entry.ProxyURL = strings.TrimSpace(entry.ProxyURL) - if entry.APIKey == "" { - continue - } - if len(entry.Headers) > 0 { - clean := make(map[string]string, len(entry.Headers)) - for hk, hv := range entry.Headers { - key := strings.TrimSpace(hk) - val := strings.TrimSpace(hv) - if key == "" || val == "" { - continue - } - clean[key] = val - } - if len(clean) == 0 { - entry.Headers = nil - } else { - entry.Headers = clean - } - } - out = append(out, entry) + seen := make(map[string]struct{}, len(cfg.GeminiKey)) + out := cfg.GeminiKey[:0] + for i := range cfg.GeminiKey { + entry := cfg.GeminiKey[i] + entry.APIKey = strings.TrimSpace(entry.APIKey) + if entry.APIKey == "" { + continue } - cfg.GeminiKey = out + entry.BaseURL = strings.TrimSpace(entry.BaseURL) + entry.ProxyURL = strings.TrimSpace(entry.ProxyURL) + entry.Headers = normalizeGeminiHeaders(entry.Headers) + if _, exists := seen[entry.APIKey]; exists { + continue + } + seen[entry.APIKey] = struct{}{} + out = append(out, entry) } + cfg.GeminiKey = out - if len(cfg.GeminiKey) == 0 && len(cfg.GlAPIKey) > 0 { - out := make([]GeminiKey, 0, len(cfg.GlAPIKey)) - for i := range cfg.GlAPIKey { - key := strings.TrimSpace(cfg.GlAPIKey[i]) + if len(cfg.GlAPIKey) > 0 { + for _, raw := range cfg.GlAPIKey { + key := strings.TrimSpace(raw) if key == "" { continue } - out = append(out, GeminiKey{APIKey: key}) + if _, exists := seen[key]; exists { + continue + } + cfg.GeminiKey = append(cfg.GeminiKey, GeminiKey{APIKey: key}) + seen[key] = struct{}{} } - cfg.GeminiKey = out } - cfg.GlAPIKey = cfg.GlAPIKey[:0] - if len(cfg.GeminiKey) > 0 { - cfg.GlAPIKey = make([]string, 0, len(cfg.GeminiKey)) - for i := range cfg.GeminiKey { - cfg.GlAPIKey = append(cfg.GlAPIKey, cfg.GeminiKey[i].APIKey) - } - } + cfg.GlAPIKey = nil } func syncInlineAccessProvider(cfg *Config) { @@ -372,6 +356,25 @@ func looksLikeBcrypt(s string) bool { return len(s) > 4 && (s[:4] == "$2a$" || s[:4] == "$2b$" || s[:4] == "$2y$") } +func normalizeGeminiHeaders(headers map[string]string) map[string]string { + if len(headers) == 0 { + return nil + } + clean := make(map[string]string, len(headers)) + for k, v := range headers { + key := strings.TrimSpace(k) + val := strings.TrimSpace(v) + if key == "" || val == "" { + continue + } + clean[key] = val + } + if len(clean) == 0 { + return nil + } + return clean +} + // hashSecret hashes the given secret using bcrypt. func hashSecret(secret string) (string, error) { // Use default cost for simplicity.