From 7a30e65175afbaecd9690b7256592fd32cc07c40 Mon Sep 17 00:00:00 2001 From: hkfires <10558748+hkfires@users.noreply.github.com> Date: Thu, 25 Sep 2025 14:36:31 +0800 Subject: [PATCH 1/3] refactor(gemini-web): Remove file-based PSIDTS cookie caching --- internal/provider/gemini-web/client.go | 15 --------------- internal/provider/gemini-web/state.go | 18 +++++++++--------- 2 files changed, 9 insertions(+), 24 deletions(-) diff --git a/internal/provider/gemini-web/client.go b/internal/provider/gemini-web/client.go index 68a0d102..8dd28e3e 100644 --- a/internal/provider/gemini-web/client.go +++ b/internal/provider/gemini-web/client.go @@ -9,8 +9,6 @@ import ( "net/http" "net/http/cookiejar" "net/url" - "os" - "path/filepath" "regexp" "strings" "time" @@ -126,19 +124,6 @@ func getAccessToken(baseCookies map[string]string, proxy string, verbose bool, i } } - cacheDir := "temp" - _ = os.MkdirAll(cacheDir, 0o755) - if v1, ok1 := baseCookies["__Secure-1PSID"]; ok1 { - cacheFile := filepath.Join(cacheDir, ".cached_1psidts_"+v1+".txt") - if b, err := os.ReadFile(cacheFile); err == nil { - cv := strings.TrimSpace(string(b)) - if cv != "" { - merged := map[string]string{"__Secure-1PSID": v1, "__Secure-1PSIDTS": cv} - trySets = append(trySets, merged) - } - } - } - if len(extraCookies) > 0 { trySets = append(trySets, extraCookies) } diff --git a/internal/provider/gemini-web/state.go b/internal/provider/gemini-web/state.go index bb7cc58a..37828dcb 100644 --- a/internal/provider/gemini-web/state.go +++ b/internal/provider/gemini-web/state.go @@ -98,16 +98,16 @@ func (s *GeminiWebState) Label() string { } func (s *GeminiWebState) loadConversationCaches() { - if path := s.convPath(); path != "" { - if store, err := LoadConvStore(path); err == nil { - s.convStore = store - } + path := s.convPath() + if path == "" { + return } - if path := s.convPath(); path != "" { - if items, index, err := LoadConvData(path); err == nil { - s.convData = items - s.convIndex = index - } + if store, err := LoadConvStore(path); err == nil { + s.convStore = store + } + if items, index, err := LoadConvData(path); err == nil { + s.convData = items + s.convIndex = index } } From a45d2109f3bb68af39d9aeb300e04fe719d8d67e Mon Sep 17 00:00:00 2001 From: hkfires <10558748+hkfires@users.noreply.github.com> Date: Thu, 25 Sep 2025 20:17:47 +0800 Subject: [PATCH 2/3] feat(auth): Improve Gemini web auth with email label detection --- internal/cmd/gemini-web_auth.go | 109 ++++++++++++++++++++++++++------ 1 file changed, 91 insertions(+), 18 deletions(-) diff --git a/internal/cmd/gemini-web_auth.go b/internal/cmd/gemini-web_auth.go index f6f96914..80f227f7 100644 --- a/internal/cmd/gemini-web_auth.go +++ b/internal/cmd/gemini-web_auth.go @@ -6,42 +6,107 @@ import ( "context" "crypto/sha256" "encoding/hex" + "encoding/json" "fmt" + "net/http" "os" "strings" + "time" "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/gemini" "github.com/router-for-me/CLIProxyAPI/v6/internal/config" + "github.com/router-for-me/CLIProxyAPI/v6/internal/util" sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth" log "github.com/sirupsen/logrus" ) // DoGeminiWebAuth handles the process of creating a Gemini Web token file. -// It prompts the user for their cookie values and saves them to a JSON file. +// New flow: +// 1. Prompt user to paste the full cookie string. +// 2. Extract __Secure-1PSID and __Secure-1PSIDTS from the cookie string. +// 3. Call https://accounts.google.com/ListAccounts with the cookie to obtain email. +// 4. Save auth file with the same structure, and set Label to the email. func DoGeminiWebAuth(cfg *config.Config) { reader := bufio.NewReader(os.Stdin) - fmt.Print("Enter your __Secure-1PSID cookie value: ") - secure1psid, _ := reader.ReadString('\n') - secure1psid = strings.TrimSpace(secure1psid) - - if secure1psid == "" { - log.Fatal("The __Secure-1PSID value cannot be empty.") + fmt.Print("Paste your full Google Cookie and press Enter: ") + rawCookie, _ := reader.ReadString('\n') + rawCookie = strings.TrimSpace(rawCookie) + if rawCookie == "" { + log.Fatal("Cookie cannot be empty") return } - fmt.Print("Enter your __Secure-1PSIDTS cookie value: ") - secure1psidts, _ := reader.ReadString('\n') - secure1psidts = strings.TrimSpace(secure1psidts) + // Parse K=V cookie pairs separated by ';' + cookieMap := make(map[string]string) + parts := strings.Split(rawCookie, ";") + for _, p := range parts { + p = strings.TrimSpace(p) + if p == "" { + continue + } + if eq := strings.Index(p, "="); eq > 0 { + k := strings.TrimSpace(p[:eq]) + v := strings.TrimSpace(p[eq+1:]) + if k != "" { + cookieMap[k] = v + } + } + } - if secure1psidts == "" { - fmt.Println("The __Secure-1PSIDTS value cannot be empty.") + secure1psid := strings.TrimSpace(cookieMap["__Secure-1PSID"]) + secure1psidts := strings.TrimSpace(cookieMap["__Secure-1PSIDTS"]) + if secure1psid == "" || secure1psidts == "" { + fmt.Println("Cookie does not contain __Secure-1PSID or __Secure-1PSIDTS") return } - tokenStorage := &gemini.GeminiWebTokenStorage{ - Secure1PSID: secure1psid, - Secure1PSIDTS: secure1psidts, + // Build HTTP client with proxy settings respected. + httpClient := &http.Client{Timeout: 15 * time.Second} + httpClient = util.SetProxy(cfg, httpClient) + + // Request ListAccounts to extract email as label (use POST per upstream behavior). + req, err := http.NewRequest(http.MethodPost, "https://accounts.google.com/ListAccounts", nil) + if err != nil { + fmt.Printf("Failed to create request: %v\n", err) + return + } + req.Header.Set("Cookie", rawCookie) + req.Header.Set("Accept", "application/json, text/plain, */*") + req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36") + req.Header.Set("Origin", "https://accounts.google.com") + req.Header.Set("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8") + + resp, err := httpClient.Do(req) + if err != nil { + fmt.Printf("Request to ListAccounts failed: %v\n", err) + return + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + fmt.Printf("ListAccounts returned status code: %d\n", resp.StatusCode) + return + } + + var payload []any + if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil { + fmt.Printf("Failed to parse ListAccounts response: %v\n", err) + return + } + + // Expected structure like: ["gaia.l.a.r", [["gaia.l.a",1,"Name","email@example.com", ... ]]] + email := "" + if len(payload) >= 2 { + if accounts, ok := payload[1].([]any); ok && len(accounts) >= 1 { + if first, ok := accounts[0].([]any); ok && len(first) >= 4 { + if em, ok := first[3].(string); ok { + email = strings.TrimSpace(em) + } + } + } + } + if email == "" { + fmt.Println("Failed to parse email from ListAccounts response; fallback to filename-based label") } // Generate a filename based on the SHA256 hash of the PSID @@ -49,9 +114,17 @@ 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") + + // Decide label: prefer email; fallback to file name without .json + label := email + if label == "" { + label = strings.TrimSuffix(fileName, ".json") + } + + tokenStorage := &gemini.GeminiWebTokenStorage{ + Secure1PSID: secure1psid, + Secure1PSIDTS: secure1psidts, + Label: label, } record := &sdkAuth.TokenRecord{ Provider: "gemini-web", From 96cebd2a35daac81902944c2e53c5dd83a5acf68 Mon Sep 17 00:00:00 2001 From: hkfires <10558748+hkfires@users.noreply.github.com> Date: Thu, 25 Sep 2025 20:39:15 +0800 Subject: [PATCH 3/3] feat(auth): add interactive prompts to Gemini web auth flow --- internal/cmd/gemini-web_auth.go | 71 ++++++++++++++++---------- internal/provider/gemini-web/client.go | 10 ++-- internal/provider/gemini-web/media.go | 4 +- 3 files changed, 51 insertions(+), 34 deletions(-) diff --git a/internal/cmd/gemini-web_auth.go b/internal/cmd/gemini-web_auth.go index 80f227f7..40dda308 100644 --- a/internal/cmd/gemini-web_auth.go +++ b/internal/cmd/gemini-web_auth.go @@ -56,8 +56,19 @@ func DoGeminiWebAuth(cfg *config.Config) { secure1psid := strings.TrimSpace(cookieMap["__Secure-1PSID"]) secure1psidts := strings.TrimSpace(cookieMap["__Secure-1PSIDTS"]) + // Fallback: prompt user to input missing values + if secure1psid == "" { + fmt.Print("Cookie missing __Secure-1PSID. Enter __Secure-1PSID: ") + v, _ := reader.ReadString('\n') + secure1psid = strings.TrimSpace(v) + } + if secure1psidts == "" { + fmt.Print("Cookie missing __Secure-1PSIDTS. Enter __Secure-1PSIDTS: ") + v, _ := reader.ReadString('\n') + secure1psidts = strings.TrimSpace(v) + } if secure1psid == "" || secure1psidts == "" { - fmt.Println("Cookie does not contain __Secure-1PSID or __Secure-1PSIDTS") + log.Fatal("__Secure-1PSID and __Secure-1PSIDTS cannot be empty") return } @@ -78,36 +89,34 @@ func DoGeminiWebAuth(cfg *config.Config) { req.Header.Set("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8") resp, err := httpClient.Do(req) + email := "" if err != nil { fmt.Printf("Request to ListAccounts failed: %v\n", err) - return - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - fmt.Printf("ListAccounts returned status code: %d\n", resp.StatusCode) - return - } - - var payload []any - if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil { - fmt.Printf("Failed to parse ListAccounts response: %v\n", err) - return - } - - // Expected structure like: ["gaia.l.a.r", [["gaia.l.a",1,"Name","email@example.com", ... ]]] - email := "" - if len(payload) >= 2 { - if accounts, ok := payload[1].([]any); ok && len(accounts) >= 1 { - if first, ok := accounts[0].([]any); ok && len(first) >= 4 { - if em, ok := first[3].(string); ok { - email = strings.TrimSpace(em) + } else { + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + fmt.Printf("ListAccounts returned status code: %d\n", resp.StatusCode) + } else { + var payload []any + if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil { + fmt.Printf("Failed to parse ListAccounts response: %v\n", err) + } else { + // Expected structure like: ["gaia.l.a.r", [["gaia.l.a",1,"Name","email@example.com", ... ]]] + if len(payload) >= 2 { + if accounts, ok := payload[1].([]any); ok && len(accounts) >= 1 { + if first, ok := accounts[0].([]any); ok && len(first) >= 4 { + if em, ok := first[3].(string); ok { + email = strings.TrimSpace(em) + } + } + } + } + if email == "" { + fmt.Println("Failed to parse email from ListAccounts response") } } } } - if email == "" { - fmt.Println("Failed to parse email from ListAccounts response; fallback to filename-based label") - } // Generate a filename based on the SHA256 hash of the PSID hasher := sha256.New() @@ -115,10 +124,18 @@ func DoGeminiWebAuth(cfg *config.Config) { hash := hex.EncodeToString(hasher.Sum(nil)) fileName := fmt.Sprintf("gemini-web-%s.json", hash[:16]) - // Decide label: prefer email; fallback to file name without .json + // Decide label: prefer email; fallback prompt then file name without .json + defaultLabel := strings.TrimSuffix(fileName, ".json") label := email if label == "" { - label = strings.TrimSuffix(fileName, ".json") + fmt.Printf("Enter label for this auth (default: %s): ", defaultLabel) + v, _ := reader.ReadString('\n') + v = strings.TrimSpace(v) + if v != "" { + label = v + } else { + label = defaultLabel + } } tokenStorage := &gemini.GeminiWebTokenStorage{ diff --git a/internal/provider/gemini-web/client.go b/internal/provider/gemini-web/client.go index 8dd28e3e..4ce9a263 100644 --- a/internal/provider/gemini-web/client.go +++ b/internal/provider/gemini-web/client.go @@ -292,7 +292,7 @@ func (c *GeminiClient) Close(delaySec float64) { c.Running = false } -// ensureRunning mirrors the Python decorator behavior and retries on APIError. +// ensureRunning mirrors the decorator behavior and retries on APIError. func (c *GeminiClient) ensureRunning() error { if c.Running { return nil @@ -419,7 +419,7 @@ func (c *GeminiClient) generateOnce(prompt string, files []string, model Model, }() if resp.StatusCode == 429 { - // Surface 429 as TemporarilyBlocked to match Python behavior + // Surface 429 as TemporarilyBlocked to match reference behavior c.Close(0) return empty, &TemporarilyBlocked{GeminiError{Msg: "Too many requests. IP temporarily blocked."}} } @@ -499,7 +499,7 @@ func (c *GeminiClient) generateOnce(prompt string, files []string, model Model, } } } - // Parse nested error code to align with Python mapping + // Parse nested error code to align with error mapping var top []any // Prefer lastTop from fallback scan; otherwise try parts[2] if len(lastTop) > 0 { @@ -522,7 +522,7 @@ func (c *GeminiClient) generateOnce(prompt string, files []string, model Model, } } // Debug("Invalid response: control frames only; no body found") - // Close the client to force re-initialization on next request (parity with Python client behavior) + // Close the client to force re-initialization on next request (parity with reference client behavior) c.Close(0) return empty, &APIError{Msg: "Failed to generate contents. Invalid response data received."} } @@ -745,7 +745,7 @@ func (c *GeminiClient) generateOnce(prompt string, files []string, model Model, } // extractErrorCode attempts to navigate the known nested error structure and fetch the integer code. -// Mirrors Python path: response_json[0][5][2][0][1][0] +// Mirrors reference path: response_json[0][5][2][0][1][0] func extractErrorCode(top []any) (int, bool) { if len(top) == 0 { return 0, false diff --git a/internal/provider/gemini-web/media.go b/internal/provider/gemini-web/media.go index e9dcecde..585eff90 100644 --- a/internal/provider/gemini-web/media.go +++ b/internal/provider/gemini-web/media.go @@ -52,7 +52,7 @@ func (i Image) Save(path string, filename string, cookies map[string]string, ver filename = q[0] } } - // Regex validation (align with Python: ^(.*\.\w+)) to extract name with extension. + // Regex validation (pattern: ^(.*\.\w+)) to extract name with extension. if filename != "" { re := regexp.MustCompile(`^(.*\.\w+)`) if m := re.FindStringSubmatch(filename); len(m) >= 2 { @@ -70,7 +70,7 @@ func (i Image) Save(path string, filename string, cookies map[string]string, ver 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). + // Helper to set raw Cookie header using provided cookies (parity with the reference client behavior). buildCookieHeader := func(m map[string]string) string { if len(m) == 0 { return ""