Merge pull request #63 from router-for-me/gemini-web

Gemini-web
This commit is contained in:
Luis Pater
2025-09-25 11:53:22 +08:00
committed by GitHub
8 changed files with 73 additions and 112 deletions

View File

@@ -714,6 +714,8 @@ func (h *Handler) CreateGeminiWebToken(c *gin.Context) {
Secure1PSID: payload.Secure1PSID,
Secure1PSIDTS: payload.Secure1PSIDTS,
}
// Provide a stable label (gemini-web-<hash>) for logging and identification
tokenStorage.Label = strings.TrimSuffix(fileName, ".json")
record := &sdkAuth.TokenRecord{
Provider: "gemini-web",

View File

@@ -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-<hash>".
// 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)
}

View File

@@ -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-<hash>
if tokenStorage != nil {
tokenStorage.Label = strings.TrimSuffix(fileName, ".json")
}
record := &sdkAuth.TokenRecord{
Provider: "gemini-web",
FileName: fileName,

View File

@@ -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 {
@@ -431,21 +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
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})
}
applyCookies(req, c.Cookies)
resp, err := c.httpClient.Do(req)
if err != nil {

View File

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

View File

@@ -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"
@@ -97,12 +98,12 @@ func (s *GeminiWebState) Label() string {
}
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
@@ -110,20 +111,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 }
@@ -174,6 +169,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
@@ -405,7 +402,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() {
@@ -433,7 +430,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) {
@@ -570,19 +567,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 = "."

View File

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

View File

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