From 6db02f0069ea444b873a9e541dff7dd3d9a1ce16 Mon Sep 17 00:00:00 2001 From: chenjian Date: Tue, 19 May 2026 15:43:13 +0800 Subject: [PATCH 1/2] fix(accounts): show email and add usage refresh button for OpenAI OAuth - AccountsView: display email under account name by checking extra.email and credentials.email in addition to extra.email_address; OpenAI OAuth stores email under the 'email' key while Anthropic uses 'email_address' - AccountUsageCell: add per-account refresh button to the OpenAI OAuth usage section, identical to the existing Anthropic OAuth pattern; reuses loadActiveUsage, activeQueryLoading, and the activeQuery i18n key --- .../components/account/AccountUsageCell.vue | 26 ++++++++++++++++++- frontend/src/views/admin/AccountsView.vue | 6 ++--- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/account/AccountUsageCell.vue b/frontend/src/components/account/AccountUsageCell.vue index 2c04e673..64f1366b 100644 --- a/frontend/src/components/account/AccountUsageCell.vue +++ b/frontend/src/components/account/AccountUsageCell.vue @@ -126,6 +126,30 @@ :show-now-when-idle="true" color="emerald" /> +
+ +
@@ -1070,7 +1094,7 @@ const attachVisibilityObserver = () => { const loadActiveUsage = async () => { activeQueryLoading.value = true try { - usageInfo.value = await adminAPI.accounts.getUsage(props.account.id, 'active') + usageInfo.value = await adminAPI.accounts.getUsage(props.account.id, 'active', true) } catch (e: any) { console.error('Failed to load active usage:', e) } finally { diff --git a/frontend/src/views/admin/AccountsView.vue b/frontend/src/views/admin/AccountsView.vue index c2159f6f..51137c8b 100644 --- a/frontend/src/views/admin/AccountsView.vue +++ b/frontend/src/views/admin/AccountsView.vue @@ -214,11 +214,11 @@
{{ value }} - {{ row.extra.email_address }} + {{ row.extra?.email_address || row.extra?.email || row.credentials?.email }}
From 41e7ae534c5cd0e7e771678d27f62f232681fbc8 Mon Sep 17 00:00:00 2001 From: chenjian Date: Tue, 19 May 2026 15:43:21 +0800 Subject: [PATCH 2/2] fix(accounts): fix OpenAI OAuth usage quota never refreshing on manual refresh MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The refresh button had no effect because two independent gates blocked the Codex probe: 1. isOpenAICodexSnapshotStale returned false for non-WS-v2 accounts even when data was stale — this is correct for background auto-refresh (Codex quota is only auto-tracked for Codex CLI / WS v2 accounts), so this behavior is preserved. 2. shouldProbeOpenAICodexSnapshot had a 10-min in-memory rate limit with no bypass for explicit user requests. Fix: add a force parameter threaded from handler → GetUsage → getOpenAIUsage → shouldProbeOpenAICodexSnapshot. When force=true (manual refresh button), both gates are bypassed and the probe always runs. Background auto-loads continue to respect the original WS v2 logic and 10-min rate limit. --- .../internal/handler/admin/account_handler.go | 5 +++-- .../internal/service/account_usage_service.go | 21 ++++++++++++------- .../service/account_usage_service_test.go | 2 +- frontend/src/api/admin/accounts.ts | 7 +++++-- 4 files changed, 22 insertions(+), 13 deletions(-) diff --git a/backend/internal/handler/admin/account_handler.go b/backend/internal/handler/admin/account_handler.go index ffab74d6..57fe7f76 100644 --- a/backend/internal/handler/admin/account_handler.go +++ b/backend/internal/handler/admin/account_handler.go @@ -1643,7 +1643,7 @@ func (h *OAuthHandler) SetupTokenCookieAuth(c *gin.Context) { } // GetUsage handles getting account usage information -// GET /api/v1/admin/accounts/:id/usage?source=passive|active +// GET /api/v1/admin/accounts/:id/usage?source=passive|active&force=true func (h *AccountHandler) GetUsage(c *gin.Context) { accountID, err := strconv.ParseInt(c.Param("id"), 10, 64) if err != nil { @@ -1652,12 +1652,13 @@ func (h *AccountHandler) GetUsage(c *gin.Context) { } source := c.DefaultQuery("source", "active") + force := c.Query("force") == "true" var usage *service.UsageInfo if source == "passive" { usage, err = h.accountUsageService.GetPassiveUsage(c.Request.Context(), accountID) } else { - usage, err = h.accountUsageService.GetUsage(c.Request.Context(), accountID) + usage, err = h.accountUsageService.GetUsage(c.Request.Context(), accountID, force) } if err != nil { response.ErrorFrom(c, err) diff --git a/backend/internal/service/account_usage_service.go b/backend/internal/service/account_usage_service.go index 68ba8f8c..1c871d2b 100644 --- a/backend/internal/service/account_usage_service.go +++ b/backend/internal/service/account_usage_service.go @@ -295,14 +295,16 @@ func NewAccountUsageService( // OAuth账号: 调用Anthropic API获取真实数据(需要profile scope),API响应缓存10分钟,窗口统计缓存1分钟 // Setup Token账号: 根据session_window推算5h窗口,7d数据不可用(没有profile scope) // API Key账号: 不支持usage查询 -func (s *AccountUsageService) GetUsage(ctx context.Context, accountID int64) (*UsageInfo, error) { +func (s *AccountUsageService) GetUsage(ctx context.Context, accountID int64, force ...bool) (*UsageInfo, error) { + forceProbe := len(force) > 0 && force[0] + account, err := s.accountRepo.GetByID(ctx, accountID) if err != nil { return nil, fmt.Errorf("get account failed: %w", err) } if account.Platform == PlatformOpenAI && account.Type == AccountTypeOAuth { - usage, err := s.getOpenAIUsage(ctx, account) + usage, err := s.getOpenAIUsage(ctx, account, forceProbe) if err == nil { s.tryClearRecoverableAccountError(ctx, account) } @@ -492,7 +494,7 @@ func (s *AccountUsageService) syncActiveToPassive(ctx context.Context, accountID } } -func (s *AccountUsageService) getOpenAIUsage(ctx context.Context, account *Account) (*UsageInfo, error) { +func (s *AccountUsageService) getOpenAIUsage(ctx context.Context, account *Account, force bool) (*UsageInfo, error) { now := time.Now() usage := &UsageInfo{UpdatedAt: &now} @@ -507,7 +509,7 @@ func (s *AccountUsageService) getOpenAIUsage(ctx context.Context, account *Accou usage.SevenDay = progress } - if shouldRefreshOpenAICodexSnapshot(account, usage, now) && s.shouldProbeOpenAICodexSnapshot(account.ID, now) { + if (force || shouldRefreshOpenAICodexSnapshot(account, usage, now)) && s.shouldProbeOpenAICodexSnapshot(account.ID, now, force) { if updates, err := s.probeOpenAICodexSnapshot(ctx, account); err == nil && len(updates) > 0 { mergeAccountExtra(account, updates) if usage.UpdatedAt == nil { @@ -577,13 +579,16 @@ func isOpenAICodexSnapshotStale(account *Account, now time.Time) bool { return now.Sub(ts) >= openAIProbeCacheTTL } -func (s *AccountUsageService) shouldProbeOpenAICodexSnapshot(accountID int64, now time.Time) bool { +func (s *AccountUsageService) shouldProbeOpenAICodexSnapshot(accountID int64, now time.Time, force ...bool) bool { if s == nil || s.cache == nil || accountID <= 0 { return true } - if cached, ok := s.cache.openAIProbeCache.Load(accountID); ok { - if ts, ok := cached.(time.Time); ok && now.Sub(ts) < openAIProbeCacheTTL { - return false + forceProbe := len(force) > 0 && force[0] + if !forceProbe { + if cached, ok := s.cache.openAIProbeCache.Load(accountID); ok { + if ts, ok := cached.(time.Time); ok && now.Sub(ts) < openAIProbeCacheTTL { + return false + } } } s.cache.openAIProbeCache.Store(accountID, now) diff --git a/backend/internal/service/account_usage_service_test.go b/backend/internal/service/account_usage_service_test.go index 28b49838..e0390c4c 100644 --- a/backend/internal/service/account_usage_service_test.go +++ b/backend/internal/service/account_usage_service_test.go @@ -140,7 +140,7 @@ func TestAccountUsageService_GetOpenAIUsage_DoesNotPromoteCodexExtraToRateLimit( }, } - usage, err := svc.getOpenAIUsage(context.Background(), account) + usage, err := svc.getOpenAIUsage(context.Background(), account, false) if err != nil { t.Fatalf("getOpenAIUsage() error = %v", err) } diff --git a/frontend/src/api/admin/accounts.ts b/frontend/src/api/admin/accounts.ts index 00ed4087..e5b1b973 100644 --- a/frontend/src/api/admin/accounts.ts +++ b/frontend/src/api/admin/accounts.ts @@ -232,9 +232,12 @@ export async function clearError(id: number): Promise { * @param id - Account ID * @returns Account usage info */ -export async function getUsage(id: number, source?: 'passive' | 'active'): Promise { +export async function getUsage(id: number, source?: 'passive' | 'active', force?: boolean): Promise { + const params: Record = {} + if (source) params.source = source + if (force) params.force = 'true' const { data } = await apiClient.get(`/admin/accounts/${id}/usage`, { - params: source ? { source } : undefined + params: Object.keys(params).length > 0 ? params : undefined }) return data }