mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 12:44:18 +08:00
feat(usage): enhance usage stats backend and query hooks
This commit is contained in:
@@ -35,18 +35,26 @@ pub fn get_usage_trends(
|
||||
#[tauri::command]
|
||||
pub fn get_provider_stats(
|
||||
state: State<'_, AppState>,
|
||||
start_date: Option<i64>,
|
||||
end_date: Option<i64>,
|
||||
app_type: Option<String>,
|
||||
) -> Result<Vec<ProviderStats>, AppError> {
|
||||
state.db.get_provider_stats(app_type.as_deref())
|
||||
state
|
||||
.db
|
||||
.get_provider_stats(start_date, end_date, app_type.as_deref())
|
||||
}
|
||||
|
||||
/// 获取模型统计
|
||||
#[tauri::command]
|
||||
pub fn get_model_stats(
|
||||
state: State<'_, AppState>,
|
||||
start_date: Option<i64>,
|
||||
end_date: Option<i64>,
|
||||
app_type: Option<String>,
|
||||
) -> Result<Vec<ModelStats>, AppError> {
|
||||
state.db.get_model_stats(app_type.as_deref())
|
||||
state
|
||||
.db
|
||||
.get_model_stats(start_date, end_date, app_type.as_deref())
|
||||
}
|
||||
|
||||
/// 获取请求日志列表
|
||||
|
||||
@@ -275,14 +275,9 @@ impl Database {
|
||||
let mut bucket_count: i64 = if duration <= 0 {
|
||||
1
|
||||
} else {
|
||||
((duration as f64) / bucket_seconds as f64).ceil() as i64
|
||||
(duration + bucket_seconds - 1) / bucket_seconds
|
||||
};
|
||||
|
||||
// 固定 24 小时窗口为 24 个小时桶,避免浮点误差
|
||||
if bucket_seconds == 60 * 60 {
|
||||
bucket_count = 24;
|
||||
}
|
||||
|
||||
if bucket_count < 1 {
|
||||
bucket_count = 1;
|
||||
}
|
||||
@@ -453,14 +448,50 @@ impl Database {
|
||||
/// 获取 Provider 统计
|
||||
pub fn get_provider_stats(
|
||||
&self,
|
||||
start_date: Option<i64>,
|
||||
end_date: Option<i64>,
|
||||
app_type: Option<&str>,
|
||||
) -> Result<Vec<ProviderStats>, AppError> {
|
||||
let conn = lock_conn!(self.conn);
|
||||
|
||||
let (detail_where, rollup_where) = if app_type.is_some() {
|
||||
("WHERE l.app_type = ?1", "WHERE r.app_type = ?2")
|
||||
let mut detail_conditions = Vec::new();
|
||||
let mut detail_params: Vec<Box<dyn rusqlite::ToSql>> = Vec::new();
|
||||
if let Some(start) = start_date {
|
||||
detail_conditions.push("l.created_at >= ?");
|
||||
detail_params.push(Box::new(start));
|
||||
}
|
||||
if let Some(end) = end_date {
|
||||
detail_conditions.push("l.created_at <= ?");
|
||||
detail_params.push(Box::new(end));
|
||||
}
|
||||
if let Some(at) = app_type {
|
||||
detail_conditions.push("l.app_type = ?");
|
||||
detail_params.push(Box::new(at.to_string()));
|
||||
}
|
||||
let detail_where = if detail_conditions.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
("", "")
|
||||
format!("WHERE {}", detail_conditions.join(" AND "))
|
||||
};
|
||||
|
||||
let mut rollup_conditions = Vec::new();
|
||||
let mut rollup_params: Vec<Box<dyn rusqlite::ToSql>> = Vec::new();
|
||||
if let Some(start) = start_date {
|
||||
rollup_conditions.push("r.date >= date(?, 'unixepoch', 'localtime')".to_string());
|
||||
rollup_params.push(Box::new(start));
|
||||
}
|
||||
if let Some(end) = end_date {
|
||||
rollup_conditions.push("r.date <= date(?, 'unixepoch', 'localtime')".to_string());
|
||||
rollup_params.push(Box::new(end));
|
||||
}
|
||||
if let Some(at) = app_type {
|
||||
rollup_conditions.push("r.app_type = ?".to_string());
|
||||
rollup_params.push(Box::new(at.to_string()));
|
||||
}
|
||||
let rollup_where = if rollup_conditions.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!("WHERE {}", rollup_conditions.join(" AND "))
|
||||
};
|
||||
|
||||
// UNION detail logs + rollup data, then aggregate
|
||||
@@ -506,6 +537,9 @@ impl Database {
|
||||
);
|
||||
|
||||
let mut stmt = conn.prepare(&sql)?;
|
||||
let mut params: Vec<Box<dyn rusqlite::ToSql>> = detail_params;
|
||||
params.extend(rollup_params);
|
||||
let param_refs: Vec<&dyn rusqlite::ToSql> = params.iter().map(|p| p.as_ref()).collect();
|
||||
let row_mapper = |row: &rusqlite::Row| {
|
||||
let request_count: i64 = row.get(3)?;
|
||||
let success_count: i64 = row.get(6)?;
|
||||
@@ -526,11 +560,7 @@ impl Database {
|
||||
})
|
||||
};
|
||||
|
||||
let rows = if let Some(at) = app_type {
|
||||
stmt.query_map(params![at, at], row_mapper)?
|
||||
} else {
|
||||
stmt.query_map([], row_mapper)?
|
||||
};
|
||||
let rows = stmt.query_map(param_refs.as_slice(), row_mapper)?;
|
||||
|
||||
let mut stats = Vec::new();
|
||||
for row in rows {
|
||||
@@ -541,13 +571,52 @@ impl Database {
|
||||
}
|
||||
|
||||
/// 获取模型统计
|
||||
pub fn get_model_stats(&self, app_type: Option<&str>) -> Result<Vec<ModelStats>, AppError> {
|
||||
pub fn get_model_stats(
|
||||
&self,
|
||||
start_date: Option<i64>,
|
||||
end_date: Option<i64>,
|
||||
app_type: Option<&str>,
|
||||
) -> Result<Vec<ModelStats>, AppError> {
|
||||
let conn = lock_conn!(self.conn);
|
||||
|
||||
let (detail_where, rollup_where) = if app_type.is_some() {
|
||||
("WHERE app_type = ?1", "WHERE app_type = ?2")
|
||||
let mut detail_conditions = Vec::new();
|
||||
let mut detail_params: Vec<Box<dyn rusqlite::ToSql>> = Vec::new();
|
||||
if let Some(start) = start_date {
|
||||
detail_conditions.push("l.created_at >= ?");
|
||||
detail_params.push(Box::new(start));
|
||||
}
|
||||
if let Some(end) = end_date {
|
||||
detail_conditions.push("l.created_at <= ?");
|
||||
detail_params.push(Box::new(end));
|
||||
}
|
||||
if let Some(at) = app_type {
|
||||
detail_conditions.push("l.app_type = ?");
|
||||
detail_params.push(Box::new(at.to_string()));
|
||||
}
|
||||
let detail_where = if detail_conditions.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
("", "")
|
||||
format!("WHERE {}", detail_conditions.join(" AND "))
|
||||
};
|
||||
|
||||
let mut rollup_conditions = Vec::new();
|
||||
let mut rollup_params: Vec<Box<dyn rusqlite::ToSql>> = Vec::new();
|
||||
if let Some(start) = start_date {
|
||||
rollup_conditions.push("r.date >= date(?, 'unixepoch', 'localtime')".to_string());
|
||||
rollup_params.push(Box::new(start));
|
||||
}
|
||||
if let Some(end) = end_date {
|
||||
rollup_conditions.push("r.date <= date(?, 'unixepoch', 'localtime')".to_string());
|
||||
rollup_params.push(Box::new(end));
|
||||
}
|
||||
if let Some(at) = app_type {
|
||||
rollup_conditions.push("r.app_type = ?".to_string());
|
||||
rollup_params.push(Box::new(at.to_string()));
|
||||
}
|
||||
let rollup_where = if rollup_conditions.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!("WHERE {}", rollup_conditions.join(" AND "))
|
||||
};
|
||||
|
||||
// UNION detail logs + rollup data
|
||||
@@ -558,27 +627,30 @@ impl Database {
|
||||
SUM(total_tokens) as total_tokens,
|
||||
SUM(total_cost) as total_cost
|
||||
FROM (
|
||||
SELECT model,
|
||||
SELECT l.model,
|
||||
COUNT(*) as request_count,
|
||||
COALESCE(SUM(input_tokens + output_tokens), 0) as total_tokens,
|
||||
COALESCE(SUM(CAST(total_cost_usd AS REAL)), 0) as total_cost
|
||||
FROM proxy_request_logs
|
||||
COALESCE(SUM(l.input_tokens + l.output_tokens), 0) as total_tokens,
|
||||
COALESCE(SUM(CAST(l.total_cost_usd AS REAL)), 0) as total_cost
|
||||
FROM proxy_request_logs l
|
||||
{detail_where}
|
||||
GROUP BY model
|
||||
GROUP BY l.model
|
||||
UNION ALL
|
||||
SELECT model,
|
||||
SELECT r.model,
|
||||
COALESCE(SUM(request_count), 0),
|
||||
COALESCE(SUM(input_tokens + output_tokens), 0),
|
||||
COALESCE(SUM(CAST(total_cost_usd AS REAL)), 0)
|
||||
FROM usage_daily_rollups
|
||||
FROM usage_daily_rollups r
|
||||
{rollup_where}
|
||||
GROUP BY model
|
||||
GROUP BY r.model
|
||||
)
|
||||
GROUP BY model
|
||||
ORDER BY total_cost DESC"
|
||||
);
|
||||
|
||||
let mut stmt = conn.prepare(&sql)?;
|
||||
let mut params: Vec<Box<dyn rusqlite::ToSql>> = detail_params;
|
||||
params.extend(rollup_params);
|
||||
let param_refs: Vec<&dyn rusqlite::ToSql> = params.iter().map(|p| p.as_ref()).collect();
|
||||
let row_mapper = |row: &rusqlite::Row| {
|
||||
let request_count: i64 = row.get(1)?;
|
||||
let total_cost: f64 = row.get(3)?;
|
||||
@@ -597,11 +669,7 @@ impl Database {
|
||||
})
|
||||
};
|
||||
|
||||
let rows = if let Some(at) = app_type {
|
||||
stmt.query_map(params![at, at], row_mapper)?
|
||||
} else {
|
||||
stmt.query_map([], row_mapper)?
|
||||
};
|
||||
let rows = stmt.query_map(param_refs.as_slice(), row_mapper)?;
|
||||
|
||||
let mut stats = Vec::new();
|
||||
for row in rows {
|
||||
@@ -1168,7 +1236,7 @@ mod tests {
|
||||
)?;
|
||||
}
|
||||
|
||||
let stats = db.get_model_stats(None)?;
|
||||
let stats = db.get_model_stats(None, None, None)?;
|
||||
assert_eq!(stats.len(), 1);
|
||||
assert_eq!(stats[0].model, "claude-3-sonnet");
|
||||
assert_eq!(stats[0].request_count, 1);
|
||||
@@ -1176,6 +1244,73 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_provider_stats_with_time_filter() -> Result<(), AppError> {
|
||||
let db = Database::memory()?;
|
||||
|
||||
{
|
||||
let conn = lock_conn!(db.conn);
|
||||
conn.execute(
|
||||
"INSERT INTO proxy_request_logs (
|
||||
request_id, provider_id, app_type, model,
|
||||
input_tokens, output_tokens, total_cost_usd,
|
||||
latency_ms, status_code, created_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
params!["old", "p1", "claude", "claude-3", 100, 50, "0.01", 100, 200, 1000],
|
||||
)?;
|
||||
conn.execute(
|
||||
"INSERT INTO proxy_request_logs (
|
||||
request_id, provider_id, app_type, model,
|
||||
input_tokens, output_tokens, total_cost_usd,
|
||||
latency_ms, status_code, created_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
params!["new", "p1", "claude", "claude-3", 200, 75, "0.02", 120, 200, 2000],
|
||||
)?;
|
||||
}
|
||||
|
||||
let stats = db.get_provider_stats(Some(1500), Some(2500), Some("claude"))?;
|
||||
assert_eq!(stats.len(), 1);
|
||||
assert_eq!(stats[0].provider_id, "p1");
|
||||
assert_eq!(stats[0].request_count, 1);
|
||||
assert_eq!(stats[0].total_tokens, 275);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_daily_trends_respects_shorter_than_24_hours() -> Result<(), AppError> {
|
||||
let db = Database::memory()?;
|
||||
|
||||
{
|
||||
let conn = lock_conn!(db.conn);
|
||||
conn.execute(
|
||||
"INSERT INTO proxy_request_logs (
|
||||
request_id, provider_id, app_type, model,
|
||||
input_tokens, output_tokens, total_cost_usd,
|
||||
latency_ms, status_code, created_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
params![
|
||||
"req-short",
|
||||
"p1",
|
||||
"claude",
|
||||
"claude-3",
|
||||
100,
|
||||
50,
|
||||
"0.01",
|
||||
100,
|
||||
200,
|
||||
10_800
|
||||
],
|
||||
)?;
|
||||
}
|
||||
|
||||
let stats = db.get_daily_trends(Some(0), Some(15 * 60 * 60), Some("claude"))?;
|
||||
assert_eq!(stats.len(), 15);
|
||||
assert_eq!(stats[3].request_count, 1);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_model_pricing_matching() -> Result<(), AppError> {
|
||||
let db = Database::memory()?;
|
||||
|
||||
+12
-4
@@ -63,12 +63,20 @@ export const usageApi = {
|
||||
return invoke("get_usage_trends", { startDate, endDate, appType });
|
||||
},
|
||||
|
||||
getProviderStats: async (appType?: string): Promise<ProviderStats[]> => {
|
||||
return invoke("get_provider_stats", { appType });
|
||||
getProviderStats: async (
|
||||
startDate?: number,
|
||||
endDate?: number,
|
||||
appType?: string,
|
||||
): Promise<ProviderStats[]> => {
|
||||
return invoke("get_provider_stats", { startDate, endDate, appType });
|
||||
},
|
||||
|
||||
getModelStats: async (appType?: string): Promise<ModelStats[]> => {
|
||||
return invoke("get_model_stats", { appType });
|
||||
getModelStats: async (
|
||||
startDate?: number,
|
||||
endDate?: number,
|
||||
appType?: string,
|
||||
): Promise<ModelStats[]> => {
|
||||
return invoke("get_model_stats", { startDate, endDate, appType });
|
||||
},
|
||||
|
||||
getRequestLogs: async (
|
||||
|
||||
+112
-55
@@ -1,6 +1,7 @@
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { usageApi } from "@/lib/api/usage";
|
||||
import type { LogFilters } from "@/types/usage";
|
||||
import { resolveUsageRange } from "@/lib/usageRange";
|
||||
import type { LogFilters, UsageRangeSelection } from "@/types/usage";
|
||||
|
||||
const DEFAULT_REFETCH_INTERVAL_MS = 30000;
|
||||
|
||||
@@ -9,51 +10,94 @@ type UsageQueryOptions = {
|
||||
refetchIntervalInBackground?: boolean;
|
||||
};
|
||||
|
||||
type RequestLogsTimeMode = "rolling" | "fixed";
|
||||
|
||||
type RequestLogsQueryArgs = {
|
||||
filters: LogFilters;
|
||||
timeMode: RequestLogsTimeMode;
|
||||
range: UsageRangeSelection;
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
rollingWindowSeconds?: number;
|
||||
options?: UsageQueryOptions;
|
||||
};
|
||||
|
||||
type RequestLogsKey = {
|
||||
timeMode: RequestLogsTimeMode;
|
||||
rollingWindowSeconds?: number;
|
||||
preset: UsageRangeSelection["preset"];
|
||||
customStartDate?: number;
|
||||
customEndDate?: number;
|
||||
appType?: string;
|
||||
providerName?: string;
|
||||
model?: string;
|
||||
statusCode?: number;
|
||||
startDate?: number;
|
||||
endDate?: number;
|
||||
};
|
||||
|
||||
// Query keys
|
||||
export const usageKeys = {
|
||||
all: ["usage"] as const,
|
||||
summary: (days: number, appType?: string) =>
|
||||
[...usageKeys.all, "summary", days, appType ?? "all"] as const,
|
||||
trends: (days: number, appType?: string) =>
|
||||
[...usageKeys.all, "trends", days, appType ?? "all"] as const,
|
||||
providerStats: (appType?: string) =>
|
||||
[...usageKeys.all, "provider-stats", appType ?? "all"] as const,
|
||||
modelStats: (appType?: string) =>
|
||||
[...usageKeys.all, "model-stats", appType ?? "all"] as const,
|
||||
summary: (
|
||||
preset: UsageRangeSelection["preset"],
|
||||
customStartDate: number | undefined,
|
||||
customEndDate: number | undefined,
|
||||
appType?: string,
|
||||
) =>
|
||||
[
|
||||
...usageKeys.all,
|
||||
"summary",
|
||||
preset,
|
||||
customStartDate ?? 0,
|
||||
customEndDate ?? 0,
|
||||
appType ?? "all",
|
||||
] as const,
|
||||
trends: (
|
||||
preset: UsageRangeSelection["preset"],
|
||||
customStartDate: number | undefined,
|
||||
customEndDate: number | undefined,
|
||||
appType?: string,
|
||||
) =>
|
||||
[
|
||||
...usageKeys.all,
|
||||
"trends",
|
||||
preset,
|
||||
customStartDate ?? 0,
|
||||
customEndDate ?? 0,
|
||||
appType ?? "all",
|
||||
] as const,
|
||||
providerStats: (
|
||||
preset: UsageRangeSelection["preset"],
|
||||
customStartDate: number | undefined,
|
||||
customEndDate: number | undefined,
|
||||
appType?: string,
|
||||
) =>
|
||||
[
|
||||
...usageKeys.all,
|
||||
"provider-stats",
|
||||
preset,
|
||||
customStartDate ?? 0,
|
||||
customEndDate ?? 0,
|
||||
appType ?? "all",
|
||||
] as const,
|
||||
modelStats: (
|
||||
preset: UsageRangeSelection["preset"],
|
||||
customStartDate: number | undefined,
|
||||
customEndDate: number | undefined,
|
||||
appType?: string,
|
||||
) =>
|
||||
[
|
||||
...usageKeys.all,
|
||||
"model-stats",
|
||||
preset,
|
||||
customStartDate ?? 0,
|
||||
customEndDate ?? 0,
|
||||
appType ?? "all",
|
||||
] as const,
|
||||
logs: (key: RequestLogsKey, page: number, pageSize: number) =>
|
||||
[
|
||||
...usageKeys.all,
|
||||
"logs",
|
||||
key.timeMode,
|
||||
key.rollingWindowSeconds ?? 0,
|
||||
key.preset,
|
||||
key.customStartDate ?? 0,
|
||||
key.customEndDate ?? 0,
|
||||
key.appType ?? "",
|
||||
key.providerName ?? "",
|
||||
key.model ?? "",
|
||||
key.statusCode ?? -1,
|
||||
key.startDate ?? 0,
|
||||
key.endDate ?? 0,
|
||||
page,
|
||||
pageSize,
|
||||
] as const,
|
||||
@@ -64,23 +108,22 @@ export const usageKeys = {
|
||||
[...usageKeys.all, "limits", providerId, appType] as const,
|
||||
};
|
||||
|
||||
const getWindow = (days: number) => {
|
||||
const endDate = Math.floor(Date.now() / 1000);
|
||||
const startDate = endDate - days * 24 * 60 * 60;
|
||||
return { startDate, endDate };
|
||||
};
|
||||
|
||||
// Hooks
|
||||
export function useUsageSummary(
|
||||
days: number,
|
||||
range: UsageRangeSelection,
|
||||
appType?: string,
|
||||
options?: UsageQueryOptions,
|
||||
) {
|
||||
const effectiveAppType = appType === "all" ? undefined : appType;
|
||||
return useQuery({
|
||||
queryKey: usageKeys.summary(days, appType),
|
||||
queryKey: usageKeys.summary(
|
||||
range.preset,
|
||||
range.customStartDate,
|
||||
range.customEndDate,
|
||||
appType,
|
||||
),
|
||||
queryFn: () => {
|
||||
const { startDate, endDate } = getWindow(days);
|
||||
const { startDate, endDate } = resolveUsageRange(range);
|
||||
return usageApi.getUsageSummary(startDate, endDate, effectiveAppType);
|
||||
},
|
||||
refetchInterval: options?.refetchInterval ?? DEFAULT_REFETCH_INTERVAL_MS,
|
||||
@@ -89,15 +132,20 @@ export function useUsageSummary(
|
||||
}
|
||||
|
||||
export function useUsageTrends(
|
||||
days: number,
|
||||
range: UsageRangeSelection,
|
||||
appType?: string,
|
||||
options?: UsageQueryOptions,
|
||||
) {
|
||||
const effectiveAppType = appType === "all" ? undefined : appType;
|
||||
return useQuery({
|
||||
queryKey: usageKeys.trends(days, appType),
|
||||
queryKey: usageKeys.trends(
|
||||
range.preset,
|
||||
range.customStartDate,
|
||||
range.customEndDate,
|
||||
appType,
|
||||
),
|
||||
queryFn: () => {
|
||||
const { startDate, endDate } = getWindow(days);
|
||||
const { startDate, endDate } = resolveUsageRange(range);
|
||||
return usageApi.getUsageTrends(startDate, endDate, effectiveAppType);
|
||||
},
|
||||
refetchInterval: options?.refetchInterval ?? DEFAULT_REFETCH_INTERVAL_MS,
|
||||
@@ -106,61 +154,70 @@ export function useUsageTrends(
|
||||
}
|
||||
|
||||
export function useProviderStats(
|
||||
range: UsageRangeSelection,
|
||||
appType?: string,
|
||||
options?: UsageQueryOptions,
|
||||
) {
|
||||
const effectiveAppType = appType === "all" ? undefined : appType;
|
||||
return useQuery({
|
||||
queryKey: usageKeys.providerStats(appType),
|
||||
queryFn: () => usageApi.getProviderStats(effectiveAppType),
|
||||
queryKey: usageKeys.providerStats(
|
||||
range.preset,
|
||||
range.customStartDate,
|
||||
range.customEndDate,
|
||||
appType,
|
||||
),
|
||||
queryFn: () => {
|
||||
const { startDate, endDate } = resolveUsageRange(range);
|
||||
return usageApi.getProviderStats(startDate, endDate, effectiveAppType);
|
||||
},
|
||||
refetchInterval: options?.refetchInterval ?? DEFAULT_REFETCH_INTERVAL_MS,
|
||||
refetchIntervalInBackground: options?.refetchIntervalInBackground ?? false,
|
||||
});
|
||||
}
|
||||
|
||||
export function useModelStats(appType?: string, options?: UsageQueryOptions) {
|
||||
export function useModelStats(
|
||||
range: UsageRangeSelection,
|
||||
appType?: string,
|
||||
options?: UsageQueryOptions,
|
||||
) {
|
||||
const effectiveAppType = appType === "all" ? undefined : appType;
|
||||
return useQuery({
|
||||
queryKey: usageKeys.modelStats(appType),
|
||||
queryFn: () => usageApi.getModelStats(effectiveAppType),
|
||||
queryKey: usageKeys.modelStats(
|
||||
range.preset,
|
||||
range.customStartDate,
|
||||
range.customEndDate,
|
||||
appType,
|
||||
),
|
||||
queryFn: () => {
|
||||
const { startDate, endDate } = resolveUsageRange(range);
|
||||
return usageApi.getModelStats(startDate, endDate, effectiveAppType);
|
||||
},
|
||||
refetchInterval: options?.refetchInterval ?? DEFAULT_REFETCH_INTERVAL_MS,
|
||||
refetchIntervalInBackground: options?.refetchIntervalInBackground ?? false,
|
||||
});
|
||||
}
|
||||
|
||||
const getRollingRange = (windowSeconds: number) => {
|
||||
const endDate = Math.floor(Date.now() / 1000);
|
||||
const startDate = endDate - windowSeconds;
|
||||
return { startDate, endDate };
|
||||
};
|
||||
|
||||
export function useRequestLogs({
|
||||
filters,
|
||||
timeMode,
|
||||
range,
|
||||
page = 0,
|
||||
pageSize = 20,
|
||||
rollingWindowSeconds = 24 * 60 * 60,
|
||||
options,
|
||||
}: RequestLogsQueryArgs) {
|
||||
const key: RequestLogsKey = {
|
||||
timeMode,
|
||||
rollingWindowSeconds:
|
||||
timeMode === "rolling" ? rollingWindowSeconds : undefined,
|
||||
preset: range.preset,
|
||||
customStartDate: range.customStartDate,
|
||||
customEndDate: range.customEndDate,
|
||||
appType: filters.appType,
|
||||
providerName: filters.providerName,
|
||||
model: filters.model,
|
||||
statusCode: filters.statusCode,
|
||||
startDate: timeMode === "fixed" ? filters.startDate : undefined,
|
||||
endDate: timeMode === "fixed" ? filters.endDate : undefined,
|
||||
};
|
||||
|
||||
return useQuery({
|
||||
queryKey: usageKeys.logs(key, page, pageSize),
|
||||
queryFn: () => {
|
||||
const effectiveFilters =
|
||||
timeMode === "rolling"
|
||||
? { ...filters, ...getRollingRange(rollingWindowSeconds) }
|
||||
: filters;
|
||||
const effectiveFilters = { ...filters, ...resolveUsageRange(range) };
|
||||
return usageApi.getRequestLogs(effectiveFilters, page, pageSize);
|
||||
},
|
||||
refetchInterval: options?.refetchInterval ?? DEFAULT_REFETCH_INTERVAL_MS, // 每30秒自动刷新
|
||||
|
||||
+8
-2
@@ -121,12 +121,18 @@ export interface ProviderLimitStatus {
|
||||
monthlyExceeded: boolean;
|
||||
}
|
||||
|
||||
export type TimeRange = "1d" | "7d" | "30d";
|
||||
export type UsageRangePreset = "today" | "1d" | "7d" | "14d" | "30d" | "custom";
|
||||
|
||||
export interface UsageRangeSelection {
|
||||
preset: UsageRangePreset;
|
||||
customStartDate?: number;
|
||||
customEndDate?: number;
|
||||
}
|
||||
|
||||
export type AppTypeFilter = "all" | "claude" | "codex" | "gemini";
|
||||
|
||||
export interface StatsFilters {
|
||||
timeRange: TimeRange;
|
||||
timeRange: UsageRangePreset;
|
||||
providerId?: string;
|
||||
appType?: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user