fix(usage): unify OpenAI Live and WebSocket records

This commit is contained in:
ZheFox
2026-08-21 11:53:53 +08:00
parent 9996e75a34
commit acde38b8e7
32 changed files with 629 additions and 95 deletions
+40 -14
View File
@@ -170,6 +170,11 @@ import {
normalizeRequestStatus,
resolveDisplayRequestStatus,
} from '@/features/usage/utils/status'
import { matchesUsageRecordSearch } from '@/features/usage/utils/recordSearch'
import {
isUserLocalOnlyRecordStatus,
shouldUseServerUserRecordFilters,
} from '@/features/usage/utils/recordFilterPolicy'
import type { DateRangeParams, FilterStatusValue, RequestStatus, UsageRecord } from '@/features/usage/types'
import type { UserOption } from '@/features/usage/components/UsageRecordsTable.vue'
import { log } from '@/utils/logger'
@@ -390,6 +395,10 @@ const filteredRecords = computed(() => {
: [...currentRecords.value]
if (!isAdminPage.value) {
if (isUserLocalOnlyRecordStatus(filterStatus.value) && filterSearch.value.trim()) {
records = records.filter(record => matchesUsageRecordSearch(record, filterSearch.value))
}
if (filterModel.value !== '__all__') {
records = records.filter(record => record.model === filterModel.value)
}
@@ -840,9 +849,21 @@ onUnmounted(() => {
stopGlobalAutoRefresh()
})
// 用户页面的前端分页(后端一次性返回所有记录,前端分页+筛选)
// Retry/fallback are derived from the locally loaded records and are not accepted by the
// normal-user records API. Keep those statuses entirely local, including when combined with
// search/API-format filters, so an unsupported status never produces a misleading server total.
// 普通用户的 API 格式/传输类型/搜索筛选由后端执行,避免只筛选当前已加载页。
// 模型及后端不支持的 retry/fallback 筛选仍保持现有的本地分页语义。
const userUsesServerRecordFilters = computed(() => !isAdminPage.value && (
shouldUseServerUserRecordFilters({
search: filterSearch.value,
apiFormat: filterApiFormat.value,
status: filterStatus.value,
})
))
const paginatedRecords = computed(() => {
if (!isAdminPage.value) {
if (!isAdminPage.value && !userUsesServerRecordFilters.value) {
const start = (currentPage.value - 1) * pageSize.value
const end = start + pageSize.value
return filteredRecords.value.slice(start, end)
@@ -852,7 +873,7 @@ const paginatedRecords = computed(() => {
// 用户页面使用前端筛选后的总数,管理员页面使用后端返回的总数
const effectiveTotalRecords = computed(() => {
if (!isAdminPage.value) {
if (!isAdminPage.value && !userUsesServerRecordFilters.value) {
return filteredRecords.value.length
}
return totalRecords.value
@@ -930,26 +951,26 @@ async function handleTimeRangeChange(value: DateRangeParams) {
return
}
await loadStats(timeRange.value)
// 用户页面:loadStats 已包含记录加载
if (userUsesServerRecordFilters.value) {
await loadRecords({ page: 1, pageSize: pageSize.value }, getCurrentFilters(), timeRange.value)
}
}
// 处理分页变化
async function handlePageChange(page: number) {
currentPage.value = page
if (isAdminPage.value) {
if (isAdminPage.value || userUsesServerRecordFilters.value) {
await loadRecords({ page, pageSize: pageSize.value }, getCurrentFilters(), timeRange.value)
}
// 用户页面使用前端分页,无需重新请求
}
// 处理每页大小变化
async function handlePageSizeChange(size: number) {
pageSize.value = size
currentPage.value = 1 // 重置到第一页
if (isAdminPage.value) {
if (isAdminPage.value || userUsesServerRecordFilters.value) {
await loadRecords({ page: 1, pageSize: size }, getCurrentFilters(), timeRange.value)
}
// 用户页面使用前端分页,无需重新请求
}
// 获取当前筛选参数
@@ -973,9 +994,11 @@ async function handleFilterSearchChange(value: string) {
if (isAdminPage.value) {
await loadRecords({ page: 1, pageSize: pageSize.value }, getCurrentFilters(), timeRange.value)
} else if (userUsesServerRecordFilters.value) {
await loadRecords({ page: 1, pageSize: pageSize.value }, getCurrentFilters(), timeRange.value)
} else {
await loadStats(timeRange.value)
}
// 用户页面:search 需要重新从后端拉取数据(后端支持 search 参数)
// 但通过 filteredRecords 做前端过滤已覆盖,无需额外请求
}
async function handleFilterUserChange(value: string) {
@@ -1012,8 +1035,10 @@ async function handleFilterApiFormatChange(value: string) {
filterApiFormat.value = value
currentPage.value = 1
if (isAdminPage.value) {
if (isAdminPage.value || userUsesServerRecordFilters.value) {
await loadRecords({ page: 1, pageSize: pageSize.value }, getCurrentFilters(), timeRange.value)
} else {
await loadStats(timeRange.value)
}
}
@@ -1021,8 +1046,10 @@ async function handleFilterStatusChange(value: string) {
filterStatus.value = value as FilterStatusValue
currentPage.value = 1
if (isAdminPage.value) {
if (isAdminPage.value || userUsesServerRecordFilters.value) {
await loadRecords({ page: 1, pageSize: pageSize.value }, getCurrentFilters(), timeRange.value)
} else {
await loadStats(timeRange.value)
}
}
@@ -1041,7 +1068,7 @@ async function refreshData() {
if (refreshInFlight) return refreshInFlight
refreshInFlight = (async () => {
if (isAdminPage.value) {
if (isAdminPage.value || userUsesServerRecordFilters.value) {
await loadRecords(
{ page: currentPage.value, pageSize: pageSize.value },
getCurrentFilters(),
@@ -1051,7 +1078,6 @@ async function refreshData() {
}
await loadStats(timeRange.value)
// 用户页面:loadStats 已包含记录加载
})()
try {
@@ -0,0 +1,45 @@
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { describe, expect, it } from 'vitest'
const source = readFileSync(
resolve(process.cwd(), 'src/views/shared/Usage.vue'),
'utf8',
)
function functionBlock(name: string, nextName: string): string {
return source.split(`async function ${name}`)[1]?.split(`async function ${nextName}`)[0] ?? ''
}
describe('usage record server filters', () => {
it('uses server pagination for normal-user API format and transport filters', () => {
expect(source).toContain('shouldUseServerUserRecordFilters({')
expect(source).toContain('!isAdminPage.value && !userUsesServerRecordFilters.value')
const apiFormatHandler = functionBlock('handleFilterApiFormatChange', 'handleFilterStatusChange')
const statusHandler = functionBlock('handleFilterStatusChange', 'handleFilterClientFamilyChange')
expect(apiFormatHandler).toContain('isAdminPage.value || userUsesServerRecordFilters.value')
expect(apiFormatHandler).toContain('await loadRecords(')
expect(statusHandler).toContain('isAdminPage.value || userUsesServerRecordFilters.value')
expect(statusHandler).toContain('await loadRecords(')
})
it('keeps filtered normal-user pagination and refreshes on the server', () => {
const pageHandler = functionBlock('handlePageChange', 'handlePageSizeChange')
const pageSizeHandler = functionBlock('handlePageSizeChange', 'handleFilterSearchChange')
const refreshHandler = functionBlock('refreshData', 'handleManualRefresh')
expect(pageHandler).toContain('isAdminPage.value || userUsesServerRecordFilters.value')
expect(pageSizeHandler).toContain('isAdminPage.value || userUsesServerRecordFilters.value')
expect(refreshHandler).toContain('isAdminPage.value || userUsesServerRecordFilters.value')
expect(refreshHandler).toContain('await loadRecords(')
})
it('keeps retry and fallback filters local because the user API does not accept them', () => {
expect(source).toContain('isUserLocalOnlyRecordStatus(filterStatus.value)')
expect(source).toContain('matchesUsageRecordSearch(record, filterSearch.value)')
const statusHandler = functionBlock('handleFilterStatusChange', 'handleFilterClientFamilyChange')
expect(statusHandler).toContain('await loadStats(timeRange.value)')
})
})