mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-03 13:00:52 +08:00
feat: Add support for iFlow provider
This commit is contained in:
148
sdk/auth/iflow.go
Normal file
148
sdk/auth/iflow.go
Normal file
@@ -0,0 +1,148 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/iflow"
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/browser"
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
|
||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
||||
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// IFlowAuthenticator implements the OAuth login flow for iFlow accounts.
|
||||
type IFlowAuthenticator struct{}
|
||||
|
||||
// NewIFlowAuthenticator constructs a new authenticator instance.
|
||||
func NewIFlowAuthenticator() *IFlowAuthenticator { return &IFlowAuthenticator{} }
|
||||
|
||||
// Provider returns the provider key for the authenticator.
|
||||
func (a *IFlowAuthenticator) Provider() string { return "iflow" }
|
||||
|
||||
// RefreshLead indicates how soon before expiry a refresh should be attempted.
|
||||
func (a *IFlowAuthenticator) RefreshLead() *time.Duration {
|
||||
d := 3 * time.Hour
|
||||
return &d
|
||||
}
|
||||
|
||||
// Login performs the OAuth code flow using a local callback server.
|
||||
func (a *IFlowAuthenticator) Login(ctx context.Context, cfg *config.Config, opts *LoginOptions) (*coreauth.Auth, error) {
|
||||
if cfg == nil {
|
||||
return nil, fmt.Errorf("cliproxy auth: configuration is required")
|
||||
}
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
if opts == nil {
|
||||
opts = &LoginOptions{}
|
||||
}
|
||||
|
||||
authSvc := iflow.NewIFlowAuth(cfg)
|
||||
|
||||
oauthServer := iflow.NewOAuthServer(iflow.CallbackPort)
|
||||
if err := oauthServer.Start(); err != nil {
|
||||
if strings.Contains(err.Error(), "already in use") {
|
||||
return nil, fmt.Errorf("iflow authentication server port in use: %w", err)
|
||||
}
|
||||
return nil, fmt.Errorf("iflow authentication server failed: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
stopCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
if stopErr := oauthServer.Stop(stopCtx); stopErr != nil {
|
||||
log.Warnf("iflow oauth server stop error: %v", stopErr)
|
||||
}
|
||||
}()
|
||||
|
||||
state, err := misc.GenerateRandomState()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("iflow auth: failed to generate state: %w", err)
|
||||
}
|
||||
|
||||
authURL, redirectURI := authSvc.AuthorizationURL(state, iflow.CallbackPort)
|
||||
|
||||
if !opts.NoBrowser {
|
||||
fmt.Println("Opening browser for iFlow authentication")
|
||||
if !browser.IsAvailable() {
|
||||
log.Warn("No browser available; please open the URL manually")
|
||||
util.PrintSSHTunnelInstructions(iflow.CallbackPort)
|
||||
fmt.Printf("Visit the following URL to continue authentication:\n%s\n", authURL)
|
||||
} else if err = browser.OpenURL(authURL); err != nil {
|
||||
log.Warnf("Failed to open browser automatically: %v", err)
|
||||
util.PrintSSHTunnelInstructions(iflow.CallbackPort)
|
||||
fmt.Printf("Visit the following URL to continue authentication:\n%s\n", authURL)
|
||||
}
|
||||
} else {
|
||||
util.PrintSSHTunnelInstructions(iflow.CallbackPort)
|
||||
fmt.Printf("Visit the following URL to continue authentication:\n%s\n", authURL)
|
||||
}
|
||||
|
||||
fmt.Println("Waiting for iFlow authentication callback...")
|
||||
|
||||
result, err := oauthServer.WaitForCallback(5 * time.Minute)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("iflow auth: callback wait failed: %w", err)
|
||||
}
|
||||
if result.Error != "" {
|
||||
return nil, fmt.Errorf("iflow auth: provider returned error %s", result.Error)
|
||||
}
|
||||
if result.State != state {
|
||||
return nil, fmt.Errorf("iflow auth: state mismatch")
|
||||
}
|
||||
|
||||
tokenData, err := authSvc.ExchangeCodeForTokens(ctx, result.Code, redirectURI)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("iflow authentication failed: %w", err)
|
||||
}
|
||||
|
||||
tokenStorage := authSvc.CreateTokenStorage(tokenData)
|
||||
|
||||
email := ""
|
||||
if opts.Metadata != nil {
|
||||
email = opts.Metadata["email"]
|
||||
if email == "" {
|
||||
email = opts.Metadata["alias"]
|
||||
}
|
||||
}
|
||||
|
||||
if email == "" && opts.Prompt != nil {
|
||||
email, err = opts.Prompt("Please input your email address or alias for iFlow:")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
email = strings.TrimSpace(email)
|
||||
if email == "" {
|
||||
return nil, &EmailRequiredError{Prompt: "Please provide an email address or alias for iFlow."}
|
||||
}
|
||||
|
||||
tokenStorage.Email = email
|
||||
|
||||
fileName := fmt.Sprintf("iflow-%s.json", tokenStorage.Email)
|
||||
metadata := map[string]any{
|
||||
"email": tokenStorage.Email,
|
||||
"api_key": tokenStorage.APIKey,
|
||||
"access_token": tokenStorage.AccessToken,
|
||||
"refresh_token": tokenStorage.RefreshToken,
|
||||
"expired": tokenStorage.Expire,
|
||||
}
|
||||
|
||||
fmt.Println("iFlow authentication successful")
|
||||
|
||||
return &coreauth.Auth{
|
||||
ID: fileName,
|
||||
Provider: a.Provider(),
|
||||
FileName: fileName,
|
||||
Storage: tokenStorage,
|
||||
Metadata: metadata,
|
||||
Attributes: map[string]string{
|
||||
"api_key": tokenStorage.APIKey,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
@@ -10,6 +10,7 @@ func init() {
|
||||
registerRefreshLead("codex", func() Authenticator { return NewCodexAuthenticator() })
|
||||
registerRefreshLead("claude", func() Authenticator { return NewClaudeAuthenticator() })
|
||||
registerRefreshLead("qwen", func() Authenticator { return NewQwenAuthenticator() })
|
||||
registerRefreshLead("iflow", func() Authenticator { return NewIFlowAuthenticator() })
|
||||
registerRefreshLead("gemini", func() Authenticator { return NewGeminiAuthenticator() })
|
||||
registerRefreshLead("gemini-cli", func() Authenticator { return NewGeminiAuthenticator() })
|
||||
registerRefreshLead("gemini-web", func() Authenticator { return NewGeminiWebAuthenticator() })
|
||||
|
||||
@@ -250,6 +250,8 @@ func (s *Service) ensureExecutorsForAuth(a *coreauth.Auth) {
|
||||
s.coreManager.RegisterExecutor(executor.NewCodexExecutor(s.cfg))
|
||||
case "qwen":
|
||||
s.coreManager.RegisterExecutor(executor.NewQwenExecutor(s.cfg))
|
||||
case "iflow":
|
||||
s.coreManager.RegisterExecutor(executor.NewIFlowExecutor(s.cfg))
|
||||
default:
|
||||
providerKey := strings.ToLower(strings.TrimSpace(a.Provider))
|
||||
if providerKey == "" {
|
||||
@@ -496,6 +498,8 @@ func (s *Service) registerModelsForAuth(a *coreauth.Auth) {
|
||||
models = registry.GetOpenAIModels()
|
||||
case "qwen":
|
||||
models = registry.GetQwenModels()
|
||||
case "iflow":
|
||||
models = registry.GetIFlowModels()
|
||||
default:
|
||||
// Handle OpenAI-compatibility providers by name using config
|
||||
if s.cfg != nil {
|
||||
|
||||
Reference in New Issue
Block a user