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

View File

@@ -5,6 +5,7 @@ import {
isUsageRecordSuccessful,
mapRequestStatusToTimelineStatus,
normalizeRequestStatus,
resolveDisplayRequestStatus,
resolveTimelineFinalStatus,
} from '../status'
import type { UsageRecord } from '../../types'
@@ -64,6 +65,18 @@ describe('usage status helpers', () => {
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', () => {
expect(resolveTimelineFinalStatus({
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(
status: RequestStatusLike
): TimelineFinalStatus | undefined {