Files
CC-Switch/src/lib/query/usage.ts
T
Arc 9171ad752c feat(usage): add live end time option for custom date range (#4438)
* add glm-5.2 pricing

* feat(usage): add live end time option for custom date range

Add a "End time follows current time" checkbox in the custom date range
picker. When enabled, the end time becomes read-only and automatically
tracks the current moment, so usage data always reflects up-to-the-second
consumption from the chosen start time.

This is especially useful under the Coding Plan 5-hour quota window —
users can set the start time to when their 5h window began and keep
the end time live to monitor real-time token consumption.

* style(usage): fix prettier formatting for UsageDashboard

* fix(usage): include liveEndTime in React Query cache keys

Without liveEndTime in the query key, a live custom range and a fixed
custom range with the same stored endpoints share the same cache entry.
After the live range refreshes (endDate = now), switching to the fixed
range shows stale data fetched through "now" instead of the original
end time.
2026-06-23 23:04:00 +08:00

393 lines
10 KiB
TypeScript

import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { usageApi } from "@/lib/api/usage";
import { resolveUsageRange } from "@/lib/usageRange";
import type {
LogFilters,
UsageRangeSelection,
UsageScopeFilters,
} from "@/types/usage";
const DEFAULT_REFETCH_INTERVAL_MS = 30000;
type UsageQueryOptions = {
refetchInterval?: number | false;
refetchIntervalInBackground?: boolean;
};
type RequestLogsQueryArgs = {
filters: LogFilters;
range: UsageRangeSelection;
page?: number;
pageSize?: number;
options?: UsageQueryOptions;
};
type RequestLogsKey = {
preset: UsageRangeSelection["preset"];
customStartDate?: number;
customEndDate?: number;
liveEndTime?: boolean;
appType?: string;
providerName?: string;
model?: string;
statusCode?: number;
};
// Query keys
export const usageKeys = {
all: ["usage"] as const,
summary: (
preset: UsageRangeSelection["preset"],
customStartDate: number | undefined,
customEndDate: number | undefined,
filters?: UsageScopeFilters,
liveEndTime?: boolean,
) =>
[
...usageKeys.all,
"summary",
preset,
customStartDate ?? 0,
customEndDate ?? 0,
liveEndTime ?? false,
filters?.appType ?? null,
filters?.providerName ?? null,
filters?.model ?? null,
] as const,
summaryByApp: (
preset: UsageRangeSelection["preset"],
customStartDate: number | undefined,
customEndDate: number | undefined,
filters?: Pick<UsageScopeFilters, "providerName" | "model">,
liveEndTime?: boolean,
) =>
[
...usageKeys.all,
"summary-by-app",
preset,
customStartDate ?? 0,
customEndDate ?? 0,
liveEndTime ?? false,
filters?.providerName ?? null,
filters?.model ?? null,
] as const,
trends: (
preset: UsageRangeSelection["preset"],
customStartDate: number | undefined,
customEndDate: number | undefined,
filters?: UsageScopeFilters,
liveEndTime?: boolean,
) =>
[
...usageKeys.all,
"trends",
preset,
customStartDate ?? 0,
customEndDate ?? 0,
liveEndTime ?? false,
filters?.appType ?? null,
filters?.providerName ?? null,
filters?.model ?? null,
] as const,
providerStats: (
preset: UsageRangeSelection["preset"],
customStartDate: number | undefined,
customEndDate: number | undefined,
filters?: UsageScopeFilters,
liveEndTime?: boolean,
) =>
[
...usageKeys.all,
"provider-stats",
preset,
customStartDate ?? 0,
customEndDate ?? 0,
liveEndTime ?? false,
filters?.appType ?? null,
filters?.providerName ?? null,
filters?.model ?? null,
] as const,
modelStats: (
preset: UsageRangeSelection["preset"],
customStartDate: number | undefined,
customEndDate: number | undefined,
filters?: UsageScopeFilters,
liveEndTime?: boolean,
) =>
[
...usageKeys.all,
"model-stats",
preset,
customStartDate ?? 0,
customEndDate ?? 0,
liveEndTime ?? false,
filters?.appType ?? null,
filters?.providerName ?? null,
filters?.model ?? null,
] as const,
logs: (key: RequestLogsKey, page: number, pageSize: number) =>
[
...usageKeys.all,
"logs",
key.preset,
key.customStartDate ?? 0,
key.customEndDate ?? 0,
key.liveEndTime ?? false,
key.appType ?? "",
key.providerName ?? "",
key.model ?? "",
key.statusCode ?? -1,
page,
pageSize,
] as const,
detail: (requestId: string) =>
[...usageKeys.all, "detail", requestId] as const,
pricing: () => [...usageKeys.all, "pricing"] as const,
limits: (providerId: string, appType: string) =>
[...usageKeys.all, "limits", providerId, appType] as const,
script: (providerId: string, appType: string) =>
[...usageKeys.all, providerId, appType] as const,
};
/** 把 UI 侧的 "all" 哨兵归一成 undefined(后端语义:不过滤)。 */
function normalizeScopeFilters(filters?: UsageScopeFilters): UsageScopeFilters {
return {
appType: filters?.appType === "all" ? undefined : filters?.appType,
providerName: filters?.providerName,
model: filters?.model,
};
}
// Hooks
export function useUsageSummary(
range: UsageRangeSelection,
filters?: UsageScopeFilters,
options?: UsageQueryOptions,
) {
const effective = normalizeScopeFilters(filters);
return useQuery({
queryKey: usageKeys.summary(
range.preset,
range.customStartDate,
range.customEndDate,
effective,
range.liveEndTime,
),
queryFn: () => {
const { startDate, endDate } = resolveUsageRange(range);
return usageApi.getUsageSummary(
startDate,
endDate,
effective.appType,
effective.providerName,
effective.model,
);
},
refetchInterval: options?.refetchInterval ?? DEFAULT_REFETCH_INTERVAL_MS,
refetchIntervalInBackground: options?.refetchIntervalInBackground ?? false,
});
}
export function useUsageSummaryByApp(
range: UsageRangeSelection,
filters?: Pick<UsageScopeFilters, "providerName" | "model">,
options?: UsageQueryOptions,
) {
return useQuery({
queryKey: usageKeys.summaryByApp(
range.preset,
range.customStartDate,
range.customEndDate,
filters,
range.liveEndTime,
),
queryFn: () => {
const { startDate, endDate } = resolveUsageRange(range);
return usageApi.getUsageSummaryByApp(
startDate,
endDate,
filters?.providerName,
filters?.model,
);
},
refetchInterval: options?.refetchInterval ?? DEFAULT_REFETCH_INTERVAL_MS,
refetchIntervalInBackground: options?.refetchIntervalInBackground ?? false,
});
}
export function useUsageTrends(
range: UsageRangeSelection,
filters?: UsageScopeFilters,
options?: UsageQueryOptions,
) {
const effective = normalizeScopeFilters(filters);
return useQuery({
queryKey: usageKeys.trends(
range.preset,
range.customStartDate,
range.customEndDate,
effective,
range.liveEndTime,
),
queryFn: () => {
const { startDate, endDate } = resolveUsageRange(range);
return usageApi.getUsageTrends(
startDate,
endDate,
effective.appType,
effective.providerName,
effective.model,
);
},
refetchInterval: options?.refetchInterval ?? DEFAULT_REFETCH_INTERVAL_MS,
refetchIntervalInBackground: options?.refetchIntervalInBackground ?? false,
});
}
export function useProviderStats(
range: UsageRangeSelection,
filters?: UsageScopeFilters,
options?: UsageQueryOptions,
) {
const effective = normalizeScopeFilters(filters);
return useQuery({
queryKey: usageKeys.providerStats(
range.preset,
range.customStartDate,
range.customEndDate,
effective,
range.liveEndTime,
),
queryFn: () => {
const { startDate, endDate } = resolveUsageRange(range);
return usageApi.getProviderStats(
startDate,
endDate,
effective.appType,
effective.providerName,
effective.model,
);
},
refetchInterval: options?.refetchInterval ?? DEFAULT_REFETCH_INTERVAL_MS,
refetchIntervalInBackground: options?.refetchIntervalInBackground ?? false,
});
}
export function useModelStats(
range: UsageRangeSelection,
filters?: UsageScopeFilters,
options?: UsageQueryOptions,
) {
const effective = normalizeScopeFilters(filters);
return useQuery({
queryKey: usageKeys.modelStats(
range.preset,
range.customStartDate,
range.customEndDate,
effective,
range.liveEndTime,
),
queryFn: () => {
const { startDate, endDate } = resolveUsageRange(range);
return usageApi.getModelStats(
startDate,
endDate,
effective.appType,
effective.providerName,
effective.model,
);
},
refetchInterval: options?.refetchInterval ?? DEFAULT_REFETCH_INTERVAL_MS,
refetchIntervalInBackground: options?.refetchIntervalInBackground ?? false,
});
}
export function useRequestLogs({
filters,
range,
page = 0,
pageSize = 20,
options,
}: RequestLogsQueryArgs) {
const key: RequestLogsKey = {
preset: range.preset,
customStartDate: range.customStartDate,
customEndDate: range.customEndDate,
liveEndTime: range.liveEndTime,
appType: filters.appType,
providerName: filters.providerName,
model: filters.model,
statusCode: filters.statusCode,
};
return useQuery({
queryKey: usageKeys.logs(key, page, pageSize),
queryFn: () => {
const effectiveFilters = { ...filters, ...resolveUsageRange(range) };
return usageApi.getRequestLogs(effectiveFilters, page, pageSize);
},
refetchInterval: options?.refetchInterval ?? DEFAULT_REFETCH_INTERVAL_MS, // 每30秒自动刷新
refetchIntervalInBackground: options?.refetchIntervalInBackground ?? false,
});
}
export function useRequestDetail(requestId: string) {
return useQuery({
queryKey: usageKeys.detail(requestId),
queryFn: () => usageApi.getRequestDetail(requestId),
enabled: !!requestId,
});
}
export function useModelPricing() {
return useQuery({
queryKey: usageKeys.pricing(),
queryFn: usageApi.getModelPricing,
});
}
export function useProviderLimits(providerId: string, appType: string) {
return useQuery({
queryKey: usageKeys.limits(providerId, appType),
queryFn: () => usageApi.checkProviderLimits(providerId, appType),
enabled: !!providerId && !!appType,
});
}
export function useUpdateModelPricing() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (params: {
modelId: string;
displayName: string;
inputCost: string;
outputCost: string;
cacheReadCost: string;
cacheCreationCost: string;
}) =>
usageApi.updateModelPricing(
params.modelId,
params.displayName,
params.inputCost,
params.outputCost,
params.cacheReadCost,
params.cacheCreationCost,
),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: usageKeys.all });
},
});
}
export function useDeleteModelPricing() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (modelId: string) => usageApi.deleteModelPricing(modelId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: usageKeys.all });
},
});
}