mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
fix: 修正用量失败状态判断与 Codex 会话头
This commit is contained in:
@@ -205,7 +205,7 @@
|
||||
<div class="flex items-center gap-1.5">
|
||||
<!-- 状态 Badge -->
|
||||
<Badge
|
||||
v-if="record.status === 'failed' || (record.status_code && record.status_code >= 400) || record.error_message"
|
||||
v-if="isUsageRecordFailed(record)"
|
||||
variant="destructive"
|
||||
class="whitespace-nowrap text-[10px] px-1.5 h-4 leading-4 inline-flex items-center"
|
||||
>
|
||||
@@ -521,7 +521,7 @@
|
||||
传输中
|
||||
</Badge>
|
||||
<Badge
|
||||
v-else-if="record.status === 'failed' || (record.status_code && record.status_code >= 400) || record.error_message"
|
||||
v-else-if="isUsageRecordFailed(record)"
|
||||
variant="destructive"
|
||||
class="whitespace-nowrap"
|
||||
>
|
||||
@@ -678,6 +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 { useRowClick } from '@/composables/useRowClick'
|
||||
import { formatApiFormat } from '@/api/endpoints/types/api-format'
|
||||
import type { DateRangeParams, UsageRecord } from '../types'
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ref, computed, type Ref } from 'vue'
|
||||
import type { UsageRecord, FilterStatusValue } from '../types'
|
||||
import { isUsageRecordFailed } from '../utils/status'
|
||||
|
||||
export interface UseUsageFiltersOptions {
|
||||
/** 所有记录的响应式引用 */
|
||||
@@ -64,22 +65,18 @@ export function useUsageFilters(options: UseUsageFiltersOptions) {
|
||||
if (filterStatus.value !== '__all__') {
|
||||
if (filterStatus.value === 'stream') {
|
||||
records = records.filter(record =>
|
||||
record.is_stream && !record.error_message && (!record.status_code || record.status_code === 200)
|
||||
record.is_stream && !isUsageRecordFailed(record)
|
||||
)
|
||||
} else if (filterStatus.value === 'standard') {
|
||||
records = records.filter(record =>
|
||||
!record.is_stream && !record.error_message && (!record.status_code || record.status_code === 200)
|
||||
!record.is_stream && !isUsageRecordFailed(record)
|
||||
)
|
||||
} else if (filterStatus.value === 'active') {
|
||||
records = records.filter(record =>
|
||||
record.status === 'pending' || record.status === 'streaming'
|
||||
)
|
||||
} else if (filterStatus.value === 'failed') {
|
||||
records = records.filter(record =>
|
||||
record.status === 'failed' ||
|
||||
(record.status_code && record.status_code >= 400) ||
|
||||
record.error_message
|
||||
)
|
||||
records = records.filter(record => isUsageRecordFailed(record))
|
||||
} else if (filterStatus.value === 'cancelled') {
|
||||
records = records.filter(record => record.status === 'cancelled')
|
||||
}
|
||||
|
||||
43
frontend/src/features/usage/utils/__tests__/status.spec.ts
Normal file
43
frontend/src/features/usage/utils/__tests__/status.spec.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { isUsageRecordFailed, isUsageRecordSuccessful } from '../status'
|
||||
import type { UsageRecord } from '../../types'
|
||||
|
||||
function buildUsageRecord(overrides: Partial<UsageRecord> = {}): UsageRecord {
|
||||
return {
|
||||
id: 'usage-1',
|
||||
model: 'gpt-5',
|
||||
input_tokens: 10,
|
||||
output_tokens: 20,
|
||||
total_tokens: 30,
|
||||
cost: 0,
|
||||
is_stream: false,
|
||||
created_at: '2026-04-10T00:00:00Z',
|
||||
status: 'completed',
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
describe('usage status helpers', () => {
|
||||
it('treats explicit completed status as authoritative over stale legacy failure fields', () => {
|
||||
const record = buildUsageRecord({
|
||||
status: 'completed',
|
||||
status_code: 429,
|
||||
error_message: 'rate limited on first attempt'
|
||||
})
|
||||
|
||||
expect(isUsageRecordFailed(record)).toBe(false)
|
||||
expect(isUsageRecordSuccessful(record)).toBe(true)
|
||||
})
|
||||
|
||||
it('falls back to legacy failure signals when status is missing', () => {
|
||||
const record = buildUsageRecord({
|
||||
status: undefined,
|
||||
status_code: 429,
|
||||
error_message: 'rate limited'
|
||||
})
|
||||
|
||||
expect(isUsageRecordFailed(record)).toBe(true)
|
||||
expect(isUsageRecordSuccessful(record)).toBe(false)
|
||||
})
|
||||
})
|
||||
28
frontend/src/features/usage/utils/status.ts
Normal file
28
frontend/src/features/usage/utils/status.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { UsageRecord } from '../types'
|
||||
|
||||
function hasLegacyFailureSignal(
|
||||
record: Pick<UsageRecord, 'status_code' | 'error_message'>
|
||||
): boolean {
|
||||
return (typeof record.status_code === 'number' && record.status_code >= 400) ||
|
||||
(typeof record.error_message === 'string' && record.error_message.trim().length > 0)
|
||||
}
|
||||
|
||||
export function isUsageRecordFailed(
|
||||
record: Pick<UsageRecord, 'status' | 'status_code' | 'error_message'>
|
||||
): boolean {
|
||||
const status = typeof record.status === 'string' ? record.status.trim().toLowerCase() : ''
|
||||
if (status) {
|
||||
return status === 'failed'
|
||||
}
|
||||
return hasLegacyFailureSignal(record)
|
||||
}
|
||||
|
||||
export function isUsageRecordSuccessful(
|
||||
record: Pick<UsageRecord, 'status' | 'status_code' | 'error_message'>
|
||||
): boolean {
|
||||
const status = typeof record.status === 'string' ? record.status.trim().toLowerCase() : ''
|
||||
if (status) {
|
||||
return status === 'completed'
|
||||
}
|
||||
return !hasLegacyFailureSignal(record)
|
||||
}
|
||||
@@ -144,6 +144,7 @@ import {
|
||||
getDateRangeFromPeriod
|
||||
} from '@/features/usage/composables'
|
||||
import { reconcileActiveRequestDiscovery } from '@/features/usage/utils/activeRequestDiscovery'
|
||||
import { isUsageRecordFailed } from '@/features/usage/utils/status'
|
||||
import type { DateRangeParams, FilterStatusValue } from '@/features/usage/types'
|
||||
import type { UserOption } from '@/features/usage/components/UsageRecordsTable.vue'
|
||||
import { log } from '@/utils/logger'
|
||||
@@ -294,25 +295,18 @@ const filteredRecords = computed(() => {
|
||||
if (filterStatus.value !== '__all__') {
|
||||
if (filterStatus.value === 'stream') {
|
||||
records = records.filter(record =>
|
||||
record.is_stream && !record.error_message && (!record.status_code || record.status_code === 200)
|
||||
record.is_stream && !isUsageRecordFailed(record)
|
||||
)
|
||||
} else if (filterStatus.value === 'standard') {
|
||||
records = records.filter(record =>
|
||||
!record.is_stream && !record.error_message && (!record.status_code || record.status_code === 200)
|
||||
!record.is_stream && !isUsageRecordFailed(record)
|
||||
)
|
||||
} else if (filterStatus.value === 'active') {
|
||||
records = records.filter(record =>
|
||||
record.status === 'pending' || record.status === 'streaming'
|
||||
)
|
||||
} else if (filterStatus.value === 'failed') {
|
||||
// 失败请求需要同时考虑新旧两种判断方式:
|
||||
// 1. 新方式:status = "failed"
|
||||
// 2. 旧方式:status_code >= 400 或 error_message 不为空
|
||||
records = records.filter(record =>
|
||||
record.status === 'failed' ||
|
||||
(record.status_code && record.status_code >= 400) ||
|
||||
record.error_message
|
||||
)
|
||||
records = records.filter(record => isUsageRecordFailed(record))
|
||||
} else if (filterStatus.value === 'cancelled') {
|
||||
records = records.filter(record => record.status === 'cancelled')
|
||||
}
|
||||
@@ -530,10 +524,10 @@ function stopActiveDiscovery() {
|
||||
}
|
||||
}
|
||||
|
||||
// 监听活跃请求状态,自动启动/停止刷新
|
||||
// 活跃请求的 1 秒轮询受“自动刷新”开关控制
|
||||
// 监听活跃请求状态,已显示的活跃行始终自刷新到终态。
|
||||
// “自动刷新”开关只控制全局 3 秒刷新和新活跃请求发现。
|
||||
watch(hasActiveRequests, (hasActive) => {
|
||||
if (globalAutoRefresh.value && hasActive && isPageVisible.value) {
|
||||
if (hasActive && isPageVisible.value) {
|
||||
startAutoRefresh()
|
||||
} else {
|
||||
stopAutoRefresh()
|
||||
@@ -568,7 +562,6 @@ function handleAutoRefreshChange(value: boolean) {
|
||||
}
|
||||
startGlobalAutoRefresh()
|
||||
} else {
|
||||
stopAutoRefresh()
|
||||
stopActiveDiscovery()
|
||||
stopGlobalAutoRefresh()
|
||||
}
|
||||
@@ -582,11 +575,11 @@ function handleVisibilityChange() {
|
||||
stopGlobalAutoRefresh()
|
||||
return
|
||||
}
|
||||
if (hasActiveRequests.value) {
|
||||
startAutoRefresh()
|
||||
}
|
||||
if (globalAutoRefresh.value) {
|
||||
startActiveDiscovery()
|
||||
if (hasActiveRequests.value) {
|
||||
startAutoRefresh()
|
||||
}
|
||||
refreshData()
|
||||
startGlobalAutoRefresh()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user