From 2e836cee88ec015749dae441f0656a43656f41ed Mon Sep 17 00:00:00 2001 From: Luis Pater Date: Mon, 22 Sep 2025 23:23:31 +0800 Subject: [PATCH] feat(auth): standardize `last_refresh` metadata handling across executors - Added `last_refresh` timestamp to metadata for Codex, Claude, Qwen, and Gemini executors. - Implemented `extractLastRefreshTimestamp` utility for parsing diverse timestamp formats in management handlers. - Ensured consistent update and preservation of `last_refresh` in file-based auth handling. --- .../api/handlers/management/auth_files.go | 65 ++++++++++++++++++- internal/runtime/executor/claude_executor.go | 3 + internal/runtime/executor/codex_executor.go | 3 + .../runtime/executor/gemini_web_executor.go | 2 + internal/runtime/executor/qwen_executor.go | 3 + 5 files changed, 75 insertions(+), 1 deletion(-) diff --git a/internal/api/handlers/management/auth_files.go b/internal/api/handlers/management/auth_files.go index 270567c5..651d87c0 100644 --- a/internal/api/handlers/management/auth_files.go +++ b/internal/api/handlers/management/auth_files.go @@ -9,6 +9,7 @@ import ( "net/url" "os" "path/filepath" + "strconv" "strings" "time" @@ -31,6 +32,61 @@ var ( oauthStatus = make(map[string]string) ) +var lastRefreshKeys = []string{"last_refresh", "lastRefresh", "last_refreshed_at", "lastRefreshedAt"} + +func extractLastRefreshTimestamp(meta map[string]any) (time.Time, bool) { + if len(meta) == 0 { + return time.Time{}, false + } + for _, key := range lastRefreshKeys { + if val, ok := meta[key]; ok { + if ts, ok1 := parseLastRefreshValue(val); ok1 { + return ts, true + } + } + } + return time.Time{}, false +} + +func parseLastRefreshValue(v any) (time.Time, bool) { + switch val := v.(type) { + case string: + s := strings.TrimSpace(val) + if s == "" { + return time.Time{}, false + } + layouts := []string{time.RFC3339, time.RFC3339Nano, "2006-01-02 15:04:05", "2006-01-02T15:04:05Z07:00"} + for _, layout := range layouts { + if ts, err := time.Parse(layout, s); err == nil { + return ts.UTC(), true + } + } + if unix, err := strconv.ParseInt(s, 10, 64); err == nil && unix > 0 { + return time.Unix(unix, 0).UTC(), true + } + case float64: + if val <= 0 { + return time.Time{}, false + } + return time.Unix(int64(val), 0).UTC(), true + case int64: + if val <= 0 { + return time.Time{}, false + } + return time.Unix(val, 0).UTC(), true + case int: + if val <= 0 { + return time.Time{}, false + } + return time.Unix(int64(val), 0).UTC(), true + case json.Number: + if i, err := val.Int64(); err == nil && i > 0 { + return time.Unix(i, 0).UTC(), true + } + } + return time.Time{}, false +} + // List auth files func (h *Handler) ListAuthFiles(c *gin.Context) { entries, err := os.ReadDir(h.cfg.AuthDir) @@ -239,6 +295,8 @@ func (h *Handler) registerAuthFromFile(ctx context.Context, path string, data [] if email, ok := metadata["email"].(string); ok && email != "" { label = email } + lastRefresh, hasLastRefresh := extractLastRefreshTimestamp(metadata) + attr := map[string]string{ "path": path, "source": path, @@ -253,9 +311,14 @@ func (h *Handler) registerAuthFromFile(ctx context.Context, path string, data [] CreatedAt: time.Now(), UpdatedAt: time.Now(), } + if hasLastRefresh { + auth.LastRefreshedAt = lastRefresh + } if existing, ok := h.authManager.GetByID(path); ok { auth.CreatedAt = existing.CreatedAt - auth.LastRefreshedAt = existing.LastRefreshedAt + if !hasLastRefresh { + auth.LastRefreshedAt = existing.LastRefreshedAt + } auth.NextRefreshAfter = existing.NextRefreshAfter auth.Runtime = existing.Runtime _, err := h.authManager.Update(ctx, auth) diff --git a/internal/runtime/executor/claude_executor.go b/internal/runtime/executor/claude_executor.go index ce45dac1..c4c7d107 100644 --- a/internal/runtime/executor/claude_executor.go +++ b/internal/runtime/executor/claude_executor.go @@ -8,6 +8,7 @@ import ( "io" "net/http" "strings" + "time" claudeauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/claude" "github.com/router-for-me/CLIProxyAPI/v6/internal/config" @@ -171,6 +172,8 @@ func (e *ClaudeExecutor) Refresh(ctx context.Context, auth *cliproxyauth.Auth) ( auth.Metadata["email"] = td.Email auth.Metadata["expired"] = td.Expire auth.Metadata["type"] = "claude" + now := time.Now().Format(time.RFC3339) + auth.Metadata["last_refresh"] = now return auth, nil } diff --git a/internal/runtime/executor/codex_executor.go b/internal/runtime/executor/codex_executor.go index fb73a866..b61975a9 100644 --- a/internal/runtime/executor/codex_executor.go +++ b/internal/runtime/executor/codex_executor.go @@ -7,6 +7,7 @@ import ( "io" "net/http" "strings" + "time" codexauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/codex" "github.com/router-for-me/CLIProxyAPI/v6/internal/config" @@ -222,6 +223,8 @@ func (e *CodexExecutor) Refresh(ctx context.Context, auth *cliproxyauth.Auth) (* // Use unified key in files auth.Metadata["expired"] = td.Expire auth.Metadata["type"] = "codex" + now := time.Now().Format(time.RFC3339) + auth.Metadata["last_refresh"] = now return auth, nil } diff --git a/internal/runtime/executor/gemini_web_executor.go b/internal/runtime/executor/gemini_web_executor.go index 30177362..34bd4c02 100644 --- a/internal/runtime/executor/gemini_web_executor.go +++ b/internal/runtime/executor/gemini_web_executor.go @@ -6,6 +6,7 @@ import ( "fmt" "net/http" "sync" + "time" "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/gemini" "github.com/router-for-me/CLIProxyAPI/v6/internal/config" @@ -121,6 +122,7 @@ func (e *GeminiWebExecutor) Refresh(ctx context.Context, auth *cliproxyauth.Auth auth.Metadata["secure_1psid"] = ts.Secure1PSID auth.Metadata["secure_1psidts"] = ts.Secure1PSIDTS auth.Metadata["type"] = "gemini-web" + auth.Metadata["last_refresh"] = time.Now().Format(time.RFC3339) return auth, nil } diff --git a/internal/runtime/executor/qwen_executor.go b/internal/runtime/executor/qwen_executor.go index c5468d97..e2e50bf5 100644 --- a/internal/runtime/executor/qwen_executor.go +++ b/internal/runtime/executor/qwen_executor.go @@ -8,6 +8,7 @@ import ( "io" "net/http" "strings" + "time" qwenauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/qwen" "github.com/router-for-me/CLIProxyAPI/v6/internal/config" @@ -179,6 +180,8 @@ func (e *QwenExecutor) Refresh(ctx context.Context, auth *cliproxyauth.Auth) (*c // Use "expired" for consistency with existing file format auth.Metadata["expired"] = td.Expire auth.Metadata["type"] = "qwen" + now := time.Now().Format(time.RFC3339) + auth.Metadata["last_refresh"] = now return auth, nil }