From e9707c2f9ee45c228bb6a2afb594e0df12e06056 Mon Sep 17 00:00:00 2001 From: hkfires <10558748+hkfires@users.noreply.github.com> Date: Wed, 24 Sep 2025 10:24:12 +0800 Subject: [PATCH 1/4] refactor(gemini-web): Move provider logic to its own package The Gemini Web API client logic has been relocated from `internal/client/gemini-web` to a new, more specific `internal/provider/gemini-web` package. This refactoring improves code organization and modularity by better isolating provider-specific implementations. As a result of this move, the `GeminiWebState` struct and its methods have been exported (capitalized) to make them accessible from the executor. All call sites have been updated to use the new package path and the exported identifiers. --- .../{client => provider}/gemini-web/auth.go | 0 .../{client => provider}/gemini-web/client.go | 0 .../gemini-web/convert_ext.go | 0 .../{client => provider}/gemini-web/errors.go | 0 .../gemini-web/logging.go | 0 .../{client => provider}/gemini-web/media.go | 0 .../{client => provider}/gemini-web/models.go | 0 .../gemini-web/persistence.go | 0 .../{client => provider}/gemini-web/prompt.go | 0 .../gemini-web/request.go | 0 .../gemini-web/state.go} | 170 +++++++++++------- .../{client => provider}/gemini-web/types.go | 0 .../runtime/executor/gemini_web_executor.go | 29 +-- sdk/cliproxy/service.go | 2 +- 14 files changed, 117 insertions(+), 84 deletions(-) rename internal/{client => provider}/gemini-web/auth.go (100%) rename internal/{client => provider}/gemini-web/client.go (100%) rename internal/{client => provider}/gemini-web/convert_ext.go (100%) rename internal/{client => provider}/gemini-web/errors.go (100%) rename internal/{client => provider}/gemini-web/logging.go (100%) rename internal/{client => provider}/gemini-web/media.go (100%) rename internal/{client => provider}/gemini-web/models.go (100%) rename internal/{client => provider}/gemini-web/persistence.go (100%) rename internal/{client => provider}/gemini-web/prompt.go (100%) rename internal/{client => provider}/gemini-web/request.go (100%) rename internal/{runtime/executor/gemini_web_state.go => provider/gemini-web/state.go} (68%) rename internal/{client => provider}/gemini-web/types.go (100%) diff --git a/internal/client/gemini-web/auth.go b/internal/provider/gemini-web/auth.go similarity index 100% rename from internal/client/gemini-web/auth.go rename to internal/provider/gemini-web/auth.go diff --git a/internal/client/gemini-web/client.go b/internal/provider/gemini-web/client.go similarity index 100% rename from internal/client/gemini-web/client.go rename to internal/provider/gemini-web/client.go diff --git a/internal/client/gemini-web/convert_ext.go b/internal/provider/gemini-web/convert_ext.go similarity index 100% rename from internal/client/gemini-web/convert_ext.go rename to internal/provider/gemini-web/convert_ext.go diff --git a/internal/client/gemini-web/errors.go b/internal/provider/gemini-web/errors.go similarity index 100% rename from internal/client/gemini-web/errors.go rename to internal/provider/gemini-web/errors.go diff --git a/internal/client/gemini-web/logging.go b/internal/provider/gemini-web/logging.go similarity index 100% rename from internal/client/gemini-web/logging.go rename to internal/provider/gemini-web/logging.go diff --git a/internal/client/gemini-web/media.go b/internal/provider/gemini-web/media.go similarity index 100% rename from internal/client/gemini-web/media.go rename to internal/provider/gemini-web/media.go diff --git a/internal/client/gemini-web/models.go b/internal/provider/gemini-web/models.go similarity index 100% rename from internal/client/gemini-web/models.go rename to internal/provider/gemini-web/models.go diff --git a/internal/client/gemini-web/persistence.go b/internal/provider/gemini-web/persistence.go similarity index 100% rename from internal/client/gemini-web/persistence.go rename to internal/provider/gemini-web/persistence.go diff --git a/internal/client/gemini-web/prompt.go b/internal/provider/gemini-web/prompt.go similarity index 100% rename from internal/client/gemini-web/prompt.go rename to internal/provider/gemini-web/prompt.go diff --git a/internal/client/gemini-web/request.go b/internal/provider/gemini-web/request.go similarity index 100% rename from internal/client/gemini-web/request.go rename to internal/provider/gemini-web/request.go diff --git a/internal/runtime/executor/gemini_web_state.go b/internal/provider/gemini-web/state.go similarity index 68% rename from internal/runtime/executor/gemini_web_state.go rename to internal/provider/gemini-web/state.go index 2b10a3f0..aed61b74 100644 --- a/internal/runtime/executor/gemini_web_state.go +++ b/internal/provider/gemini-web/state.go @@ -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) + } +} diff --git a/internal/client/gemini-web/types.go b/internal/provider/gemini-web/types.go similarity index 100% rename from internal/client/gemini-web/types.go rename to internal/provider/gemini-web/types.go diff --git a/internal/runtime/executor/gemini_web_executor.go b/internal/runtime/executor/gemini_web_executor.go index a9cc57c5..1d42b182 100644 --- a/internal/runtime/executor/gemini_web_executor.go +++ b/internal/runtime/executor/gemini_web_executor.go @@ -9,6 +9,7 @@ import ( "time" "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/gemini" + geminiwebapi "github.com/router-for-me/CLIProxyAPI/v6/internal/provider/gemini-web" "github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" @@ -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 diff --git a/sdk/cliproxy/service.go b/sdk/cliproxy/service.go index 9d6a34d5..23c37614 100644 --- a/sdk/cliproxy/service.go +++ b/sdk/cliproxy/service.go @@ -10,7 +10,7 @@ import ( "time" "github.com/router-for-me/CLIProxyAPI/v6/internal/api" - geminiwebclient "github.com/router-for-me/CLIProxyAPI/v6/internal/client/gemini-web" + geminiwebclient "github.com/router-for-me/CLIProxyAPI/v6/internal/provider/gemini-web" "github.com/router-for-me/CLIProxyAPI/v6/internal/config" "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" "github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/executor" From 06a3e9792d974fad55ba245e4df24bb7953e2efc Mon Sep 17 00:00:00 2001 From: hkfires <10558748+hkfires@users.noreply.github.com> Date: Wed, 24 Sep 2025 14:21:58 +0800 Subject: [PATCH 2/4] chore: Ignore .serena directory --- .dockerignore | 1 + .gitignore | 1 + 2 files changed, 2 insertions(+) diff --git a/.dockerignore b/.dockerignore index cb4c5bfc..a794020d 100644 --- a/.dockerignore +++ b/.dockerignore @@ -30,3 +30,4 @@ config.yaml bin/* .claude/* .vscode/* +.serena/* diff --git a/.gitignore b/.gitignore index ea0cda89..30431a17 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ auths/* !auths/.gitkeep .vscode/* .claude/* +.serena/* AGENTS.md CLAUDE.md *.exe From d4f5ec2492b77c284e19ed51abdf688ea1251025 Mon Sep 17 00:00:00 2001 From: hkfires <10558748+hkfires@users.noreply.github.com> Date: Wed, 24 Sep 2025 15:52:24 +0800 Subject: [PATCH 3/4] Removed the cookie snapshot feature. --- .../runtime/executor/gemini_web_executor.go | 2 +- internal/util/cookie_snapshot.go | 280 ------------------ internal/watcher/watcher.go | 8 +- sdk/cliproxy/service.go | 2 +- 4 files changed, 5 insertions(+), 287 deletions(-) delete mode 100644 internal/util/cookie_snapshot.go diff --git a/internal/runtime/executor/gemini_web_executor.go b/internal/runtime/executor/gemini_web_executor.go index 1d42b182..5f2e09a6 100644 --- a/internal/runtime/executor/gemini_web_executor.go +++ b/internal/runtime/executor/gemini_web_executor.go @@ -9,9 +9,9 @@ import ( "time" "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/gemini" - geminiwebapi "github.com/router-for-me/CLIProxyAPI/v6/internal/provider/gemini-web" "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" diff --git a/internal/util/cookie_snapshot.go b/internal/util/cookie_snapshot.go deleted file mode 100644 index 2572feea..00000000 --- a/internal/util/cookie_snapshot.go +++ /dev/null @@ -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 -} diff --git a/internal/watcher/watcher.go b/internal/watcher/watcher.go index 7fbe869f..fb1667ce 100644 --- a/internal/watcher/watcher.go +++ b/internal/watcher/watcher.go @@ -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++ } } diff --git a/sdk/cliproxy/service.go b/sdk/cliproxy/service.go index 23c37614..98c9f05c 100644 --- a/sdk/cliproxy/service.go +++ b/sdk/cliproxy/service.go @@ -10,8 +10,8 @@ import ( "time" "github.com/router-for-me/CLIProxyAPI/v6/internal/api" - geminiwebclient "github.com/router-for-me/CLIProxyAPI/v6/internal/provider/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" From c76b8785f8186684e4e108a8340975e2ac6ac920 Mon Sep 17 00:00:00 2001 From: hkfires <10558748+hkfires@users.noreply.github.com> Date: Wed, 24 Sep 2025 22:13:10 +0800 Subject: [PATCH 4/4] refactor(gemini-web): Standardize logging with logrus --- .gitignore | 3 +- internal/provider/gemini-web/auth.go | 32 +++++- internal/provider/gemini-web/client.go | 4 +- internal/provider/gemini-web/logging.go | 131 ------------------------ internal/provider/gemini-web/media.go | 7 +- 5 files changed, 37 insertions(+), 140 deletions(-) delete mode 100644 internal/provider/gemini-web/logging.go diff --git a/.gitignore b/.gitignore index 30431a17..800d9a7d 100644 --- a/.gitignore +++ b/.gitignore @@ -11,5 +11,4 @@ auths/* AGENTS.md CLAUDE.md *.exe -temp/* -.serena/ \ No newline at end of file +temp/* \ No newline at end of file diff --git a/internal/provider/gemini-web/auth.go b/internal/provider/gemini-web/auth.go index 05d8bd48..c10f76ee 100644 --- a/internal/provider/gemini-web/auth.go +++ b/internal/provider/gemini-web/auth.go @@ -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 +} diff --git a/internal/provider/gemini-web/client.go b/internal/provider/gemini-web/client.go index 8f84eaa3..829f21ee 100644 --- a/internal/provider/gemini-web/client.go +++ b/internal/provider/gemini-web/client.go @@ -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 } diff --git a/internal/provider/gemini-web/logging.go b/internal/provider/gemini-web/logging.go deleted file mode 100644 index fe892d90..00000000 --- a/internal/provider/gemini-web/logging.go +++ /dev/null @@ -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() -} diff --git a/internal/provider/gemini-web/media.go b/internal/provider/gemini-web/media.go index 58651453..3c843c62 100644 --- a/internal/provider/gemini-web/media.go +++ b/internal/provider/gemini-web/media.go @@ -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