mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-03 04:50:52 +08:00
refactor(gemini-web): Remove auto-refresh, auto-close, and caching
This commit simplifies the Gemini web client by removing several complex, stateful features. The previous implementation for auto-refreshing cookies and auto-closing the client involved background goroutines, timers, and file system caching, which made the client's lifecycle difficult to manage. The following features have been removed: - The cookie auto-refresh mechanism, including the background goroutine (`rotateCookies`) and related configuration fields. - The file-based caching for the `__Secure-1PSIDTS` token. The `rotate1PSIDTS` function now fetches a new token on every call. - The auto-close functionality, which used timers to close the client after a period of inactivity. - Associated configuration options and methods (`WithAccountLabel`, `WithOnCookiesRefreshed`, `Close`, etc.). By removing this logic, the client becomes more stateless and predictable. The responsibility for managing the client's lifecycle and handling token expiration is now shifted to the caller, leading to a simpler and more robust integration.
This commit is contained in:
@@ -296,7 +296,6 @@ The server uses a YAML configuration file (`config.yaml`) located in the project
|
|||||||
| `gemini-web.code-mode` | boolean | false | Enables code mode for optimized responses in coding-related tasks. |
|
| `gemini-web.code-mode` | boolean | false | Enables code mode for optimized responses in coding-related tasks. |
|
||||||
| `gemini-web.max-chars-per-request` | integer | 1,000,000 | The maximum number of characters to send to Gemini Web in a single request. |
|
| `gemini-web.max-chars-per-request` | integer | 1,000,000 | The maximum number of characters to send to Gemini Web in a single request. |
|
||||||
| `gemini-web.disable-continuation-hint` | boolean | false | Disables the continuation hint for split prompts. |
|
| `gemini-web.disable-continuation-hint` | boolean | false | Disables the continuation hint for split prompts. |
|
||||||
| `gemini-web.token-refresh-seconds` | integer | 540 | The interval in seconds for background cookie auto-refresh. |
|
|
||||||
|
|
||||||
### Example Configuration File
|
### Example Configuration File
|
||||||
|
|
||||||
@@ -337,7 +336,6 @@ gemini-web:
|
|||||||
context: true # Enable conversation context reuse
|
context: true # Enable conversation context reuse
|
||||||
code-mode: false # Enable code mode
|
code-mode: false # Enable code mode
|
||||||
max-chars-per-request: 1000000 # Max characters per request
|
max-chars-per-request: 1000000 # Max characters per request
|
||||||
token-refresh-seconds: 540 # Cookie refresh interval in seconds
|
|
||||||
|
|
||||||
# Request authentication providers
|
# Request authentication providers
|
||||||
auth:
|
auth:
|
||||||
|
|||||||
@@ -308,7 +308,6 @@ console.log(await claudeResponse.json());
|
|||||||
| `gemini-web.code-mode` | boolean | false | 是否启用代码模式,优化代码相关任务的响应。 |
|
| `gemini-web.code-mode` | boolean | false | 是否启用代码模式,优化代码相关任务的响应。 |
|
||||||
| `gemini-web.max-chars-per-request` | integer | 1,000,000 | 单次请求发送给 Gemini Web 的最大字符数。 |
|
| `gemini-web.max-chars-per-request` | integer | 1,000,000 | 单次请求发送给 Gemini Web 的最大字符数。 |
|
||||||
| `gemini-web.disable-continuation-hint` | boolean | false | 当提示被拆分时,是否禁用连续提示的暗示。 |
|
| `gemini-web.disable-continuation-hint` | boolean | false | 当提示被拆分时,是否禁用连续提示的暗示。 |
|
||||||
| `gemini-web.token-refresh-seconds` | integer | 540 | 后台 Cookie 自动刷新的间隔(秒)。 |
|
|
||||||
|
|
||||||
### 配置文件示例
|
### 配置文件示例
|
||||||
|
|
||||||
@@ -349,7 +348,6 @@ gemini-web:
|
|||||||
context: true # 启用会话上下文重用
|
context: true # 启用会话上下文重用
|
||||||
code-mode: false # 启用代码模式
|
code-mode: false # 启用代码模式
|
||||||
max-chars-per-request: 1000000 # 单次请求最大字符数
|
max-chars-per-request: 1000000 # 单次请求最大字符数
|
||||||
token-refresh-seconds: 540 # Cookie 刷新间隔(秒)
|
|
||||||
|
|
||||||
# 请求鉴权提供方
|
# 请求鉴权提供方
|
||||||
auth:
|
auth:
|
||||||
|
|||||||
@@ -71,21 +71,19 @@ openai-compatibility:
|
|||||||
alias: "kimi-k2" # The alias used in the API.
|
alias: "kimi-k2" # The alias used in the API.
|
||||||
|
|
||||||
# Gemini Web settings
|
# Gemini Web settings
|
||||||
# gemini-web:
|
gemini-web:
|
||||||
# # Conversation reuse: set to true to enable (default), false to disable.
|
# Conversation reuse: set to true to enable (default), false to disable.
|
||||||
# context: true
|
context: true
|
||||||
# # Maximum characters per single request to Gemini Web. Requests exceeding this
|
# Maximum characters per single request to Gemini Web. Requests exceeding this
|
||||||
# # size split into chunks. Only the last chunk carries files and yields the final answer.
|
# size split into chunks. Only the last chunk carries files and yields the final answer.
|
||||||
# max-chars-per-request: 1000000
|
max-chars-per-request: 1000000
|
||||||
# # Disable the short continuation hint appended to intermediate chunks
|
# Disable the short continuation hint appended to intermediate chunks
|
||||||
# # when splitting long prompts. Default is false (hint enabled by default).
|
# when splitting long prompts. Default is false (hint enabled by default).
|
||||||
# disable-continuation-hint: false
|
disable-continuation-hint: false
|
||||||
# # Background token auto-refresh interval seconds (defaults to 540 if unset or <= 0)
|
# Code mode:
|
||||||
# token-refresh-seconds: 540
|
# - true: enable XML wrapping hint and attach the coding-partner Gem.
|
||||||
# # Code mode:
|
# Thought merging (<think> into visible content) applies to STREAMING only;
|
||||||
# # - true: enable XML wrapping hint and attach the coding-partner Gem.
|
# non-stream responses keep reasoning/thought parts separate for clients
|
||||||
# # Thought merging (<think> into visible content) applies to STREAMING only;
|
# that expect explicit reasoning fields.
|
||||||
# # non-stream responses keep reasoning/thought parts separate for clients
|
# - false: disable XML hint and keep <think> separate
|
||||||
# # that expect explicit reasoning fields.
|
code-mode: false
|
||||||
# # - false: disable XML hint and keep <think> separate
|
|
||||||
# code-mode: false
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
@@ -18,12 +19,16 @@ type GeminiWebTokenStorage struct {
|
|||||||
Secure1PSID string `json:"secure_1psid"`
|
Secure1PSID string `json:"secure_1psid"`
|
||||||
Secure1PSIDTS string `json:"secure_1psidts"`
|
Secure1PSIDTS string `json:"secure_1psidts"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
|
LastRefresh string `json:"last_refresh,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// SaveTokenToFile serializes the Gemini Web token storage to a JSON file.
|
// SaveTokenToFile serializes the Gemini Web token storage to a JSON file.
|
||||||
func (ts *GeminiWebTokenStorage) SaveTokenToFile(authFilePath string) error {
|
func (ts *GeminiWebTokenStorage) SaveTokenToFile(authFilePath string) error {
|
||||||
misc.LogSavingCredentials(authFilePath)
|
misc.LogSavingCredentials(authFilePath)
|
||||||
ts.Type = "gemini-web"
|
ts.Type = "gemini-web"
|
||||||
|
if ts.LastRefresh == "" {
|
||||||
|
ts.LastRefresh = time.Now().Format(time.RFC3339)
|
||||||
|
}
|
||||||
if err := os.MkdirAll(filepath.Dir(authFilePath), 0700); err != nil {
|
if err := os.MkdirAll(filepath.Dir(authFilePath), 0700); err != nil {
|
||||||
return fmt.Errorf("failed to create directory: %v", err)
|
return fmt.Errorf("failed to create directory: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -151,28 +151,13 @@ func getAccessToken(baseCookies map[string]string, proxy string, verbose bool, i
|
|||||||
return "", nil, &AuthError{Msg: "Failed to retrieve token."}
|
return "", nil, &AuthError{Msg: "Failed to retrieve token."}
|
||||||
}
|
}
|
||||||
|
|
||||||
// rotate1psidts refreshes __Secure-1PSIDTS and caches it locally.
|
// rotate1PSIDTS refreshes __Secure-1PSIDTS
|
||||||
func rotate1psidts(cookies map[string]string, proxy string, insecure bool) (string, error) {
|
func rotate1PSIDTS(cookies map[string]string, proxy string, insecure bool) (string, error) {
|
||||||
psid, ok := cookies["__Secure-1PSID"]
|
_, ok := cookies["__Secure-1PSID"]
|
||||||
if !ok {
|
if !ok {
|
||||||
return "", &AuthError{Msg: "__Secure-1PSID missing"}
|
return "", &AuthError{Msg: "__Secure-1PSID missing"}
|
||||||
}
|
}
|
||||||
|
|
||||||
cacheDir := "temp"
|
|
||||||
_ = os.MkdirAll(cacheDir, 0o755)
|
|
||||||
cacheFile := filepath.Join(cacheDir, ".cached_1psidts_"+psid+".txt")
|
|
||||||
|
|
||||||
if st, err := os.Stat(cacheFile); err == nil {
|
|
||||||
if time.Since(st.ModTime()) <= time.Minute {
|
|
||||||
if b, errReadFile := os.ReadFile(cacheFile); errReadFile == nil {
|
|
||||||
v := strings.TrimSpace(string(b))
|
|
||||||
if v != "" {
|
|
||||||
return v, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tr := &http.Transport{}
|
tr := &http.Transport{}
|
||||||
if proxy != "" {
|
if proxy != "" {
|
||||||
if pu, err := url.Parse(proxy); err == nil {
|
if pu, err := url.Parse(proxy); err == nil {
|
||||||
@@ -205,7 +190,6 @@ func rotate1psidts(cookies map[string]string, proxy string, insecure bool) (stri
|
|||||||
|
|
||||||
for _, c := range resp.Cookies() {
|
for _, c := range resp.Cookies() {
|
||||||
if c.Name == "__Secure-1PSIDTS" {
|
if c.Name == "__Secure-1PSIDTS" {
|
||||||
_ = os.WriteFile(cacheFile, []byte(c.Value), 0o644)
|
|
||||||
return c.Value, nil
|
return c.Value, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package geminiwebapi
|
package geminiwebapi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -10,7 +9,6 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -22,18 +20,7 @@ type GeminiClient struct {
|
|||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
AccessToken string
|
AccessToken string
|
||||||
Timeout time.Duration
|
Timeout time.Duration
|
||||||
AutoClose bool
|
|
||||||
CloseDelay time.Duration
|
|
||||||
closeMu sync.Mutex
|
|
||||||
closeTimer *time.Timer
|
|
||||||
AutoRefresh bool
|
|
||||||
RefreshInterval time.Duration
|
|
||||||
rotateCancel context.CancelFunc
|
|
||||||
insecure bool
|
insecure bool
|
||||||
accountLabel string
|
|
||||||
// onCookiesRefreshed is an optional callback invoked after cookies
|
|
||||||
// are refreshed and the __Secure-1PSIDTS value changes.
|
|
||||||
onCookiesRefreshed func()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var NanoBananaModel = map[string]struct{}{
|
var NanoBananaModel = map[string]struct{}{
|
||||||
@@ -47,10 +34,6 @@ func NewGeminiClient(secure1psid string, secure1psidts string, proxy string, opt
|
|||||||
Proxy: proxy,
|
Proxy: proxy,
|
||||||
Running: false,
|
Running: false,
|
||||||
Timeout: 300 * time.Second,
|
Timeout: 300 * time.Second,
|
||||||
AutoClose: false,
|
|
||||||
CloseDelay: 300 * time.Second,
|
|
||||||
AutoRefresh: true,
|
|
||||||
RefreshInterval: 540 * time.Second,
|
|
||||||
insecure: false,
|
insecure: false,
|
||||||
}
|
}
|
||||||
if secure1psid != "" {
|
if secure1psid != "" {
|
||||||
@@ -70,21 +53,8 @@ func WithInsecureTLS(insecure bool) func(*GeminiClient) {
|
|||||||
return func(c *GeminiClient) { c.insecure = insecure }
|
return func(c *GeminiClient) { c.insecure = insecure }
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithAccountLabel sets an identifying label (e.g., token filename sans .json)
|
|
||||||
// for logging purposes.
|
|
||||||
func WithAccountLabel(label string) func(*GeminiClient) {
|
|
||||||
return func(c *GeminiClient) { c.accountLabel = label }
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithOnCookiesRefreshed registers a callback invoked when cookies are refreshed
|
|
||||||
// and the __Secure-1PSIDTS value changes. The callback runs in the background
|
|
||||||
// refresh goroutine; keep it lightweight and non-blocking.
|
|
||||||
func WithOnCookiesRefreshed(cb func()) func(*GeminiClient) {
|
|
||||||
return func(c *GeminiClient) { c.onCookiesRefreshed = cb }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Init initializes the access token and http client.
|
// Init initializes the access token and http client.
|
||||||
func (c *GeminiClient) Init(timeoutSec float64, autoClose bool, closeDelaySec float64, autoRefresh bool, refreshIntervalSec float64, verbose bool) error {
|
func (c *GeminiClient) Init(timeoutSec float64, verbose bool) error {
|
||||||
// get access token
|
// get access token
|
||||||
token, validCookies, err := getAccessToken(c.Cookies, c.Proxy, verbose, c.insecure)
|
token, validCookies, err := getAccessToken(c.Cookies, c.Proxy, verbose, c.insecure)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -108,17 +78,6 @@ func (c *GeminiClient) Init(timeoutSec float64, autoClose bool, closeDelaySec fl
|
|||||||
c.Running = true
|
c.Running = true
|
||||||
|
|
||||||
c.Timeout = time.Duration(timeoutSec * float64(time.Second))
|
c.Timeout = time.Duration(timeoutSec * float64(time.Second))
|
||||||
c.AutoClose = autoClose
|
|
||||||
c.CloseDelay = time.Duration(closeDelaySec * float64(time.Second))
|
|
||||||
if c.AutoClose {
|
|
||||||
c.resetCloseTimer()
|
|
||||||
}
|
|
||||||
|
|
||||||
c.AutoRefresh = autoRefresh
|
|
||||||
c.RefreshInterval = time.Duration(refreshIntervalSec * float64(time.Second))
|
|
||||||
if c.AutoRefresh {
|
|
||||||
c.startAutoRefresh()
|
|
||||||
}
|
|
||||||
if verbose {
|
if verbose {
|
||||||
Success("Gemini client initialized successfully.")
|
Success("Gemini client initialized successfully.")
|
||||||
}
|
}
|
||||||
@@ -130,94 +89,6 @@ func (c *GeminiClient) Close(delaySec float64) {
|
|||||||
time.Sleep(time.Duration(delaySec * float64(time.Second)))
|
time.Sleep(time.Duration(delaySec * float64(time.Second)))
|
||||||
}
|
}
|
||||||
c.Running = false
|
c.Running = false
|
||||||
c.closeMu.Lock()
|
|
||||||
if c.closeTimer != nil {
|
|
||||||
c.closeTimer.Stop()
|
|
||||||
c.closeTimer = nil
|
|
||||||
}
|
|
||||||
c.closeMu.Unlock()
|
|
||||||
// Transport/client closed by GC; nothing explicit
|
|
||||||
if c.rotateCancel != nil {
|
|
||||||
c.rotateCancel()
|
|
||||||
c.rotateCancel = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *GeminiClient) resetCloseTimer() {
|
|
||||||
c.closeMu.Lock()
|
|
||||||
defer c.closeMu.Unlock()
|
|
||||||
if c.closeTimer != nil {
|
|
||||||
c.closeTimer.Stop()
|
|
||||||
c.closeTimer = nil
|
|
||||||
}
|
|
||||||
c.closeTimer = time.AfterFunc(c.CloseDelay, func() { c.Close(0) })
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *GeminiClient) startAutoRefresh() {
|
|
||||||
if c.rotateCancel != nil {
|
|
||||||
c.rotateCancel()
|
|
||||||
}
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
c.rotateCancel = cancel
|
|
||||||
go func() {
|
|
||||||
ticker := time.NewTicker(c.RefreshInterval)
|
|
||||||
defer ticker.Stop()
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
case <-ticker.C:
|
|
||||||
// Step 1: rotate __Secure-1PSIDTS
|
|
||||||
oldTS := ""
|
|
||||||
if c.Cookies != nil {
|
|
||||||
oldTS = c.Cookies["__Secure-1PSIDTS"]
|
|
||||||
}
|
|
||||||
newTS, err := rotate1psidts(c.Cookies, c.Proxy, c.insecure)
|
|
||||||
if err != nil {
|
|
||||||
Warning("Failed to refresh cookies. Background auto refresh canceled: %v", err)
|
|
||||||
cancel()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare a snapshot of cookies for access token refresh
|
|
||||||
nextCookies := map[string]string{}
|
|
||||||
for k, v := range c.Cookies {
|
|
||||||
nextCookies[k] = v
|
|
||||||
}
|
|
||||||
if newTS != "" {
|
|
||||||
nextCookies["__Secure-1PSIDTS"] = newTS
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 2: refresh access token using updated cookies
|
|
||||||
token, validCookies, err := getAccessToken(nextCookies, c.Proxy, false, c.insecure)
|
|
||||||
if err != nil {
|
|
||||||
// Apply rotated cookies even if token refresh fails, then retry on next tick
|
|
||||||
c.Cookies = nextCookies
|
|
||||||
Warning("Failed to refresh access token after cookie rotation: %v", err)
|
|
||||||
} else {
|
|
||||||
c.AccessToken = token
|
|
||||||
c.Cookies = validCookies
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.accountLabel != "" {
|
|
||||||
DebugRaw("Cookies refreshed [%s]. New __Secure-1PSIDTS: %s", c.accountLabel, MaskToken28(nextCookies["__Secure-1PSIDTS"]))
|
|
||||||
} else {
|
|
||||||
DebugRaw("Cookies refreshed. New __Secure-1PSIDTS: %s", MaskToken28(nextCookies["__Secure-1PSIDTS"]))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trigger persistence only when TS actually changes
|
|
||||||
if c.onCookiesRefreshed != nil {
|
|
||||||
currentTS := ""
|
|
||||||
if c.Cookies != nil {
|
|
||||||
currentTS = c.Cookies["__Secure-1PSIDTS"]
|
|
||||||
}
|
|
||||||
if currentTS != "" && currentTS != oldTS {
|
|
||||||
c.onCookiesRefreshed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ensureRunning mirrors the Python decorator behavior and retries on APIError.
|
// ensureRunning mirrors the Python decorator behavior and retries on APIError.
|
||||||
@@ -225,7 +96,15 @@ func (c *GeminiClient) ensureRunning() error {
|
|||||||
if c.Running {
|
if c.Running {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return c.Init(float64(c.Timeout/time.Second), c.AutoClose, float64(c.CloseDelay/time.Second), c.AutoRefresh, float64(c.RefreshInterval/time.Second), false)
|
return c.Init(float64(c.Timeout/time.Second), false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RotateTS performs a RotateCookies request and returns the new __Secure-1PSIDTS value (if any).
|
||||||
|
func (c *GeminiClient) RotateTS() (string, error) {
|
||||||
|
if c == nil {
|
||||||
|
return "", fmt.Errorf("gemini web client is nil")
|
||||||
|
}
|
||||||
|
return rotate1PSIDTS(c.Cookies, c.Proxy, c.insecure)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GenerateContent sends a prompt (with optional files) and parses the response into ModelOutput.
|
// GenerateContent sends a prompt (with optional files) and parses the response into ModelOutput.
|
||||||
@@ -237,9 +116,6 @@ func (c *GeminiClient) GenerateContent(prompt string, files []string, model Mode
|
|||||||
if err := c.ensureRunning(); err != nil {
|
if err := c.ensureRunning(); err != nil {
|
||||||
return empty, err
|
return empty, err
|
||||||
}
|
}
|
||||||
if c.AutoClose {
|
|
||||||
c.resetCloseTimer()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retry wrapper similar to decorator (retry=2)
|
// Retry wrapper similar to decorator (retry=2)
|
||||||
retries := 2
|
retries := 2
|
||||||
|
|||||||
@@ -120,10 +120,6 @@ type GeminiWebConfig struct {
|
|||||||
// DisableContinuationHint, when true, disables the continuation hint for split prompts.
|
// DisableContinuationHint, when true, disables the continuation hint for split prompts.
|
||||||
// The hint is enabled by default.
|
// The hint is enabled by default.
|
||||||
DisableContinuationHint bool `yaml:"disable-continuation-hint,omitempty" json:"disable-continuation-hint,omitempty"`
|
DisableContinuationHint bool `yaml:"disable-continuation-hint,omitempty" json:"disable-continuation-hint,omitempty"`
|
||||||
|
|
||||||
// TokenRefreshSeconds controls the background cookie auto-refresh interval in seconds.
|
|
||||||
// When unset or <= 0, defaults to 540 seconds.
|
|
||||||
TokenRefreshSeconds int `yaml:"token-refresh-seconds" json:"token-refresh-seconds"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoteManagement holds management API configuration under 'remote-management'.
|
// RemoteManagement holds management API configuration under 'remote-management'.
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -18,7 +16,6 @@ import (
|
|||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/constant"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/constant"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/translator/translator"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/translator/translator"
|
||||||
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
|
||||||
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
|
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
"github.com/tidwall/sjson"
|
"github.com/tidwall/sjson"
|
||||||
@@ -26,7 +23,6 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
geminiWebDefaultTimeoutSec = 300
|
geminiWebDefaultTimeoutSec = 300
|
||||||
geminiWebDefaultRefreshIntervalSec = 540
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type geminiWebState struct {
|
type geminiWebState struct {
|
||||||
@@ -48,17 +44,9 @@ type geminiWebState struct {
|
|||||||
convData map[string]geminiwebapi.ConversationRecord
|
convData map[string]geminiwebapi.ConversationRecord
|
||||||
convIndex map[string]string
|
convIndex map[string]string
|
||||||
|
|
||||||
refreshInterval time.Duration
|
|
||||||
lastRefresh time.Time
|
lastRefresh time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *geminiWebState) RefreshLead() time.Duration {
|
|
||||||
if s.refreshInterval > 0 {
|
|
||||||
return s.refreshInterval
|
|
||||||
}
|
|
||||||
return 9 * time.Minute
|
|
||||||
}
|
|
||||||
|
|
||||||
func newGeminiWebState(cfg *config.Config, token *gemini.GeminiWebTokenStorage, storagePath string) *geminiWebState {
|
func newGeminiWebState(cfg *config.Config, token *gemini.GeminiWebTokenStorage, storagePath string) *geminiWebState {
|
||||||
state := &geminiWebState{
|
state := &geminiWebState{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
@@ -84,11 +72,6 @@ func newGeminiWebState(cfg *config.Config, token *gemini.GeminiWebTokenStorage,
|
|||||||
state.accountID = suffix
|
state.accountID = suffix
|
||||||
}
|
}
|
||||||
state.loadConversationCaches()
|
state.loadConversationCaches()
|
||||||
intervalSec := geminiWebDefaultRefreshIntervalSec
|
|
||||||
if cfg != nil && cfg.GeminiWeb.TokenRefreshSeconds > 0 {
|
|
||||||
intervalSec = cfg.GeminiWeb.TokenRefreshSeconds
|
|
||||||
}
|
|
||||||
state.refreshInterval = time.Duration(intervalSec) * time.Second
|
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,15 +119,9 @@ func (s *geminiWebState) ensureClient() error {
|
|||||||
s.token.Secure1PSID,
|
s.token.Secure1PSID,
|
||||||
s.token.Secure1PSIDTS,
|
s.token.Secure1PSIDTS,
|
||||||
proxyURL,
|
proxyURL,
|
||||||
geminiwebapi.WithOnCookiesRefreshed(s.onCookiesRefreshed),
|
|
||||||
)
|
)
|
||||||
timeout := geminiWebDefaultTimeoutSec
|
timeout := geminiWebDefaultTimeoutSec
|
||||||
refresh := geminiWebDefaultRefreshIntervalSec
|
if err := s.client.Init(float64(timeout), false); err != nil {
|
||||||
if s.cfg != nil && s.cfg.GeminiWeb.TokenRefreshSeconds > 0 {
|
|
||||||
refresh = s.cfg.GeminiWeb.TokenRefreshSeconds
|
|
||||||
}
|
|
||||||
// Use explicit refresh; background auto-refresh disabled here
|
|
||||||
if err := s.client.Init(float64(timeout), false, 300, false, float64(refresh), false); err != nil {
|
|
||||||
s.client = nil
|
s.client = nil
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -162,38 +139,25 @@ func (s *geminiWebState) refresh(ctx context.Context) error {
|
|||||||
s.token.Secure1PSID,
|
s.token.Secure1PSID,
|
||||||
s.token.Secure1PSIDTS,
|
s.token.Secure1PSIDTS,
|
||||||
proxyURL,
|
proxyURL,
|
||||||
geminiwebapi.WithOnCookiesRefreshed(s.onCookiesRefreshed),
|
|
||||||
)
|
)
|
||||||
timeout := geminiWebDefaultTimeoutSec
|
timeout := geminiWebDefaultTimeoutSec
|
||||||
refresh := geminiWebDefaultRefreshIntervalSec
|
if err := s.client.Init(float64(timeout), false); err != nil {
|
||||||
if s.cfg != nil && s.cfg.GeminiWeb.TokenRefreshSeconds > 0 {
|
|
||||||
refresh = s.cfg.GeminiWeb.TokenRefreshSeconds
|
|
||||||
}
|
|
||||||
// Use explicit refresh; background auto-refresh disabled here
|
|
||||||
if err := s.client.Init(float64(timeout), false, 300, false, float64(refresh), false); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// Attempt rotation proactively to persist new TS sooner
|
// Attempt rotation proactively to persist new TS sooner
|
||||||
_ = s.tryRotatePSIDTS(proxyURL)
|
if newTS, err := s.client.RotateTS(); err == nil && newTS != "" && newTS != s.token.Secure1PSIDTS {
|
||||||
|
s.tokenMu.Lock()
|
||||||
|
s.token.Secure1PSIDTS = newTS
|
||||||
|
s.tokenDirty = true
|
||||||
|
if s.client != nil && s.client.Cookies != nil {
|
||||||
|
s.client.Cookies["__Secure-1PSIDTS"] = newTS
|
||||||
|
}
|
||||||
|
s.tokenMu.Unlock()
|
||||||
|
}
|
||||||
s.lastRefresh = time.Now()
|
s.lastRefresh = time.Now()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *geminiWebState) onCookiesRefreshed() {
|
|
||||||
s.tokenMu.Lock()
|
|
||||||
defer s.tokenMu.Unlock()
|
|
||||||
if s.client == nil || s.client.Cookies == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if v := s.client.Cookies["__Secure-1PSID"]; v != "" {
|
|
||||||
s.token.Secure1PSID = v
|
|
||||||
}
|
|
||||||
if v := s.client.Cookies["__Secure-1PSIDTS"]; v != "" {
|
|
||||||
s.token.Secure1PSIDTS = v
|
|
||||||
}
|
|
||||||
s.tokenDirty = true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *geminiWebState) tokenSnapshot() *gemini.GeminiWebTokenStorage {
|
func (s *geminiWebState) tokenSnapshot() *gemini.GeminiWebTokenStorage {
|
||||||
s.tokenMu.Lock()
|
s.tokenMu.Lock()
|
||||||
defer s.tokenMu.Unlock()
|
defer s.tokenMu.Unlock()
|
||||||
@@ -201,70 +165,6 @@ func (s *geminiWebState) tokenSnapshot() *gemini.GeminiWebTokenStorage {
|
|||||||
return &c
|
return &c
|
||||||
}
|
}
|
||||||
|
|
||||||
// tryRotatePSIDTS performs a best-effort rotation of __Secure-1PSIDTS using
|
|
||||||
// the public RotateCookies endpoint. On success it updates both the in-memory
|
|
||||||
// token and the live client's cookie jar so that subsequent requests adopt the
|
|
||||||
// new value. Any error is ignored by the caller to avoid disrupting refresh.
|
|
||||||
func (s *geminiWebState) tryRotatePSIDTS(proxy string) error {
|
|
||||||
cookies := map[string]string{
|
|
||||||
"__Secure-1PSID": s.token.Secure1PSID,
|
|
||||||
"__Secure-1PSIDTS": s.token.Secure1PSIDTS,
|
|
||||||
}
|
|
||||||
|
|
||||||
tr := &http.Transport{}
|
|
||||||
if proxy != "" {
|
|
||||||
if pu, err := url.Parse(proxy); err == nil {
|
|
||||||
tr.Proxy = http.ProxyURL(pu)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
client := &http.Client{Transport: tr, Timeout: 60 * time.Second}
|
|
||||||
|
|
||||||
req, _ := http.NewRequest(http.MethodPost, geminiwebapi.EndpointRotateCookies, bytes.NewReader([]byte("[000,\"-0000000000000000000\"]")))
|
|
||||||
for k, vs := range geminiwebapi.HeadersRotateCookies {
|
|
||||||
for _, v := range vs {
|
|
||||||
req.Header.Add(k, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for k, v := range cookies {
|
|
||||||
req.AddCookie(&http.Cookie{Name: k, Value: v})
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer func() { _ = resp.Body.Close() }()
|
|
||||||
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
|
|
||||||
for _, c := range resp.Cookies() {
|
|
||||||
if c == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if c.Name == "__Secure-1PSIDTS" && c.Value != "" && c.Value != s.token.Secure1PSIDTS {
|
|
||||||
s.tokenMu.Lock()
|
|
||||||
s.token.Secure1PSIDTS = c.Value
|
|
||||||
s.tokenDirty = true
|
|
||||||
if s.client != nil && s.client.Cookies != nil {
|
|
||||||
s.client.Cookies["__Secure-1PSIDTS"] = c.Value
|
|
||||||
}
|
|
||||||
s.tokenMu.Unlock()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *geminiWebState) ShouldRefresh(now time.Time, _ *cliproxyauth.Auth) bool {
|
|
||||||
interval := s.refreshInterval
|
|
||||||
if interval <= 0 {
|
|
||||||
interval = time.Duration(geminiWebDefaultRefreshIntervalSec) * time.Second
|
|
||||||
}
|
|
||||||
if s.lastRefresh.IsZero() {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return now.Sub(s.lastRefresh) >= interval
|
|
||||||
}
|
|
||||||
|
|
||||||
type geminiWebPrepared struct {
|
type geminiWebPrepared struct {
|
||||||
handlerType string
|
handlerType string
|
||||||
translatedRaw []byte
|
translatedRaw []byte
|
||||||
|
|||||||
@@ -461,9 +461,6 @@ func (w *Watcher) reloadConfig() bool {
|
|||||||
if oldConfig.GeminiWeb.DisableContinuationHint != newConfig.GeminiWeb.DisableContinuationHint {
|
if oldConfig.GeminiWeb.DisableContinuationHint != newConfig.GeminiWeb.DisableContinuationHint {
|
||||||
log.Debugf(" gemini-web.disable-continuation-hint: %t -> %t", oldConfig.GeminiWeb.DisableContinuationHint, newConfig.GeminiWeb.DisableContinuationHint)
|
log.Debugf(" gemini-web.disable-continuation-hint: %t -> %t", oldConfig.GeminiWeb.DisableContinuationHint, newConfig.GeminiWeb.DisableContinuationHint)
|
||||||
}
|
}
|
||||||
if oldConfig.GeminiWeb.TokenRefreshSeconds != newConfig.GeminiWeb.TokenRefreshSeconds {
|
|
||||||
log.Debugf(" gemini-web.token-refresh-seconds: %d -> %d", oldConfig.GeminiWeb.TokenRefreshSeconds, newConfig.GeminiWeb.TokenRefreshSeconds)
|
|
||||||
}
|
|
||||||
if oldConfig.GeminiWeb.CodeMode != newConfig.GeminiWeb.CodeMode {
|
if oldConfig.GeminiWeb.CodeMode != newConfig.GeminiWeb.CodeMode {
|
||||||
log.Debugf(" gemini-web.code-mode: %t -> %t", oldConfig.GeminiWeb.CodeMode, newConfig.GeminiWeb.CodeMode)
|
log.Debugf(" gemini-web.code-mode: %t -> %t", oldConfig.GeminiWeb.CodeMode, newConfig.GeminiWeb.CodeMode)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,6 @@ func (a *GeminiWebAuthenticator) Login(ctx context.Context, cfg *config.Config,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *GeminiWebAuthenticator) RefreshLead() *time.Duration {
|
func (a *GeminiWebAuthenticator) RefreshLead() *time.Duration {
|
||||||
d := 9 * time.Minute
|
d := 3 * time.Hour
|
||||||
return &d
|
return &d
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -322,9 +322,6 @@ func (s *Service) Run(ctx context.Context) error {
|
|||||||
// Prefer core auth manager auto refresh if available.
|
// Prefer core auth manager auto refresh if available.
|
||||||
if s.coreManager != nil {
|
if s.coreManager != nil {
|
||||||
interval := 15 * time.Minute
|
interval := 15 * time.Minute
|
||||||
if sec := s.cfg.GeminiWeb.TokenRefreshSeconds; sec > 0 {
|
|
||||||
interval = time.Duration(sec) * time.Second
|
|
||||||
}
|
|
||||||
s.coreManager.StartAutoRefresh(context.Background(), interval)
|
s.coreManager.StartAutoRefresh(context.Background(), interval)
|
||||||
log.Infof("core auth auto-refresh started (interval=%s)", interval)
|
log.Infof("core auth auto-refresh started (interval=%s)", interval)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user