fix(usage): streaming 状态在首字节到达前显示为 pending

新增 resolveDisplayRequestStatus 函数,当记录状态为 streaming 但
first_byte_time_ms 为空时回退显示为 pending,避免误导用户。
This commit is contained in:
fawney19
2026-04-15 17:50:45 +08:00
parent 77d413d777
commit c569081340
3 changed files with 38 additions and 11 deletions

View File

@@ -212,14 +212,14 @@
失败 失败
</Badge> </Badge>
<Badge <Badge
v-else-if="record.status === 'pending'" v-else-if="getDisplayStatus(record) === 'pending'"
variant="outline" variant="outline"
class="whitespace-nowrap animate-pulse border-muted-foreground/30 text-muted-foreground text-[10px] px-1.5 h-4 leading-4 inline-flex items-center" class="whitespace-nowrap animate-pulse border-muted-foreground/30 text-muted-foreground text-[10px] px-1.5 h-4 leading-4 inline-flex items-center"
> >
等待 等待
</Badge> </Badge>
<Badge <Badge
v-else-if="record.status === 'streaming'" v-else-if="getDisplayStatus(record) === 'streaming'"
variant="outline" variant="outline"
class="whitespace-nowrap animate-pulse border-primary/50 text-primary text-[10px] px-1.5 h-4 leading-4 inline-flex items-center" class="whitespace-nowrap animate-pulse border-primary/50 text-primary text-[10px] px-1.5 h-4 leading-4 inline-flex items-center"
> >
@@ -256,11 +256,11 @@
<div class="flex items-center gap-1.5"> <div class="flex items-center gap-1.5">
<!-- 耗时 --> <!-- 耗时 -->
<span <span
v-if="record.status === 'pending' || record.status === 'streaming'" v-if="getDisplayStatus(record) === 'pending' || getDisplayStatus(record) === 'streaming'"
class="text-primary tabular-nums" class="text-primary tabular-nums"
><ElapsedTimeText ><ElapsedTimeText
:created-at="record.created_at" :created-at="record.created_at"
:status="record.status" :status="getDisplayStatus(record)"
:response-time-ms="record.response_time_ms ?? null" :response-time-ms="record.response_time_ms ?? null"
/></span> /></span>
<span <span
@@ -507,14 +507,14 @@
<TableCell class="text-center py-4 w-[70px]"> <TableCell class="text-center py-4 w-[70px]">
<!-- 优先显示请求状态 --> <!-- 优先显示请求状态 -->
<Badge <Badge
v-if="record.status === 'pending'" v-if="getDisplayStatus(record) === 'pending'"
variant="outline" variant="outline"
class="whitespace-nowrap animate-pulse border-muted-foreground/30 text-muted-foreground" class="whitespace-nowrap animate-pulse border-muted-foreground/30 text-muted-foreground"
> >
等待中 等待中
</Badge> </Badge>
<Badge <Badge
v-else-if="record.status === 'streaming'" v-else-if="getDisplayStatus(record) === 'streaming'"
variant="outline" variant="outline"
class="whitespace-nowrap animate-pulse border-primary/50 text-primary" class="whitespace-nowrap animate-pulse border-primary/50 text-primary"
> >
@@ -577,19 +577,19 @@
<TableCell class="text-right py-4 w-[70px]"> <TableCell class="text-right py-4 w-[70px]">
<!-- pending 状态只显示增长的总时间 --> <!-- pending 状态只显示增长的总时间 -->
<div <div
v-if="record.status === 'pending'" v-if="getDisplayStatus(record) === 'pending'"
class="flex flex-col items-end text-xs gap-0.5" class="flex flex-col items-end text-xs gap-0.5"
> >
<span class="text-muted-foreground">-</span> <span class="text-muted-foreground">-</span>
<span class="text-primary tabular-nums"><ElapsedTimeText <span class="text-primary tabular-nums"><ElapsedTimeText
:created-at="record.created_at" :created-at="record.created_at"
:status="record.status" :status="getDisplayStatus(record)"
:response-time-ms="record.response_time_ms ?? null" :response-time-ms="record.response_time_ms ?? null"
/></span> /></span>
</div> </div>
<!-- streaming 状态首字固定 + 总时间增长 --> <!-- streaming 状态首字固定 + 总时间增长 -->
<div <div
v-else-if="record.status === 'streaming'" v-else-if="getDisplayStatus(record) === 'streaming'"
class="flex flex-col items-end text-xs gap-0.5" class="flex flex-col items-end text-xs gap-0.5"
> >
<span <span
@@ -602,7 +602,7 @@
>-</span> >-</span>
<span class="text-primary tabular-nums"><ElapsedTimeText <span class="text-primary tabular-nums"><ElapsedTimeText
:created-at="record.created_at" :created-at="record.created_at"
:status="record.status" :status="getDisplayStatus(record)"
:response-time-ms="record.response_time_ms ?? null" :response-time-ms="record.response_time_ms ?? null"
/></span> /></span>
</div> </div>
@@ -678,7 +678,7 @@ import { RefreshCcw, Search } from 'lucide-vue-next'
import { formatTokens, formatCurrency } from '@/utils/format' import { formatTokens, formatCurrency } from '@/utils/format'
import { formatDateTime } from '../composables' import { formatDateTime } from '../composables'
import { getEffectiveInputTokens } from '../token-normalization' import { getEffectiveInputTokens } from '../token-normalization'
import { isUsageRecordFailed } from '../utils/status' import { isUsageRecordFailed, resolveDisplayRequestStatus } from '../utils/status'
import { useRowClick } from '@/composables/useRowClick' import { useRowClick } from '@/composables/useRowClick'
import { formatApiFormat } from '@/api/endpoints/types/api-format' import { formatApiFormat } from '@/api/endpoints/types/api-format'
import type { DateRangeParams, UsageRecord } from '../types' import type { DateRangeParams, UsageRecord } from '../types'
@@ -759,6 +759,10 @@ const emitSearchDebounced = useDebounceFn((value: string) => {
emit('update:filterSearch', value) emit('update:filterSearch', value)
}, 300) }, 300)
function getDisplayStatus(record: UsageRecord) {
return resolveDisplayRequestStatus(record)
}
watch(() => props.filterSearch, (value) => { watch(() => props.filterSearch, (value) => {
if (value !== localSearch.value) { if (value !== localSearch.value) {
localSearch.value = value localSearch.value = value

View File

@@ -5,6 +5,7 @@ import {
isUsageRecordSuccessful, isUsageRecordSuccessful,
mapRequestStatusToTimelineStatus, mapRequestStatusToTimelineStatus,
normalizeRequestStatus, normalizeRequestStatus,
resolveDisplayRequestStatus,
resolveTimelineFinalStatus, resolveTimelineFinalStatus,
} from '../status' } from '../status'
import type { UsageRecord } from '../../types' import type { UsageRecord } from '../../types'
@@ -64,6 +65,18 @@ describe('usage status helpers', () => {
expect(mapRequestStatusToTimelineStatus('failed')).toBe('failed') expect(mapRequestStatusToTimelineStatus('failed')).toBe('failed')
}) })
it('shows streaming as pending until first byte is recorded', () => {
expect(resolveDisplayRequestStatus(buildUsageRecord({
status: 'streaming',
first_byte_time_ms: undefined,
}))).toBe('pending')
expect(resolveDisplayRequestStatus(buildUsageRecord({
status: 'streaming',
first_byte_time_ms: 320,
}))).toBe('streaming')
})
it('treats explicit success status code as authoritative for the timeline', () => { it('treats explicit success status code as authoritative for the timeline', () => {
expect(resolveTimelineFinalStatus({ expect(resolveTimelineFinalStatus({
traceFinalStatus: 'success', traceFinalStatus: 'success',

View File

@@ -76,6 +76,16 @@ export function normalizeRequestStatus(status: RequestStatusLike): RequestStatus
} }
} }
export function resolveDisplayRequestStatus(
record: Pick<UsageRecord, 'status' | 'first_byte_time_ms'>
): RequestStatus | undefined {
const status = normalizeRequestStatus(record.status)
if (status === 'streaming' && record.first_byte_time_ms == null) {
return 'pending'
}
return status
}
export function mapRequestStatusToTimelineStatus( export function mapRequestStatusToTimelineStatus(
status: RequestStatusLike status: RequestStatusLike
): TimelineFinalStatus | undefined { ): TimelineFinalStatus | undefined {