From 0d4cb9e9fbfc97b08181751d859912ed107b50d3 Mon Sep 17 00:00:00 2001 From: hkfires <10558748+hkfires@users.noreply.github.com> Date: Thu, 25 Sep 2025 09:00:35 +0800 Subject: [PATCH 1/4] refactor(gemini-web): Improve client robustness and code reuse --- internal/provider/gemini-web/client.go | 59 +++++++++----------------- internal/provider/gemini-web/state.go | 3 ++ 2 files changed, 23 insertions(+), 39 deletions(-) diff --git a/internal/provider/gemini-web/client.go b/internal/provider/gemini-web/client.go index 396a9dc9..80185237 100644 --- a/internal/provider/gemini-web/client.go +++ b/internal/provider/gemini-web/client.go @@ -97,8 +97,12 @@ func getAccessToken(baseCookies map[string]string, proxy string, verbose bool, i { client := newHTTPClient(httpOptions{ProxyURL: proxy, Insecure: insecure, FollowRedirects: true}) req, _ := http.NewRequest(http.MethodGet, EndpointGoogle, nil) - resp, _ := client.Do(req) - if resp != nil { + resp, err := client.Do(req) + if err != nil { + if verbose { + log.Debugf("priming google cookies failed: %v", err) + } + } else if resp != nil { if u, err := url.Parse(EndpointGoogle); err == nil { for _, c := range client.Jar.Cookies(u) { extraCookies[c.Name] = c.Value @@ -172,18 +176,10 @@ func rotate1PSIDTS(cookies map[string]string, proxy string, insecure bool) (stri return "", &AuthError{Msg: "__Secure-1PSID missing"} } - tr := &http.Transport{} - if proxy != "" { - if pu, err := url.Parse(proxy); err == nil { - tr.Proxy = http.ProxyURL(pu) - } - } - if insecure { - tr.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} - } - client := &http.Client{Transport: tr, Timeout: 60 * time.Second} + // Reuse shared HTTP client helper for consistency. + client := newHTTPClient(httpOptions{ProxyURL: proxy, Insecure: insecure, FollowRedirects: true}) - req, _ := http.NewRequest(http.MethodPost, EndpointRotateCookies, io.NopCloser(stringsReader("[000,\"-0000000000000000000\"]"))) + req, _ := http.NewRequest(http.MethodPost, EndpointRotateCookies, strings.NewReader("[000,\"-0000000000000000000\"]")) applyHeaders(req, HeadersRotateCookies) applyCookies(req, cookies) @@ -207,25 +203,18 @@ func rotate1PSIDTS(cookies map[string]string, proxy string, insecure bool) (stri return c.Value, nil } } + // Fallback: check cookie jar in case the Set-Cookie was on a redirect hop + if u, err := url.Parse(EndpointRotateCookies); err == nil && client.Jar != nil { + for _, c := range client.Jar.Cookies(u) { + if c.Name == "__Secure-1PSIDTS" && c.Value != "" { + return c.Value, nil + } + } + } return "", nil } -type constReader struct { - s string - i int -} - -func (r *constReader) Read(p []byte) (int, error) { - if r.i >= len(r.s) { - return 0, io.EOF - } - n := copy(p, r.s[r.i:]) - r.i += n - return n, nil -} - -func stringsReader(s string) io.Reader { return &constReader{s: s} } - +// MaskToken28 masks a sensitive token for safe logging. Keep middle partially visible. func MaskToken28(s string) string { n := len(s) if n == 0 { @@ -432,16 +421,8 @@ func (c *GeminiClient) generateOnce(prompt string, files []string, model Model, req, _ := http.NewRequest(http.MethodPost, EndpointGenerate, strings.NewReader(form.Encode())) // headers - for k, v := range HeadersGemini { - for _, vv := range v { - req.Header.Add(k, vv) - } - } - for k, v := range model.ModelHeader { - for _, vv := range v { - req.Header.Add(k, vv) - } - } + applyHeaders(req, HeadersGemini) + applyHeaders(req, model.ModelHeader) req.Header.Set("Content-Type", "application/x-www-form-urlencoded;charset=utf-8") for k, v := range c.Cookies { req.AddCookie(&http.Cookie{Name: k, Value: v}) diff --git a/internal/provider/gemini-web/state.go b/internal/provider/gemini-web/state.go index 4442dad7..22bede00 100644 --- a/internal/provider/gemini-web/state.go +++ b/internal/provider/gemini-web/state.go @@ -21,6 +21,7 @@ import ( "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/translator" cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" + log "github.com/sirupsen/logrus" "github.com/tidwall/gjson" "github.com/tidwall/sjson" bolt "go.etcd.io/bbolt" @@ -158,6 +159,8 @@ func (s *GeminiWebState) Refresh(ctx context.Context) error { s.client.Cookies["__Secure-1PSIDTS"] = newTS } s.tokenMu.Unlock() + // Detailed debug log: provider and account. + log.Debugf("gemini web account %s rotated 1PSIDTS: %s", s.accountID, MaskToken28(newTS)) } s.lastRefresh = time.Now() return nil From 9810834f205aa5a30d18f148ad0bb0bbdff9878f Mon Sep 17 00:00:00 2001 From: hkfires <10558748+hkfires@users.noreply.github.com> Date: Thu, 25 Sep 2025 09:18:26 +0800 Subject: [PATCH 2/4] refactor(gemini-web): Centralize HTTP client creation for media --- internal/provider/gemini-web/media.go | 36 +++++---------------------- 1 file changed, 6 insertions(+), 30 deletions(-) diff --git a/internal/provider/gemini-web/media.go b/internal/provider/gemini-web/media.go index c21bc262..e9dcecde 100644 --- a/internal/provider/gemini-web/media.go +++ b/internal/provider/gemini-web/media.go @@ -2,7 +2,6 @@ package geminiwebapi import ( "bytes" - "crypto/tls" "encoding/base64" "encoding/json" "errors" @@ -11,8 +10,6 @@ import ( "math" "mime/multipart" "net/http" - "net/http/cookiejar" - "net/url" "os" "path/filepath" "regexp" @@ -69,18 +66,9 @@ func (i Image) Save(path string, filename string, cookies map[string]string, ver } } } - // Build client with cookie jar so cookies persist across redirects. - tr := &http.Transport{} - if i.Proxy != "" { - if pu, err := url.Parse(i.Proxy); err == nil { - tr.Proxy = http.ProxyURL(pu) - } - } - if insecure { - tr.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} - } - jar, _ := cookiejar.New(nil) - client := &http.Client{Transport: tr, Timeout: 120 * time.Second, Jar: jar} + // Build client using shared helper to keep proxy/TLS behavior consistent. + client := newHTTPClient(httpOptions{ProxyURL: i.Proxy, Insecure: insecure, FollowRedirects: true}) + client.Timeout = 120 * time.Second // Helper to set raw Cookie header using provided cookies (to mirror Python client behavior). buildCookieHeader := func(m map[string]string) string { @@ -352,23 +340,11 @@ func uploadFile(path string, proxy string, insecure bool) (string, error) { } _ = mw.Close() - tr := &http.Transport{} - if proxy != "" { - if pu, errParse := url.Parse(proxy); errParse == nil { - tr.Proxy = http.ProxyURL(pu) - } - } - if insecure { - tr.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} - } - client := &http.Client{Transport: tr, Timeout: 300 * time.Second} + client := newHTTPClient(httpOptions{ProxyURL: proxy, Insecure: insecure, FollowRedirects: true}) + client.Timeout = 300 * time.Second req, _ := http.NewRequest(http.MethodPost, EndpointUpload, &buf) - for k, v := range HeadersUpload { - for _, vv := range v { - req.Header.Add(k, vv) - } - } + applyHeaders(req, HeadersUpload) req.Header.Set("Content-Type", mw.FormDataContentType()) req.Header.Set("Accept", "*/*") req.Header.Set("Connection", "keep-alive") From 7f2e2fee56f5dcd3a9f813b8531c30efd270c7f7 Mon Sep 17 00:00:00 2001 From: hkfires <10558748+hkfires@users.noreply.github.com> Date: Thu, 25 Sep 2025 09:40:27 +0800 Subject: [PATCH 3/4] refactor(gemini-web): Consolidate conversation data into single BoltDB file --- internal/provider/gemini-web/client.go | 5 +--- internal/provider/gemini-web/state.go | 40 ++++++++------------------ 2 files changed, 13 insertions(+), 32 deletions(-) diff --git a/internal/provider/gemini-web/client.go b/internal/provider/gemini-web/client.go index 80185237..68a0d102 100644 --- a/internal/provider/gemini-web/client.go +++ b/internal/provider/gemini-web/client.go @@ -420,13 +420,10 @@ func (c *GeminiClient) generateOnce(prompt string, files []string, model Model, form.Set("f.req", string(outerJSON)) req, _ := http.NewRequest(http.MethodPost, EndpointGenerate, strings.NewReader(form.Encode())) - // headers applyHeaders(req, HeadersGemini) applyHeaders(req, model.ModelHeader) req.Header.Set("Content-Type", "application/x-www-form-urlencoded;charset=utf-8") - for k, v := range c.Cookies { - req.AddCookie(&http.Cookie{Name: k, Value: v}) - } + applyCookies(req, c.Cookies) resp, err := c.httpClient.Do(req) if err != nil { diff --git a/internal/provider/gemini-web/state.go b/internal/provider/gemini-web/state.go index 22bede00..c0f4d11c 100644 --- a/internal/provider/gemini-web/state.go +++ b/internal/provider/gemini-web/state.go @@ -82,12 +82,12 @@ func NewGeminiWebState(cfg *config.Config, token *gemini.GeminiWebTokenStorage, } func (s *GeminiWebState) loadConversationCaches() { - if path := s.convStorePath(); path != "" { + if path := s.convPath(); path != "" { if store, err := LoadConvStore(path); err == nil { s.convStore = store } } - if path := s.convDataPath(); path != "" { + if path := s.convPath(); path != "" { if items, index, err := LoadConvData(path); err == nil { s.convData = items s.convIndex = index @@ -95,20 +95,14 @@ func (s *GeminiWebState) loadConversationCaches() { } } -func (s *GeminiWebState) convStorePath() string { +// convPath returns the BoltDB file path used for both account metadata and conversation data. +func (s *GeminiWebState) convPath() string { base := s.storagePath if base == "" { - base = s.accountID + ".json" + // Use accountID directly as base name; ConvBoltPath will append .bolt. + base = s.accountID } - return ConvStorePath(base) -} - -func (s *GeminiWebState) convDataPath() string { - base := s.storagePath - if base == "" { - base = s.accountID + ".json" - } - return ConvDataPath(base) + return ConvBoltPath(base) } func (s *GeminiWebState) GetRequestMutex() *sync.Mutex { return &s.reqMu } @@ -392,7 +386,7 @@ func (s *GeminiWebState) persistConversation(modelName string, prep *geminiWebPr storeSnapshot[k] = cp } s.convMu.Unlock() - _ = SaveConvStore(s.convStorePath(), storeSnapshot) + _ = SaveConvStore(s.convPath(), storeSnapshot) } if !s.useReusableContext() { @@ -420,7 +414,7 @@ func (s *GeminiWebState) persistConversation(modelName string, prep *geminiWebPr indexSnapshot[k] = v } s.convMu.Unlock() - _ = SaveConvData(s.convDataPath(), dataSnapshot, indexSnapshot) + _ = SaveConvData(s.convPath(), dataSnapshot, indexSnapshot) } func (s *GeminiWebState) addAPIResponseData(ctx context.Context, line []byte) { @@ -557,19 +551,9 @@ func HashConversation(clientID, model string, msgs []StoredMessage) string { return Sha256Hex(b.String()) } -// ConvStorePath returns the path for account-level metadata persistence based on token file path. -func ConvStorePath(tokenFilePath string) string { - wd, err := os.Getwd() - if err != nil || wd == "" { - wd = "." - } - convDir := filepath.Join(wd, "conv") - base := strings.TrimSuffix(filepath.Base(tokenFilePath), filepath.Ext(tokenFilePath)) - return filepath.Join(convDir, base+".bolt") -} - -// ConvDataPath returns the path for full conversation persistence based on token file path. -func ConvDataPath(tokenFilePath string) string { +// ConvBoltPath returns the BoltDB file path used for both account metadata and conversation data. +// Different logical datasets are kept in separate buckets within this single DB file. +func ConvBoltPath(tokenFilePath string) string { wd, err := os.Getwd() if err != nil || wd == "" { wd = "." From 20f3e625297e41feb5f6e62e556ef5bd3a94dd7f Mon Sep 17 00:00:00 2001 From: hkfires <10558748+hkfires@users.noreply.github.com> Date: Thu, 25 Sep 2025 10:32:06 +0800 Subject: [PATCH 4/4] feat(auth): Add stable label to Gemini Web tokens for logging --- internal/api/handlers/management/auth_files.go | 2 ++ internal/auth/gemini/gemini-web_token.go | 14 ++++++++++++++ internal/cmd/gemini-web_auth.go | 4 ++++ sdk/cliproxy/auth/manager.go | 7 ++++--- sdk/cliproxy/auth/types.go | 15 +++++++-------- 5 files changed, 31 insertions(+), 11 deletions(-) diff --git a/internal/api/handlers/management/auth_files.go b/internal/api/handlers/management/auth_files.go index 5d0c750e..a95d487e 100644 --- a/internal/api/handlers/management/auth_files.go +++ b/internal/api/handlers/management/auth_files.go @@ -714,6 +714,8 @@ func (h *Handler) CreateGeminiWebToken(c *gin.Context) { Secure1PSID: payload.Secure1PSID, Secure1PSIDTS: payload.Secure1PSIDTS, } + // Provide a stable label (gemini-web-) for logging and identification + tokenStorage.Label = strings.TrimSuffix(fileName, ".json") record := &sdkAuth.TokenRecord{ Provider: "gemini-web", diff --git a/internal/auth/gemini/gemini-web_token.go b/internal/auth/gemini/gemini-web_token.go index c0f6c81e..1fc20e4e 100644 --- a/internal/auth/gemini/gemini-web_token.go +++ b/internal/auth/gemini/gemini-web_token.go @@ -8,6 +8,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "time" "github.com/router-for-me/CLIProxyAPI/v6/internal/misc" @@ -20,12 +21,25 @@ type GeminiWebTokenStorage struct { Secure1PSIDTS string `json:"secure_1psidts"` Type string `json:"type"` LastRefresh string `json:"last_refresh,omitempty"` + // Label is a stable account identifier used for logging, e.g. "gemini-web-". + // It is derived from the auth file name when not explicitly set. + Label string `json:"label,omitempty"` } // SaveTokenToFile serializes the Gemini Web token storage to a JSON file. func (ts *GeminiWebTokenStorage) SaveTokenToFile(authFilePath string) error { misc.LogSavingCredentials(authFilePath) ts.Type = "gemini-web" + // Auto-derive a stable label from the file name if missing. + if ts.Label == "" { + base := filepath.Base(authFilePath) + if strings.HasSuffix(strings.ToLower(base), ".json") { + base = strings.TrimSuffix(base, filepath.Ext(base)) + } + if base != "" { + ts.Label = base + } + } if ts.LastRefresh == "" { ts.LastRefresh = time.Now().Format(time.RFC3339) } diff --git a/internal/cmd/gemini-web_auth.go b/internal/cmd/gemini-web_auth.go index f312122f..f6f96914 100644 --- a/internal/cmd/gemini-web_auth.go +++ b/internal/cmd/gemini-web_auth.go @@ -49,6 +49,10 @@ func DoGeminiWebAuth(cfg *config.Config) { hasher.Write([]byte(secure1psid)) hash := hex.EncodeToString(hasher.Sum(nil)) fileName := fmt.Sprintf("gemini-web-%s.json", hash[:16]) + // Set a stable label for logging, e.g. gemini-web- + if tokenStorage != nil { + tokenStorage.Label = strings.TrimSuffix(fileName, ".json") + } record := &sdkAuth.TokenRecord{ Provider: "gemini-web", FileName: fileName, diff --git a/sdk/cliproxy/auth/manager.go b/sdk/cliproxy/auth/manager.go index 72584724..edda9273 100644 --- a/sdk/cliproxy/auth/manager.go +++ b/sdk/cliproxy/auth/manager.go @@ -286,7 +286,8 @@ func (m *Manager) executeWithProvider(ctx context.Context, provider string, req } else if accountType == "oauth" { log.Debugf("Use OAuth %s for model %s", accountInfo, req.Model) } else if accountType == "cookie" { - log.Debugf("Use Cookie %s for model %s", util.HideAPIKey(accountInfo), req.Model) + // Only Gemini Web uses cookie; print stable account label as-is. + log.Debugf("Use Cookie %s for model %s", accountInfo, req.Model) } tried[auth.ID] = struct{}{} @@ -333,7 +334,7 @@ func (m *Manager) executeCountWithProvider(ctx context.Context, provider string, } else if accountType == "oauth" { log.Debugf("Use OAuth %s for model %s", accountInfo, req.Model) } else if accountType == "cookie" { - log.Debugf("Use Cookie %s for model %s", util.HideAPIKey(accountInfo), req.Model) + log.Debugf("Use Cookie %s for model %s", accountInfo, req.Model) } tried[auth.ID] = struct{}{} @@ -380,7 +381,7 @@ func (m *Manager) executeStreamWithProvider(ctx context.Context, provider string } else if accountType == "oauth" { log.Debugf("Use OAuth %s for model %s", accountInfo, req.Model) } else if accountType == "cookie" { - log.Debugf("Use Cookie %s for model %s", util.HideAPIKey(accountInfo), req.Model) + log.Debugf("Use Cookie %s for model %s", accountInfo, req.Model) } tried[auth.ID] = struct{}{} diff --git a/sdk/cliproxy/auth/types.go b/sdk/cliproxy/auth/types.go index 492cc570..c3a56cfe 100644 --- a/sdk/cliproxy/auth/types.go +++ b/sdk/cliproxy/auth/types.go @@ -129,6 +129,13 @@ func (a *Auth) AccountInfo() (string, string) { return "", "" } if strings.ToLower(a.Provider) == "gemini-web" { + // Prefer explicit label written into auth file (e.g., gemini-web-) + if a.Metadata != nil { + if v, ok := a.Metadata["label"].(string); ok && strings.TrimSpace(v) != "" { + return "cookie", strings.TrimSpace(v) + } + } + // Minimal fallback to cookie value for backward compatibility if a.Metadata != nil { if v, ok := a.Metadata["secure_1psid"].(string); ok && v != "" { return "cookie", v @@ -137,14 +144,6 @@ func (a *Auth) AccountInfo() (string, string) { return "cookie", v } } - if a.Attributes != nil { - if v := a.Attributes["secure_1psid"]; v != "" { - return "cookie", v - } - if v := a.Attributes["api_key"]; v != "" { - return "cookie", v - } - } } if a.Metadata != nil { if v, ok := a.Metadata["email"].(string); ok {