diff --git a/src-tauri/src/commands/usage_cache.rs b/src-tauri/src/commands/usage_cache.rs index 32c68f8..ef68364 100644 --- a/src-tauri/src/commands/usage_cache.rs +++ b/src-tauri/src/commands/usage_cache.rs @@ -17,6 +17,7 @@ use super::usage::{ pub struct UsageCacheState { pub conn: Arc>>, pub last_scan_time: Arc>>, + pub is_scanning: Arc>, // 防止并发扫描 } #[derive(Debug, Serialize, Deserialize)] @@ -119,6 +120,37 @@ fn generate_unique_hash(entry: &UsageEntry, has_io_tokens: bool, has_cache_token #[command] pub async fn usage_scan_update(state: State<'_, UsageCacheState>) -> Result { + // 检查是否正在扫描 + { + let mut is_scanning = state.is_scanning.lock().map_err(|e| e.to_string())?; + if *is_scanning { + return Ok(ScanResult { + files_scanned: 0, + entries_added: 0, + entries_skipped: 0, + scan_time_ms: 0, + }); + } + *is_scanning = true; + } + + // 确保在函数退出时重置扫描状态 + struct ScanGuard<'a> { + is_scanning: &'a Arc>, + } + + impl<'a> Drop for ScanGuard<'a> { + fn drop(&mut self) { + if let Ok(mut is_scanning) = self.is_scanning.lock() { + *is_scanning = false; + } + } + } + + let _guard = ScanGuard { + is_scanning: &state.is_scanning, + }; + let start_time = Utc::now().timestamp_millis(); // Initialize or get connection @@ -288,8 +320,17 @@ pub async fn usage_get_stats_cached( days: Option, state: State<'_, UsageCacheState>, ) -> Result { - // First ensure cache is up to date - usage_scan_update(state.clone()).await?; + // 优化:只在数据库未初始化时才扫描 + let needs_init = { + let conn_guard = state.conn.lock().map_err(|e| e.to_string())?; + conn_guard.is_none() + }; + + if needs_init { + // 首次调用,需要初始化和扫描 + usage_scan_update(state.clone()).await?; + } + // 移除自动扫描逻辑,让系统只在手动触发时扫描 let conn_guard = state.conn.lock().map_err(|e| e.to_string())?; let conn = conn_guard.as_ref().ok_or("Database not initialized")?; @@ -568,4 +609,76 @@ pub async fn usage_clear_cache(state: State<'_, UsageCacheState>) -> Result) -> Result { + let conn_guard = state.conn.lock().map_err(|e| e.to_string())?; + let conn = conn_guard.as_ref().ok_or("Database not initialized")?; + + let claude_path = dirs::home_dir() + .ok_or("Failed to get home directory")? + .join(".claude"); + let projects_dir = claude_path.join("projects"); + + // 获取已知文件的修改时间和大小 + let mut stmt = conn + .prepare("SELECT file_path, file_size, mtime_ms FROM scanned_files") + .map_err(|e| e.to_string())?; + + let mut known_files = std::collections::HashMap::new(); + let rows = stmt.query_map([], |row| { + Ok(( + row.get::<_, String>(0)?, + (row.get::<_, i64>(1)?, row.get::<_, i64>(2)?), + )) + }).map_err(|e| e.to_string())?; + + for row in rows { + if let Ok((path, data)) = row { + known_files.insert(path, data); + } + } + + // 快速检查是否有文件变化 + if let Ok(projects) = fs::read_dir(&projects_dir) { + for project in projects.flatten() { + if project.file_type().map(|t| t.is_dir()).unwrap_or(false) { + let project_path = project.path(); + + for entry in walkdir::WalkDir::new(&project_path) + .into_iter() + .filter_map(Result::ok) + .filter(|e| e.path().extension().and_then(|s| s.to_str()) == Some("jsonl")) + { + let path = entry.path(); + let path_str = path.to_string_lossy().to_string(); + let current_size = get_file_size(path); + let current_mtime = get_file_mtime_ms(path); + + if let Some((stored_size, stored_mtime)) = known_files.get(&path_str) { + if current_size != *stored_size || current_mtime != *stored_mtime { + return Ok(true); // 发现变化 + } + } else { + return Ok(true); // 发现新文件 + } + } + } + } + } + + Ok(false) // 没有变化 +} + +#[command] +pub async fn usage_force_scan(state: State<'_, UsageCacheState>) -> Result { + // 手动触发完整扫描 + usage_scan_update(state).await +} + +#[command] +pub async fn usage_check_updates(state: State<'_, UsageCacheState>) -> Result { + // 检查是否有文件更新 + check_files_changed(&state).await } \ No newline at end of file diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 0ea1379..21182ed 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -44,7 +44,7 @@ use commands::usage_index::{ usage_get_summary, usage_import_diffs, usage_scan_index, usage_scan_progress, UsageIndexState, }; use commands::usage_cache::{ - usage_scan_update, usage_get_stats_cached, usage_clear_cache, UsageCacheState, + usage_scan_update, usage_get_stats_cached, usage_clear_cache, usage_force_scan, usage_check_updates, UsageCacheState, }; use commands::storage::{ storage_list_tables, storage_read_table, storage_update_row, storage_delete_row, @@ -261,6 +261,8 @@ fn main() { usage_scan_update, usage_get_stats_cached, usage_clear_cache, + usage_force_scan, + usage_check_updates, // MCP (Model Context Protocol) mcp_add, diff --git a/src/components/UsageDashboard.tsx b/src/components/UsageDashboard.tsx index eb0187e..d9fdc72 100644 --- a/src/components/UsageDashboard.tsx +++ b/src/components/UsageDashboard.tsx @@ -4,25 +4,23 @@ import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Badge } from "@/components/ui/badge"; -import { api, type UsageStats, type ProjectUsage } from "@/lib/api"; +import { api, type UsageStats } from "@/lib/api"; import { ArrowLeft, TrendingUp, - Calendar, Filter, Loader2, DollarSign, Activity, FileText, - Briefcase + Briefcase, + RefreshCw } from "lucide-react"; import { cn } from "@/lib/utils"; import { useTranslation } from "@/hooks/useTranslation"; import { LineChart, Line, - AreaChart, - Area, XAxis, YAxis, CartesianGrid, @@ -52,9 +50,10 @@ interface UsageDashboardProps { export const UsageDashboard: React.FC = ({ onBack }) => { const { t } = useTranslation(); const [loading, setLoading] = useState(true); + const [isRefreshing, setIsRefreshing] = useState(false); const [error, setError] = useState(null); const [stats, setStats] = useState(null); - const [sessionStats, setSessionStats] = useState(null); + // const [sessionStats, setSessionStats] = useState(null); const [selectedDateRange, setSelectedDateRange] = useState<"all" | "24h" | "7d" | "30d">("all"); const [activeTab, setActiveTab] = useState("overview"); const [hourlyStats, setHourlyStats] = useState([]); @@ -63,44 +62,39 @@ export const UsageDashboard: React.FC = ({ onBack }) => { loadUsageStats(); }, [selectedDateRange]); + const handleManualRefresh = async () => { + try { + setIsRefreshing(true); + // 强制扫描更新 + await api.forceUsageScan(); + // 重新加载数据 + await loadUsageStats(); + } catch (err) { + console.error("Failed to refresh usage stats:", err); + } finally { + setIsRefreshing(false); + } + }; + const loadUsageStats = async () => { try { setLoading(true); setError(null); let statsData: UsageStats; - let sessionData: ProjectUsage[]; if (selectedDateRange === "all") { statsData = await api.getUsageStats(); - sessionData = await api.getSessionStats(); + // sessionData = await api.getSessionStats(); } else { const days = selectedDateRange === "24h" ? 1 : selectedDateRange === "7d" ? 7 : 30; // 使用缓存版本的API,传入天数参数 statsData = await api.getUsageStats(days); - - // 对于session数据,继续使用日期范围方式 - const endDate = new Date(); - const startDate = new Date(); - startDate.setDate(startDate.getDate() - days); - - const formatDateForApi = (date: Date) => { - const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, '0'); - const day = String(date.getDate()).padStart(2, '0'); - return `${year}${month}${day}`; - } - - sessionData = await api.getSessionStats( - formatDateForApi(startDate), - formatDateForApi(endDate), - 'desc' - ); } setStats(statsData); - setSessionStats(sessionData); + // setSessionStats(sessionData); // Generate 24-hour hourly stats when in 24h view // For 24h view, we need to aggregate the last 24 hours of data @@ -154,7 +148,6 @@ export const UsageDashboard: React.FC = ({ onBack }) => { // If no data, create empty hours if (last24HoursTotals.total_cost === 0) { for (let i = 0; i < 24; i++) { - const hourIndex = (currentHour - i + 24) % 24; const timeAgo = i === 0 ? 'Now' : i === 1 ? '1h ago' : `${i}h ago`; hours.unshift({ hour: timeAgo, @@ -300,6 +293,16 @@ export const UsageDashboard: React.FC = ({ onBack }) => { {/* Date Range Filter */}
+
{(["all", "30d", "7d", "24h"] as const).map((range) => ( diff --git a/src/lib/api.ts b/src/lib/api.ts index 4030cfa..ee0c6f2 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -1283,6 +1283,32 @@ export const api = { } }, + /** + * Force scan for usage data updates + * @returns Promise resolving to scan result + */ + async forceUsageScan(): Promise { + try { + return await invoke("usage_force_scan"); + } catch (error) { + console.error("Failed to force usage scan:", error); + throw error; + } + }, + + /** + * Check if there are usage data updates available + * @returns Promise resolving to boolean indicating if updates are available + */ + async checkUsageUpdates(): Promise { + try { + return await invoke("usage_check_updates"); + } catch (error) { + console.error("Failed to check usage updates:", error); + throw error; + } + }, + /** * Creates a checkpoint for the current session state */