优化数据加载
This commit is contained in:
@@ -17,6 +17,7 @@ use super::usage::{
|
|||||||
pub struct UsageCacheState {
|
pub struct UsageCacheState {
|
||||||
pub conn: Arc<Mutex<Option<Connection>>>,
|
pub conn: Arc<Mutex<Option<Connection>>>,
|
||||||
pub last_scan_time: Arc<Mutex<Option<i64>>>,
|
pub last_scan_time: Arc<Mutex<Option<i64>>>,
|
||||||
|
pub is_scanning: Arc<Mutex<bool>>, // 防止并发扫描
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
@@ -119,6 +120,37 @@ fn generate_unique_hash(entry: &UsageEntry, has_io_tokens: bool, has_cache_token
|
|||||||
|
|
||||||
#[command]
|
#[command]
|
||||||
pub async fn usage_scan_update(state: State<'_, UsageCacheState>) -> Result<ScanResult, String> {
|
pub async fn usage_scan_update(state: State<'_, UsageCacheState>) -> Result<ScanResult, String> {
|
||||||
|
// 检查是否正在扫描
|
||||||
|
{
|
||||||
|
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<Mutex<bool>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
let start_time = Utc::now().timestamp_millis();
|
||||||
|
|
||||||
// Initialize or get connection
|
// Initialize or get connection
|
||||||
@@ -288,8 +320,17 @@ pub async fn usage_get_stats_cached(
|
|||||||
days: Option<u32>,
|
days: Option<u32>,
|
||||||
state: State<'_, UsageCacheState>,
|
state: State<'_, UsageCacheState>,
|
||||||
) -> Result<UsageStats, String> {
|
) -> Result<UsageStats, String> {
|
||||||
// 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_guard = state.conn.lock().map_err(|e| e.to_string())?;
|
||||||
let conn = conn_guard.as_ref().ok_or("Database not initialized")?;
|
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<Stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
Ok("No cache to clear.".to_string())
|
Ok("No cache to clear.".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 快速检查文件是否变化(不解析内容)
|
||||||
|
pub async fn check_files_changed(state: &State<'_, UsageCacheState>) -> Result<bool, String> {
|
||||||
|
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<ScanResult, String> {
|
||||||
|
// 手动触发完整扫描
|
||||||
|
usage_scan_update(state).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[command]
|
||||||
|
pub async fn usage_check_updates(state: State<'_, UsageCacheState>) -> Result<bool, String> {
|
||||||
|
// 检查是否有文件更新
|
||||||
|
check_files_changed(&state).await
|
||||||
}
|
}
|
@@ -44,7 +44,7 @@ use commands::usage_index::{
|
|||||||
usage_get_summary, usage_import_diffs, usage_scan_index, usage_scan_progress, UsageIndexState,
|
usage_get_summary, usage_import_diffs, usage_scan_index, usage_scan_progress, UsageIndexState,
|
||||||
};
|
};
|
||||||
use commands::usage_cache::{
|
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::{
|
use commands::storage::{
|
||||||
storage_list_tables, storage_read_table, storage_update_row, storage_delete_row,
|
storage_list_tables, storage_read_table, storage_update_row, storage_delete_row,
|
||||||
@@ -261,6 +261,8 @@ fn main() {
|
|||||||
usage_scan_update,
|
usage_scan_update,
|
||||||
usage_get_stats_cached,
|
usage_get_stats_cached,
|
||||||
usage_clear_cache,
|
usage_clear_cache,
|
||||||
|
usage_force_scan,
|
||||||
|
usage_check_updates,
|
||||||
|
|
||||||
// MCP (Model Context Protocol)
|
// MCP (Model Context Protocol)
|
||||||
mcp_add,
|
mcp_add,
|
||||||
|
@@ -4,25 +4,23 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { api, type UsageStats, type ProjectUsage } from "@/lib/api";
|
import { api, type UsageStats } from "@/lib/api";
|
||||||
import {
|
import {
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
TrendingUp,
|
TrendingUp,
|
||||||
Calendar,
|
|
||||||
Filter,
|
Filter,
|
||||||
Loader2,
|
Loader2,
|
||||||
DollarSign,
|
DollarSign,
|
||||||
Activity,
|
Activity,
|
||||||
FileText,
|
FileText,
|
||||||
Briefcase
|
Briefcase,
|
||||||
|
RefreshCw
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { useTranslation } from "@/hooks/useTranslation";
|
import { useTranslation } from "@/hooks/useTranslation";
|
||||||
import {
|
import {
|
||||||
LineChart,
|
LineChart,
|
||||||
Line,
|
Line,
|
||||||
AreaChart,
|
|
||||||
Area,
|
|
||||||
XAxis,
|
XAxis,
|
||||||
YAxis,
|
YAxis,
|
||||||
CartesianGrid,
|
CartesianGrid,
|
||||||
@@ -52,9 +50,10 @@ interface UsageDashboardProps {
|
|||||||
export const UsageDashboard: React.FC<UsageDashboardProps> = ({ onBack }) => {
|
export const UsageDashboard: React.FC<UsageDashboardProps> = ({ onBack }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [stats, setStats] = useState<UsageStats | null>(null);
|
const [stats, setStats] = useState<UsageStats | null>(null);
|
||||||
const [sessionStats, setSessionStats] = useState<ProjectUsage[] | null>(null);
|
// const [sessionStats, setSessionStats] = useState<ProjectUsage[] | null>(null);
|
||||||
const [selectedDateRange, setSelectedDateRange] = useState<"all" | "24h" | "7d" | "30d">("all");
|
const [selectedDateRange, setSelectedDateRange] = useState<"all" | "24h" | "7d" | "30d">("all");
|
||||||
const [activeTab, setActiveTab] = useState("overview");
|
const [activeTab, setActiveTab] = useState("overview");
|
||||||
const [hourlyStats, setHourlyStats] = useState<any[]>([]);
|
const [hourlyStats, setHourlyStats] = useState<any[]>([]);
|
||||||
@@ -63,44 +62,39 @@ export const UsageDashboard: React.FC<UsageDashboardProps> = ({ onBack }) => {
|
|||||||
loadUsageStats();
|
loadUsageStats();
|
||||||
}, [selectedDateRange]);
|
}, [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 () => {
|
const loadUsageStats = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
let statsData: UsageStats;
|
let statsData: UsageStats;
|
||||||
let sessionData: ProjectUsage[];
|
|
||||||
|
|
||||||
if (selectedDateRange === "all") {
|
if (selectedDateRange === "all") {
|
||||||
statsData = await api.getUsageStats();
|
statsData = await api.getUsageStats();
|
||||||
sessionData = await api.getSessionStats();
|
// sessionData = await api.getSessionStats();
|
||||||
} else {
|
} else {
|
||||||
const days = selectedDateRange === "24h" ? 1 : selectedDateRange === "7d" ? 7 : 30;
|
const days = selectedDateRange === "24h" ? 1 : selectedDateRange === "7d" ? 7 : 30;
|
||||||
|
|
||||||
// 使用缓存版本的API,传入天数参数
|
// 使用缓存版本的API,传入天数参数
|
||||||
statsData = await api.getUsageStats(days);
|
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);
|
setStats(statsData);
|
||||||
setSessionStats(sessionData);
|
// setSessionStats(sessionData);
|
||||||
|
|
||||||
// Generate 24-hour hourly stats when in 24h view
|
// Generate 24-hour hourly stats when in 24h view
|
||||||
// For 24h view, we need to aggregate the last 24 hours of data
|
// For 24h view, we need to aggregate the last 24 hours of data
|
||||||
@@ -154,7 +148,6 @@ export const UsageDashboard: React.FC<UsageDashboardProps> = ({ onBack }) => {
|
|||||||
// If no data, create empty hours
|
// If no data, create empty hours
|
||||||
if (last24HoursTotals.total_cost === 0) {
|
if (last24HoursTotals.total_cost === 0) {
|
||||||
for (let i = 0; i < 24; i++) {
|
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`;
|
const timeAgo = i === 0 ? 'Now' : i === 1 ? '1h ago' : `${i}h ago`;
|
||||||
hours.unshift({
|
hours.unshift({
|
||||||
hour: timeAgo,
|
hour: timeAgo,
|
||||||
@@ -300,6 +293,16 @@ export const UsageDashboard: React.FC<UsageDashboardProps> = ({ onBack }) => {
|
|||||||
|
|
||||||
{/* Date Range Filter */}
|
{/* Date Range Filter */}
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={handleManualRefresh}
|
||||||
|
disabled={isRefreshing}
|
||||||
|
className="h-8 w-8"
|
||||||
|
title={t('usage.refreshData')}
|
||||||
|
>
|
||||||
|
<RefreshCw className={cn("h-4 w-4", isRefreshing && "animate-spin")} />
|
||||||
|
</Button>
|
||||||
<Filter className="h-4 w-4 text-muted-foreground" />
|
<Filter className="h-4 w-4 text-muted-foreground" />
|
||||||
<div className="flex space-x-1">
|
<div className="flex space-x-1">
|
||||||
{(["all", "30d", "7d", "24h"] as const).map((range) => (
|
{(["all", "30d", "7d", "24h"] as const).map((range) => (
|
||||||
|
@@ -1283,6 +1283,32 @@ export const api = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Force scan for usage data updates
|
||||||
|
* @returns Promise resolving to scan result
|
||||||
|
*/
|
||||||
|
async forceUsageScan(): Promise<any> {
|
||||||
|
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<boolean> {
|
||||||
|
try {
|
||||||
|
return await invoke<boolean>("usage_check_updates");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to check usage updates:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a checkpoint for the current session state
|
* Creates a checkpoint for the current session state
|
||||||
*/
|
*/
|
||||||
|
Reference in New Issue
Block a user