feat(auth): add interactive prompts to Gemini web auth flow

This commit is contained in:
hkfires
2025-09-25 20:39:15 +08:00
parent a45d2109f3
commit 96cebd2a35
3 changed files with 51 additions and 34 deletions

View File

@@ -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{

View File

@@ -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

View File

@@ -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 ""