mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-03 04:50:52 +08:00
Merge pull request #60 from router-for-me/v6-test
Move gemini-web to provider
This commit is contained in:
@@ -30,3 +30,4 @@ config.yaml
|
||||
bin/*
|
||||
.claude/*
|
||||
.vscode/*
|
||||
.serena/*
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -7,8 +7,8 @@ auths/*
|
||||
!auths/.gitkeep
|
||||
.vscode/*
|
||||
.claude/*
|
||||
.serena/*
|
||||
AGENTS.md
|
||||
CLAUDE.md
|
||||
*.exe
|
||||
temp/*
|
||||
.serena/
|
||||
@@ -1,131 +0,0 @@
|
||||
package geminiwebapi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// init honors GEMINI_WEBAPI_LOG to keep parity with the Python client.
|
||||
func init() {
|
||||
if lvl := os.Getenv("GEMINI_WEBAPI_LOG"); lvl != "" {
|
||||
SetLogLevel(lvl)
|
||||
}
|
||||
}
|
||||
|
||||
// SetLogLevel adjusts logging verbosity using CLI-style strings.
|
||||
func SetLogLevel(level string) {
|
||||
switch strings.ToUpper(level) {
|
||||
case "TRACE":
|
||||
log.SetLevel(log.TraceLevel)
|
||||
case "DEBUG":
|
||||
log.SetLevel(log.DebugLevel)
|
||||
case "INFO":
|
||||
log.SetLevel(log.InfoLevel)
|
||||
case "WARNING", "WARN":
|
||||
log.SetLevel(log.WarnLevel)
|
||||
case "ERROR":
|
||||
log.SetLevel(log.ErrorLevel)
|
||||
case "CRITICAL", "FATAL":
|
||||
log.SetLevel(log.FatalLevel)
|
||||
default:
|
||||
log.SetLevel(log.InfoLevel)
|
||||
}
|
||||
}
|
||||
|
||||
func prefix(format string) string { return "[gemini_webapi] " + format }
|
||||
|
||||
func Debug(format string, v ...any) { log.Debugf(prefix(format), v...) }
|
||||
|
||||
// DebugRaw logs without the module prefix; use sparingly for messages
|
||||
// that should integrate with global formatting without extra tags.
|
||||
func DebugRaw(format string, v ...any) { log.Debugf(format, v...) }
|
||||
func Info(format string, v ...any) { log.Infof(prefix(format), v...) }
|
||||
func Warning(format string, v ...any) { log.Warnf(prefix(format), v...) }
|
||||
func Error(format string, v ...any) { log.Errorf(prefix(format), v...) }
|
||||
func Success(format string, v ...any) { log.Infof(prefix("SUCCESS "+format), v...) }
|
||||
|
||||
// MaskToken28 returns a fixed-length (28) masked representation showing:
|
||||
// first 8 chars + 8 asterisks + 4 middle chars + last 8 chars.
|
||||
// If the input is shorter than 20 characters, it returns a fully masked string
|
||||
// of length min(len(s), 28).
|
||||
func MaskToken28(s string) string {
|
||||
n := len(s)
|
||||
if n == 0 {
|
||||
return ""
|
||||
}
|
||||
if n < 20 {
|
||||
return strings.Repeat("*", n)
|
||||
}
|
||||
// Pick 4 middle characters around the center
|
||||
midStart := n/2 - 2
|
||||
if midStart < 8 {
|
||||
midStart = 8
|
||||
}
|
||||
if midStart+4 > n-8 {
|
||||
midStart = n - 8 - 4
|
||||
if midStart < 8 {
|
||||
midStart = 8
|
||||
}
|
||||
}
|
||||
prefixByte := s[:8]
|
||||
middle := s[midStart : midStart+4]
|
||||
suffix := s[n-8:]
|
||||
return prefixByte + strings.Repeat("*", 4) + middle + strings.Repeat("*", 4) + suffix
|
||||
}
|
||||
|
||||
// BuildUpstreamRequestLog builds a compact preview string for upstream request logging.
|
||||
func BuildUpstreamRequestLog(account string, contextOn bool, useTags, explicitContext bool, prompt string, filesCount int, reuse bool, metaLen int, gem *Gem) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString("\n\n=== GEMINI WEB UPSTREAM ===\n")
|
||||
sb.WriteString(fmt.Sprintf("account: %s\n", account))
|
||||
if contextOn {
|
||||
sb.WriteString("context_mode: on\n")
|
||||
} else {
|
||||
sb.WriteString("context_mode: off\n")
|
||||
}
|
||||
if reuse {
|
||||
sb.WriteString("reuseIdx: 1\n")
|
||||
} else {
|
||||
sb.WriteString("reuseIdx: 0\n")
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("useTags: %t\n", useTags))
|
||||
sb.WriteString(fmt.Sprintf("metadata_len: %d\n", metaLen))
|
||||
if explicitContext {
|
||||
sb.WriteString("explicit_context: true\n")
|
||||
} else {
|
||||
sb.WriteString("explicit_context: false\n")
|
||||
}
|
||||
if filesCount > 0 {
|
||||
sb.WriteString(fmt.Sprintf("files: %d\n", filesCount))
|
||||
}
|
||||
|
||||
if gem != nil {
|
||||
sb.WriteString("gem:\n")
|
||||
if gem.ID != "" {
|
||||
sb.WriteString(fmt.Sprintf(" id: %s\n", gem.ID))
|
||||
}
|
||||
if gem.Name != "" {
|
||||
sb.WriteString(fmt.Sprintf(" name: %s\n", gem.Name))
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf(" predefined: %t\n", gem.Predefined))
|
||||
} else {
|
||||
sb.WriteString("gem: none\n")
|
||||
}
|
||||
|
||||
chunks := ChunkByRunes(prompt, 4096)
|
||||
preview := prompt
|
||||
truncated := false
|
||||
if len(chunks) > 1 {
|
||||
preview = chunks[0]
|
||||
truncated = true
|
||||
}
|
||||
sb.WriteString("prompt_preview:\n")
|
||||
sb.WriteString(preview)
|
||||
if truncated {
|
||||
sb.WriteString("\n... [truncated]\n")
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
@@ -12,6 +12,8 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type httpOptions struct {
|
||||
@@ -103,7 +105,7 @@ func getAccessToken(baseCookies map[string]string, proxy string, verbose bool, i
|
||||
}
|
||||
trySets = append(trySets, merged)
|
||||
} else if verbose {
|
||||
Debug("Skipping base cookies: __Secure-1PSIDTS missing")
|
||||
log.Debug("Skipping base cookies: __Secure-1PSIDTS missing")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,7 +132,7 @@ func getAccessToken(baseCookies map[string]string, proxy string, verbose bool, i
|
||||
resp, mergedCookies, err := sendInitRequest(cookies, proxy, insecure)
|
||||
if err != nil {
|
||||
if verbose {
|
||||
Warning("Failed init request: %v", err)
|
||||
log.Warnf("Failed init request: %v", err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
@@ -143,7 +145,7 @@ func getAccessToken(baseCookies map[string]string, proxy string, verbose bool, i
|
||||
if len(matches) >= 2 {
|
||||
token := matches[1]
|
||||
if verbose {
|
||||
Success("Gemini access token acquired.")
|
||||
log.Infof("Gemini access token acquired.")
|
||||
}
|
||||
return token, mergedCookies, nil
|
||||
}
|
||||
@@ -212,3 +214,27 @@ func (r *constReader) Read(p []byte) (int, error) {
|
||||
}
|
||||
|
||||
func stringsReader(s string) io.Reader { return &constReader{s: s} }
|
||||
|
||||
func MaskToken28(s string) string {
|
||||
n := len(s)
|
||||
if n == 0 {
|
||||
return ""
|
||||
}
|
||||
if n < 20 {
|
||||
return strings.Repeat("*", n)
|
||||
}
|
||||
midStart := n/2 - 2
|
||||
if midStart < 8 {
|
||||
midStart = 8
|
||||
}
|
||||
if midStart+4 > n-8 {
|
||||
midStart = n - 8 - 4
|
||||
if midStart < 8 {
|
||||
midStart = 8
|
||||
}
|
||||
}
|
||||
prefixByte := s[:8]
|
||||
middle := s[midStart : midStart+4]
|
||||
suffix := s[n-8:]
|
||||
return prefixByte + strings.Repeat("*", 4) + middle + strings.Repeat("*", 4) + suffix
|
||||
}
|
||||
@@ -10,6 +10,8 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// GeminiClient is the async http client interface (Go port)
|
||||
@@ -79,7 +81,7 @@ func (c *GeminiClient) Init(timeoutSec float64, verbose bool) error {
|
||||
|
||||
c.Timeout = time.Duration(timeoutSec * float64(time.Second))
|
||||
if verbose {
|
||||
Success("Gemini client initialized successfully.")
|
||||
log.Infof("Gemini client initialized successfully.")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces"
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
@@ -58,7 +59,7 @@ func (i Image) Save(path string, filename string, cookies map[string]string, ver
|
||||
filename = m[1]
|
||||
} else {
|
||||
if verbose {
|
||||
Warning("Invalid filename: %s", filename)
|
||||
log.Warnf("Invalid filename: %s", filename)
|
||||
}
|
||||
if skipInvalidFilename {
|
||||
return "", nil
|
||||
@@ -125,7 +126,7 @@ func (i Image) Save(path string, filename string, cookies map[string]string, ver
|
||||
return "", fmt.Errorf("error downloading image: %d %s", resp.StatusCode, resp.Status)
|
||||
}
|
||||
if ct := resp.Header.Get("Content-Type"); ct != "" && !strings.Contains(strings.ToLower(ct), "image") {
|
||||
Warning("Content type of %s is not image, but %s.", filename, ct)
|
||||
log.Warnf("Content type of %s is not image, but %s.", filename, ct)
|
||||
}
|
||||
if path == "" {
|
||||
path = "temp"
|
||||
@@ -144,7 +145,7 @@ func (i Image) Save(path string, filename string, cookies map[string]string, ver
|
||||
return "", err
|
||||
}
|
||||
if verbose {
|
||||
Info("Image saved as %s", dest)
|
||||
log.Infof("Image saved as %s", dest)
|
||||
}
|
||||
abspath, _ := filepath.Abs(dest)
|
||||
return abspath, nil
|
||||
@@ -1,4 +1,4 @@
|
||||
package executor
|
||||
package geminiwebapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -10,8 +10,8 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/gemini"
|
||||
geminiwebapi "github.com/router-for-me/CLIProxyAPI/v6/internal/client/gemini-web"
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/constant"
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces"
|
||||
@@ -25,7 +25,7 @@ const (
|
||||
geminiWebDefaultTimeoutSec = 300
|
||||
)
|
||||
|
||||
type geminiWebState struct {
|
||||
type GeminiWebState struct {
|
||||
cfg *config.Config
|
||||
token *gemini.GeminiWebTokenStorage
|
||||
storagePath string
|
||||
@@ -34,29 +34,29 @@ type geminiWebState struct {
|
||||
accountID string
|
||||
|
||||
reqMu sync.Mutex
|
||||
client *geminiwebapi.GeminiClient
|
||||
client *GeminiClient
|
||||
|
||||
tokenMu sync.Mutex
|
||||
tokenDirty bool
|
||||
|
||||
convMu sync.RWMutex
|
||||
convStore map[string][]string
|
||||
convData map[string]geminiwebapi.ConversationRecord
|
||||
convData map[string]ConversationRecord
|
||||
convIndex map[string]string
|
||||
|
||||
lastRefresh time.Time
|
||||
}
|
||||
|
||||
func newGeminiWebState(cfg *config.Config, token *gemini.GeminiWebTokenStorage, storagePath string) *geminiWebState {
|
||||
state := &geminiWebState{
|
||||
func NewGeminiWebState(cfg *config.Config, token *gemini.GeminiWebTokenStorage, storagePath string) *GeminiWebState {
|
||||
state := &GeminiWebState{
|
||||
cfg: cfg,
|
||||
token: token,
|
||||
storagePath: storagePath,
|
||||
convStore: make(map[string][]string),
|
||||
convData: make(map[string]geminiwebapi.ConversationRecord),
|
||||
convData: make(map[string]ConversationRecord),
|
||||
convIndex: make(map[string]string),
|
||||
}
|
||||
suffix := geminiwebapi.Sha256Hex(token.Secure1PSID)
|
||||
suffix := Sha256Hex(token.Secure1PSID)
|
||||
if len(suffix) > 16 {
|
||||
suffix = suffix[:16]
|
||||
}
|
||||
@@ -75,39 +75,39 @@ func newGeminiWebState(cfg *config.Config, token *gemini.GeminiWebTokenStorage,
|
||||
return state
|
||||
}
|
||||
|
||||
func (s *geminiWebState) loadConversationCaches() {
|
||||
func (s *GeminiWebState) loadConversationCaches() {
|
||||
if path := s.convStorePath(); path != "" {
|
||||
if store, err := geminiwebapi.LoadConvStore(path); err == nil {
|
||||
if store, err := LoadConvStore(path); err == nil {
|
||||
s.convStore = store
|
||||
}
|
||||
}
|
||||
if path := s.convDataPath(); path != "" {
|
||||
if items, index, err := geminiwebapi.LoadConvData(path); err == nil {
|
||||
if items, index, err := LoadConvData(path); err == nil {
|
||||
s.convData = items
|
||||
s.convIndex = index
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *geminiWebState) convStorePath() string {
|
||||
func (s *GeminiWebState) convStorePath() string {
|
||||
base := s.storagePath
|
||||
if base == "" {
|
||||
base = s.accountID + ".json"
|
||||
}
|
||||
return geminiwebapi.ConvStorePath(base)
|
||||
return ConvStorePath(base)
|
||||
}
|
||||
|
||||
func (s *geminiWebState) convDataPath() string {
|
||||
func (s *GeminiWebState) convDataPath() string {
|
||||
base := s.storagePath
|
||||
if base == "" {
|
||||
base = s.accountID + ".json"
|
||||
}
|
||||
return geminiwebapi.ConvDataPath(base)
|
||||
return ConvDataPath(base)
|
||||
}
|
||||
|
||||
func (s *geminiWebState) getRequestMutex() *sync.Mutex { return &s.reqMu }
|
||||
func (s *GeminiWebState) GetRequestMutex() *sync.Mutex { return &s.reqMu }
|
||||
|
||||
func (s *geminiWebState) ensureClient() error {
|
||||
func (s *GeminiWebState) EnsureClient() error {
|
||||
if s.client != nil && s.client.Running {
|
||||
return nil
|
||||
}
|
||||
@@ -115,7 +115,7 @@ func (s *geminiWebState) ensureClient() error {
|
||||
if s.cfg != nil {
|
||||
proxyURL = s.cfg.ProxyURL
|
||||
}
|
||||
s.client = geminiwebapi.NewGeminiClient(
|
||||
s.client = NewGeminiClient(
|
||||
s.token.Secure1PSID,
|
||||
s.token.Secure1PSIDTS,
|
||||
proxyURL,
|
||||
@@ -129,13 +129,13 @@ func (s *geminiWebState) ensureClient() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *geminiWebState) refresh(ctx context.Context) error {
|
||||
func (s *GeminiWebState) Refresh(ctx context.Context) error {
|
||||
_ = ctx
|
||||
proxyURL := ""
|
||||
if s.cfg != nil {
|
||||
proxyURL = s.cfg.ProxyURL
|
||||
}
|
||||
s.client = geminiwebapi.NewGeminiClient(
|
||||
s.client = NewGeminiClient(
|
||||
s.token.Secure1PSID,
|
||||
s.token.Secure1PSIDTS,
|
||||
proxyURL,
|
||||
@@ -158,7 +158,7 @@ func (s *geminiWebState) refresh(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *geminiWebState) tokenSnapshot() *gemini.GeminiWebTokenStorage {
|
||||
func (s *GeminiWebState) TokenSnapshot() *gemini.GeminiWebTokenStorage {
|
||||
s.tokenMu.Lock()
|
||||
defer s.tokenMu.Unlock()
|
||||
c := *s.token
|
||||
@@ -170,15 +170,15 @@ type geminiWebPrepared struct {
|
||||
translatedRaw []byte
|
||||
prompt string
|
||||
uploaded []string
|
||||
chat *geminiwebapi.ChatSession
|
||||
cleaned []geminiwebapi.RoleText
|
||||
chat *ChatSession
|
||||
cleaned []RoleText
|
||||
underlying string
|
||||
reuse bool
|
||||
tagged bool
|
||||
originalRaw []byte
|
||||
}
|
||||
|
||||
func (s *geminiWebState) prepare(ctx context.Context, modelName string, rawJSON []byte, stream bool, original []byte) (*geminiWebPrepared, *interfaces.ErrorMessage) {
|
||||
func (s *GeminiWebState) prepare(ctx context.Context, modelName string, rawJSON []byte, stream bool, original []byte) (*geminiWebPrepared, *interfaces.ErrorMessage) {
|
||||
res := &geminiWebPrepared{originalRaw: original}
|
||||
res.translatedRaw = bytes.Clone(rawJSON)
|
||||
if handler, ok := ctx.Value("handler").(interfaces.APIHandler); ok && handler != nil {
|
||||
@@ -187,14 +187,14 @@ func (s *geminiWebState) prepare(ctx context.Context, modelName string, rawJSON
|
||||
}
|
||||
recordAPIRequest(ctx, s.cfg, res.translatedRaw)
|
||||
|
||||
messages, files, mimes, msgFileIdx, err := geminiwebapi.ParseMessagesAndFiles(res.translatedRaw)
|
||||
messages, files, mimes, msgFileIdx, err := ParseMessagesAndFiles(res.translatedRaw)
|
||||
if err != nil {
|
||||
return nil, &interfaces.ErrorMessage{StatusCode: 400, Error: fmt.Errorf("bad request: %w", err)}
|
||||
}
|
||||
cleaned := geminiwebapi.SanitizeAssistantMessages(messages)
|
||||
cleaned := SanitizeAssistantMessages(messages)
|
||||
res.cleaned = cleaned
|
||||
res.underlying = geminiwebapi.MapAliasToUnderlying(modelName)
|
||||
model, err := geminiwebapi.ModelFromName(res.underlying)
|
||||
res.underlying = MapAliasToUnderlying(modelName)
|
||||
model, err := ModelFromName(res.underlying)
|
||||
if err != nil {
|
||||
return nil, &interfaces.ErrorMessage{StatusCode: 400, Error: err}
|
||||
}
|
||||
@@ -210,11 +210,11 @@ func (s *geminiWebState) prepare(ctx context.Context, modelName string, rawJSON
|
||||
res.reuse = true
|
||||
meta = reuseMeta
|
||||
if len(remaining) == 1 {
|
||||
useMsgs = []geminiwebapi.RoleText{remaining[0]}
|
||||
useMsgs = []RoleText{remaining[0]}
|
||||
} else if len(remaining) > 1 {
|
||||
useMsgs = remaining
|
||||
} else if len(cleaned) > 0 {
|
||||
useMsgs = []geminiwebapi.RoleText{cleaned[len(cleaned)-1]}
|
||||
useMsgs = []RoleText{cleaned[len(cleaned)-1]}
|
||||
}
|
||||
if len(useMsgs) == 1 && len(messages) > 0 && len(msgFileIdx) == len(messages) {
|
||||
lastIdx := len(msgFileIdx) - 1
|
||||
@@ -242,8 +242,8 @@ func (s *geminiWebState) prepare(ctx context.Context, modelName string, rawJSON
|
||||
}
|
||||
} else {
|
||||
if len(cleaned) >= 2 && strings.EqualFold(cleaned[len(cleaned)-2].Role, "assistant") {
|
||||
keyUnderlying := geminiwebapi.AccountMetaKey(s.accountID, res.underlying)
|
||||
keyAlias := geminiwebapi.AccountMetaKey(s.accountID, modelName)
|
||||
keyUnderlying := AccountMetaKey(s.accountID, res.underlying)
|
||||
keyAlias := AccountMetaKey(s.accountID, modelName)
|
||||
s.convMu.RLock()
|
||||
fallbackMeta := s.convStore[keyUnderlying]
|
||||
if len(fallbackMeta) == 0 {
|
||||
@@ -252,7 +252,7 @@ func (s *geminiWebState) prepare(ctx context.Context, modelName string, rawJSON
|
||||
s.convMu.RUnlock()
|
||||
if len(fallbackMeta) > 0 {
|
||||
meta = fallbackMeta
|
||||
useMsgs = []geminiwebapi.RoleText{cleaned[len(cleaned)-1]}
|
||||
useMsgs = []RoleText{cleaned[len(cleaned)-1]}
|
||||
res.reuse = true
|
||||
filesSubset = nil
|
||||
mimesSubset = nil
|
||||
@@ -260,8 +260,8 @@ func (s *geminiWebState) prepare(ctx context.Context, modelName string, rawJSON
|
||||
}
|
||||
}
|
||||
} else {
|
||||
keyUnderlying := geminiwebapi.AccountMetaKey(s.accountID, res.underlying)
|
||||
keyAlias := geminiwebapi.AccountMetaKey(s.accountID, modelName)
|
||||
keyUnderlying := AccountMetaKey(s.accountID, res.underlying)
|
||||
keyAlias := AccountMetaKey(s.accountID, modelName)
|
||||
s.convMu.RLock()
|
||||
if v, ok := s.convStore[keyUnderlying]; ok && len(v) > 0 {
|
||||
meta = v
|
||||
@@ -271,26 +271,26 @@ func (s *geminiWebState) prepare(ctx context.Context, modelName string, rawJSON
|
||||
s.convMu.RUnlock()
|
||||
}
|
||||
|
||||
res.tagged = geminiwebapi.NeedRoleTags(useMsgs)
|
||||
res.tagged = NeedRoleTags(useMsgs)
|
||||
if res.reuse && len(useMsgs) == 1 {
|
||||
res.tagged = false
|
||||
}
|
||||
|
||||
enableXML := s.cfg != nil && s.cfg.GeminiWeb.CodeMode
|
||||
useMsgs = geminiwebapi.AppendXMLWrapHintIfNeeded(useMsgs, !enableXML)
|
||||
useMsgs = AppendXMLWrapHintIfNeeded(useMsgs, !enableXML)
|
||||
|
||||
res.prompt = geminiwebapi.BuildPrompt(useMsgs, res.tagged, res.tagged)
|
||||
res.prompt = BuildPrompt(useMsgs, res.tagged, res.tagged)
|
||||
if strings.TrimSpace(res.prompt) == "" {
|
||||
return nil, &interfaces.ErrorMessage{StatusCode: 400, Error: errors.New("bad request: empty prompt after filtering system/thought content")}
|
||||
}
|
||||
|
||||
uploaded, upErr := geminiwebapi.MaterializeInlineFiles(filesSubset, mimesSubset)
|
||||
uploaded, upErr := MaterializeInlineFiles(filesSubset, mimesSubset)
|
||||
if upErr != nil {
|
||||
return nil, upErr
|
||||
}
|
||||
res.uploaded = uploaded
|
||||
|
||||
if err = s.ensureClient(); err != nil {
|
||||
if err = s.EnsureClient(); err != nil {
|
||||
return nil, &interfaces.ErrorMessage{StatusCode: 500, Error: err}
|
||||
}
|
||||
chat := s.client.StartChat(model, s.getConfiguredGem(), meta)
|
||||
@@ -300,14 +300,14 @@ func (s *geminiWebState) prepare(ctx context.Context, modelName string, rawJSON
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (s *geminiWebState) send(ctx context.Context, modelName string, reqPayload []byte, opts cliproxyexecutor.Options) ([]byte, *interfaces.ErrorMessage, *geminiWebPrepared) {
|
||||
func (s *GeminiWebState) Send(ctx context.Context, modelName string, reqPayload []byte, opts cliproxyexecutor.Options) ([]byte, *interfaces.ErrorMessage, *geminiWebPrepared) {
|
||||
prep, errMsg := s.prepare(ctx, modelName, reqPayload, opts.Stream, opts.OriginalRequest)
|
||||
if errMsg != nil {
|
||||
return nil, errMsg, nil
|
||||
}
|
||||
defer geminiwebapi.CleanupFiles(prep.uploaded)
|
||||
defer CleanupFiles(prep.uploaded)
|
||||
|
||||
output, err := geminiwebapi.SendWithSplit(prep.chat, prep.prompt, prep.uploaded, s.cfg)
|
||||
output, err := SendWithSplit(prep.chat, prep.prompt, prep.uploaded, s.cfg)
|
||||
if err != nil {
|
||||
return nil, s.wrapSendError(err), nil
|
||||
}
|
||||
@@ -331,7 +331,7 @@ func (s *geminiWebState) send(ctx context.Context, modelName string, reqPayload
|
||||
}
|
||||
}
|
||||
|
||||
gemBytes, err := geminiwebapi.ConvertOutputToGemini(&output, modelName, prep.prompt)
|
||||
gemBytes, err := ConvertOutputToGemini(&output, modelName, prep.prompt)
|
||||
if err != nil {
|
||||
return nil, &interfaces.ErrorMessage{StatusCode: 500, Error: err}, nil
|
||||
}
|
||||
@@ -341,13 +341,13 @@ func (s *geminiWebState) send(ctx context.Context, modelName string, reqPayload
|
||||
return gemBytes, nil, prep
|
||||
}
|
||||
|
||||
func (s *geminiWebState) wrapSendError(genErr error) *interfaces.ErrorMessage {
|
||||
func (s *GeminiWebState) wrapSendError(genErr error) *interfaces.ErrorMessage {
|
||||
status := 500
|
||||
var usage *geminiwebapi.UsageLimitExceeded
|
||||
var blocked *geminiwebapi.TemporarilyBlocked
|
||||
var invalid *geminiwebapi.ModelInvalid
|
||||
var valueErr *geminiwebapi.ValueError
|
||||
var timeout *geminiwebapi.TimeoutError
|
||||
var usage *UsageLimitExceeded
|
||||
var blocked *TemporarilyBlocked
|
||||
var invalid *ModelInvalid
|
||||
var valueErr *ValueError
|
||||
var timeout *TimeoutError
|
||||
switch {
|
||||
case errors.As(genErr, &usage):
|
||||
status = 429
|
||||
@@ -363,14 +363,14 @@ func (s *geminiWebState) wrapSendError(genErr error) *interfaces.ErrorMessage {
|
||||
return &interfaces.ErrorMessage{StatusCode: status, Error: genErr}
|
||||
}
|
||||
|
||||
func (s *geminiWebState) persistConversation(modelName string, prep *geminiWebPrepared, output *geminiwebapi.ModelOutput) {
|
||||
func (s *GeminiWebState) persistConversation(modelName string, prep *geminiWebPrepared, output *ModelOutput) {
|
||||
if output == nil || prep == nil || prep.chat == nil {
|
||||
return
|
||||
}
|
||||
metadata := prep.chat.Metadata()
|
||||
if len(metadata) > 0 {
|
||||
keyUnderlying := geminiwebapi.AccountMetaKey(s.accountID, prep.underlying)
|
||||
keyAlias := geminiwebapi.AccountMetaKey(s.accountID, modelName)
|
||||
keyUnderlying := AccountMetaKey(s.accountID, prep.underlying)
|
||||
keyAlias := AccountMetaKey(s.accountID, modelName)
|
||||
s.convMu.Lock()
|
||||
s.convStore[keyUnderlying] = metadata
|
||||
s.convStore[keyAlias] = metadata
|
||||
@@ -384,18 +384,18 @@ func (s *geminiWebState) persistConversation(modelName string, prep *geminiWebPr
|
||||
storeSnapshot[k] = cp
|
||||
}
|
||||
s.convMu.Unlock()
|
||||
_ = geminiwebapi.SaveConvStore(s.convStorePath(), storeSnapshot)
|
||||
_ = SaveConvStore(s.convStorePath(), storeSnapshot)
|
||||
}
|
||||
|
||||
if !s.useReusableContext() {
|
||||
return
|
||||
}
|
||||
rec, ok := geminiwebapi.BuildConversationRecord(prep.underlying, s.stableClientID, prep.cleaned, output, metadata)
|
||||
rec, ok := BuildConversationRecord(prep.underlying, s.stableClientID, prep.cleaned, output, metadata)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
stableHash := geminiwebapi.HashConversation(rec.ClientID, prep.underlying, rec.Messages)
|
||||
accountHash := geminiwebapi.HashConversation(s.accountID, prep.underlying, rec.Messages)
|
||||
stableHash := HashConversation(rec.ClientID, prep.underlying, rec.Messages)
|
||||
accountHash := HashConversation(s.accountID, prep.underlying, rec.Messages)
|
||||
|
||||
s.convMu.Lock()
|
||||
s.convData[stableHash] = rec
|
||||
@@ -403,7 +403,7 @@ func (s *geminiWebState) persistConversation(modelName string, prep *geminiWebPr
|
||||
if accountHash != stableHash {
|
||||
s.convIndex["hash:"+accountHash] = stableHash
|
||||
}
|
||||
dataSnapshot := make(map[string]geminiwebapi.ConversationRecord, len(s.convData))
|
||||
dataSnapshot := make(map[string]ConversationRecord, len(s.convData))
|
||||
for k, v := range s.convData {
|
||||
dataSnapshot[k] = v
|
||||
}
|
||||
@@ -412,14 +412,14 @@ func (s *geminiWebState) persistConversation(modelName string, prep *geminiWebPr
|
||||
indexSnapshot[k] = v
|
||||
}
|
||||
s.convMu.Unlock()
|
||||
_ = geminiwebapi.SaveConvData(s.convDataPath(), dataSnapshot, indexSnapshot)
|
||||
_ = SaveConvData(s.convDataPath(), dataSnapshot, indexSnapshot)
|
||||
}
|
||||
|
||||
func (s *geminiWebState) addAPIResponseData(ctx context.Context, line []byte) {
|
||||
func (s *GeminiWebState) addAPIResponseData(ctx context.Context, line []byte) {
|
||||
appendAPIResponseChunk(ctx, s.cfg, line)
|
||||
}
|
||||
|
||||
func (s *geminiWebState) convertToTarget(ctx context.Context, modelName string, prep *geminiWebPrepared, gemBytes []byte) []byte {
|
||||
func (s *GeminiWebState) ConvertToTarget(ctx context.Context, modelName string, prep *geminiWebPrepared, gemBytes []byte) []byte {
|
||||
if prep == nil || prep.handlerType == "" {
|
||||
return gemBytes
|
||||
}
|
||||
@@ -437,7 +437,7 @@ func (s *geminiWebState) convertToTarget(ctx context.Context, modelName string,
|
||||
return []byte(out)
|
||||
}
|
||||
|
||||
func (s *geminiWebState) convertStream(ctx context.Context, modelName string, prep *geminiWebPrepared, gemBytes []byte) []string {
|
||||
func (s *GeminiWebState) ConvertStream(ctx context.Context, modelName string, prep *geminiWebPrepared, gemBytes []byte) []string {
|
||||
if prep == nil || prep.handlerType == "" {
|
||||
return []string{string(gemBytes)}
|
||||
}
|
||||
@@ -448,7 +448,7 @@ func (s *geminiWebState) convertStream(ctx context.Context, modelName string, pr
|
||||
return translator.Response(prep.handlerType, constant.GeminiWeb, ctx, modelName, prep.originalRaw, prep.translatedRaw, gemBytes, ¶m)
|
||||
}
|
||||
|
||||
func (s *geminiWebState) doneStream(ctx context.Context, modelName string, prep *geminiWebPrepared) []string {
|
||||
func (s *GeminiWebState) DoneStream(ctx context.Context, modelName string, prep *geminiWebPrepared) []string {
|
||||
if prep == nil || prep.handlerType == "" {
|
||||
return nil
|
||||
}
|
||||
@@ -459,24 +459,56 @@ func (s *geminiWebState) doneStream(ctx context.Context, modelName string, prep
|
||||
return translator.Response(prep.handlerType, constant.GeminiWeb, ctx, modelName, prep.originalRaw, prep.translatedRaw, []byte("[DONE]"), ¶m)
|
||||
}
|
||||
|
||||
func (s *geminiWebState) useReusableContext() bool {
|
||||
func (s *GeminiWebState) useReusableContext() bool {
|
||||
if s.cfg == nil {
|
||||
return true
|
||||
}
|
||||
return s.cfg.GeminiWeb.Context
|
||||
}
|
||||
|
||||
func (s *geminiWebState) findReusableSession(modelName string, msgs []geminiwebapi.RoleText) ([]string, []geminiwebapi.RoleText) {
|
||||
func (s *GeminiWebState) findReusableSession(modelName string, msgs []RoleText) ([]string, []RoleText) {
|
||||
s.convMu.RLock()
|
||||
items := s.convData
|
||||
index := s.convIndex
|
||||
s.convMu.RUnlock()
|
||||
return geminiwebapi.FindReusableSessionIn(items, index, s.stableClientID, s.accountID, modelName, msgs)
|
||||
return FindReusableSessionIn(items, index, s.stableClientID, s.accountID, modelName, msgs)
|
||||
}
|
||||
|
||||
func (s *geminiWebState) getConfiguredGem() *geminiwebapi.Gem {
|
||||
func (s *GeminiWebState) getConfiguredGem() *Gem {
|
||||
if s.cfg != nil && s.cfg.GeminiWeb.CodeMode {
|
||||
return &geminiwebapi.Gem{ID: "coding-partner", Name: "Coding partner", Predefined: true}
|
||||
return &Gem{ID: "coding-partner", Name: "Coding partner", Predefined: true}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// recordAPIRequest stores the upstream request payload in Gin context for request logging.
|
||||
func recordAPIRequest(ctx context.Context, cfg *config.Config, payload []byte) {
|
||||
if cfg == nil || !cfg.RequestLog || len(payload) == 0 {
|
||||
return
|
||||
}
|
||||
if ginCtx, ok := ctx.Value("gin").(*gin.Context); ok && ginCtx != nil {
|
||||
ginCtx.Set("API_REQUEST", bytes.Clone(payload))
|
||||
}
|
||||
}
|
||||
|
||||
// appendAPIResponseChunk appends an upstream response chunk to Gin context for request logging.
|
||||
func appendAPIResponseChunk(ctx context.Context, cfg *config.Config, chunk []byte) {
|
||||
if cfg == nil || !cfg.RequestLog {
|
||||
return
|
||||
}
|
||||
data := bytes.TrimSpace(bytes.Clone(chunk))
|
||||
if len(data) == 0 {
|
||||
return
|
||||
}
|
||||
if ginCtx, ok := ctx.Value("gin").(*gin.Context); ok && ginCtx != nil {
|
||||
if existing, exists := ginCtx.Get("API_RESPONSE"); exists {
|
||||
if prev, okBytes := existing.([]byte); okBytes {
|
||||
prev = append(prev, data...)
|
||||
prev = append(prev, []byte("\n\n")...)
|
||||
ginCtx.Set("API_RESPONSE", prev)
|
||||
return
|
||||
}
|
||||
}
|
||||
ginCtx.Set("API_RESPONSE", data)
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/gemini"
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces"
|
||||
geminiwebapi "github.com/router-for-me/CLIProxyAPI/v6/internal/provider/gemini-web"
|
||||
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
||||
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
|
||||
sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator"
|
||||
@@ -35,23 +36,23 @@ func (e *GeminiWebExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth
|
||||
if err != nil {
|
||||
return cliproxyexecutor.Response{}, err
|
||||
}
|
||||
if err = state.ensureClient(); err != nil {
|
||||
if err = state.EnsureClient(); err != nil {
|
||||
return cliproxyexecutor.Response{}, err
|
||||
}
|
||||
reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth)
|
||||
|
||||
mutex := state.getRequestMutex()
|
||||
mutex := state.GetRequestMutex()
|
||||
if mutex != nil {
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
}
|
||||
|
||||
payload := bytes.Clone(req.Payload)
|
||||
resp, errMsg, prep := state.send(ctx, req.Model, payload, opts)
|
||||
resp, errMsg, prep := state.Send(ctx, req.Model, payload, opts)
|
||||
if errMsg != nil {
|
||||
return cliproxyexecutor.Response{}, geminiWebErrorFromMessage(errMsg)
|
||||
}
|
||||
resp = state.convertToTarget(ctx, req.Model, prep, resp)
|
||||
resp = state.ConvertToTarget(ctx, req.Model, prep, resp)
|
||||
reporter.publish(ctx, parseGeminiUsage(resp))
|
||||
|
||||
from := opts.SourceFormat
|
||||
@@ -67,17 +68,17 @@ func (e *GeminiWebExecutor) ExecuteStream(ctx context.Context, auth *cliproxyaut
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = state.ensureClient(); err != nil {
|
||||
if err = state.EnsureClient(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reporter := newUsageReporter(ctx, e.Identifier(), req.Model, auth)
|
||||
|
||||
mutex := state.getRequestMutex()
|
||||
mutex := state.GetRequestMutex()
|
||||
if mutex != nil {
|
||||
mutex.Lock()
|
||||
}
|
||||
|
||||
gemBytes, errMsg, prep := state.send(ctx, req.Model, bytes.Clone(req.Payload), opts)
|
||||
gemBytes, errMsg, prep := state.Send(ctx, req.Model, bytes.Clone(req.Payload), opts)
|
||||
if errMsg != nil {
|
||||
if mutex != nil {
|
||||
mutex.Unlock()
|
||||
@@ -90,8 +91,8 @@ func (e *GeminiWebExecutor) ExecuteStream(ctx context.Context, auth *cliproxyaut
|
||||
to := sdktranslator.FromString("gemini-web")
|
||||
var param any
|
||||
|
||||
lines := state.convertStream(ctx, req.Model, prep, gemBytes)
|
||||
done := state.doneStream(ctx, req.Model, prep)
|
||||
lines := state.ConvertStream(ctx, req.Model, prep, gemBytes)
|
||||
done := state.DoneStream(ctx, req.Model, prep)
|
||||
out := make(chan cliproxyexecutor.StreamChunk)
|
||||
go func() {
|
||||
defer close(out)
|
||||
@@ -124,10 +125,10 @@ func (e *GeminiWebExecutor) Refresh(ctx context.Context, auth *cliproxyauth.Auth
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = state.refresh(ctx); err != nil {
|
||||
if err = state.Refresh(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ts := state.tokenSnapshot()
|
||||
ts := state.TokenSnapshot()
|
||||
if auth.Metadata == nil {
|
||||
auth.Metadata = make(map[string]any)
|
||||
}
|
||||
@@ -139,10 +140,10 @@ func (e *GeminiWebExecutor) Refresh(ctx context.Context, auth *cliproxyauth.Auth
|
||||
}
|
||||
|
||||
type geminiWebRuntime struct {
|
||||
state *geminiWebState
|
||||
state *geminiwebapi.GeminiWebState
|
||||
}
|
||||
|
||||
func (e *GeminiWebExecutor) stateFor(auth *cliproxyauth.Auth) (*geminiWebState, error) {
|
||||
func (e *GeminiWebExecutor) stateFor(auth *cliproxyauth.Auth) (*geminiwebapi.GeminiWebState, error) {
|
||||
if auth == nil {
|
||||
return nil, fmt.Errorf("gemini-web executor: auth is nil")
|
||||
}
|
||||
@@ -175,7 +176,7 @@ func (e *GeminiWebExecutor) stateFor(auth *cliproxyauth.Auth) (*geminiWebState,
|
||||
storagePath = p
|
||||
}
|
||||
}
|
||||
state := newGeminiWebState(cfg, ts, storagePath)
|
||||
state := geminiwebapi.NewGeminiWebState(cfg, ts, storagePath)
|
||||
runtime := &geminiWebRuntime{state: state}
|
||||
auth.Runtime = runtime
|
||||
return state, nil
|
||||
|
||||
@@ -1,280 +0,0 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
|
||||
)
|
||||
|
||||
const cookieSnapshotExt = ".cookie"
|
||||
|
||||
// CookieSnapshotPath derives the cookie snapshot file path from the main token JSON path.
|
||||
// It replaces the .json suffix with .cookie, or appends .cookie if missing.
|
||||
func CookieSnapshotPath(mainPath string) string {
|
||||
if strings.HasSuffix(mainPath, ".json") {
|
||||
return strings.TrimSuffix(mainPath, ".json") + cookieSnapshotExt
|
||||
}
|
||||
return mainPath + cookieSnapshotExt
|
||||
}
|
||||
|
||||
// IsRegularFile reports whether the given path exists and is a regular file.
|
||||
func IsRegularFile(path string) bool {
|
||||
if path == "" {
|
||||
return false
|
||||
}
|
||||
if st, err := os.Stat(path); err == nil && !st.IsDir() {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ReadJSON reads and unmarshals a JSON file into v.
|
||||
// Returns os.ErrNotExist if the file does not exist.
|
||||
func ReadJSON(path string, v any) error {
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
return err
|
||||
}
|
||||
if len(b) == 0 {
|
||||
return nil
|
||||
}
|
||||
return json.Unmarshal(b, v)
|
||||
}
|
||||
|
||||
// WriteJSON marshals v as JSON and writes to path, creating parent directories as needed.
|
||||
func WriteJSON(path string, v any) error {
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
|
||||
return err
|
||||
}
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = f.Close() }()
|
||||
enc := json.NewEncoder(f)
|
||||
return enc.Encode(v)
|
||||
}
|
||||
|
||||
// RemoveFile removes the file if it exists.
|
||||
func RemoveFile(path string) error {
|
||||
if IsRegularFile(path) {
|
||||
return os.Remove(path)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TryReadCookieSnapshotInto tries to read a cookie snapshot into v using the .cookie suffix.
|
||||
// Returns (true, nil) when a snapshot was decoded, or (false, nil) when none exists.
|
||||
func TryReadCookieSnapshotInto(mainPath string, v any) (bool, error) {
|
||||
snap := CookieSnapshotPath(mainPath)
|
||||
if err := ReadJSON(snap, v); err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// WriteCookieSnapshot writes v to the snapshot path derived from mainPath using the .cookie suffix.
|
||||
func WriteCookieSnapshot(mainPath string, v any) error {
|
||||
path := CookieSnapshotPath(mainPath)
|
||||
misc.LogSavingCredentials(path)
|
||||
if err := WriteJSON(path, v); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReadAuthFilePreferSnapshot returns the first non-empty auth payload preferring snapshots.
|
||||
func ReadAuthFilePreferSnapshot(path string) ([]byte, error) {
|
||||
return ReadAuthFileWithRetry(path, 1, 0)
|
||||
}
|
||||
|
||||
// ReadAuthFileWithRetry attempts to read an auth file multiple times and prefers cookie snapshots.
|
||||
func ReadAuthFileWithRetry(path string, attempts int, delay time.Duration) ([]byte, error) {
|
||||
if attempts < 1 {
|
||||
attempts = 1
|
||||
}
|
||||
read := func(target string) ([]byte, error) {
|
||||
var lastErr error
|
||||
for i := 0; i < attempts; i++ {
|
||||
data, err := os.ReadFile(target)
|
||||
if err == nil {
|
||||
return data, nil
|
||||
}
|
||||
lastErr = err
|
||||
if i < attempts-1 {
|
||||
time.Sleep(delay)
|
||||
}
|
||||
}
|
||||
return nil, lastErr
|
||||
}
|
||||
|
||||
candidates := []string{
|
||||
CookieSnapshotPath(path),
|
||||
path,
|
||||
}
|
||||
|
||||
for idx, candidate := range candidates {
|
||||
data, err := read(candidate)
|
||||
if err == nil {
|
||||
return data, nil
|
||||
}
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
if idx < len(candidates)-1 {
|
||||
continue
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
|
||||
// RemoveCookieSnapshots removes the snapshot file if it exists.
|
||||
func RemoveCookieSnapshots(mainPath string) {
|
||||
_ = RemoveFile(CookieSnapshotPath(mainPath))
|
||||
}
|
||||
|
||||
// Hooks provide customization points for snapshot lifecycle operations.
|
||||
type Hooks[T any] struct {
|
||||
// Apply merges snapshot data into the in-memory store during Apply().
|
||||
// Defaults to overwriting the store with the snapshot contents.
|
||||
Apply func(store *T, snapshot *T)
|
||||
|
||||
// Snapshot prepares the payload to persist during Persist().
|
||||
// Defaults to cloning the store value.
|
||||
Snapshot func(store *T) *T
|
||||
|
||||
// Merge chooses which data to flush when a snapshot exists.
|
||||
// Defaults to using the snapshot payload as-is.
|
||||
Merge func(store *T, snapshot *T) *T
|
||||
|
||||
// WriteMain persists the merged payload into the canonical token path.
|
||||
// Defaults to WriteJSON.
|
||||
WriteMain func(path string, data *T) error
|
||||
}
|
||||
|
||||
// Manager orchestrates cookie snapshot lifecycle for token storages.
|
||||
type Manager[T any] struct {
|
||||
mainPath string
|
||||
store *T
|
||||
hooks Hooks[T]
|
||||
}
|
||||
|
||||
// NewManager constructs a Manager bound to mainPath and store.
|
||||
func NewManager[T any](mainPath string, store *T, hooks Hooks[T]) *Manager[T] {
|
||||
return &Manager[T]{
|
||||
mainPath: mainPath,
|
||||
store: store,
|
||||
hooks: hooks,
|
||||
}
|
||||
}
|
||||
|
||||
// Apply loads snapshot data into the in-memory store if available.
|
||||
// Returns true when a snapshot was applied.
|
||||
func (m *Manager[T]) Apply() (bool, error) {
|
||||
if m == nil || m.store == nil || m.mainPath == "" {
|
||||
return false, nil
|
||||
}
|
||||
var snapshot T
|
||||
ok, err := TryReadCookieSnapshotInto(m.mainPath, &snapshot)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !ok {
|
||||
return false, nil
|
||||
}
|
||||
if m.hooks.Apply != nil {
|
||||
m.hooks.Apply(m.store, &snapshot)
|
||||
} else {
|
||||
*m.store = snapshot
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Persist writes the current store state to the snapshot file.
|
||||
func (m *Manager[T]) Persist() error {
|
||||
if m == nil || m.store == nil || m.mainPath == "" {
|
||||
return nil
|
||||
}
|
||||
var payload *T
|
||||
if m.hooks.Snapshot != nil {
|
||||
payload = m.hooks.Snapshot(m.store)
|
||||
} else {
|
||||
clone := new(T)
|
||||
*clone = *m.store
|
||||
payload = clone
|
||||
}
|
||||
return WriteCookieSnapshot(m.mainPath, payload)
|
||||
}
|
||||
|
||||
// FlushOptions configure Flush behaviour.
|
||||
type FlushOptions[T any] struct {
|
||||
Fallback func() *T
|
||||
Mutate func(*T)
|
||||
}
|
||||
|
||||
// FlushOption mutates FlushOptions.
|
||||
type FlushOption[T any] func(*FlushOptions[T])
|
||||
|
||||
// WithFallback provides fallback payload when no snapshot exists.
|
||||
func WithFallback[T any](fn func() *T) FlushOption[T] {
|
||||
return func(opts *FlushOptions[T]) { opts.Fallback = fn }
|
||||
}
|
||||
|
||||
// Flush commits snapshot (or fallback) into the main token file and removes the snapshot.
|
||||
func (m *Manager[T]) Flush(options ...FlushOption[T]) error {
|
||||
if m == nil || m.mainPath == "" {
|
||||
return nil
|
||||
}
|
||||
cfg := FlushOptions[T]{}
|
||||
for _, opt := range options {
|
||||
if opt != nil {
|
||||
opt(&cfg)
|
||||
}
|
||||
}
|
||||
var snapshot T
|
||||
ok, err := TryReadCookieSnapshotInto(m.mainPath, &snapshot)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var payload *T
|
||||
if ok {
|
||||
if m.hooks.Merge != nil {
|
||||
payload = m.hooks.Merge(m.store, &snapshot)
|
||||
} else {
|
||||
payload = &snapshot
|
||||
}
|
||||
} else if cfg.Fallback != nil {
|
||||
payload = cfg.Fallback()
|
||||
} else if m.store != nil {
|
||||
payload = m.store
|
||||
}
|
||||
if payload == nil {
|
||||
return RemoveFile(CookieSnapshotPath(m.mainPath))
|
||||
}
|
||||
if cfg.Mutate != nil {
|
||||
cfg.Mutate(payload)
|
||||
}
|
||||
if m.hooks.WriteMain != nil {
|
||||
if err = m.hooks.WriteMain(m.mainPath, payload); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err = WriteJSON(m.mainPath, payload); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
RemoveCookieSnapshots(m.mainPath)
|
||||
return nil
|
||||
}
|
||||
@@ -69,8 +69,6 @@ type AuthUpdate struct {
|
||||
}
|
||||
|
||||
const (
|
||||
authFileReadMaxAttempts = 5
|
||||
authFileReadRetryDelay = 0
|
||||
// replaceCheckDelay is a short delay to allow atomic replace (rename) to settle
|
||||
// before deciding whether a Remove event indicates a real deletion.
|
||||
replaceCheckDelay = 50 * time.Millisecond
|
||||
@@ -530,7 +528,7 @@ func (w *Watcher) reloadClients() {
|
||||
return nil
|
||||
}
|
||||
if !info.IsDir() && strings.HasSuffix(strings.ToLower(info.Name()), ".json") {
|
||||
if data, errReadAuthFileWithRetry := util.ReadAuthFileWithRetry(path, authFileReadMaxAttempts, authFileReadRetryDelay); errReadAuthFileWithRetry == nil && len(data) > 0 {
|
||||
if data, err := os.ReadFile(path); err == nil && len(data) > 0 {
|
||||
sum := sha256.Sum256(data)
|
||||
w.lastAuthHashes[path] = hex.EncodeToString(sum[:])
|
||||
}
|
||||
@@ -565,7 +563,7 @@ func (w *Watcher) reloadClients() {
|
||||
|
||||
// addOrUpdateClient handles the addition or update of a single client.
|
||||
func (w *Watcher) addOrUpdateClient(path string) {
|
||||
data, errRead := util.ReadAuthFileWithRetry(path, authFileReadMaxAttempts, authFileReadRetryDelay)
|
||||
data, errRead := os.ReadFile(path)
|
||||
if errRead != nil {
|
||||
log.Errorf("failed to read auth file %s: %v", filepath.Base(path), errRead)
|
||||
return
|
||||
@@ -806,7 +804,7 @@ func (w *Watcher) loadFileClients(cfg *config.Config) int {
|
||||
authFileCount++
|
||||
log.Debugf("processing auth file %d: %s", authFileCount, filepath.Base(path))
|
||||
// Count readable JSON files as successful auth entries
|
||||
if data, errCreate := util.ReadAuthFileWithRetry(path, authFileReadMaxAttempts, authFileReadRetryDelay); errCreate == nil && len(data) > 0 {
|
||||
if data, errCreate := os.ReadFile(path); errCreate == nil && len(data) > 0 {
|
||||
successfulAuthCount++
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/api"
|
||||
geminiwebclient "github.com/router-for-me/CLIProxyAPI/v6/internal/client/gemini-web"
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||
geminiwebclient "github.com/router-for-me/CLIProxyAPI/v6/internal/provider/gemini-web"
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/registry"
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/executor"
|
||||
_ "github.com/router-for-me/CLIProxyAPI/v6/internal/usage"
|
||||
|
||||
Reference in New Issue
Block a user