mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-27 08:14:33 +08:00
9171ad752c
* 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.
393 lines
10 KiB
TypeScript
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 });
|
|
},
|
|
});
|
|
}
|