feat(logging): introduce centralized logging with custom format and Gin integration

- Implemented a global logger with structured formatting for consistent log output.
- Added support for rotating log files using Lumberjack.
- Integrated new logging functionality with Gin HTTP server for unified log handling.
- Replaced direct `log.Info` calls with `fmt.Printf` in non-critical paths to simplify core functionality.
This commit is contained in:
Luis Pater
2025-09-26 00:54:52 +08:00
parent 72325f792c
commit cf734f7e7b
20 changed files with 209 additions and 148 deletions

View File

@@ -4,106 +4,30 @@
package main
import (
"bytes"
"flag"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/gin-gonic/gin"
"github.com/router-for-me/CLIProxyAPI/v6/internal/cmd"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
"github.com/router-for-me/CLIProxyAPI/v6/internal/logging"
_ "github.com/router-for-me/CLIProxyAPI/v6/internal/translator"
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth"
log "github.com/sirupsen/logrus"
"gopkg.in/natefinch/lumberjack.v2"
)
var (
Version = "dev"
Commit = "none"
BuildDate = "unknown"
logWriter *lumberjack.Logger
ginInfoWriter *io.PipeWriter
ginErrorWriter *io.PipeWriter
Version = "dev"
Commit = "none"
BuildDate = "unknown"
)
// LogFormatter defines a custom log format for logrus.
// This formatter adds timestamp, log level, and source location information
// to each log entry for better debugging and monitoring.
type LogFormatter struct {
}
// Format renders a single log entry with custom formatting.
// It includes timestamp, log level, source file and line number, and the log message.
func (m *LogFormatter) Format(entry *log.Entry) ([]byte, error) {
var b *bytes.Buffer
if entry.Buffer != nil {
b = entry.Buffer
} else {
b = &bytes.Buffer{}
}
timestamp := entry.Time.Format("2006-01-02 15:04:05")
var newLog string
// Ensure message doesn't carry trailing newlines; formatter appends one.
msg := strings.TrimRight(entry.Message, "\r\n")
// Customize the log format to include timestamp, level, caller file/line, and message.
newLog = fmt.Sprintf("[%s] [%s] [%s:%d] %s\n", timestamp, entry.Level, filepath.Base(entry.Caller.File), entry.Caller.Line, msg)
b.WriteString(newLog)
return b.Bytes(), nil
}
// init initializes the logger configuration.
// It sets up the custom log formatter, enables caller reporting,
// and configures the log output destination.
// init initializes the shared logger setup.
func init() {
logDir := "logs"
if err := os.MkdirAll(logDir, 0755); err != nil {
_, _ = fmt.Fprintf(os.Stderr, "failed to create log directory: %v\n", err)
os.Exit(1)
}
logWriter = &lumberjack.Logger{
Filename: filepath.Join(logDir, "main.log"),
MaxSize: 10,
MaxBackups: 0,
MaxAge: 0,
Compress: false,
}
log.SetOutput(logWriter)
// Enable reporting the caller function's file and line number.
log.SetReportCaller(true)
// Set the custom log formatter.
log.SetFormatter(&LogFormatter{})
ginInfoWriter = log.StandardLogger().Writer()
gin.DefaultWriter = ginInfoWriter
ginErrorWriter = log.StandardLogger().WriterLevel(log.ErrorLevel)
gin.DefaultErrorWriter = ginErrorWriter
gin.DebugPrintFunc = func(format string, values ...interface{}) {
// Trim trailing newlines from Gin's formatted messages to avoid blank lines.
// Gin's debug prints usually include a trailing "\n"; our formatter also appends one.
// Removing it here ensures a single newline per entry.
format = strings.TrimRight(format, "\r\n")
log.StandardLogger().Infof(format, values...)
}
log.RegisterExitHandler(func() {
if logWriter != nil {
_ = logWriter.Close()
}
if ginInfoWriter != nil {
_ = ginInfoWriter.Close()
}
if ginErrorWriter != nil {
_ = ginErrorWriter.Close()
}
})
logging.SetupBaseLogger()
}
// main is the entry point of the application.
@@ -111,7 +35,6 @@ func init() {
// service based on the provided flags (login, codex-login, or server mode).
func main() {
fmt.Printf("CLIProxyAPI Version: %s, Commit: %s, BuiltAt: %s\n", Version, Commit, BuildDate)
log.Infof("CLIProxyAPI Version: %s, Commit: %s, BuiltAt: %s", Version, Commit, BuildDate)
// Command-line flags to control the application's behavior.
var login bool
@@ -189,6 +112,12 @@ func main() {
log.Fatalf("failed to load config: %v", err)
}
if err = logging.ConfigureLogOutput(cfg.LoggingToFile); err != nil {
log.Fatalf("failed to configure log output: %v", err)
}
log.Infof("CLIProxyAPI Version: %s, Commit: %s, BuiltAt: %s", Version, Commit, BuildDate)
// Set the log level based on the configuration.
util.SetLogLevel(cfg)

View File

@@ -18,6 +18,9 @@ auth-dir: "~/.cli-proxy-api"
# Enable debug logging
debug: false
# When true, write application logs to rotating files instead of stdout
logging-to-file: true
# Proxy URL. Supports socks5/http/https protocols. Example: socks5://user:pass@192.168.1.1:1080/
proxy-url: ""

View File

@@ -359,7 +359,7 @@ func (h *Handler) saveTokenRecord(ctx context.Context, record *sdkAuth.TokenReco
func (h *Handler) RequestAnthropicToken(c *gin.Context) {
ctx := context.Background()
log.Info("Initializing Claude authentication...")
fmt.Println("Initializing Claude authentication...")
// Generate PKCE codes
pkceCodes, err := claude.GeneratePKCECodes()
@@ -407,7 +407,7 @@ func (h *Handler) RequestAnthropicToken(c *gin.Context) {
}
}
log.Info("Waiting for authentication callback...")
fmt.Println("Waiting for authentication callback...")
// Wait up to 5 minutes
resultMap, errWait := waitForFile(waitFile, 5*time.Minute)
if errWait != nil {
@@ -509,11 +509,11 @@ func (h *Handler) RequestAnthropicToken(c *gin.Context) {
return
}
log.Infof("Authentication successful! Token saved to %s", savedPath)
fmt.Printf("Authentication successful! Token saved to %s\n", savedPath)
if bundle.APIKey != "" {
log.Info("API key obtained and saved")
fmt.Println("API key obtained and saved")
}
log.Info("You can now use Claude services through this CLI")
fmt.Println("You can now use Claude services through this CLI")
delete(oauthStatus, state)
}()
@@ -527,7 +527,7 @@ func (h *Handler) RequestGeminiCLIToken(c *gin.Context) {
// Optional project ID from query
projectID := c.Query("project_id")
log.Info("Initializing Google authentication...")
fmt.Println("Initializing Google authentication...")
// OAuth2 configuration (mirrors internal/auth/gemini)
conf := &oauth2.Config{
@@ -549,7 +549,7 @@ func (h *Handler) RequestGeminiCLIToken(c *gin.Context) {
go func() {
// Wait for callback file written by server route
waitFile := filepath.Join(h.cfg.AuthDir, fmt.Sprintf(".oauth-gemini-%s.oauth", state))
log.Info("Waiting for authentication callback...")
fmt.Println("Waiting for authentication callback...")
deadline := time.Now().Add(5 * time.Minute)
var authCode string
for {
@@ -618,9 +618,9 @@ func (h *Handler) RequestGeminiCLIToken(c *gin.Context) {
email := gjson.GetBytes(bodyBytes, "email").String()
if email != "" {
log.Infof("Authenticated user email: %s", email)
fmt.Printf("Authenticated user email: %s\n", email)
} else {
log.Info("Failed to get user email from token")
fmt.Println("Failed to get user email from token")
oauthStatus[state] = "Failed to get user email from token"
}
@@ -657,7 +657,7 @@ func (h *Handler) RequestGeminiCLIToken(c *gin.Context) {
oauthStatus[state] = "Failed to get authenticated client"
return
}
log.Info("Authentication successful.")
fmt.Println("Authentication successful.")
record := &sdkAuth.TokenRecord{
Provider: "gemini",
@@ -676,7 +676,7 @@ func (h *Handler) RequestGeminiCLIToken(c *gin.Context) {
}
delete(oauthStatus, state)
log.Infof("You can now use Gemini CLI services through this CLI; token saved to %s", savedPath)
fmt.Printf("You can now use Gemini CLI services through this CLI; token saved to %s\n", savedPath)
}()
oauthStatus[state] = ""
@@ -737,14 +737,14 @@ func (h *Handler) CreateGeminiWebToken(c *gin.Context) {
return
}
log.Infof("Successfully saved Gemini Web token to: %s", savedPath)
fmt.Printf("Successfully saved Gemini Web token to: %s\n", savedPath)
c.JSON(http.StatusOK, gin.H{"status": "ok", "file": filepath.Base(savedPath)})
}
func (h *Handler) RequestCodexToken(c *gin.Context) {
ctx := context.Background()
log.Info("Initializing Codex authentication...")
fmt.Println("Initializing Codex authentication...")
// Generate PKCE codes
pkceCodes, err := codex.GeneratePKCECodes()
@@ -884,11 +884,11 @@ func (h *Handler) RequestCodexToken(c *gin.Context) {
log.Fatalf("Failed to save authentication tokens: %v", errSave)
return
}
log.Infof("Authentication successful! Token saved to %s", savedPath)
fmt.Printf("Authentication successful! Token saved to %s\n", savedPath)
if bundle.APIKey != "" {
log.Info("API key obtained and saved")
fmt.Println("API key obtained and saved")
}
log.Info("You can now use Codex services through this CLI")
fmt.Println("You can now use Codex services through this CLI")
delete(oauthStatus, state)
}()
@@ -899,7 +899,7 @@ func (h *Handler) RequestCodexToken(c *gin.Context) {
func (h *Handler) RequestQwenToken(c *gin.Context) {
ctx := context.Background()
log.Info("Initializing Qwen authentication...")
fmt.Println("Initializing Qwen authentication...")
state := fmt.Sprintf("gem-%d", time.Now().UnixNano())
// Initialize Qwen auth service
@@ -914,7 +914,7 @@ func (h *Handler) RequestQwenToken(c *gin.Context) {
authURL := deviceFlow.VerificationURIComplete
go func() {
log.Info("Waiting for authentication...")
fmt.Println("Waiting for authentication...")
tokenData, errPollForToken := qwenAuth.PollForToken(deviceFlow.DeviceCode, deviceFlow.CodeVerifier)
if errPollForToken != nil {
oauthStatus[state] = "Authentication failed"
@@ -939,8 +939,8 @@ func (h *Handler) RequestQwenToken(c *gin.Context) {
return
}
log.Infof("Authentication successful! Token saved to %s", savedPath)
log.Info("You can now use Qwen services through this CLI")
fmt.Printf("Authentication successful! Token saved to %s\n", savedPath)
fmt.Println("You can now use Qwen services through this CLI")
delete(oauthStatus, state)
}()

View File

@@ -452,6 +452,14 @@ func (s *Server) UpdateClients(cfg *config.Config) {
log.Debugf("request logging updated from %t to %t", s.cfg.RequestLog, cfg.RequestLog)
}
if s.cfg.LoggingToFile != cfg.LoggingToFile {
if err := logging.ConfigureLogOutput(cfg.LoggingToFile); err != nil {
log.Errorf("failed to reconfigure log output: %v", err)
} else {
log.Debugf("logging_to_file updated from %t to %t", s.cfg.LoggingToFile, cfg.LoggingToFile)
}
}
// Update log level dynamically when debug flag changes
if s.cfg.Debug != cfg.Debug {
util.SetLogLevel(cfg)
@@ -477,7 +485,7 @@ func (s *Server) UpdateClients(cfg *config.Config) {
}
total := authFiles + glAPIKeyCount + claudeAPIKeyCount + codexAPIKeyCount + openAICompatCount
log.Infof("server clients and configuration updated: %d clients (%d auth files + %d GL API keys + %d Claude API keys + %d Codex keys + %d OpenAI-compat)",
fmt.Printf("server clients and configuration updated: %d clients (%d auth files + %d GL API keys + %d Claude API keys + %d Codex keys + %d OpenAI-compat)\n",
total,
authFiles,
glAPIKeyCount,

View File

@@ -107,7 +107,7 @@ func (g *GeminiAuth) GetAuthenticatedClient(ctx context.Context, ts *GeminiToken
// If no token is found in storage, initiate the web-based OAuth flow.
if ts.Token == nil {
log.Info("Could not load token from file, starting OAuth flow.")
fmt.Printf("Could not load token from file, starting OAuth flow.\n")
token, err = g.getTokenFromWeb(ctx, conf, noBrowser...)
if err != nil {
return nil, fmt.Errorf("failed to get token from web: %w", err)
@@ -169,9 +169,9 @@ func (g *GeminiAuth) createTokenStorage(ctx context.Context, config *oauth2.Conf
emailResult := gjson.GetBytes(bodyBytes, "email")
if emailResult.Exists() && emailResult.Type == gjson.String {
log.Infof("Authenticated user email: %s", emailResult.String())
fmt.Printf("Authenticated user email: %s\n", emailResult.String())
} else {
log.Info("Failed to get user email from token")
fmt.Println("Failed to get user email from token")
}
var ifToken map[string]any
@@ -246,19 +246,19 @@ func (g *GeminiAuth) getTokenFromWeb(ctx context.Context, config *oauth2.Config,
authURL := config.AuthCodeURL("state-token", oauth2.AccessTypeOffline, oauth2.SetAuthURLParam("prompt", "consent"))
if len(noBrowser) == 1 && !noBrowser[0] {
log.Info("Opening browser for authentication...")
fmt.Println("Opening browser for authentication...")
// Check if browser is available
if !browser.IsAvailable() {
log.Warn("No browser available on this system")
util.PrintSSHTunnelInstructions(8085)
log.Infof("Please manually open this URL in your browser:\n\n%s\n", authURL)
fmt.Printf("Please manually open this URL in your browser:\n\n%s\n", authURL)
} else {
if err := browser.OpenURL(authURL); err != nil {
authErr := codex.NewAuthenticationError(codex.ErrBrowserOpenFailed, err)
log.Warn(codex.GetUserFriendlyMessage(authErr))
util.PrintSSHTunnelInstructions(8085)
log.Infof("Please manually open this URL in your browser:\n\n%s\n", authURL)
fmt.Printf("Please manually open this URL in your browser:\n\n%s\n", authURL)
// Log platform info for debugging
platformInfo := browser.GetPlatformInfo()
@@ -269,10 +269,10 @@ func (g *GeminiAuth) getTokenFromWeb(ctx context.Context, config *oauth2.Config,
}
} else {
util.PrintSSHTunnelInstructions(8085)
log.Infof("Please open this URL in your browser:\n\n%s\n", authURL)
fmt.Printf("Please open this URL in your browser:\n\n%s\n", authURL)
}
log.Info("Waiting for authentication callback...")
fmt.Println("Waiting for authentication callback...")
// Wait for the authorization code or an error.
var authCode string
@@ -296,6 +296,6 @@ func (g *GeminiAuth) getTokenFromWeb(ctx context.Context, config *oauth2.Config,
return nil, fmt.Errorf("failed to exchange token: %w", err)
}
log.Info("Authentication successful.")
fmt.Println("Authentication successful.")
return token, nil
}

View File

@@ -260,7 +260,7 @@ func (qa *QwenAuth) PollForToken(deviceCode, codeVerifier string) (*QwenTokenDat
switch errorType {
case "authorization_pending":
// User has not yet approved the authorization request. Continue polling.
log.Infof("Polling attempt %d/%d...\n", attempt+1, maxAttempts)
fmt.Printf("Polling attempt %d/%d...\n\n", attempt+1, maxAttempts)
time.Sleep(pollInterval)
continue
case "slow_down":
@@ -269,7 +269,7 @@ func (qa *QwenAuth) PollForToken(deviceCode, codeVerifier string) (*QwenTokenDat
if pollInterval > 10*time.Second {
pollInterval = 10 * time.Second
}
log.Infof("Server requested to slow down, increasing poll interval to %v\n", pollInterval)
fmt.Printf("Server requested to slow down, increasing poll interval to %v\n\n", pollInterval)
time.Sleep(pollInterval)
continue
case "expired_token":

View File

@@ -21,7 +21,7 @@ import (
// Returns:
// - An error if the URL cannot be opened, otherwise nil.
func OpenURL(url string) error {
log.Infof("Attempting to open URL in browser: %s", url)
fmt.Printf("Attempting to open URL in browser: %s\n", url)
// Try using the open-golang library first
err := open.Run(url)

View File

@@ -62,8 +62,8 @@ func DoLogin(cfg *config.Config, projectID string, options *LoginOptions) {
}
if savedPath != "" {
log.Infof("Authentication saved to %s", savedPath)
fmt.Printf("Authentication saved to %s\n", savedPath)
}
log.Info("Gemini authentication successful!")
fmt.Println("Gemini authentication successful!")
}

View File

@@ -23,6 +23,9 @@ type Config struct {
// Debug enables or disables debug-level logging and other debug features.
Debug bool `yaml:"debug" json:"debug"`
// LoggingToFile controls whether application logs are written to rotating files or stdout.
LoggingToFile bool `yaml:"logging-to-file" json:"logging-to-file"`
// ProxyURL is the URL of an optional proxy server to use for outbound requests.
ProxyURL string `yaml:"proxy-url" json:"proxy-url"`
@@ -202,6 +205,7 @@ func LoadConfig(configFile string) (*Config, error) {
// Unmarshal the YAML data into the Config struct.
var config Config
// Set defaults before unmarshal so that absent keys keep defaults.
config.LoggingToFile = true
config.GeminiWeb.Context = true
if err = yaml.Unmarshal(data, &config); err != nil {
return nil, fmt.Errorf("failed to parse config file: %w", err)

View File

@@ -0,0 +1,117 @@
package logging
import (
"bytes"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"sync"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
"gopkg.in/natefinch/lumberjack.v2"
)
var (
setupOnce sync.Once
writerMu sync.Mutex
logWriter *lumberjack.Logger
ginInfoWriter *io.PipeWriter
ginErrorWriter *io.PipeWriter
)
// LogFormatter defines a custom log format for logrus.
// This formatter adds timestamp, level, and source location to each log entry.
type LogFormatter struct{}
// Format renders a single log entry with custom formatting.
func (m *LogFormatter) Format(entry *log.Entry) ([]byte, error) {
var buffer *bytes.Buffer
if entry.Buffer != nil {
buffer = entry.Buffer
} else {
buffer = &bytes.Buffer{}
}
timestamp := entry.Time.Format("2006-01-02 15:04:05")
message := strings.TrimRight(entry.Message, "\r\n")
formatted := fmt.Sprintf("[%s] [%s] [%s:%d] %s\n", timestamp, entry.Level, filepath.Base(entry.Caller.File), entry.Caller.Line, message)
buffer.WriteString(formatted)
return buffer.Bytes(), nil
}
// SetupBaseLogger configures the shared logrus instance and Gin writers.
// It is safe to call multiple times; initialization happens only once.
func SetupBaseLogger() {
setupOnce.Do(func() {
log.SetOutput(os.Stdout)
log.SetReportCaller(true)
log.SetFormatter(&LogFormatter{})
ginInfoWriter = log.StandardLogger().Writer()
gin.DefaultWriter = ginInfoWriter
ginErrorWriter = log.StandardLogger().WriterLevel(log.ErrorLevel)
gin.DefaultErrorWriter = ginErrorWriter
gin.DebugPrintFunc = func(format string, values ...interface{}) {
format = strings.TrimRight(format, "\r\n")
log.StandardLogger().Infof(format, values...)
}
log.RegisterExitHandler(closeLogOutputs)
})
}
// ConfigureLogOutput switches the global log destination between rotating files and stdout.
func ConfigureLogOutput(loggingToFile bool) error {
SetupBaseLogger()
writerMu.Lock()
defer writerMu.Unlock()
if loggingToFile {
const logDir = "logs"
if err := os.MkdirAll(logDir, 0o755); err != nil {
return fmt.Errorf("logging: failed to create log directory: %w", err)
}
if logWriter != nil {
_ = logWriter.Close()
}
logWriter = &lumberjack.Logger{
Filename: filepath.Join(logDir, "main.log"),
MaxSize: 10,
MaxBackups: 0,
MaxAge: 0,
Compress: false,
}
log.SetOutput(logWriter)
return nil
}
if logWriter != nil {
_ = logWriter.Close()
logWriter = nil
}
log.SetOutput(os.Stdout)
return nil
}
func closeLogOutputs() {
writerMu.Lock()
defer writerMu.Unlock()
if logWriter != nil {
_ = logWriter.Close()
logWriter = nil
}
if ginInfoWriter != nil {
_ = ginInfoWriter.Close()
ginInfoWriter = nil
}
if ginErrorWriter != nil {
_ = ginErrorWriter.Close()
ginErrorWriter = nil
}
}

View File

@@ -1,6 +1,7 @@
package misc
import (
"fmt"
"path/filepath"
"strings"
@@ -15,7 +16,7 @@ func LogSavingCredentials(path string) {
return
}
// Use filepath.Clean so logs remain stable even if callers pass redundant separators.
log.Infof("Saving credentials to %s", filepath.Clean(path))
fmt.Printf("Saving credentials to %s\n", filepath.Clean(path))
}
// LogCredentialSeparator adds a visual separator to group auth/key processing logs.

View File

@@ -147,7 +147,7 @@ func getAccessToken(baseCookies map[string]string, proxy string, verbose bool, i
if len(matches) >= 2 {
token := matches[1]
if verbose {
log.Infof("Gemini access token acquired.")
fmt.Println("Gemini access token acquired.")
}
return token, mergedCookies, nil
}
@@ -280,7 +280,7 @@ func (c *GeminiClient) Init(timeoutSec float64, verbose bool) error {
c.Timeout = time.Duration(timeoutSec * float64(time.Second))
if verbose {
log.Infof("Gemini client initialized successfully.")
fmt.Println("Gemini client initialized successfully.")
}
return nil
}

View File

@@ -136,7 +136,7 @@ func (i Image) Save(path string, filename string, cookies map[string]string, ver
return "", err
}
if verbose {
log.Infof("Image saved as %s", dest)
fmt.Printf("Image saved as %s\n", dest)
}
abspath, _ := filepath.Abs(dest)
return abspath, nil

View File

@@ -120,7 +120,7 @@ func GetIPAddress() string {
func PrintSSHTunnelInstructions(port int) {
ipAddress := GetIPAddress()
border := "================================================================================"
log.Infof("To authenticate from a remote machine, an SSH tunnel may be required.")
fmt.Println("To authenticate from a remote machine, an SSH tunnel may be required.")
fmt.Println(border)
fmt.Println(" Run one of the following commands on your local machine (NOT the server):")
fmt.Println()

View File

@@ -380,7 +380,7 @@ func (w *Watcher) handleEvent(event fsnotify.Event) {
log.Debugf("config file content unchanged (hash match), skipping reload")
return
}
log.Infof("config file changed, reloading: %s", w.configPath)
fmt.Printf("config file changed, reloading: %s\n", w.configPath)
if w.reloadConfig() {
w.clientsMutex.Lock()
w.lastConfigHash = newHash
@@ -390,7 +390,7 @@ func (w *Watcher) handleEvent(event fsnotify.Event) {
}
// Handle auth directory changes incrementally (.json only)
log.Infof("auth file changed (%s): %s, processing incrementally", event.Op.String(), filepath.Base(event.Name))
fmt.Printf("auth file changed (%s): %s, processing incrementally\n", event.Op.String(), filepath.Base(event.Name))
if event.Op&fsnotify.Create == fsnotify.Create || event.Op&fsnotify.Write == fsnotify.Write {
w.addOrUpdateClient(event.Name)
} else if event.Op&fsnotify.Remove == fsnotify.Remove {

View File

@@ -80,22 +80,22 @@ func (a *ClaudeAuthenticator) Login(ctx context.Context, cfg *config.Config, opt
state = returnedState
if !opts.NoBrowser {
log.Info("Opening browser for Claude authentication")
fmt.Println("Opening browser for Claude authentication")
if !browser.IsAvailable() {
log.Warn("No browser available; please open the URL manually")
util.PrintSSHTunnelInstructions(a.CallbackPort)
log.Infof("Visit the following URL to continue authentication:\n%s", authURL)
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(a.CallbackPort)
log.Infof("Visit the following URL to continue authentication:\n%s", authURL)
fmt.Printf("Visit the following URL to continue authentication:\n%s\n", authURL)
}
} else {
util.PrintSSHTunnelInstructions(a.CallbackPort)
log.Infof("Visit the following URL to continue authentication:\n%s", authURL)
fmt.Printf("Visit the following URL to continue authentication:\n%s\n", authURL)
}
log.Info("Waiting for Claude authentication callback...")
fmt.Println("Waiting for Claude authentication callback...")
result, err := oauthServer.WaitForCallback(5 * time.Minute)
if err != nil {
@@ -131,9 +131,9 @@ func (a *ClaudeAuthenticator) Login(ctx context.Context, cfg *config.Config, opt
"email": tokenStorage.Email,
}
log.Info("Claude authentication successful")
fmt.Println("Claude authentication successful")
if authBundle.APIKey != "" {
log.Info("Claude API key obtained and stored")
fmt.Println("Claude API key obtained and stored")
}
return &TokenRecord{

View File

@@ -79,22 +79,22 @@ func (a *CodexAuthenticator) Login(ctx context.Context, cfg *config.Config, opts
}
if !opts.NoBrowser {
log.Info("Opening browser for Codex authentication")
fmt.Println("Opening browser for Codex authentication")
if !browser.IsAvailable() {
log.Warn("No browser available; please open the URL manually")
util.PrintSSHTunnelInstructions(a.CallbackPort)
log.Infof("Visit the following URL to continue authentication:\n%s", authURL)
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(a.CallbackPort)
log.Infof("Visit the following URL to continue authentication:\n%s", authURL)
fmt.Printf("Visit the following URL to continue authentication:\n%s\n", authURL)
}
} else {
util.PrintSSHTunnelInstructions(a.CallbackPort)
log.Infof("Visit the following URL to continue authentication:\n%s", authURL)
fmt.Printf("Visit the following URL to continue authentication:\n%s\n", authURL)
}
log.Info("Waiting for Codex authentication callback...")
fmt.Println("Waiting for Codex authentication callback...")
result, err := oauthServer.WaitForCallback(5 * time.Minute)
if err != nil {
@@ -130,9 +130,9 @@ func (a *CodexAuthenticator) Login(ctx context.Context, cfg *config.Config, opts
"email": tokenStorage.Email,
}
log.Info("Codex authentication successful")
fmt.Println("Codex authentication successful")
if authBundle.APIKey != "" {
log.Info("Codex API key obtained and stored")
fmt.Println("Codex API key obtained and stored")
}
return &TokenRecord{

View File

@@ -8,7 +8,6 @@ import (
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/gemini"
// legacy client removed
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
log "github.com/sirupsen/logrus"
)
// GeminiAuthenticator implements the login flow for Google Gemini CLI accounts.
@@ -57,7 +56,7 @@ func (a *GeminiAuthenticator) Login(ctx context.Context, cfg *config.Config, opt
"project_id": ts.ProjectID,
}
log.Info("Gemini authentication successful")
fmt.Println("Gemini authentication successful")
return &TokenRecord{
Provider: a.Provider(),

View File

@@ -51,19 +51,19 @@ func (a *QwenAuthenticator) Login(ctx context.Context, cfg *config.Config, opts
authURL := deviceFlow.VerificationURIComplete
if !opts.NoBrowser {
log.Info("Opening browser for Qwen authentication")
fmt.Println("Opening browser for Qwen authentication")
if !browser.IsAvailable() {
log.Warn("No browser available; please open the URL manually")
log.Infof("Visit the following URL to continue authentication:\n%s", authURL)
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)
log.Infof("Visit the following URL to continue authentication:\n%s", authURL)
fmt.Printf("Visit the following URL to continue authentication:\n%s\n", authURL)
}
} else {
log.Infof("Visit the following URL to continue authentication:\n%s", authURL)
fmt.Printf("Visit the following URL to continue authentication:\n%s\n", authURL)
}
log.Info("Waiting for Qwen authentication...")
fmt.Println("Waiting for Qwen authentication...")
tokenData, err := authSvc.PollForToken(deviceFlow.DeviceCode, deviceFlow.CodeVerifier)
if err != nil {
@@ -101,7 +101,7 @@ func (a *QwenAuthenticator) Login(ctx context.Context, cfg *config.Config, opts
"email": tokenStorage.Email,
}
log.Info("Qwen authentication successful")
fmt.Println("Qwen authentication successful")
return &TokenRecord{
Provider: a.Provider(),

View File

@@ -331,7 +331,7 @@ func (s *Service) Run(ctx context.Context) error {
}()
time.Sleep(100 * time.Millisecond)
log.Info("API server started successfully")
fmt.Println("API server started successfully")
if s.hooks.OnAfterStart != nil {
s.hooks.OnAfterStart(s)