diff --git a/apps/src-tauri/src/app_shell/tray.rs b/apps/src-tauri/src/app_shell/tray.rs index d47fdd7ca..ddde70a8e 100644 --- a/apps/src-tauri/src/app_shell/tray.rs +++ b/apps/src-tauri/src/app_shell/tray.rs @@ -86,19 +86,19 @@ pub(crate) fn setup_tray(app: &tauri::AppHandle) -> Result<(), tauri::Error> { fn build_tray_menu(app: &tauri::AppHandle) -> Result, tauri::Error> { let show_main = MenuItem::with_id(app, TRAY_MENU_SHOW_MAIN, "显示主窗口", true, None::<&str>)?; let summary = codexmanager_service::read_tray_usage_reset_summary(); - let (primary_label, secondary_label) = - tray_usage_reset_labels(summary.primary_resets_at, summary.secondary_resets_at); + let (five_hour_label, seven_day_label) = + tray_usage_reset_labels(summary.five_hour_resets_at, summary.seven_day_resets_at); let primary = MenuItem::with_id( app, TRAY_MENU_PRIMARY_RESET, - primary_label, + five_hour_label, false, None::<&str>, )?; let secondary = MenuItem::with_id( app, TRAY_MENU_SECONDARY_RESET, - secondary_label, + seven_day_label, false, None::<&str>, )?; @@ -151,12 +151,12 @@ pub(crate) fn refresh_tray_menu_after_usage_update(app: &tauri::AppHandle) { } fn tray_usage_reset_labels( - primary_resets_at: Option, - secondary_resets_at: Option, + five_hour_resets_at: Option, + seven_day_resets_at: Option, ) -> (String, String) { ( - format!("5小时重置:{}", format_tray_reset_time(secondary_resets_at)), - format!("7天重置:{}", format_tray_reset_time(primary_resets_at)), + format!("5小时重置:{}", format_tray_reset_time(five_hour_resets_at)), + format!("7天重置:{}", format_tray_reset_time(seven_day_resets_at)), ) } @@ -241,23 +241,23 @@ mod tests { assert_eq!(stale_labels.0, "5小时重置:暂无"); assert_eq!(stale_labels.1, "7天重置:暂无"); - let primary_resets_at = 1_700_000_000; - let secondary_resets_at = 1_700_604_800; + let five_hour_resets_at = 1_700_000_000; + let seven_day_resets_at = 1_700_604_800; let updated_labels = - tray_usage_reset_labels(Some(primary_resets_at), Some(secondary_resets_at)); + tray_usage_reset_labels(Some(five_hour_resets_at), Some(seven_day_resets_at)); assert_ne!(updated_labels, stale_labels); assert_eq!( updated_labels.0, format!( "5小时重置:{}", - format_tray_reset_time(Some(primary_resets_at)) + format_tray_reset_time(Some(five_hour_resets_at)) ) ); assert_eq!( updated_labels.1, format!( "7天重置:{}", - format_tray_reset_time(Some(secondary_resets_at)) + format_tray_reset_time(Some(seven_day_resets_at)) ) ); } diff --git a/crates/service/src/usage/usage_tray_summary.rs b/crates/service/src/usage/usage_tray_summary.rs index 60542a1d8..2b2969e3c 100644 --- a/crates/service/src/usage/usage_tray_summary.rs +++ b/crates/service/src/usage/usage_tray_summary.rs @@ -2,12 +2,21 @@ use codexmanager_core::storage::now_ts; use crate::storage_helpers::open_storage; +const MINUTES_PER_DAY: i64 = 24 * 60; +const WINDOW_ROUNDING_BIAS_MINUTES: i64 = 3; + #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct TrayUsageResetSummary { - pub primary_resets_at: Option, - pub secondary_resets_at: Option, - pub primary_known_count: usize, - pub secondary_known_count: usize, + pub five_hour_resets_at: Option, + pub seven_day_resets_at: Option, + pub five_hour_known_count: usize, + pub seven_day_known_count: usize, +} + +#[derive(Clone, Copy)] +enum ResetWindow { + FiveHour, + SevenDay, } pub fn read_tray_usage_reset_summary() -> TrayUsageResetSummary { @@ -20,16 +29,51 @@ pub fn read_tray_usage_reset_summary() -> TrayUsageResetSummary { let now = now_ts(); let mut summary = TrayUsageResetSummary::default(); for item in items { - if let Some(resets_at) = future_ts(item.resets_at, now) { - summary.primary_known_count += 1; - summary.primary_resets_at = min_ts(summary.primary_resets_at, resets_at); + add_reset_window( + &mut summary, + item.window_minutes, + item.resets_at, + ResetWindow::FiveHour, + now, + ); + add_reset_window( + &mut summary, + item.secondary_window_minutes, + item.secondary_resets_at, + ResetWindow::SevenDay, + now, + ); + } + summary +} + +fn add_reset_window( + summary: &mut TrayUsageResetSummary, + window_minutes: Option, + resets_at: Option, + fallback: ResetWindow, + now: i64, +) { + let Some(resets_at) = future_ts(resets_at, now) else { + return; + }; + let window = match window_minutes { + Some(minutes) if minutes > MINUTES_PER_DAY + WINDOW_ROUNDING_BIAS_MINUTES => { + ResetWindow::SevenDay } - if let Some(resets_at) = future_ts(item.secondary_resets_at, now) { - summary.secondary_known_count += 1; - summary.secondary_resets_at = min_ts(summary.secondary_resets_at, resets_at); + Some(_) => ResetWindow::FiveHour, + None => fallback, + }; + match window { + ResetWindow::FiveHour => { + summary.five_hour_known_count += 1; + summary.five_hour_resets_at = min_ts(summary.five_hour_resets_at, resets_at); + } + ResetWindow::SevenDay => { + summary.seven_day_known_count += 1; + summary.seven_day_resets_at = min_ts(summary.seven_day_resets_at, resets_at); } } - summary } fn future_ts(value: Option, now: i64) -> Option { @@ -46,7 +90,7 @@ fn min_ts(current: Option, candidate: i64) -> Option { #[cfg(test)] mod tests { - use super::{future_ts, min_ts}; + use super::{add_reset_window, future_ts, min_ts, ResetWindow, TrayUsageResetSummary}; #[test] fn future_ts_ignores_missing_or_elapsed_values() { @@ -62,4 +106,45 @@ mod tests { assert_eq!(min_ts(Some(180), 120), Some(120)); assert_eq!(min_ts(Some(90), 120), Some(90)); } + + #[test] + fn single_primary_seven_day_window_is_not_reported_as_five_hour() { + let mut summary = TrayUsageResetSummary::default(); + + add_reset_window( + &mut summary, + Some(10_080), + Some(200), + ResetWindow::FiveHour, + 100, + ); + + assert_eq!(summary.five_hour_resets_at, None); + assert_eq!(summary.five_hour_known_count, 0); + assert_eq!(summary.seven_day_resets_at, Some(200)); + assert_eq!(summary.seven_day_known_count, 1); + } + + #[test] + fn reset_windows_are_classified_by_duration_when_fields_are_swapped() { + let mut summary = TrayUsageResetSummary::default(); + + add_reset_window( + &mut summary, + Some(10_080), + Some(300), + ResetWindow::FiveHour, + 100, + ); + add_reset_window( + &mut summary, + Some(300), + Some(200), + ResetWindow::SevenDay, + 100, + ); + + assert_eq!(summary.five_hour_resets_at, Some(200)); + assert_eq!(summary.seven_day_resets_at, Some(300)); + } }