From f228a4dccae0e9c11f2497363f4f0da68f4c7eb6 Mon Sep 17 00:00:00 2001 From: hkfires <10558748+hkfires@users.noreply.github.com> Date: Fri, 26 Sep 2025 09:43:26 +0800 Subject: [PATCH] feat(auth): Enhance Gemini web auth with flexible input and UI --- internal/cmd/gemini-web_auth.go | 147 +++++++++++++++++--------------- sdk/cliproxy/auth/types.go | 17 ++++ 2 files changed, 97 insertions(+), 67 deletions(-) diff --git a/internal/cmd/gemini-web_auth.go b/internal/cmd/gemini-web_auth.go index 1873362e..ff1ea1cd 100644 --- a/internal/cmd/gemini-web_auth.go +++ b/internal/cmd/gemini-web_auth.go @@ -18,9 +18,16 @@ import ( "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" ) +// banner prints a simple ASCII banner for clarity without ANSI colors. +func banner(title string) { + line := strings.Repeat("=", len(title)+8) + fmt.Println(line) + fmt.Println("=== " + title + " ===") + fmt.Println(line) +} + // DoGeminiWebAuth handles the process of creating a Gemini Web token file. // New flow: // 1. Prompt user to paste the full cookie string. @@ -32,78 +39,82 @@ func DoGeminiWebAuth(cfg *config.Config) { reader := bufio.NewReader(os.Stdin) isMacOS := strings.HasPrefix(runtime.GOOS, "darwin") + cookieProvided := false + banner("Gemini Web Cookie Sign-in") if !isMacOS { - fmt.Print("Paste your full Google Cookie and press Enter: ") + // NOTE: Provide extra guidance for macOS users or anyone unsure about retrieving cookies. + fmt.Println("--- Cookie Input ---") + fmt.Println(">> Paste your full Google Cookie and press Enter") + fmt.Println("Tip: If you are on macOS, or don't know how to get the cookie, just press Enter and follow the prompts.") + fmt.Print("Cookie: ") rawCookie, _ := reader.ReadString('\n') rawCookie = strings.TrimSpace(rawCookie) if rawCookie == "" { - log.Fatal("Cookie cannot be empty") - return - } - - // 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 + // Skip cookie-based parsing; fall back to manual field prompts. + fmt.Println("==> No cookie provided. Proceeding with manual input.") + } else { + cookieProvided = true + // 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 + } } } - } - secure1psid = strings.TrimSpace(cookieMap["__Secure-1PSID"]) - secure1psidts = strings.TrimSpace(cookieMap["__Secure-1PSIDTS"]) + secure1psid = strings.TrimSpace(cookieMap["__Secure-1PSID"]) + secure1psidts = strings.TrimSpace(cookieMap["__Secure-1PSIDTS"]) - // Build HTTP client with proxy settings respected. - httpClient := &http.Client{Timeout: 15 * time.Second} - httpClient = util.SetProxy(cfg, httpClient) + // 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) - } else { - defer func() { - _ = resp.Body.Close() - }() - if resp.StatusCode != http.StatusOK { - fmt.Printf("ListAccounts returned status code: %d\n", resp.StatusCode) + // 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.Println("!! Failed to create request:", err) } else { - var payload []any - if err = json.NewDecoder(resp.Body).Decode(&payload); err != nil { - fmt.Printf("Failed to parse ListAccounts response: %v\n", err) + 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.Println("!! Request to ListAccounts failed:", 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, ok1 := accounts[0].([]any); ok1 && len(first) >= 4 { - if em, ok2 := first[3].(string); ok2 { - email = strings.TrimSpace(em) + defer func() { _ = 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.Println("!! Failed to parse ListAccounts response:", 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, ok1 := accounts[0].([]any); ok1 && len(first) >= 4 { + if em, ok2 := first[3].(string); ok2 { + 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") - } } } } @@ -111,23 +122,24 @@ func DoGeminiWebAuth(cfg *config.Config) { // Fallback: prompt user to input missing values if secure1psid == "" { - if !isMacOS { - fmt.Print("Cookie missing __Secure-1PSID. ") + if cookieProvided && !isMacOS { + fmt.Println("!! Cookie missing __Secure-1PSID.") } fmt.Print("Enter __Secure-1PSID: ") v, _ := reader.ReadString('\n') secure1psid = strings.TrimSpace(v) } if secure1psidts == "" { - if !isMacOS { - fmt.Print("Cookie missing __Secure-1PSID. ") + if cookieProvided && !isMacOS { + fmt.Println("!! Cookie missing __Secure-1PSIDTS.") } fmt.Print("Enter __Secure-1PSIDTS: ") v, _ := reader.ReadString('\n') secure1psidts = strings.TrimSpace(v) } if secure1psid == "" || secure1psidts == "" { - log.Fatal("__Secure-1PSID and __Secure-1PSIDTS cannot be empty") + // Use print instead of logger to avoid log redirection. + fmt.Println("!! __Secure-1PSID and __Secure-1PSIDTS cannot be empty") return } if isMacOS { @@ -146,7 +158,7 @@ func DoGeminiWebAuth(cfg *config.Config) { defaultLabel := strings.TrimSuffix(fileName, ".json") label := email if label == "" { - fmt.Printf("Enter label for this auth (default: %s): ", defaultLabel) + fmt.Print(fmt.Sprintf("Enter label for this auth (default: %s): ", defaultLabel)) v, _ := reader.ReadString('\n') v = strings.TrimSpace(v) if v != "" { @@ -169,9 +181,10 @@ func DoGeminiWebAuth(cfg *config.Config) { store := sdkAuth.GetTokenStore() savedPath, err := store.Save(context.Background(), cfg, record) if err != nil { - fmt.Printf("Failed to save Gemini Web token to file: %v\n", err) + fmt.Println("!! Failed to save Gemini Web token to file:", err) return } - fmt.Printf("Successfully saved Gemini Web token to: %s\n", savedPath) + fmt.Println("==> Successfully saved Gemini Web token!") + fmt.Println("==> Saved to:", savedPath) } diff --git a/sdk/cliproxy/auth/types.go b/sdk/cliproxy/auth/types.go index c3a56cfe..75fb5643 100644 --- a/sdk/cliproxy/auth/types.go +++ b/sdk/cliproxy/auth/types.go @@ -128,6 +128,7 @@ func (a *Auth) AccountInfo() (string, string) { if a == nil { return "", "" } + // For Gemini Web, prefer explicit cookie label for stability. if strings.ToLower(a.Provider) == "gemini-web" { // Prefer explicit label written into auth file (e.g., gemini-web-) if a.Metadata != nil { @@ -145,6 +146,22 @@ func (a *Auth) AccountInfo() (string, string) { } } } + // For Gemini CLI, include project ID in the OAuth account info if present. + if strings.ToLower(a.Provider) == "gemini-cli" { + if a.Metadata != nil { + email, _ := a.Metadata["email"].(string) + email = strings.TrimSpace(email) + if email != "" { + if p, ok := a.Metadata["project_id"].(string); ok { + p = strings.TrimSpace(p) + if p != "" { + return "oauth", email + " (" + p + ")" + } + } + return "oauth", email + } + } + } if a.Metadata != nil { if v, ok := a.Metadata["email"].(string); ok { return "oauth", v