Revert "Merge remote-tracking branch 'origin/pr/592'"

This reverts commit 3504875922, reversing
changes made to 5c3a1aecbe.
This commit is contained in:
fawney19
2026-05-28 17:10:27 +08:00
parent ef2a96bcc4
commit ffd8d273c4
26 changed files with 92 additions and 1034 deletions
@@ -3,25 +3,26 @@
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { computed, onUnmounted, ref, watch } from 'vue'
const props = withDefaults(defineProps<{
createdAt?: string | null
status?: string | null
responseTimeMs?: number | null
displayNowMs?: number | null
precision?: number
}>(), {
createdAt: null,
status: null,
responseTimeMs: null,
displayNowMs: null,
precision: 2,
})
const now = ref(Date.now())
const precision = computed(() => Math.max(0, props.precision))
const isActive = computed(() => props.status === 'pending' || props.status === 'streaming')
let rafId: number | null = null
function parseCreatedAtMs(value: string | null | undefined): number {
if (!value) return Number.NaN
// 后端有时返回无时区时间,按 UTC 解析,和列表时间显示逻辑保持一致
@@ -29,6 +30,35 @@ function parseCreatedAtMs(value: string | null | undefined): number {
return new Date(normalized).getTime()
}
function stopRaf() {
if (rafId == null) return
cancelAnimationFrame(rafId)
rafId = null
}
function tick() {
now.value = Date.now()
rafId = requestAnimationFrame(tick)
}
function startRaf() {
stopRaf()
now.value = Date.now()
rafId = requestAnimationFrame(tick)
}
watch(isActive, (active) => {
if (active) {
startRaf()
} else {
stopRaf()
}
}, { immediate: true })
onUnmounted(() => {
stopRaf()
})
const displayText = computed(() => {
if (!isActive.value) {
if (props.responseTimeMs == null) return '-'
@@ -40,11 +70,7 @@ const displayText = computed(() => {
const createdAtMs = parseCreatedAtMs(props.createdAt)
if (Number.isNaN(createdAtMs)) return '-'
// 活跃请求里的 response_time_ms 可能只是首字或中间值;终态才使用后端最终耗时。
const nowMs = typeof props.displayNowMs === 'number' && Number.isFinite(props.displayNowMs)
? props.displayNowMs
: Date.now()
const elapsedMs = Math.max(0, nowMs - createdAtMs)
const elapsedMs = Math.max(0, now.value - createdAtMs)
return `${(elapsedMs / 1000).toFixed(precision.value)}s`
})
</script>
@@ -324,7 +324,6 @@
:created-at="record.created_at"
:status="getDisplayStatus(record)"
:response-time-ms="record.response_time_ms ?? null"
:display-now-ms="displayNowMs ?? null"
/>
</span>
<span
@@ -941,7 +940,6 @@
:created-at="record.created_at"
:status="getDisplayStatus(record)"
:response-time-ms="record.response_time_ms ?? null"
:display-now-ms="displayNowMs ?? null"
/>
</span>
</div>
@@ -1116,7 +1114,6 @@ const props = defineProps<{
pageSizeOptions: number[]
// 自动刷新
autoRefresh: boolean
displayNowMs?: number | null
}>()
const emit = defineEmits<{
@@ -1,56 +0,0 @@
import { afterEach, describe, expect, it } from 'vitest'
import { createApp, type App } from 'vue'
import ElapsedTimeText from '../ElapsedTimeText.vue'
const mountedApps: Array<{ app: App, root: HTMLElement }> = []
function mountElapsedTimeText(props: Record<string, unknown>) {
const root = document.createElement('div')
document.body.appendChild(root)
const app = createApp(ElapsedTimeText, props)
app.mount(root)
mountedApps.push({ app, root })
return root
}
afterEach(() => {
for (const { app, root } of mountedApps.splice(0)) {
app.unmount()
root.remove()
}
})
describe('ElapsedTimeText', () => {
it('uses the supplied display clock for active requests', () => {
const root = mountElapsedTimeText({
createdAt: '2026-05-28T12:00:00Z',
status: 'streaming',
responseTimeMs: 10_000,
displayNowMs: Date.parse('2026-05-28T12:00:40Z'),
})
expect(root.textContent).toBe('40.00s')
})
it('keeps terminal requests pinned to the backend final duration', () => {
const root = mountElapsedTimeText({
createdAt: '2026-05-28T12:00:00Z',
status: 'completed',
responseTimeMs: 42_340,
displayNowMs: Date.parse('2026-05-28T12:01:30Z'),
})
expect(root.textContent).toBe('42.34s')
})
it('clamps active elapsed time at zero when the display clock is behind', () => {
const root = mountElapsedTimeText({
createdAt: '2026-05-28T12:00:40Z',
status: 'pending',
displayNowMs: Date.parse('2026-05-28T12:00:00Z'),
})
expect(root.textContent).toBe('0.00s')
})
})
@@ -91,16 +91,8 @@ vi.mock('lucide-vue-next', async () => {
vi.mock('../ElapsedTimeText.vue', () => ({
default: defineComponent({
name: 'ElapsedTimeTextStub',
props: {
displayNowMs: {
type: Number,
default: null,
},
},
setup(props) {
return () => h('span', {
'data-display-now-ms': props.displayNowMs == null ? '' : String(props.displayNowMs),
}, 'elapsed')
setup() {
return () => h('span', 'elapsed')
},
}),
}))
@@ -251,16 +243,6 @@ describe('UsageRecordsTable', () => {
expect(root.querySelector('[data-active-latency-state="waiting-first-byte"]')).toBeNull()
})
it('passes the shared display clock to active elapsed text', () => {
const root = mountUsageRecordsTable([buildRecord({
status: 'streaming',
response_time_ms: null,
first_byte_time_ms: 500,
})], { displayNowMs: 1_779_999_000_000 })
expect(root.querySelector('[data-display-now-ms="1779999000000"]')).not.toBeNull()
})
it('shows failed when Codex image progress fails before the usage record finalizes', () => {
const root = mountUsageRecordsTable([buildRecord({
status: 'pending',
@@ -1,101 +0,0 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { nextTick, ref } from 'vue'
import { useActiveElapsedDisplayClock } from '../useActiveElapsedDisplayClock'
type TestRecord = {
status: string
}
afterEach(() => {
vi.useRealTimers()
})
describe('useActiveElapsedDisplayClock', () => {
it('ticks only while visible active records exist', async () => {
vi.useFakeTimers()
let nowMs = 1_000
const records = ref<TestRecord[]>([])
const isPageVisible = ref(true)
const serverClockOffsetMs = ref(0)
const hasServerClockOffset = ref(false)
const clock = useActiveElapsedDisplayClock({
records,
isPageVisible,
serverClockOffsetMs,
hasServerClockOffset,
resolveStatus: record => record.status,
intervalMs: 250,
now: () => nowMs,
})
expect(clock.hasVisibleActiveRecords.value).toBe(false)
expect(clock.displayNowMs.value).toBe(1_000)
nowMs = 1_250
vi.advanceTimersByTime(250)
expect(clock.displayNowMs.value).toBe(1_000)
records.value = [{ status: 'streaming' }]
await nextTick()
expect(clock.hasVisibleActiveRecords.value).toBe(true)
expect(clock.displayNowMs.value).toBe(1_250)
nowMs = 1_500
vi.advanceTimersByTime(250)
expect(clock.displayNowMs.value).toBe(1_500)
isPageVisible.value = false
await nextTick()
nowMs = 1_750
vi.advanceTimersByTime(250)
expect(clock.displayNowMs.value).toBe(1_500)
clock.stopActiveElapsedDisplayClock()
})
it('applies server clock offset to the display time', () => {
vi.useFakeTimers()
const clock = useActiveElapsedDisplayClock({
records: ref<TestRecord[]>([{ status: 'pending' }]),
isPageVisible: ref(true),
serverClockOffsetMs: ref(-11_000),
hasServerClockOffset: ref(true),
resolveStatus: record => record.status,
intervalMs: 250,
now: () => 2_000,
})
expect(clock.calibratedDisplayNowMs.value).toBe(-9_000)
clock.stopActiveElapsedDisplayClock()
})
it('stops when active records disappear', async () => {
vi.useFakeTimers()
let nowMs = 3_000
const records = ref<TestRecord[]>([{ status: 'pending' }])
const clock = useActiveElapsedDisplayClock({
records,
isPageVisible: ref(true),
serverClockOffsetMs: ref(0),
hasServerClockOffset: ref(false),
resolveStatus: record => record.status,
intervalMs: 250,
now: () => nowMs,
})
nowMs = 3_250
vi.advanceTimersByTime(250)
expect(clock.displayNowMs.value).toBe(3_250)
records.value = [{ status: 'completed' }]
await nextTick()
nowMs = 3_500
vi.advanceTimersByTime(250)
expect(clock.displayNowMs.value).toBe(3_250)
clock.stopActiveElapsedDisplayClock()
})
})
@@ -1,102 +0,0 @@
import { describe, expect, it } from 'vitest'
import {
calculateServerClockOffsetMs,
shouldUseServerClockSample,
useServerClock
} from '../useServerClock'
describe('useServerClock', () => {
it('calculates offset from the response receive time', () => {
const offset = calculateServerClockOffsetMs({
server_now_unix_ms: 10_500,
client_send_unix_ms: 20_000,
client_receive_unix_ms: 20_200,
round_trip_ms: 200,
})
expect(offset).toBe(-9_700)
})
it('ignores missing or invalid timing samples', () => {
expect(calculateServerClockOffsetMs(undefined)).toBeNull()
expect(calculateServerClockOffsetMs({
server_now_unix_ms: Number.NaN,
client_send_unix_ms: 20_000,
client_receive_unix_ms: 20_200,
round_trip_ms: 200,
})).toBeNull()
expect(calculateServerClockOffsetMs({
server_now_unix_ms: 10_500,
client_send_unix_ms: 20_200,
client_receive_unix_ms: 20_000,
round_trip_ms: 200,
})).toBeNull()
expect(calculateServerClockOffsetMs({
server_now_unix_ms: 10_500,
client_send_unix_ms: 20_000,
client_receive_unix_ms: 20_200,
round_trip_ms: Number.NaN,
})).toBeNull()
})
it('keeps the previous offset when a response has no server timing', () => {
const clock = useServerClock()
clock.updateServerClockOffset({
server_now_unix_ms: 10_500,
client_send_unix_ms: 20_000,
client_receive_unix_ms: 20_200,
round_trip_ms: 200,
})
clock.updateServerClockOffset(undefined)
expect(clock.hasServerClockOffset.value).toBe(true)
expect(clock.serverClockOffsetMs.value).toBe(-9_700)
expect(clock.serverClockSampleRoundTripMs.value).toBe(200)
})
it('does not let a much slower sample overwrite a better clock offset', () => {
const clock = useServerClock()
clock.updateServerClockOffset({
server_now_unix_ms: 10_500,
client_send_unix_ms: 20_000,
client_receive_unix_ms: 20_050,
round_trip_ms: 50,
})
clock.updateServerClockOffset({
server_now_unix_ms: 20_500,
client_send_unix_ms: 30_000,
client_receive_unix_ms: 30_500,
round_trip_ms: 500,
})
expect(clock.serverClockOffsetMs.value).toBe(-9_550)
expect(clock.serverClockSampleRoundTripMs.value).toBe(50)
})
it('accepts a faster sample after an initial slow sample', () => {
const clock = useServerClock()
clock.updateServerClockOffset({
server_now_unix_ms: 10_500,
client_send_unix_ms: 20_000,
client_receive_unix_ms: 20_500,
round_trip_ms: 500,
})
clock.updateServerClockOffset({
server_now_unix_ms: 20_500,
client_send_unix_ms: 30_000,
client_receive_unix_ms: 30_050,
round_trip_ms: 50,
})
expect(clock.serverClockOffsetMs.value).toBe(-9_550)
expect(clock.serverClockSampleRoundTripMs.value).toBe(50)
})
it('allows small RTT regressions so the offset can stay fresh', () => {
expect(shouldUseServerClockSample(140, 50)).toBe(true)
expect(shouldUseServerClockSample(151, 50)).toBe(false)
})
})
@@ -1,5 +1,4 @@
export { useUsageData } from './useUsageData'
export { useActiveElapsedDisplayClock } from './useActiveElapsedDisplayClock'
export { useUsageFilters } from './useUsageFilters'
export { useUsagePagination } from './useUsagePagination'
export { getDateRangeFromPeriod, formatDateTime, getSuccessRateColor } from './useDateRange'
@@ -1,84 +0,0 @@
import { computed, ref, watch, type ComputedRef, type Ref } from 'vue'
type ActiveElapsedStatus = string | null | undefined
export interface UseActiveElapsedDisplayClockOptions<TRecord> {
records: Ref<TRecord[]> | ComputedRef<TRecord[]>
isPageVisible: Ref<boolean> | ComputedRef<boolean>
serverClockOffsetMs: Ref<number> | ComputedRef<number>
hasServerClockOffset: Ref<boolean> | ComputedRef<boolean>
resolveStatus: (record: TRecord) => ActiveElapsedStatus
intervalMs?: number
now?: () => number
}
export function useActiveElapsedDisplayClock<TRecord>(
options: UseActiveElapsedDisplayClockOptions<TRecord>
) {
const intervalMs = options.intervalMs ?? 250
const now = options.now ?? Date.now
const displayNowMs = ref(now())
let activeElapsedDisplayTimer: ReturnType<typeof setInterval> | null = null
const hasVisibleActiveRecords = computed(() => {
return options.records.value.some((record) => {
const displayStatus = options.resolveStatus(record)
return displayStatus === 'pending' || displayStatus === 'streaming'
})
})
const calibratedDisplayNowMs = computed(() => {
return options.hasServerClockOffset.value
? displayNowMs.value + options.serverClockOffsetMs.value
: displayNowMs.value
})
function tickActiveElapsedDisplay() {
displayNowMs.value = now()
}
function startActiveElapsedDisplayTimer() {
if (activeElapsedDisplayTimer) return
if (!options.isPageVisible.value || !hasVisibleActiveRecords.value) return
tickActiveElapsedDisplay()
activeElapsedDisplayTimer = setInterval(tickActiveElapsedDisplay, intervalMs)
}
function stopActiveElapsedDisplayTimer() {
if (activeElapsedDisplayTimer) {
clearInterval(activeElapsedDisplayTimer)
activeElapsedDisplayTimer = null
}
}
function syncActiveElapsedDisplayTimer() {
if (options.isPageVisible.value && hasVisibleActiveRecords.value) {
startActiveElapsedDisplayTimer()
} else {
stopActiveElapsedDisplayTimer()
}
}
const stopActiveElapsedDisplayWatch = watch(
[hasVisibleActiveRecords, options.isPageVisible],
syncActiveElapsedDisplayTimer,
{ immediate: true }
)
function stopActiveElapsedDisplayClock() {
stopActiveElapsedDisplayWatch()
stopActiveElapsedDisplayTimer()
}
return {
displayNowMs,
calibratedDisplayNowMs,
hasVisibleActiveRecords,
tickActiveElapsedDisplay,
startActiveElapsedDisplayTimer,
stopActiveElapsedDisplayTimer,
syncActiveElapsedDisplayTimer,
stopActiveElapsedDisplayClock,
}
}
@@ -1,62 +0,0 @@
import { ref } from 'vue'
import type { ServerTimingMetadata } from '@/api/serverTiming'
const SERVER_CLOCK_RTT_REGRESSION_TOLERANCE_MS = 100
export function calculateServerClockOffsetMs(timing: ServerTimingMetadata | null | undefined): number | null {
if (!timing) return null
const {
server_now_unix_ms: serverNowUnixMs,
client_send_unix_ms: clientSendUnixMs,
client_receive_unix_ms: clientReceiveUnixMs,
round_trip_ms: roundTripMs,
} = timing
if (
!Number.isFinite(serverNowUnixMs) ||
!Number.isFinite(clientSendUnixMs) ||
!Number.isFinite(clientReceiveUnixMs) ||
!Number.isFinite(roundTripMs)
) {
return null
}
if (clientReceiveUnixMs < clientSendUnixMs || roundTripMs < 0) {
return null
}
return serverNowUnixMs - clientReceiveUnixMs
}
export function shouldUseServerClockSample(
nextRoundTripMs: number,
currentRoundTripMs: number | null | undefined
): boolean {
if (!Number.isFinite(nextRoundTripMs) || nextRoundTripMs < 0) return false
if (currentRoundTripMs == null || !Number.isFinite(currentRoundTripMs)) return true
return nextRoundTripMs <= currentRoundTripMs + SERVER_CLOCK_RTT_REGRESSION_TOLERANCE_MS
}
export function useServerClock() {
const serverClockOffsetMs = ref(0)
const hasServerClockOffset = ref(false)
const serverClockSampleRoundTripMs = ref<number | null>(null)
function updateServerClockOffset(timing: ServerTimingMetadata | null | undefined): void {
const offsetMs = calculateServerClockOffsetMs(timing)
if (offsetMs == null) return
if (!shouldUseServerClockSample(timing?.round_trip_ms ?? Number.NaN, serverClockSampleRoundTripMs.value)) {
return
}
serverClockOffsetMs.value = offsetMs
serverClockSampleRoundTripMs.value = timing?.round_trip_ms ?? null
hasServerClockOffset.value = true
}
return {
serverClockOffsetMs,
hasServerClockOffset,
serverClockSampleRoundTripMs,
updateServerClockOffset,
}
}
@@ -14,7 +14,6 @@ import { createDefaultStats } from '../types'
import { log } from '@/utils/logger'
import { getErrorStatus } from '@/types/api-error'
import { isUsageProviderVisible, normalizeUsageProviderStats } from '../utils/providerStats'
import { useServerClock } from './useServerClock'
export interface UseUsageDataOptions {
isAdminPage: Ref<boolean>
@@ -66,11 +65,6 @@ export function useUsageData(options: UseUsageDataOptions) {
// 可用的筛选选项(从统计数据获取,而不是从记录中)
const availableModels = ref<string[]>([])
const availableProviders = ref<string[]>([])
const {
serverClockOffsetMs,
hasServerClockOffset,
updateServerClockOffset,
} = useServerClock()
// 增强的模型统计(包含效率分析)
const enhancedModelStats = computed<EnhancedModelStatsItem[]>(() => {
@@ -219,7 +213,6 @@ export function useUsageData(options: UseUsageDataOptions) {
if (requestId !== loadStatsRequestId) {
return false
}
updateServerClockOffset(userData.server_timing)
stats.value = {
total_requests: userData.total_requests || 0,
@@ -373,7 +366,6 @@ export function useUsageData(options: UseUsageDataOptions) {
if (requestId !== loadRecordsRequestId) {
return
}
updateServerClockOffset(response.server_timing)
const nextRecords = (response.records || []) as UsageRecord[]
currentRecords.value = mergeRecordStatus(currentRecords.value, nextRecords)
totalRecords.value = response.total || 0
@@ -383,7 +375,6 @@ export function useUsageData(options: UseUsageDataOptions) {
if (requestId !== loadRecordsRequestId) {
return
}
updateServerClockOffset(userData.server_timing)
const nextRecords = (userData.records || []) as UsageRecord[]
currentRecords.value = mergeRecordStatus(currentRecords.value, nextRecords)
totalRecords.value = userData.pagination?.total || currentRecords.value.length
@@ -566,8 +557,6 @@ export function useUsageData(options: UseUsageDataOptions) {
apiFormatStats,
currentRecords,
totalRecords,
serverClockOffsetMs,
hasServerClockOffset,
// 筛选选项
availableModels,
@@ -579,7 +568,6 @@ export function useUsageData(options: UseUsageDataOptions) {
// 方法
loadStats,
loadRecords,
refreshData,
updateServerClockOffset
refreshData
}
}