refactor(auth): Introduce generic cookie snapshot manager

This commit introduces a generic `cookies.Manager` to centralize the logic for handling cookie snapshots, which was previously duplicated across the Gemini and PaLM clients. This refactoring eliminates code duplication and improves maintainability.

The new `cookies.Manager[T]` in `internal/auth/cookies` orchestrates the lifecycle of cookie data between a temporary snapshot file and the main token file. It provides `Apply`, `Persist`, and `Flush` methods to manage this process.

Key changes:
- A generic `Manager` is created in `internal/auth/cookies`, usable for any token storage type.
- A `Hooks` struct allows for customizable behavior, such as custom merging strategies for different token types.
- Duplicated snapshot handling code has been removed from the `gemini-web` and `palm` persistence packages.
- The `GeminiWebClient` and `PaLMClient` have been updated to use the new `cookies.Manager`.
- The `auth_gemini` and `auth_palm` CLI commands now leverage the client's `Flush` method, simplifying the command logic.
- Cookie snapshot utility functions have been moved from `internal/util/files.go` to a new `internal/util/cookies.go` for better organization.
This commit is contained in:
hkfires
2025-09-18 20:06:14 +08:00
parent 7632204966
commit 56b2dabcca
4 changed files with 228 additions and 146 deletions

View File

@@ -40,10 +40,11 @@ const (
type GeminiWebClient struct {
ClientBase
gwc *geminiWeb.GeminiClient
tokenFilePath string
convStore map[string][]string
convMutex sync.RWMutex
gwc *geminiWeb.GeminiClient
tokenFilePath string
snapshotManager *util.Manager[gemini.GeminiWebTokenStorage]
convStore map[string][]string
convMutex sync.RWMutex
// JSON-based conversation persistence
convData map[string]geminiWeb.ConversationRecord
@@ -113,13 +114,33 @@ func NewGeminiWebClient(cfg *config.Config, ts *gemini.GeminiWebTokenStorage, to
client.convIndex = index
}
client.InitializeModelRegistry(clientID)
// Prefer cookie snapshot at startup if present
if ok, err := geminiWeb.ApplyCookieSnapshotToTokenStorage(tokenFilePath, ts); err == nil && ok {
log.Debugf("Loaded Gemini Web cookie snapshot: %s", filepath.Base(util.CookieSnapshotPath(tokenFilePath)))
if tokenFilePath != "" {
client.snapshotManager = util.NewManager[gemini.GeminiWebTokenStorage](
tokenFilePath,
ts,
util.Hooks[gemini.GeminiWebTokenStorage]{
Apply: func(store, snapshot *gemini.GeminiWebTokenStorage) {
if snapshot.Secure1PSID != "" {
store.Secure1PSID = snapshot.Secure1PSID
}
if snapshot.Secure1PSIDTS != "" {
store.Secure1PSIDTS = snapshot.Secure1PSIDTS
}
},
WriteMain: func(path string, data *gemini.GeminiWebTokenStorage) error {
return data.SaveTokenToFile(path)
},
},
)
if applied, err := client.snapshotManager.Apply(); err != nil {
log.Warnf("Failed to apply Gemini Web cookie snapshot for %s: %v", filepath.Base(tokenFilePath), err)
} else if applied {
log.Debugf("Loaded Gemini Web cookie snapshot: %s", filepath.Base(util.CookieSnapshotPath(tokenFilePath)))
}
}
client.InitializeModelRegistry(clientID)
client.gwc = geminiWeb.NewGeminiClient(ts.Secure1PSID, ts.Secure1PSIDTS, cfg.ProxyURL, geminiWeb.WithAccountLabel(strings.TrimSuffix(filepath.Base(tokenFilePath), ".json")))
timeoutSec := geminiWebDefaultTimeoutSec
refreshIntervalSec := cfg.GeminiWeb.TokenRefreshSeconds
@@ -794,8 +815,14 @@ func (c *GeminiWebClient) SaveTokenToFile() error {
ts.Secure1PSIDTS = v
}
}
if c.snapshotManager == nil {
if c.tokenFilePath == "" {
return nil
}
return ts.SaveTokenToFile(c.tokenFilePath)
}
log.Debugf("Saving Gemini Web cookie snapshot to %s", filepath.Base(util.CookieSnapshotPath(c.tokenFilePath)))
return geminiWeb.SaveCookieSnapshot(c.tokenFilePath, c.gwc.Cookies)
return c.snapshotManager.Persist()
}
// startCookiePersist periodically writes refreshed cookies into the cookie snapshot file.
@@ -1022,11 +1049,25 @@ func (c *GeminiWebClient) backgroundInitRetry() {
// flushCookieSnapshotToMain merges snapshot cookies into the main token file.
func (c *GeminiWebClient) flushCookieSnapshotToMain() {
if c.tokenFilePath == "" {
if c.snapshotManager == nil {
return
}
base := c.tokenStorage.(*gemini.GeminiWebTokenStorage)
if err := geminiWeb.FlushCookieSnapshotToMain(c.tokenFilePath, c.gwc.Cookies, base); err != nil {
ts := c.tokenStorage.(*gemini.GeminiWebTokenStorage)
var opts []util.FlushOption[gemini.GeminiWebTokenStorage]
if c.gwc != nil && c.gwc.Cookies != nil {
gwCookies := c.gwc.Cookies
opts = append(opts, util.WithFallback(func() *gemini.GeminiWebTokenStorage {
merged := *ts
if v := gwCookies["__Secure-1PSID"]; v != "" {
merged.Secure1PSID = v
}
if v := gwCookies["__Secure-1PSIDTS"]; v != "" {
merged.Secure1PSIDTS = v
}
return &merged
}))
}
if err := c.snapshotManager.Flush(opts...); err != nil {
log.Errorf("Failed to flush cookie snapshot to main for %s: %v", filepath.Base(c.tokenFilePath), err)
}
}