mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-06-16 21:03:58 +08:00
fix(usage): normalize high-precision RFC3339 timestamps
This commit is contained in:
@@ -9,6 +9,7 @@ import type { GeminiKeyConfig, ProviderKeyConfig, OpenAIProviderConfig } from '@
|
||||
import type { AuthFileItem } from '@/types/authFile';
|
||||
import type { CredentialInfo } from '@/types/sourceInfo';
|
||||
import { buildSourceInfoMap, resolveSourceDisplay } from '@/utils/sourceResolver';
|
||||
import { parseTimestampMs } from '@/utils/timestamp';
|
||||
import {
|
||||
collectUsageDetails,
|
||||
extractLatencyMs,
|
||||
@@ -131,7 +132,7 @@ export function RequestEventsDetailsCard({
|
||||
const timestampMs =
|
||||
typeof detail.__timestampMs === 'number' && detail.__timestampMs > 0
|
||||
? detail.__timestampMs
|
||||
: Date.parse(timestamp);
|
||||
: parseTimestampMs(timestamp);
|
||||
const date = Number.isNaN(timestampMs) ? null : new Date(timestampMs);
|
||||
const sourceRaw = String(detail.source ?? '').trim();
|
||||
const authIndexRaw = detail.auth_index as unknown;
|
||||
|
||||
@@ -9,6 +9,7 @@ import iconKimiLight from '@/assets/icons/kimi-light.svg';
|
||||
import iconQwen from '@/assets/icons/qwen.svg';
|
||||
import iconVertex from '@/assets/icons/vertex.svg';
|
||||
import type { AuthFileItem } from '@/types';
|
||||
import { parseTimestamp } from '@/utils/timestamp';
|
||||
import {
|
||||
normalizeAuthIndex,
|
||||
normalizeUsageSourceId,
|
||||
@@ -279,7 +280,7 @@ export const formatModified = (item: AuthFileItem): string => {
|
||||
const date =
|
||||
Number.isFinite(asNumber) && !Number.isNaN(asNumber)
|
||||
? new Date(asNumber < 1e12 ? asNumber * 1000 : asNumber)
|
||||
: new Date(String(raw));
|
||||
: parseTimestamp(raw) ?? new Date(String(raw));
|
||||
return Number.isNaN(date.getTime()) ? '-' : date.toLocaleString();
|
||||
};
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { USAGE_STATS_STALE_TIME_MS, useUsageStatsStore } from '@/stores';
|
||||
import type { AuthFileItem, Config } from '@/types';
|
||||
import type { CredentialInfo, SourceInfo } from '@/types/sourceInfo';
|
||||
import { buildSourceInfoMap, resolveSourceDisplay } from '@/utils/sourceResolver';
|
||||
import { parseTimestampMs } from '@/utils/timestamp';
|
||||
import {
|
||||
collectUsageDetailsWithEndpoint,
|
||||
normalizeAuthIndex,
|
||||
@@ -183,7 +184,7 @@ export function useTraceResolver(options: UseTraceResolverOptions): UseTraceReso
|
||||
if (!logPath) return [];
|
||||
|
||||
const logTimestampMs = traceLogLine.timestamp
|
||||
? Date.parse(traceLogLine.timestamp)
|
||||
? parseTimestampMs(traceLogLine.timestamp)
|
||||
: Number.NaN;
|
||||
|
||||
// Step 1: filter by path match
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import { apiClient } from './client';
|
||||
import type { AuthFilesResponse } from '@/types/authFile';
|
||||
import type { OAuthModelAliasEntry } from '@/types';
|
||||
import { parseTimestampMs } from '@/utils/timestamp';
|
||||
|
||||
type StatusError = { status?: number };
|
||||
type AuthFileStatusResponse = { status: string; disabled: boolean };
|
||||
@@ -185,7 +186,7 @@ const readDateField = (entry: AuthFileEntry): number => {
|
||||
if (Number.isFinite(asNumber)) {
|
||||
return asNumber < 1e12 ? asNumber * 1000 : asNumber;
|
||||
}
|
||||
const parsed = Date.parse(trimmed);
|
||||
const parsed = parseTimestampMs(trimmed);
|
||||
if (!Number.isNaN(parsed)) {
|
||||
return parsed;
|
||||
}
|
||||
|
||||
+4
-2
@@ -1,3 +1,5 @@
|
||||
import { parseTimestamp } from './timestamp';
|
||||
|
||||
/**
|
||||
* 格式化工具函数
|
||||
* 从原项目 src/utils/string.js 迁移
|
||||
@@ -47,7 +49,7 @@ export function formatFileSize(bytes: number): string {
|
||||
* 格式化日期时间
|
||||
*/
|
||||
export function formatDateTime(date: string | Date, locale?: string): string {
|
||||
const d = typeof date === 'string' ? new Date(date) : date;
|
||||
const d = typeof date === 'string' ? parseTimestamp(date) ?? new Date(date) : date;
|
||||
|
||||
if (isNaN(d.getTime())) {
|
||||
return 'Invalid Date';
|
||||
@@ -73,7 +75,7 @@ export function formatUnixTimestamp(value: unknown, locale?: string): string {
|
||||
const asNumber = typeof value === 'number' ? value : Number(value);
|
||||
const date = (() => {
|
||||
if (!Number.isFinite(asNumber) || Number.isNaN(asNumber)) {
|
||||
return new Date(String(value));
|
||||
return parseTimestamp(value) ?? new Date(String(value));
|
||||
}
|
||||
|
||||
const abs = Math.abs(asNumber);
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
const RFC3339_HIGH_PRECISION_REGEX =
|
||||
/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(\.(\d+))?(Z|[+-]\d{2}:\d{2})?$/i;
|
||||
|
||||
/**
|
||||
* Some browsers mis-handle RFC3339 timestamps that include sub-millisecond
|
||||
* precision. Normalize them to millisecond precision before parsing.
|
||||
*/
|
||||
export function normalizeTimestampForDateParse(value: string): string {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return '';
|
||||
|
||||
const match = trimmed.match(RFC3339_HIGH_PRECISION_REGEX);
|
||||
if (!match) return trimmed;
|
||||
|
||||
const [, base, , fractionDigits = '', timezone = ''] = match;
|
||||
if (fractionDigits.length <= 3) {
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
return `${base}.${fractionDigits.slice(0, 3)}${timezone}`;
|
||||
}
|
||||
|
||||
export function parseTimestampMs(value: unknown): number {
|
||||
if (typeof value === 'number' && Number.isFinite(value)) {
|
||||
return value;
|
||||
}
|
||||
if (typeof value !== 'string') {
|
||||
return Number.NaN;
|
||||
}
|
||||
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
return Number.NaN;
|
||||
}
|
||||
|
||||
const normalized = normalizeTimestampForDateParse(trimmed);
|
||||
const normalizedParsed = Date.parse(normalized);
|
||||
if (!Number.isNaN(normalizedParsed)) {
|
||||
return normalizedParsed;
|
||||
}
|
||||
|
||||
if (normalized !== trimmed) {
|
||||
const originalParsed = Date.parse(trimmed);
|
||||
if (!Number.isNaN(originalParsed)) {
|
||||
return originalParsed;
|
||||
}
|
||||
}
|
||||
|
||||
return Number.NaN;
|
||||
}
|
||||
|
||||
export function parseTimestamp(value: unknown): Date | null {
|
||||
const timestampMs = parseTimestampMs(value);
|
||||
if (!Number.isFinite(timestampMs)) {
|
||||
return null;
|
||||
}
|
||||
return new Date(timestampMs);
|
||||
}
|
||||
+13
-12
@@ -13,6 +13,7 @@ import {
|
||||
finalizeLatencyStats,
|
||||
} from './usage/latency';
|
||||
import { maskApiKey } from './format';
|
||||
import { parseTimestampMs } from './timestamp';
|
||||
|
||||
export type { DurationFormatOptions, LatencyStats } from './usage/latency';
|
||||
export {
|
||||
@@ -195,7 +196,7 @@ export function filterUsageByTimeRange<T>(
|
||||
if (!detailRecord || typeof detailRecord.timestamp !== 'string') {
|
||||
return;
|
||||
}
|
||||
const timestamp = Date.parse(detailRecord.timestamp);
|
||||
const timestamp = parseTimestampMs(detailRecord.timestamp);
|
||||
if (Number.isNaN(timestamp) || timestamp < windowStart || timestamp > nowMs) {
|
||||
return;
|
||||
}
|
||||
@@ -545,7 +546,7 @@ export function collectUsageDetails(usageData: unknown): UsageDetail[] {
|
||||
modelDetails.forEach((detailRaw) => {
|
||||
if (!isRecord(detailRaw) || typeof detailRaw.timestamp !== 'string') return;
|
||||
const timestamp = detailRaw.timestamp;
|
||||
const timestampMs = Date.parse(timestamp);
|
||||
const timestampMs = parseTimestampMs(timestamp);
|
||||
const tokensRaw = isRecord(detailRaw.tokens) ? detailRaw.tokens : {};
|
||||
const latencyMs = extractLatencyMs(detailRaw);
|
||||
details.push({
|
||||
@@ -618,7 +619,7 @@ export function collectUsageDetailsWithEndpoint(usageData: unknown): UsageDetail
|
||||
modelDetails.forEach((detailRaw) => {
|
||||
if (!isRecord(detailRaw) || typeof detailRaw.timestamp !== 'string') return;
|
||||
const timestamp = detailRaw.timestamp;
|
||||
const timestampMs = Date.parse(timestamp);
|
||||
const timestampMs = parseTimestampMs(timestamp);
|
||||
const tokensRaw = isRecord(detailRaw.tokens) ? detailRaw.tokens : {};
|
||||
const latencyMs = extractLatencyMs(detailRaw);
|
||||
details.push({
|
||||
@@ -721,7 +722,7 @@ export function calculateRecentPerMinuteRates(
|
||||
const timestamp =
|
||||
typeof detail.__timestampMs === 'number'
|
||||
? detail.__timestampMs
|
||||
: Date.parse(detail.timestamp);
|
||||
: parseTimestampMs(detail.timestamp);
|
||||
if (!Number.isFinite(timestamp) || timestamp < windowStart || timestamp > now) {
|
||||
return;
|
||||
}
|
||||
@@ -1131,7 +1132,7 @@ export function buildHourlySeriesByModel(
|
||||
const timestamp =
|
||||
typeof detail.__timestampMs === 'number'
|
||||
? detail.__timestampMs
|
||||
: Date.parse(detail.timestamp);
|
||||
: parseTimestampMs(detail.timestamp);
|
||||
if (!Number.isFinite(timestamp) || timestamp <= 0) {
|
||||
return;
|
||||
}
|
||||
@@ -1190,7 +1191,7 @@ export function buildDailySeriesByModel(
|
||||
const timestamp =
|
||||
typeof detail.__timestampMs === 'number'
|
||||
? detail.__timestampMs
|
||||
: Date.parse(detail.timestamp);
|
||||
: parseTimestampMs(detail.timestamp);
|
||||
if (!Number.isFinite(timestamp) || timestamp <= 0) {
|
||||
return;
|
||||
}
|
||||
@@ -1416,7 +1417,7 @@ export function calculateStatusBarData(
|
||||
const timestamp =
|
||||
typeof detail.__timestampMs === 'number'
|
||||
? detail.__timestampMs
|
||||
: Date.parse(detail.timestamp);
|
||||
: parseTimestampMs(detail.timestamp);
|
||||
if (
|
||||
!Number.isFinite(timestamp) ||
|
||||
timestamp <= 0 ||
|
||||
@@ -1524,7 +1525,7 @@ export function calculateServiceHealthData(usageDetails: UsageDetail[]): Service
|
||||
const timestamp =
|
||||
typeof detail.__timestampMs === 'number'
|
||||
? detail.__timestampMs
|
||||
: Date.parse(detail.timestamp);
|
||||
: parseTimestampMs(detail.timestamp);
|
||||
if (
|
||||
!Number.isFinite(timestamp) ||
|
||||
timestamp <= 0 ||
|
||||
@@ -1734,7 +1735,7 @@ export function buildHourlyTokenBreakdown(
|
||||
const timestamp =
|
||||
typeof detail.__timestampMs === 'number'
|
||||
? detail.__timestampMs
|
||||
: Date.parse(detail.timestamp);
|
||||
: parseTimestampMs(detail.timestamp);
|
||||
if (!Number.isFinite(timestamp) || timestamp <= 0) return;
|
||||
const normalized = new Date(timestamp);
|
||||
normalized.setMinutes(0, 0, 0);
|
||||
@@ -1776,7 +1777,7 @@ export function buildDailyTokenBreakdown(usageData: unknown): TokenBreakdownSeri
|
||||
const timestamp =
|
||||
typeof detail.__timestampMs === 'number'
|
||||
? detail.__timestampMs
|
||||
: Date.parse(detail.timestamp);
|
||||
: parseTimestampMs(detail.timestamp);
|
||||
if (!Number.isFinite(timestamp) || timestamp <= 0) return;
|
||||
const dayLabel = formatDayLabel(new Date(timestamp));
|
||||
if (!dayLabel) return;
|
||||
@@ -1853,7 +1854,7 @@ export function buildHourlyCostSeries(
|
||||
const timestamp =
|
||||
typeof detail.__timestampMs === 'number'
|
||||
? detail.__timestampMs
|
||||
: Date.parse(detail.timestamp);
|
||||
: parseTimestampMs(detail.timestamp);
|
||||
if (!Number.isFinite(timestamp) || timestamp <= 0) return;
|
||||
const normalized = new Date(timestamp);
|
||||
normalized.setMinutes(0, 0, 0);
|
||||
@@ -1888,7 +1889,7 @@ export function buildDailyCostSeries(
|
||||
const timestamp =
|
||||
typeof detail.__timestampMs === 'number'
|
||||
? detail.__timestampMs
|
||||
: Date.parse(detail.timestamp);
|
||||
: parseTimestampMs(detail.timestamp);
|
||||
if (!Number.isFinite(timestamp) || timestamp <= 0) return;
|
||||
const dayLabel = formatDayLabel(new Date(timestamp));
|
||||
if (!dayLabel) return;
|
||||
|
||||
Reference in New Issue
Block a user