diff --git a/internal/api/handlers/management/auth_files.go b/internal/api/handlers/management/auth_files.go index 5909dffc9..408186b9d 100644 --- a/internal/api/handlers/management/auth_files.go +++ b/internal/api/handlers/management/auth_files.go @@ -1565,8 +1565,8 @@ func (h *Handler) RequestQwenToken(c *gin.Context) { return } - // Create token storage - tokenStorage := qwenAuth.CreateTokenStorage(tokenData) + // Create token storage - default to false for use_global_proxy for qwen + tokenStorage := qwenAuth.CreateTokenStorage(tokenData, false) tokenStorage.Email = fmt.Sprintf("qwen-%d", time.Now().UnixMilli()) record := &coreauth.Auth{ @@ -1742,7 +1742,8 @@ func (h *Handler) RequestIFlowCookieToken(c *gin.Context) { tokenData.Cookie = cookieValue - tokenStorage := authSvc.CreateCookieTokenStorage(tokenData) + // For management API, default to false for use_global_proxy for iFlow + tokenStorage := authSvc.CreateCookieTokenStorage(tokenData, false) email := strings.TrimSpace(tokenStorage.Email) if email == "" { c.JSON(http.StatusBadRequest, gin.H{"status": "error", "error": "failed to extract email from token"}) diff --git a/internal/auth/iflow/iflow_auth.go b/internal/auth/iflow/iflow_auth.go index fa9f38c3e..759774781 100644 --- a/internal/auth/iflow/iflow_auth.go +++ b/internal/auth/iflow/iflow_auth.go @@ -489,7 +489,7 @@ func ShouldRefreshAPIKey(expireTime string) (bool, time.Duration, error) { } // CreateCookieTokenStorage converts cookie-based token data into persistence storage -func (ia *IFlowAuth) CreateCookieTokenStorage(data *IFlowTokenData) *IFlowTokenStorage { +func (ia *IFlowAuth) CreateCookieTokenStorage(data *IFlowTokenData, useGlobalProxy bool) *IFlowTokenStorage { if data == nil { return nil } @@ -502,12 +502,13 @@ func (ia *IFlowAuth) CreateCookieTokenStorage(data *IFlowTokenData) *IFlowTokenS } return &IFlowTokenStorage{ - APIKey: data.APIKey, - Email: data.Email, - Expire: data.Expire, - Cookie: cookieToSave, - LastRefresh: time.Now().Format(time.RFC3339), - Type: "iflow", + APIKey: data.APIKey, + Email: data.Email, + Expire: data.Expire, + Cookie: cookieToSave, + LastRefresh: time.Now().Format(time.RFC3339), + Type: "iflow", + UseGlobalProxy: useGlobalProxy, } } diff --git a/internal/auth/iflow/iflow_token.go b/internal/auth/iflow/iflow_token.go index 6d2beb392..f3ef6b765 100644 --- a/internal/auth/iflow/iflow_token.go +++ b/internal/auth/iflow/iflow_token.go @@ -11,16 +11,17 @@ import ( // IFlowTokenStorage persists iFlow OAuth credentials alongside the derived API key. type IFlowTokenStorage struct { - AccessToken string `json:"access_token"` - RefreshToken string `json:"refresh_token"` - LastRefresh string `json:"last_refresh"` - Expire string `json:"expired"` - APIKey string `json:"api_key"` - Email string `json:"email"` - TokenType string `json:"token_type"` - Scope string `json:"scope"` - Cookie string `json:"cookie"` - Type string `json:"type"` + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` + LastRefresh string `json:"last_refresh"` + Expire string `json:"expired"` + APIKey string `json:"api_key"` + Email string `json:"email"` + TokenType string `json:"token_type"` + Scope string `json:"scope"` + Cookie string `json:"cookie"` + Type string `json:"type"` + UseGlobalProxy bool `json:"use_global_proxy"` } // SaveTokenToFile serialises the token storage to disk. @@ -42,3 +43,8 @@ func (ts *IFlowTokenStorage) SaveTokenToFile(authFilePath string) error { } return nil } + +// SetUseGlobalProxy implements the GlobalProxySetter interface +func (ts *IFlowTokenStorage) SetUseGlobalProxy(value bool) { + ts.UseGlobalProxy = value +} diff --git a/internal/auth/qwen/qwen_auth.go b/internal/auth/qwen/qwen_auth.go index cb58b86d3..e7746e4aa 100644 --- a/internal/auth/qwen/qwen_auth.go +++ b/internal/auth/qwen/qwen_auth.go @@ -337,13 +337,14 @@ func (o *QwenAuth) RefreshTokensWithRetry(ctx context.Context, refreshToken stri } // CreateTokenStorage creates a QwenTokenStorage object from a QwenTokenData object. -func (o *QwenAuth) CreateTokenStorage(tokenData *QwenTokenData) *QwenTokenStorage { +func (o *QwenAuth) CreateTokenStorage(tokenData *QwenTokenData, useGlobalProxy bool) *QwenTokenStorage { storage := &QwenTokenStorage{ - AccessToken: tokenData.AccessToken, - RefreshToken: tokenData.RefreshToken, - LastRefresh: time.Now().Format(time.RFC3339), - ResourceURL: tokenData.ResourceURL, - Expire: tokenData.Expire, + AccessToken: tokenData.AccessToken, + RefreshToken: tokenData.RefreshToken, + LastRefresh: time.Now().Format(time.RFC3339), + ResourceURL: tokenData.ResourceURL, + Expire: tokenData.Expire, + UseGlobalProxy: useGlobalProxy, } return storage diff --git a/internal/auth/qwen/qwen_token.go b/internal/auth/qwen/qwen_token.go index 4a2b3a2d5..3a33543dc 100644 --- a/internal/auth/qwen/qwen_token.go +++ b/internal/auth/qwen/qwen_token.go @@ -30,6 +30,8 @@ type QwenTokenStorage struct { Type string `json:"type"` // Expire is the timestamp when the current access token expires. Expire string `json:"expired"` + // UseGlobalProxy indicates whether to use the global proxy from config.yaml. + UseGlobalProxy bool `json:"use_global_proxy"` } // SaveTokenToFile serializes the Qwen token storage to a JSON file. @@ -61,3 +63,8 @@ func (ts *QwenTokenStorage) SaveTokenToFile(authFilePath string) error { } return nil } + +// SetUseGlobalProxy implements the GlobalProxySetter interface +func (ts *QwenTokenStorage) SetUseGlobalProxy(value bool) { + ts.UseGlobalProxy = value +} diff --git a/internal/cmd/iflow_cookie.go b/internal/cmd/iflow_cookie.go index 358b80627..cbcd90aac 100644 --- a/internal/cmd/iflow_cookie.go +++ b/internal/cmd/iflow_cookie.go @@ -11,6 +11,7 @@ import ( "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/iflow" "github.com/router-for-me/CLIProxyAPI/v6/internal/config" + "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth" ) // DoIFlowCookieAuth performs the iFlow cookie-based authentication. @@ -49,6 +50,16 @@ func DoIFlowCookieAuth(cfg *config.Config, options *LoginOptions) { return } + // Ask if user wants to use global proxy + useGlobalProxy := false // Default to false for iFlow + if options != nil && options.Prompt != nil { + // Create sdk auth LoginOptions from cmd LoginOptions to use AskUseGlobalProxy + sdkOpts := &auth.LoginOptions{ + Prompt: options.Prompt, + } + useGlobalProxy = auth.AskUseGlobalProxy(sdkOpts, false) + } + // Authenticate with cookie auth := iflow.NewIFlowAuth(cfg) ctx := context.Background() @@ -60,7 +71,7 @@ func DoIFlowCookieAuth(cfg *config.Config, options *LoginOptions) { } // Create token storage - tokenStorage := auth.CreateCookieTokenStorage(tokenData) + tokenStorage := auth.CreateCookieTokenStorage(tokenData, useGlobalProxy) // Get auth file path using email in filename authFilePath := getAuthFilePath(cfg, "iflow", tokenData.Email) diff --git a/internal/runtime/executor/proxy_helpers.go b/internal/runtime/executor/proxy_helpers.go index ab0f626ac..c69f537aa 100644 --- a/internal/runtime/executor/proxy_helpers.go +++ b/internal/runtime/executor/proxy_helpers.go @@ -16,8 +16,8 @@ import ( // newProxyAwareHTTPClient creates an HTTP client with proper proxy configuration priority: // 1. Use auth.ProxyURL if configured (highest priority) -// 2. Use cfg.ProxyURL if auth proxy is not configured -// 3. Use RoundTripper from context if neither are configured +// 2. Use cfg.ProxyURL if auth.UseGlobalProxy is true and auth.ProxyURL is not configured +// 3. Use RoundTripper from context if no proxy is configured // // Parameters: // - ctx: The context containing optional RoundTripper @@ -33,14 +33,14 @@ func newProxyAwareHTTPClient(ctx context.Context, cfg *config.Config, auth *clip httpClient.Timeout = timeout } - // Priority 1: Use auth.ProxyURL if configured + // Priority 1: Use auth.ProxyURL if configured (highest priority) var proxyURL string if auth != nil { proxyURL = strings.TrimSpace(auth.ProxyURL) } - // Priority 2: Use cfg.ProxyURL if auth proxy is not configured - if proxyURL == "" && cfg != nil { + // Priority 2: Use cfg.ProxyURL if auth.UseGlobalProxy is true and no auth proxy is set + if proxyURL == "" && auth != nil && auth.UseGlobalProxy && cfg != nil { proxyURL = strings.TrimSpace(cfg.ProxyURL) } diff --git a/sdk/auth/filestore.go b/sdk/auth/filestore.go index 3c2d60c4a..61cea9f4d 100644 --- a/sdk/auth/filestore.go +++ b/sdk/auth/filestore.go @@ -197,6 +197,12 @@ func (s *FileTokenStore) readAuthFile(path, baseDir string) (*cliproxyauth.Auth, if email, ok := metadata["email"].(string); ok && email != "" { auth.Attributes["email"] = email } + // Load use_global_proxy from metadata, default to true if not set + // This maintains backward compatibility for existing auth files + auth.UseGlobalProxy = true // Default to true + if useGlobalProxy, ok := metadata["use_global_proxy"].(bool); ok { + auth.UseGlobalProxy = useGlobalProxy + } return auth, nil } diff --git a/sdk/auth/manager.go b/sdk/auth/manager.go index c6469a7d1..0ac8b4dfc 100644 --- a/sdk/auth/manager.go +++ b/sdk/auth/manager.go @@ -3,6 +3,7 @@ package auth import ( "context" "fmt" + "strings" "github.com/router-for-me/CLIProxyAPI/v6/internal/config" coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" @@ -43,6 +44,11 @@ func (m *Manager) SetStore(store coreauth.Store) { m.store = store } +// GlobalProxySetter is an interface for token storage to set the use_global_proxy value +type GlobalProxySetter interface { + SetUseGlobalProxy(bool) +} + // Login executes the provider login flow and persists the resulting auth record. func (m *Manager) Login(ctx context.Context, provider string, cfg *config.Config, opts *LoginOptions) (*coreauth.Auth, string, error) { auth, ok := m.authenticators[provider] @@ -58,6 +64,23 @@ func (m *Manager) Login(ctx context.Context, provider string, cfg *config.Config return nil, "", fmt.Errorf("cliproxy auth: authenticator %s returned nil record", provider) } + // Always ask about using global proxy if auth doesn't have proxy URL configured + // This allows the setting to work even if user adds proxy to config.yaml later + if record.ProxyURL == "" { + // Determine default value based on provider + defaultValue := true + if provider == "qwen" || provider == "iflow" { + defaultValue = false + } + shouldUseGlobalProxy := AskUseGlobalProxy(opts, defaultValue) + record.UseGlobalProxy = shouldUseGlobalProxy + + // Update the storage if it implements GlobalProxySetter + if storage, ok := record.Storage.(GlobalProxySetter); ok { + storage.SetUseGlobalProxy(shouldUseGlobalProxy) + } + } + if m.store == nil { return record, "", nil } @@ -74,3 +97,35 @@ func (m *Manager) Login(ctx context.Context, provider string, cfg *config.Config } return record, savedPath, nil } + +// AskUseGlobalProxy asks the user whether to use the global proxy from config.yaml. +func AskUseGlobalProxy(opts *LoginOptions, defaultValue bool) bool { + if opts == nil || opts.Prompt == nil { + // If no prompt function available, return the default value + return defaultValue + } + + fmt.Println() + fmt.Println("Would you like to use the global proxy from config.yaml for this authentication?") + fmt.Println("(This allows the proxy to be applied automatically if you add it to config.yaml later)") + if defaultValue { + fmt.Println("yes/no, default: yes") + } else { + fmt.Println("yes/no, default: no") + } + + answer, err := opts.Prompt("Use global proxy? ") + if err != nil { + // If we can't get user input, return the default value + return defaultValue + } + + answer = strings.TrimSpace(strings.ToLower(answer)) + if defaultValue { + // If default is true, only turn off if user says no + return answer != "n" && answer != "no" + } else { + // If default is false, only turn on if user says yes + return answer == "y" || answer == "yes" + } +} diff --git a/sdk/auth/qwen.go b/sdk/auth/qwen.go index 151fba681..c1430f0b2 100644 --- a/sdk/auth/qwen.go +++ b/sdk/auth/qwen.go @@ -71,7 +71,9 @@ func (a *QwenAuthenticator) Login(ctx context.Context, cfg *config.Config, opts return nil, fmt.Errorf("qwen authentication failed: %w", err) } - tokenStorage := authSvc.CreateTokenStorage(tokenData) + // Note: use_global_proxy will be set by manager.go's Login method + // Default to false for qwen + tokenStorage := authSvc.CreateTokenStorage(tokenData, false) email := "" if opts.Metadata != nil { diff --git a/sdk/cliproxy/auth/types.go b/sdk/cliproxy/auth/types.go index 25e88b96e..0677a90b8 100644 --- a/sdk/cliproxy/auth/types.go +++ b/sdk/cliproxy/auth/types.go @@ -35,6 +35,8 @@ type Auth struct { Unavailable bool `json:"unavailable"` // ProxyURL overrides the global proxy setting for this auth if provided. ProxyURL string `json:"proxy_url,omitempty"` + // UseGlobalProxy indicates whether to use the global proxy from config.yaml. + UseGlobalProxy bool `json:"use_global_proxy"` // Attributes stores provider specific metadata needed by executors (immutable configuration). Attributes map[string]string `json:"attributes,omitempty"` // Metadata stores runtime mutable provider state (e.g. tokens, cookies).