From 1ac63cac5d9943a243e476b11684c3ebef8e7bb1 Mon Sep 17 00:00:00 2001 From: Cmochance <3216202644@qq.com> Date: Thu, 21 May 2026 20:05:20 +0800 Subject: [PATCH] feat(quota): optimize quota refresh logic, add relative countdowns, parallelize card refreshes --- src-tauri/shared/front/actions.ts | 83 ++++++-------------- src-tauri/shared/front/i18n.ts | 4 + src-tauri/shared/front/render.ts | 50 +++++++----- src-tauri/shared/front/state.ts | 4 +- src-tauri/shared/front/tauri.ts | 2 + src-tauri/shared/front/types.ts | 1 + src-tauri/shared/runtime/chatgpt_api.rs | 3 +- src-tauri/shared/runtime/codex_app_server.rs | 25 +++--- src-tauri/shared/runtime/metadata.rs | 2 + src-tauri/shared/runtime/models.rs | 1 + src-tauri/shared/runtime/profiles_index.rs | 12 +++ src-tauri/shared/runtime/quota_cache.rs | 2 + src-tauri/shared/runtime/session_usage.rs | 4 + 13 files changed, 98 insertions(+), 95 deletions(-) diff --git a/src-tauri/shared/front/actions.ts b/src-tauri/shared/front/actions.ts index 2e2e232..eebba20 100644 --- a/src-tauri/shared/front/actions.ts +++ b/src-tauri/shared/front/actions.ts @@ -94,7 +94,7 @@ let pendingLoginRetry: (() => Promise) | null = null; let cancelledLoginProfile: string | null = null; function isRefreshPending(profile: string): boolean { - return state.refreshActiveProfile === profile || state.refreshQueue.includes(profile); + return state.refreshActiveProfiles.includes(profile); } function clearDialogError(element: HTMLParagraphElement): void { @@ -261,71 +261,30 @@ async function handleSwitchProfile(profile: string): Promise { } } -async function drainRefreshQueue(): Promise { - if (state.refreshWorkerActive) { - return; - } - - state.refreshWorkerActive = true; +async function performProfileRefresh(profile: string): Promise { + state.refreshActiveProfiles.push(profile); + rerenderDashboard(); try { - while (state.refreshQueue.length > 0) { - const profile = state.refreshQueue.shift(); - if (!profile) { - continue; - } - - state.refreshActiveProfile = profile; - rerenderDashboard(); - try { - await refreshProfile(profile); - showToast(t(state.locale, "refreshedProfile", { profile })); - // The backend already wrote the new quota / plan into the - // profiles index. Re-reading the snapshot picks those up - // for every card without paying for a JSONL scan. - // - // `getCurrentLiveQuota` only matters when the refreshed - // profile is also the active one — the live JSONL session - // count can be newer than the API value we just persisted - // (an in-flight `codex` session keeps appending - // `token_count` events) and `select_current_quota` picks - // the newer of the two. For non-active refreshes we skip - // it; the active card panel for that profile is rebuilt - // when the user switches to it, and the 15s ticker keeps - // the panel honest in the meantime. - try { - const snapshot = await getProfilesSnapshot(); - applySnapshot(snapshot); - if (snapshot.current_card?.folder_name === profile) { - applyCurrentQuota(await getCurrentLiveQuota()); - } - } catch (error) { - // Best-effort: a transient snapshot fetch failure leaves - // the cards on their pre-refresh state, which matches the - // pre-PR `refreshAllData(false)` behavior. Surface only - // to the console so a systematic failure is debuggable - // without spamming the user with a toast they can't act - // on. - console.warn("Snapshot refresh after profile refresh failed:", error); - } - } catch (error) { - showToast(refreshProfileErrorMessage(error), true); - } finally { - state.refreshActiveProfile = null; - rerenderDashboard(); + await refreshProfile(profile); + showToast(t(state.locale, "refreshedProfile", { profile })); + try { + const snapshot = await getProfilesSnapshot(); + applySnapshot(snapshot); + if (snapshot.current_card?.folder_name === profile) { + applyCurrentQuota(await getCurrentLiveQuota()); } + } catch (error) { + console.warn("Snapshot refresh after profile refresh failed:", error); } + } catch (error) { + showToast(refreshProfileErrorMessage(error), true); } finally { - state.refreshWorkerActive = false; + state.refreshActiveProfiles = state.refreshActiveProfiles.filter(p => p !== profile); rerenderDashboard(); } } function handleRefreshProfile(profile: string): void { - // Mirror `handleLoginProfile`'s `isRefreshPending(profile)` guard in - // the opposite direction: when the same profile already has a login - // in flight, both flows would otherwise race on writing per-profile - // `auth.json`. Cross-profile refresh during a login is still allowed - // (different sandbox + different `auth.json`). if ( state.loading || state.loginActiveProfile === profile @@ -334,9 +293,7 @@ function handleRefreshProfile(profile: string): void { return; } - state.refreshQueue.push(profile); - rerenderDashboard(); - void drainRefreshQueue(); + void performProfileRefresh(profile); } function loginErrorCode(error: unknown): string | undefined { @@ -1007,6 +964,12 @@ export function bootstrap(): void { void refreshActiveQuotaSilently(); }, 5 * 60_000); + // Relative countdown timer tick: rerender the dashboard every 15 seconds + // to update the remaining relative countdown times. + window.setInterval(() => { + rerenderDashboard(); + }, 15_000); + // Bulk plan refresh: forces an OAuth refresh on every OAuth profile so // the cached id_token claims (plan tier, subscription expiry) move // forward even for inactive profiles that the 5-min ticker never diff --git a/src-tauri/shared/front/i18n.ts b/src-tauri/shared/front/i18n.ts index 34e6421..48e8f70 100644 --- a/src-tauri/shared/front/i18n.ts +++ b/src-tauri/shared/front/i18n.ts @@ -52,6 +52,8 @@ const enMessages = { weeklyAllowance: "Weekly allowance", refresh: "Refresh {value}", refreshButton: "Refresh", + resetsIn: "Resets in {value}", + resetting: "Resetting...", loginButton: "Login", baseButton: "Base", planUnknownPaid: "Unknown paid plan", @@ -299,6 +301,8 @@ const messages: Record = { weeklyAllowance: "周额度", refresh: "刷新时间 {value}", refreshButton: "刷新", + resetsIn: "{value} 后重置", + resetting: "重置中...", loginButton: "登录", baseButton: "Base", planUnknownPaid: "未知付费", diff --git a/src-tauri/shared/front/render.ts b/src-tauri/shared/front/render.ts index fc9feba..86dd902 100644 --- a/src-tauri/shared/front/render.ts +++ b/src-tauri/shared/front/render.ts @@ -148,8 +148,28 @@ function formatPercent(value: number | null): string { return value == null ? "--" : `${value}%`; } -function formatRefresh(value: string | null): string { - return value || "--"; +function formatRefresh(entry: QuotaWindow | undefined): string { + if (!entry) { + return "--"; + } + if (entry.reset_at_timestamp != null) { + const diff = entry.reset_at_timestamp - Math.floor(Date.now() / 1000); + if (diff > 0) { + const h = Math.floor(diff / 3600); + const m = Math.floor((diff % 3600) / 60); + if (h > 0) { + return t(state.locale, "resetsIn", { value: `${h}h ${m}m` }); + } else if (m > 0) { + const s = diff % 60; + return t(state.locale, "resetsIn", { value: `${m}m ${s}s` }); + } else { + return t(state.locale, "resetsIn", { value: `${diff}s` }); + } + } else { + return t(state.locale, "resetting"); + } + } + return entry.refresh_at || "--"; } function escapeHtml(value: string): string { @@ -333,7 +353,7 @@ function buildMetricLineMarkup(
${escapeHtml(label)} - ${escapeHtml(formatRefresh(entry?.refresh_at ?? null))} + ${escapeHtml(formatRefresh(entry))} ${escapeHtml(formatPercent(unavailable ? null : entry?.remaining_percent ?? null))}
@@ -508,10 +528,8 @@ export function renderProfiles( elements.profilesGrid.innerHTML = dashboard.profiles .map((profile) => { - const refreshRunning = state.refreshActiveProfile === profile.folder_name; - const refreshQueued = - !refreshRunning && state.refreshQueue.includes(profile.folder_name); - const refreshPending = refreshRunning || refreshQueued; + const refreshRunning = state.refreshActiveProfiles.includes(profile.folder_name); + const refreshPending = refreshRunning; const loginRunning = state.loginActiveProfile === profile.folder_name; // Any in-flight login (on this card or any other) blocks new logins // because the OAuth port and `.switch.lock` are global resources. @@ -526,20 +544,16 @@ export function renderProfiles( const baseDisabled = state.loading || cardBusy; const switchDisabled = !profile.auth_present || state.loading || cardBusy || loginPending || profile.status === "current"; - // The login button stays clickable while *this* card's login is in - // flight so the user can cancel the codex login process when they - // close the OAuth tab without finishing. It's still disabled when - // some other card holds the global login lock. + + const refreshTitle = refreshRunning + ? t(state.locale, "profileRefreshRunning") + : refreshDisabled + ? t(state.locale, "profileRefreshDisabled") + : t(state.locale, "profileRefreshReady"); + const loginDisabled = state.loading || refreshPending || (loginPending && !loginRunning); const unavailable = isProfileUnavailable(profile); - const refreshTitle = refreshRunning - ? t(state.locale, "profileRefreshRunning") - : refreshQueued - ? t(state.locale, "profileRefreshQueued") - : refreshDisabled - ? t(state.locale, "profileRefreshDisabled") - : t(state.locale, "profileRefreshReady"); const planTooltip = planFreshnessTitle(profile.plan_name, profile.last_plan_check_ms); const planClasses = [ diff --git a/src-tauri/shared/front/state.ts b/src-tauri/shared/front/state.ts index b8b6e01..dd29321 100644 --- a/src-tauri/shared/front/state.ts +++ b/src-tauri/shared/front/state.ts @@ -5,9 +5,7 @@ import type { ThemeId } from "@front-shared/theme"; export const state = { page: 1, loading: false, - refreshQueue: [] as string[], - refreshActiveProfile: null as string | null, - refreshWorkerActive: false, + refreshActiveProfiles: [] as string[], loginActiveProfile: null as string | null, currentProfile: null as string | null, route: "dashboard" as ShellRoute, diff --git a/src-tauri/shared/front/tauri.ts b/src-tauri/shared/front/tauri.ts index b0d7c9e..d62675e 100644 --- a/src-tauri/shared/front/tauri.ts +++ b/src-tauri/shared/front/tauri.ts @@ -40,10 +40,12 @@ function quota( five_hour: { remaining_percent: fiveHourPercent, refresh_at: fiveHourRefresh, + reset_at_timestamp: null, }, weekly: { remaining_percent: weeklyPercent, refresh_at: weeklyRefresh, + reset_at_timestamp: null, }, }; } diff --git a/src-tauri/shared/front/types.ts b/src-tauri/shared/front/types.ts index 683554b..17c52e0 100644 --- a/src-tauri/shared/front/types.ts +++ b/src-tauri/shared/front/types.ts @@ -1,6 +1,7 @@ export interface QuotaWindow { remaining_percent: number | null; refresh_at: string | null; + reset_at_timestamp: number | null; } export interface QuotaSummary { diff --git a/src-tauri/shared/runtime/chatgpt_api.rs b/src-tauri/shared/runtime/chatgpt_api.rs index 326a501..46593f6 100644 --- a/src-tauri/shared/runtime/chatgpt_api.rs +++ b/src-tauri/shared/runtime/chatgpt_api.rs @@ -137,7 +137,7 @@ pub fn looks_like_relogin_required(error_code: &str, message: &str) -> bool { } /// Refresh the access token a little before its actual expiry so a 401 /// in-flight does not bubble up to the caller. -const EXPIRY_SKEW_SECONDS: i64 = 60; +const EXPIRY_SKEW_SECONDS: i64 = 300; /// Outcome of a single ChatGPT-API refresh round-trip. /// @@ -626,6 +626,7 @@ fn quota_window_from_rate_limit(window: &RateLimitWindow) -> QuotaWindow { QuotaWindow { remaining_percent, refresh_at, + reset_at_timestamp: window.reset_at, } } diff --git a/src-tauri/shared/runtime/codex_app_server.rs b/src-tauri/shared/runtime/codex_app_server.rs index 0bc498b..b91e651 100644 --- a/src-tauri/shared/runtime/codex_app_server.rs +++ b/src-tauri/shared/runtime/codex_app_server.rs @@ -386,22 +386,21 @@ fn quota_window_from_app_server(window: &Value) -> QuotaWindow { }); let remaining_percent = used_percent.map(|used| (100.0 - used).round().clamp(0.0, 100.0) as u8); - let refresh_at = window - .get("resetsAt") - .and_then(Value::as_i64) - .and_then(|seconds| { - Utc.timestamp_opt(seconds, 0) - .single() - .map(|datetime| { - datetime - .with_timezone(&Local) - .format("%Y-%m-%d %H:%M") - .to_string() - }) - }); + let reset_at_timestamp = window.get("resetsAt").and_then(Value::as_i64); + let refresh_at = reset_at_timestamp.and_then(|seconds| { + Utc.timestamp_opt(seconds, 0) + .single() + .map(|datetime| { + datetime + .with_timezone(&Local) + .format("%Y-%m-%d %H:%M") + .to_string() + }) + }); QuotaWindow { remaining_percent, refresh_at, + reset_at_timestamp, } } diff --git a/src-tauri/shared/runtime/metadata.rs b/src-tauri/shared/runtime/metadata.rs index a43b7dc..b1ce2cc 100644 --- a/src-tauri/shared/runtime/metadata.rs +++ b/src-tauri/shared/runtime/metadata.rs @@ -423,6 +423,7 @@ mod tests { five_hour: QuotaWindow { remaining_percent: Some(99), refresh_at: Some("2026-04-21 13:37".to_string()), + ..QuotaWindow::default() }, ..QuotaSummary::default() } @@ -568,6 +569,7 @@ mod tests { weekly: QuotaWindow { remaining_percent: Some(82), refresh_at: Some("2026-05-15 12:00".to_string()), + ..QuotaWindow::default() }, }, ..ProfileMetadata::default() diff --git a/src-tauri/shared/runtime/models.rs b/src-tauri/shared/runtime/models.rs index 6a430cb..0243193 100644 --- a/src-tauri/shared/runtime/models.rs +++ b/src-tauri/shared/runtime/models.rs @@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize}; pub struct QuotaWindow { pub remaining_percent: Option, pub refresh_at: Option, + pub reset_at_timestamp: Option, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] diff --git a/src-tauri/shared/runtime/profiles_index.rs b/src-tauri/shared/runtime/profiles_index.rs index 2cbd7a2..759f419 100644 --- a/src-tauri/shared/runtime/profiles_index.rs +++ b/src-tauri/shared/runtime/profiles_index.rs @@ -231,6 +231,18 @@ pub fn load_profiles_index(codex_home: Option<&Path>) -> AppResult> = OnceLock::new(); + let _guard = LOCK + .get_or_init(|| std::sync::Mutex::new(())) + .lock() + .unwrap_or_else(|e| e.into_inner()); + + // Double check cache after acquiring the lock to avoid redundant disk reads/writes + if let Some(cached) = try_load_cached_index(&codex_home) { + return Ok(cached); + } + let backup_root = get_backup_root(Some(&codex_home)); let (mut index, mut changed) = match load_profiles_index_file(&codex_home) { Some(index) => (index, false), diff --git a/src-tauri/shared/runtime/quota_cache.rs b/src-tauri/shared/runtime/quota_cache.rs index fa5f305..1a215ad 100644 --- a/src-tauri/shared/runtime/quota_cache.rs +++ b/src-tauri/shared/runtime/quota_cache.rs @@ -259,10 +259,12 @@ mod tests { five_hour: QuotaWindow { remaining_percent: Some(80), refresh_at: Some("2026-05-10 10:00".to_string()), + ..QuotaWindow::default() }, weekly: QuotaWindow { remaining_percent: Some(50), refresh_at: None, + ..QuotaWindow::default() }, } } diff --git a/src-tauri/shared/runtime/session_usage.rs b/src-tauri/shared/runtime/session_usage.rs index a1ab770..1a8e090 100644 --- a/src-tauri/shared/runtime/session_usage.rs +++ b/src-tauri/shared/runtime/session_usage.rs @@ -75,6 +75,7 @@ fn normalize_quota_window(window: QuotaWindow) -> QuotaWindow { QuotaWindow { remaining_percent: window.remaining_percent.map(|value| value.min(100)), refresh_at: window.refresh_at, + reset_at_timestamp: window.reset_at_timestamp, } } @@ -113,10 +114,12 @@ fn quota_window_from_rate_limit(window: Option) -> Quota .used_percent .map(|used_percent| (100.0 - used_percent).round().clamp(0.0, 100.0) as u8); let refresh_at = window.resets_at.and_then(format_reset_time); + let reset_at_timestamp = window.resets_at; QuotaWindow { remaining_percent, refresh_at, + reset_at_timestamp, } } @@ -429,6 +432,7 @@ mod tests { five_hour: QuotaWindow { remaining_percent: Some(99), refresh_at: None, + ..QuotaWindow::default() }, weekly: QuotaWindow::default(), },