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.
This commit is contained in:
Luis Pater
2025-09-22 23:23:31 +08:00
parent e41d127732
commit 2e836cee88
5 changed files with 75 additions and 1 deletions

View File

@@ -9,6 +9,7 @@ import (
"net/url" "net/url"
"os" "os"
"path/filepath" "path/filepath"
"strconv"
"strings" "strings"
"time" "time"
@@ -31,6 +32,61 @@ var (
oauthStatus = make(map[string]string) 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 // List auth files
func (h *Handler) ListAuthFiles(c *gin.Context) { func (h *Handler) ListAuthFiles(c *gin.Context) {
entries, err := os.ReadDir(h.cfg.AuthDir) 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 != "" { if email, ok := metadata["email"].(string); ok && email != "" {
label = email label = email
} }
lastRefresh, hasLastRefresh := extractLastRefreshTimestamp(metadata)
attr := map[string]string{ attr := map[string]string{
"path": path, "path": path,
"source": path, "source": path,
@@ -253,9 +311,14 @@ func (h *Handler) registerAuthFromFile(ctx context.Context, path string, data []
CreatedAt: time.Now(), CreatedAt: time.Now(),
UpdatedAt: time.Now(), UpdatedAt: time.Now(),
} }
if hasLastRefresh {
auth.LastRefreshedAt = lastRefresh
}
if existing, ok := h.authManager.GetByID(path); ok { if existing, ok := h.authManager.GetByID(path); ok {
auth.CreatedAt = existing.CreatedAt auth.CreatedAt = existing.CreatedAt
auth.LastRefreshedAt = existing.LastRefreshedAt if !hasLastRefresh {
auth.LastRefreshedAt = existing.LastRefreshedAt
}
auth.NextRefreshAfter = existing.NextRefreshAfter auth.NextRefreshAfter = existing.NextRefreshAfter
auth.Runtime = existing.Runtime auth.Runtime = existing.Runtime
_, err := h.authManager.Update(ctx, auth) _, err := h.authManager.Update(ctx, auth)

View File

@@ -8,6 +8,7 @@ import (
"io" "io"
"net/http" "net/http"
"strings" "strings"
"time"
claudeauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/claude" claudeauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/claude"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "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["email"] = td.Email
auth.Metadata["expired"] = td.Expire auth.Metadata["expired"] = td.Expire
auth.Metadata["type"] = "claude" auth.Metadata["type"] = "claude"
now := time.Now().Format(time.RFC3339)
auth.Metadata["last_refresh"] = now
return auth, nil return auth, nil
} }

View File

@@ -7,6 +7,7 @@ import (
"io" "io"
"net/http" "net/http"
"strings" "strings"
"time"
codexauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/codex" codexauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/codex"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "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 // Use unified key in files
auth.Metadata["expired"] = td.Expire auth.Metadata["expired"] = td.Expire
auth.Metadata["type"] = "codex" auth.Metadata["type"] = "codex"
now := time.Now().Format(time.RFC3339)
auth.Metadata["last_refresh"] = now
return auth, nil return auth, nil
} }

View File

@@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"sync" "sync"
"time"
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/gemini" "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/gemini"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "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_1psid"] = ts.Secure1PSID
auth.Metadata["secure_1psidts"] = ts.Secure1PSIDTS auth.Metadata["secure_1psidts"] = ts.Secure1PSIDTS
auth.Metadata["type"] = "gemini-web" auth.Metadata["type"] = "gemini-web"
auth.Metadata["last_refresh"] = time.Now().Format(time.RFC3339)
return auth, nil return auth, nil
} }

View File

@@ -8,6 +8,7 @@ import (
"io" "io"
"net/http" "net/http"
"strings" "strings"
"time"
qwenauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/qwen" qwenauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/qwen"
"github.com/router-for-me/CLIProxyAPI/v6/internal/config" "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 // Use "expired" for consistency with existing file format
auth.Metadata["expired"] = td.Expire auth.Metadata["expired"] = td.Expire
auth.Metadata["type"] = "qwen" auth.Metadata["type"] = "qwen"
now := time.Now().Format(time.RFC3339)
auth.Metadata["last_refresh"] = now
return auth, nil return auth, nil
} }