mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-04 16:37:46 +08:00
fix(usage): preserve failed status for active request refreshes
This commit is contained in:
@@ -828,6 +828,16 @@ import {
|
|||||||
type RenderBlock,
|
type RenderBlock,
|
||||||
} from '../conversation'
|
} from '../conversation'
|
||||||
|
|
||||||
|
type RequestStateStatus = 'pending' | 'streaming' | 'completed' | 'failed' | 'cancelled'
|
||||||
|
|
||||||
|
const REQUEST_STATE_STATUSES = new Set<RequestStateStatus>([
|
||||||
|
'pending',
|
||||||
|
'streaming',
|
||||||
|
'completed',
|
||||||
|
'failed',
|
||||||
|
'cancelled',
|
||||||
|
])
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
requestId: string | null
|
requestId: string | null
|
||||||
@@ -838,7 +848,7 @@ const emit = defineEmits<{
|
|||||||
requestState: [state: {
|
requestState: [state: {
|
||||||
id: string
|
id: string
|
||||||
requestId?: string | null
|
requestId?: string | null
|
||||||
status?: 'pending' | 'streaming' | 'completed' | 'failed' | 'cancelled'
|
status?: RequestStateStatus
|
||||||
statusCode?: number | null
|
statusCode?: number | null
|
||||||
responseTimeMs?: number | null
|
responseTimeMs?: number | null
|
||||||
imageProgress?: ImageProgress | null
|
imageProgress?: ImageProgress | null
|
||||||
@@ -922,7 +932,7 @@ function formatErrorDomainMeta(domain: NormalizedErrorDomain): string {
|
|||||||
|
|
||||||
function mapTraceFinalStatusToRequestStatus(
|
function mapTraceFinalStatusToRequestStatus(
|
||||||
status?: RequestTrace['final_status'] | null
|
status?: RequestTrace['final_status'] | null
|
||||||
): 'pending' | 'streaming' | 'completed' | 'failed' | 'cancelled' | undefined {
|
): RequestStateStatus | undefined {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'success':
|
case 'success':
|
||||||
return 'completed'
|
return 'completed'
|
||||||
@@ -939,6 +949,49 @@ function mapTraceFinalStatusToRequestStatus(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeRequestStateStatus(status: unknown): RequestStateStatus | undefined {
|
||||||
|
const normalized = typeof status === 'string' ? status.trim().toLowerCase() : ''
|
||||||
|
return REQUEST_STATE_STATUSES.has(normalized as RequestStateStatus)
|
||||||
|
? normalized as RequestStateStatus
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasRequestFailureSignal(statusCode?: number | null, errorMessage?: string | null): boolean {
|
||||||
|
return (typeof statusCode === 'number' && statusCode >= 400) ||
|
||||||
|
(typeof errorMessage === 'string' && errorMessage.trim().length > 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveRequestStateStatus(
|
||||||
|
status: unknown,
|
||||||
|
statusCode?: number | null,
|
||||||
|
errorMessage?: string | null
|
||||||
|
): RequestStateStatus | undefined {
|
||||||
|
const normalized = normalizeRequestStateStatus(status)
|
||||||
|
if ((normalized == null || normalized === 'pending' || normalized === 'streaming') &&
|
||||||
|
hasRequestFailureSignal(statusCode, errorMessage)) {
|
||||||
|
return 'failed'
|
||||||
|
}
|
||||||
|
return normalized
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveRequestStateStatusFromDetail(nextDetail: Pick<RequestDetail, 'status' | 'status_code' | 'error_message'>): RequestStateStatus | undefined {
|
||||||
|
return resolveRequestStateStatus(nextDetail.status, nextDetail.status_code, nextDetail.error_message)
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitDetailRequestState(nextDetail: RequestDetail) {
|
||||||
|
const id = props.requestId
|
||||||
|
if (!id) return
|
||||||
|
|
||||||
|
emit('requestState', {
|
||||||
|
id,
|
||||||
|
requestId: nextDetail.request_id || nextDetail.id || null,
|
||||||
|
status: resolveRequestStateStatusFromDetail(nextDetail),
|
||||||
|
statusCode: nextDetail.status_code ?? undefined,
|
||||||
|
responseTimeMs: nextDetail.response_time_ms ?? undefined,
|
||||||
|
errorMessage: nextDetail.error_message ?? undefined,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function handleTraceState(state: {
|
function handleTraceState(state: {
|
||||||
loaded: boolean
|
loaded: boolean
|
||||||
hasTrace: boolean
|
hasTrace: boolean
|
||||||
@@ -953,7 +1006,11 @@ function handleTraceState(state: {
|
|||||||
const id = props.requestId
|
const id = props.requestId
|
||||||
if (!id) return
|
if (!id) return
|
||||||
|
|
||||||
const status = mapTraceFinalStatusToRequestStatus(state.finalStatus)
|
const status = resolveRequestStateStatus(
|
||||||
|
mapTraceFinalStatusToRequestStatus(state.finalStatus),
|
||||||
|
state.statusCode,
|
||||||
|
state.errorMessage
|
||||||
|
)
|
||||||
const imageFailed = state.imageProgress?.phase === 'failed'
|
const imageFailed = state.imageProgress?.phase === 'failed'
|
||||||
if (!status && !state.imageProgress && state.statusCode == null && state.latencyMs == null) return
|
if (!status && !state.imageProgress && state.statusCode == null && state.latencyMs == null) return
|
||||||
|
|
||||||
@@ -2094,8 +2151,9 @@ async function loadDetail(id: string, silent = false) {
|
|||||||
const prevKey = previousDetail?.request_id || previousDetail?.id
|
const prevKey = previousDetail?.request_id || previousDetail?.id
|
||||||
const currKey = response.request_id || response.id
|
const currKey = response.request_id || response.id
|
||||||
const sameRequest = !!prevKey && prevKey === currKey
|
const sameRequest = !!prevKey && prevKey === currKey
|
||||||
detail.value = {
|
const nextDetail: RequestDetail = {
|
||||||
...response,
|
...response,
|
||||||
|
status: resolveRequestStateStatusFromDetail(response) ?? response.status,
|
||||||
request_body: sameRequest ? previousDetail?.request_body : undefined,
|
request_body: sameRequest ? previousDetail?.request_body : undefined,
|
||||||
provider_request_body: sameRequest ? previousDetail?.provider_request_body : undefined,
|
provider_request_body: sameRequest ? previousDetail?.provider_request_body : undefined,
|
||||||
response_body: sameRequest ? previousDetail?.response_body : undefined,
|
response_body: sameRequest ? previousDetail?.response_body : undefined,
|
||||||
@@ -2108,7 +2166,9 @@ async function loadDetail(id: string, silent = false) {
|
|||||||
error_flow: response.error_flow,
|
error_flow: response.error_flow,
|
||||||
scheduling_failure: response.scheduling_failure,
|
scheduling_failure: response.scheduling_failure,
|
||||||
}
|
}
|
||||||
|
detail.value = nextDetail
|
||||||
bodiesLoadedForRequestId.value = sameRequest ? bodiesLoadedForRequestId.value : null
|
bodiesLoadedForRequestId.value = sameRequest ? bodiesLoadedForRequestId.value : null
|
||||||
|
emitDetailRequestState(nextDetail)
|
||||||
|
|
||||||
// 首次加载时优先停留在轻量 tab,避免默认触发大 body 加载
|
// 首次加载时优先停留在轻量 tab,避免默认触发大 body 加载
|
||||||
if (!silent) {
|
if (!silent) {
|
||||||
|
|||||||
@@ -654,7 +654,14 @@
|
|||||||
<TableCell v-if="isColumnVisible('status')" class="text-center py-4 w-[10%]">
|
<TableCell v-if="isColumnVisible('status')" class="text-center py-4 w-[10%]">
|
||||||
<!-- 优先显示请求状态 -->
|
<!-- 优先显示请求状态 -->
|
||||||
<Badge
|
<Badge
|
||||||
v-if="getDisplayStatus(record) === 'pending'"
|
v-if="isUsageRecordFailed(record)"
|
||||||
|
variant="destructive"
|
||||||
|
class="whitespace-nowrap"
|
||||||
|
>
|
||||||
|
失败
|
||||||
|
</Badge>
|
||||||
|
<Badge
|
||||||
|
v-else-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"
|
||||||
>
|
>
|
||||||
@@ -667,13 +674,6 @@
|
|||||||
>
|
>
|
||||||
传输中
|
传输中
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge
|
|
||||||
v-else-if="isUsageRecordFailed(record)"
|
|
||||||
variant="destructive"
|
|
||||||
class="whitespace-nowrap"
|
|
||||||
>
|
|
||||||
失败
|
|
||||||
</Badge>
|
|
||||||
<Badge
|
<Badge
|
||||||
v-else-if="record.status === 'cancelled'"
|
v-else-if="record.status === 'cancelled'"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|||||||
@@ -257,6 +257,19 @@ describe('UsageRecordsTable', () => {
|
|||||||
expect(root.textContent).not.toContain('等待中')
|
expect(root.textContent).not.toContain('等待中')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('shows failed instead of waiting when an active row has an HTTP error code', () => {
|
||||||
|
const root = mountUsageRecordsTable([buildRecord({
|
||||||
|
status: 'pending',
|
||||||
|
status_code: 524,
|
||||||
|
error_message: 'error code: 524',
|
||||||
|
response_time_ms: null,
|
||||||
|
first_byte_time_ms: null,
|
||||||
|
})])
|
||||||
|
|
||||||
|
expect(root.textContent).toContain('失败')
|
||||||
|
expect(root.textContent).not.toContain('等待中')
|
||||||
|
})
|
||||||
|
|
||||||
it('renders output TPS in the non-admin usage table', () => {
|
it('renders output TPS in the non-admin usage table', () => {
|
||||||
const root = mountUsageRecordsTable([buildRecord()], { isAdmin: false })
|
const root = mountUsageRecordsTable([buildRecord()], { isAdmin: false })
|
||||||
|
|
||||||
|
|||||||
@@ -102,6 +102,47 @@ describe('useUsageData', () => {
|
|||||||
expect(totalRecords.value).toBe(1)
|
expect(totalRecords.value).toBe(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('keeps locally resolved failure fields when a stale active record refreshes', async () => {
|
||||||
|
const isAdminPage = ref(true)
|
||||||
|
const { loadRecords, currentRecords } = useUsageData({ isAdminPage })
|
||||||
|
const dateRange = { preset: 'today', tz_offset_minutes: 0 }
|
||||||
|
|
||||||
|
getAllUsageRecordsMock.mockResolvedValueOnce({
|
||||||
|
records: [buildUsageRecord({
|
||||||
|
status: 'failed',
|
||||||
|
status_code: 524,
|
||||||
|
error_message: 'error code: 524',
|
||||||
|
response_time_ms: 125_000,
|
||||||
|
})],
|
||||||
|
total: 1,
|
||||||
|
limit: 20,
|
||||||
|
offset: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
await loadRecords({ page: 1, pageSize: 20 }, undefined, dateRange)
|
||||||
|
|
||||||
|
getAllUsageRecordsMock.mockResolvedValueOnce({
|
||||||
|
records: [buildUsageRecord({
|
||||||
|
status: 'pending',
|
||||||
|
status_code: undefined,
|
||||||
|
error_message: undefined,
|
||||||
|
response_time_ms: null,
|
||||||
|
})],
|
||||||
|
total: 1,
|
||||||
|
limit: 20,
|
||||||
|
offset: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
await loadRecords({ page: 1, pageSize: 20 }, undefined, dateRange)
|
||||||
|
|
||||||
|
expect(currentRecords.value[0]).toMatchObject({
|
||||||
|
status: 'failed',
|
||||||
|
status_code: 524,
|
||||||
|
error_message: 'error code: 524',
|
||||||
|
response_time_ms: 125_000,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it('continues loading admin breakdowns when the summary request fails', async () => {
|
it('continues loading admin breakdowns when the summary request fails', async () => {
|
||||||
const isAdminPage = ref(true)
|
const isAdminPage = ref(true)
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -509,6 +509,9 @@ export function useUsageData(options: UseUsageDataOptions) {
|
|||||||
actual_cost: existing.actual_cost ?? record.actual_cost,
|
actual_cost: existing.actual_cost ?? record.actual_cost,
|
||||||
response_time_ms: mergePositiveDurationMs(existing.response_time_ms, record.response_time_ms),
|
response_time_ms: mergePositiveDurationMs(existing.response_time_ms, record.response_time_ms),
|
||||||
first_byte_time_ms: mergePositiveDurationMs(existing.first_byte_time_ms, record.first_byte_time_ms),
|
first_byte_time_ms: mergePositiveDurationMs(existing.first_byte_time_ms, record.first_byte_time_ms),
|
||||||
|
status_code: existing.status_code ?? record.status_code,
|
||||||
|
error_message: existing.error_message ?? record.error_message,
|
||||||
|
image_progress: existing.image_progress ?? record.image_progress,
|
||||||
is_stream: upstreamIsStream,
|
is_stream: upstreamIsStream,
|
||||||
upstream_is_stream: upstreamIsStream,
|
upstream_is_stream: upstreamIsStream,
|
||||||
client_requested_stream: clientRequestedStream,
|
client_requested_stream: clientRequestedStream,
|
||||||
|
|||||||
@@ -156,6 +156,7 @@ import {
|
|||||||
hasUsageFallback,
|
hasUsageFallback,
|
||||||
isUsageRecordFailed,
|
isUsageRecordFailed,
|
||||||
isUsageUpstreamStream,
|
isUsageUpstreamStream,
|
||||||
|
normalizeRequestStatus,
|
||||||
resolveDisplayRequestStatus,
|
resolveDisplayRequestStatus,
|
||||||
} from '@/features/usage/utils/status'
|
} from '@/features/usage/utils/status'
|
||||||
import type { DateRangeParams, FilterStatusValue, RequestStatus } from '@/features/usage/types'
|
import type { DateRangeParams, FilterStatusValue, RequestStatus } from '@/features/usage/types'
|
||||||
@@ -974,6 +975,8 @@ function handleDetailRequestState(update: {
|
|||||||
const record = currentRecords.value.find(record => record.id === update.id)
|
const record = currentRecords.value.find(record => record.id === update.id)
|
||||||
if (!record) return
|
if (!record) return
|
||||||
|
|
||||||
|
const nextStatus = resolveDetailUpdateStatus(update)
|
||||||
|
|
||||||
const statusPriority: Record<RequestStatus, number> = {
|
const statusPriority: Record<RequestStatus, number> = {
|
||||||
pending: 0,
|
pending: 0,
|
||||||
streaming: 1,
|
streaming: 1,
|
||||||
@@ -981,11 +984,11 @@ function handleDetailRequestState(update: {
|
|||||||
failed: 2,
|
failed: 2,
|
||||||
cancelled: 2,
|
cancelled: 2,
|
||||||
}
|
}
|
||||||
if (update.status) {
|
if (nextStatus) {
|
||||||
const currentRank = record.status ? statusPriority[record.status] : 0
|
const currentRank = record.status ? statusPriority[record.status] : 0
|
||||||
const nextRank = statusPriority[update.status]
|
const nextRank = statusPriority[nextStatus]
|
||||||
if (nextRank >= currentRank) {
|
if (nextRank >= currentRank) {
|
||||||
record.status = update.status
|
record.status = nextStatus
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ('statusCode' in update) {
|
if ('statusCode' in update) {
|
||||||
@@ -1005,6 +1008,24 @@ function handleDetailRequestState(update: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveDetailUpdateStatus(update: {
|
||||||
|
status?: RequestStatus
|
||||||
|
statusCode?: number | null
|
||||||
|
imageProgress?: ImageProgress | null
|
||||||
|
errorMessage?: string | null
|
||||||
|
}): RequestStatus | undefined {
|
||||||
|
const status = normalizeRequestStatus(update.status)
|
||||||
|
const hasFailureSignal =
|
||||||
|
(typeof update.statusCode === 'number' && update.statusCode >= 400) ||
|
||||||
|
(typeof update.errorMessage === 'string' && update.errorMessage.trim().length > 0) ||
|
||||||
|
update.imageProgress?.phase === 'failed'
|
||||||
|
|
||||||
|
if ((status == null || status === 'pending' || status === 'streaming') && hasFailureSignal) {
|
||||||
|
return 'failed'
|
||||||
|
}
|
||||||
|
return status
|
||||||
|
}
|
||||||
|
|
||||||
function prefetchRequestDetail(id: string) {
|
function prefetchRequestDetail(id: string) {
|
||||||
if (!isAdminPage.value) return
|
if (!isAdminPage.value) return
|
||||||
void dashboardApi.prefetchRequestDetail(id).catch(error => {
|
void dashboardApi.prefetchRequestDetail(id).catch(error => {
|
||||||
|
|||||||
Reference in New Issue
Block a user