mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 09:20:22 +08:00
perf: 并行化 admin 聚合路由并完善前端缓存预取
- gateway: usage detail / provider summary / pool overview / users list 改为 tokio join 并行拉取依赖数据 - usage: interval timeline 支持自动刷新并按查询区间动态展示,取消服务端 120 分钟过滤并在 ScatterChart 统一封顶 - frontend: 新增管理端导航预取工具及 SidebarNav/MainLayout 触发,admin 读接口统一走 cachedRequest 的短期缓存 - dashboard: request detail 支持短 TTL 缓存并在 UsageRecordsTable mousedown 时预取 - data: migrate 测试在 wait_for_postgres 失败时清理子进程,避免遗留
This commit is contained in:
@@ -717,6 +717,7 @@ let modelSelectionRequestId = 0
|
||||
let modelProvidersRequestId = 0
|
||||
let batchManageModelsRequestId = 0
|
||||
let providerOptionsRequest: Promise<void> | null = null
|
||||
const GLOBAL_MODELS_LIST_CACHE_TTL_MS = 10 * 1000
|
||||
|
||||
// 模型目录分页
|
||||
const catalogCurrentPage = ref(1)
|
||||
@@ -1036,11 +1037,13 @@ const globalModelsQueryParams = computed(() => ({
|
||||
|
||||
let modelSearchDebounceTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
async function loadGlobalModels() {
|
||||
async function loadGlobalModels(options: { cacheTtlMs?: number } = {}) {
|
||||
const requestId = ++globalModelsRequestId
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await listGlobalModels(globalModelsQueryParams.value)
|
||||
const response = await listGlobalModels(globalModelsQueryParams.value, {
|
||||
cacheTtlMs: options.cacheTtlMs ?? 0,
|
||||
})
|
||||
if (requestId !== globalModelsRequestId) return
|
||||
|
||||
const pageModels = response.models || []
|
||||
@@ -1533,9 +1536,11 @@ watch(globalModelsQueryParams, (newParams, oldParams) => {
|
||||
&& newParams.skip === oldParams?.skip
|
||||
&& newParams.limit === oldParams?.limit
|
||||
if (isSearchOnly) {
|
||||
modelSearchDebounceTimer = setTimeout(loadGlobalModels, 300)
|
||||
modelSearchDebounceTimer = setTimeout(() => {
|
||||
void loadGlobalModels({ cacheTtlMs: GLOBAL_MODELS_LIST_CACHE_TTL_MS })
|
||||
}, 300)
|
||||
} else {
|
||||
loadGlobalModels()
|
||||
void loadGlobalModels({ cacheTtlMs: GLOBAL_MODELS_LIST_CACHE_TTL_MS })
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
|
||||
@@ -1258,12 +1258,15 @@ let keysRequestId = 0
|
||||
let keysSearchDebounceTimer: number | null = null
|
||||
let suppressFiltersWatch = false
|
||||
let hasHydratedInitialProviderSelection = false
|
||||
const POOL_OVERVIEW_CACHE_TTL_MS = 10 * 1000
|
||||
const POOL_KEYS_CACHE_TTL_MS = 10 * 1000
|
||||
const POOL_SCHEDULING_PRESETS_CACHE_TTL_MS = 5 * 60 * 1000
|
||||
|
||||
async function loadOverview() {
|
||||
async function loadOverview(options: { cacheTtlMs?: number } = {}) {
|
||||
const requestId = ++overviewRequestId
|
||||
overviewLoading.value = true
|
||||
try {
|
||||
const res = await getPoolOverview()
|
||||
const res = await getPoolOverview({ cacheTtlMs: options.cacheTtlMs ?? 0 })
|
||||
if (requestId !== overviewRequestId) return
|
||||
const allProviders = Array.isArray(res.items) ? res.items : []
|
||||
const enabledProviders = allProviders.filter(item => item.pool_enabled)
|
||||
@@ -1283,6 +1286,7 @@ async function loadOverview() {
|
||||
preserveSearch: true,
|
||||
preserveStatus: true,
|
||||
preservePagination: true,
|
||||
cacheTtlMs: options.cacheTtlMs ? POOL_KEYS_CACHE_TTL_MS : 0,
|
||||
})
|
||||
}
|
||||
return
|
||||
@@ -1296,6 +1300,7 @@ async function loadOverview() {
|
||||
preserveSearch: shouldPreserveViewState,
|
||||
preserveStatus: shouldPreserveViewState,
|
||||
preservePagination: shouldPreserveViewState,
|
||||
cacheTtlMs: options.cacheTtlMs ? POOL_KEYS_CACHE_TTL_MS : 0,
|
||||
})
|
||||
} else {
|
||||
selectedProviderId.value = null
|
||||
@@ -1333,7 +1338,7 @@ const selectedProviderIdProxy = computed({
|
||||
get: () => selectedProviderId.value ?? '',
|
||||
set: (val: string) => {
|
||||
if (val && val !== selectedProviderId.value) {
|
||||
selectProvider(val)
|
||||
void selectProvider(val, { cacheTtlMs: POOL_KEYS_CACHE_TTL_MS })
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -1363,9 +1368,9 @@ function normalizePresetName(value: unknown): string {
|
||||
return String(value ?? '').trim().toLowerCase()
|
||||
}
|
||||
|
||||
async function loadSchedulingPresetMetas(): Promise<void> {
|
||||
async function loadSchedulingPresetMetas(options: { cacheTtlMs?: number } = {}): Promise<void> {
|
||||
try {
|
||||
const metas = await getPoolSchedulingPresets()
|
||||
const metas = await getPoolSchedulingPresets({ cacheTtlMs: options.cacheTtlMs ?? 0 })
|
||||
const next: Record<string, string> = {}
|
||||
for (const meta of metas as PoolPresetMeta[]) {
|
||||
const name = normalizePresetName(meta.name)
|
||||
@@ -1492,6 +1497,7 @@ async function selectProvider(
|
||||
preserveSearch?: boolean
|
||||
preserveStatus?: boolean
|
||||
preservePagination?: boolean
|
||||
cacheTtlMs?: number
|
||||
} = {},
|
||||
) {
|
||||
const requestId = ++selectProviderRequestId
|
||||
@@ -1522,7 +1528,7 @@ async function selectProvider(
|
||||
keysSearchDebounceTimer = null
|
||||
}
|
||||
resetKeyPage(currentPage.value, pageSize.value)
|
||||
const keysTask = loadKeys()
|
||||
const keysTask = loadKeys({ cacheTtlMs: options.cacheTtlMs ?? 0 })
|
||||
// Provider summary is non-blocking for key list rendering.
|
||||
void loadProviderData(id)
|
||||
await keysTask
|
||||
@@ -1620,6 +1626,7 @@ watch(
|
||||
preserveSearch: true,
|
||||
preserveStatus: true,
|
||||
preservePagination: true,
|
||||
cacheTtlMs: POOL_KEYS_CACHE_TTL_MS,
|
||||
})
|
||||
},
|
||||
{ immediate: true },
|
||||
@@ -1790,7 +1797,7 @@ async function refreshCurrentPage() {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadKeys() {
|
||||
async function loadKeys(options: { cacheTtlMs?: number } = {}) {
|
||||
if (!selectedProviderId.value) return
|
||||
const requestId = ++keysRequestId
|
||||
const providerId = selectedProviderId.value
|
||||
@@ -1805,6 +1812,8 @@ async function loadKeys() {
|
||||
page_size: pageSizeValue,
|
||||
search,
|
||||
status,
|
||||
}, {
|
||||
cacheTtlMs: options.cacheTtlMs ?? 0,
|
||||
})
|
||||
if (requestId !== keysRequestId || selectedProviderId.value !== providerId) return
|
||||
const resolvedPage = resolvePoolManagementPageAfterLoad({
|
||||
@@ -1829,13 +1838,13 @@ async function loadKeys() {
|
||||
}
|
||||
|
||||
watch([currentPage, pageSize], () => {
|
||||
void loadKeys()
|
||||
void loadKeys({ cacheTtlMs: POOL_KEYS_CACHE_TTL_MS })
|
||||
})
|
||||
|
||||
watch(statusFilter, () => {
|
||||
if (suppressFiltersWatch) return
|
||||
currentPage.value = 1
|
||||
void loadKeys()
|
||||
void loadKeys({ cacheTtlMs: POOL_KEYS_CACHE_TTL_MS })
|
||||
})
|
||||
|
||||
watch(searchQuery, () => {
|
||||
@@ -1846,7 +1855,7 @@ watch(searchQuery, () => {
|
||||
}
|
||||
keysSearchDebounceTimer = window.setTimeout(() => {
|
||||
keysSearchDebounceTimer = null
|
||||
void loadKeys()
|
||||
void loadKeys({ cacheTtlMs: POOL_KEYS_CACHE_TTL_MS })
|
||||
}, 300)
|
||||
})
|
||||
|
||||
@@ -3035,8 +3044,8 @@ function formatRelativeTime(isoStr: string): string {
|
||||
// --- Init ---
|
||||
onMounted(() => {
|
||||
startCountdownTimer()
|
||||
void loadSchedulingPresetMetas()
|
||||
void loadOverview()
|
||||
void loadSchedulingPresetMetas({ cacheTtlMs: POOL_SCHEDULING_PRESETS_CACHE_TTL_MS })
|
||||
void loadOverview({ cacheTtlMs: POOL_OVERVIEW_CACHE_TTL_MS })
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
|
||||
@@ -324,6 +324,9 @@ let deletePollAbort: AbortController | null = null
|
||||
const DELETE_POLL_INTERVAL_MS = 2000
|
||||
const DELETE_POLL_MAX_MS = 30 * 60 * 1000
|
||||
const DELETE_POLL_MAX_FAILURES = 3
|
||||
const PROVIDER_SUMMARY_CACHE_TTL_MS = 10 * 1000
|
||||
const PROVIDER_PRIORITY_MODE_CACHE_TTL_MS = 30 * 1000
|
||||
const PROVIDER_MODEL_FILTER_CACHE_TTL_MS = 10 * 1000
|
||||
|
||||
async function pollProviderDeleteTask(providerId: string, taskId: string) {
|
||||
deletePollAbort?.abort()
|
||||
@@ -533,9 +536,11 @@ const maxProviderPriority = computed(() => {
|
||||
})
|
||||
|
||||
// 加载优先级模式
|
||||
async function loadPriorityMode() {
|
||||
async function loadPriorityMode(options: { cacheTtlMs?: number } = {}) {
|
||||
try {
|
||||
const response = await adminApi.getSystemConfig('provider_priority_mode')
|
||||
const response = await adminApi.getSystemConfig('provider_priority_mode', {
|
||||
cacheTtlMs: options.cacheTtlMs ?? 0,
|
||||
})
|
||||
if (response.value) {
|
||||
priorityMode.value = response.value as 'provider' | 'global_key'
|
||||
}
|
||||
@@ -545,9 +550,12 @@ async function loadPriorityMode() {
|
||||
}
|
||||
|
||||
// 加载全局模型列表(用于模型筛选下拉)
|
||||
async function loadGlobalModelList() {
|
||||
async function loadGlobalModelList(options: { cacheTtlMs?: number } = {}) {
|
||||
try {
|
||||
const response = await getGlobalModels({ is_active: true, limit: 1000 })
|
||||
const response = await getGlobalModels(
|
||||
{ is_active: true, limit: 1000 },
|
||||
{ cacheTtlMs: options.cacheTtlMs ?? 0 },
|
||||
)
|
||||
globalModels.value = response.models.map(m => ({ id: m.id, name: m.name }))
|
||||
} catch {
|
||||
globalModels.value = []
|
||||
@@ -555,11 +563,13 @@ async function loadGlobalModelList() {
|
||||
}
|
||||
|
||||
// 加载提供商列表(服务端分页)
|
||||
async function loadProviders() {
|
||||
async function loadProviders(options: { cacheTtlMs?: number } = {}) {
|
||||
const requestId = ++providersRequestId
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await getProvidersSummary(queryParams.value)
|
||||
const response = await getProvidersSummary(queryParams.value, {
|
||||
cacheTtlMs: options.cacheTtlMs ?? 0,
|
||||
})
|
||||
if (requestId !== providersRequestId) return
|
||||
providers.value = response.items
|
||||
total.value = response.total
|
||||
@@ -587,9 +597,11 @@ watch(queryParams, (newParams, oldParams) => {
|
||||
newParams.api_format === oldParams?.api_format &&
|
||||
newParams.model_id === oldParams?.model_id
|
||||
if (isSearchOnly) {
|
||||
debounceTimer = setTimeout(loadProviders, 300)
|
||||
debounceTimer = setTimeout(() => {
|
||||
void loadProviders({ cacheTtlMs: PROVIDER_SUMMARY_CACHE_TTL_MS })
|
||||
}, 300)
|
||||
} else {
|
||||
loadProviders()
|
||||
void loadProviders({ cacheTtlMs: PROVIDER_SUMMARY_CACHE_TTL_MS })
|
||||
}
|
||||
}, { deep: true })
|
||||
|
||||
@@ -658,7 +670,7 @@ function openOpsConfigDialog(provider: ProviderWithEndpointsSummary) {
|
||||
// 扩展操作配置保存回调
|
||||
function handleOpsConfigSaved() {
|
||||
opsConfigDialogOpen.value = false
|
||||
loadProviders()
|
||||
void loadProviders()
|
||||
}
|
||||
|
||||
// 处理提供商编辑完成
|
||||
@@ -680,7 +692,7 @@ async function handlePrioritySaved() {
|
||||
|
||||
// 处理提供商添加
|
||||
function handleProviderAdded() {
|
||||
loadProviders()
|
||||
void loadProviders()
|
||||
}
|
||||
|
||||
// 删除提供商
|
||||
@@ -716,7 +728,7 @@ async function handleDeleteProvider(provider: ProviderWithEndpointsSummary) {
|
||||
|
||||
showSuccess('提供商已删除')
|
||||
providerDeleteProgress.value = null
|
||||
loadProviders()
|
||||
void loadProviders()
|
||||
} catch (err: unknown) {
|
||||
providerDeleteProgress.value = null
|
||||
showError(parseApiError(err, '删除提供商失败'), '错误')
|
||||
@@ -753,10 +765,10 @@ function handleGlobalClick(event: MouseEvent) {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadProviders()
|
||||
loadPriorityMode()
|
||||
loadGlobalModelList()
|
||||
loadArchitectureSchemas()
|
||||
void loadProviders({ cacheTtlMs: PROVIDER_SUMMARY_CACHE_TTL_MS })
|
||||
void loadPriorityMode({ cacheTtlMs: PROVIDER_PRIORITY_MODE_CACHE_TTL_MS })
|
||||
void loadGlobalModelList({ cacheTtlMs: PROVIDER_MODEL_FILTER_CACHE_TTL_MS })
|
||||
void loadArchitectureSchemas()
|
||||
document.addEventListener('click', handleGlobalClick, true)
|
||||
// 每秒更新一次倒计时
|
||||
startTick()
|
||||
|
||||
@@ -1128,6 +1128,9 @@ const filterStatus = ref('all')
|
||||
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const USERS_PAGE_CACHE_TTL_MS = 10 * 1000
|
||||
const USER_WALLETS_CACHE_TTL_MS = 10 * 1000
|
||||
let userWalletsRequestId = 0
|
||||
|
||||
const filteredUsers = computed(() => {
|
||||
let filtered = [...usersStore.users]
|
||||
@@ -1173,31 +1176,38 @@ watch([searchQuery, filterRole, filterStatus], () => {
|
||||
currentPage.value = 1
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
await refreshUsers()
|
||||
onMounted(() => {
|
||||
void refreshUsers({ preferCache: true })
|
||||
})
|
||||
|
||||
async function refreshUsers() {
|
||||
await Promise.all([
|
||||
usersStore.fetchUsers(),
|
||||
loadUserWallets()
|
||||
])
|
||||
async function refreshUsers(options: { preferCache?: boolean } = {}) {
|
||||
const cacheTtlMs = options.preferCache ? USERS_PAGE_CACHE_TTL_MS : 0
|
||||
await usersStore.fetchUsers({ cacheTtlMs })
|
||||
void loadUserWallets({
|
||||
cacheTtlMs: options.preferCache ? USER_WALLETS_CACHE_TTL_MS : 0,
|
||||
})
|
||||
}
|
||||
|
||||
function formatDate(dateString: string) {
|
||||
return new Date(dateString).toLocaleDateString('zh-CN')
|
||||
}
|
||||
|
||||
async function loadUserWallets() {
|
||||
async function loadUserWallets(options: { cacheTtlMs?: number } = {}) {
|
||||
const requestId = ++userWalletsRequestId
|
||||
try {
|
||||
const wallets = await adminWalletApi.listAllWallets()
|
||||
const wallets = await adminWalletApi.listAllWallets(
|
||||
{ owner_type: 'user' },
|
||||
{ cacheTtlMs: options.cacheTtlMs ?? 0 },
|
||||
)
|
||||
if (requestId !== userWalletsRequestId) return
|
||||
userWalletMap.value = wallets
|
||||
.filter((wallet) => wallet.owner_type === 'user' && !!wallet.user_id)
|
||||
.filter((wallet) => !!wallet.user_id)
|
||||
.reduce<Record<string, AdminWallet>>((acc, wallet) => {
|
||||
acc[wallet.user_id as string] = wallet
|
||||
return acc
|
||||
}, {})
|
||||
} catch (err) {
|
||||
if (requestId !== userWalletsRequestId) return
|
||||
log.error('加载用户钱包失败:', err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,9 +35,10 @@
|
||||
:has-error="heatmapError"
|
||||
/>
|
||||
<IntervalTimelineCard
|
||||
:title="isAdminPage ? '请求间隔时间线' : '我的请求间隔'"
|
||||
:title="intervalTimelineTitle"
|
||||
:is-admin="isAdminPage"
|
||||
:hours="24"
|
||||
:hours="intervalTimelineHours"
|
||||
:refresh-interval-ms="30000"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -108,6 +109,7 @@
|
||||
@update:page-size="handlePageSizeChange"
|
||||
@update:auto-refresh="handleAutoRefreshChange"
|
||||
@refresh="refreshData"
|
||||
@prefetch-detail="prefetchRequestDetail"
|
||||
@show-detail="showRequestDetail"
|
||||
/>
|
||||
|
||||
@@ -129,6 +131,7 @@ import { useAuthStore } from '@/stores/auth'
|
||||
import { usageApi } from '@/api/usage'
|
||||
import { usersApi } from '@/api/users'
|
||||
import { meApi } from '@/api/me'
|
||||
import { dashboardApi } from '@/api/dashboard'
|
||||
import { PanelTopClose, PanelTopOpen } from 'lucide-vue-next'
|
||||
import {
|
||||
UsageModelTable,
|
||||
@@ -171,6 +174,44 @@ const currentPage = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const pageSizeOptions = [10, 20, 50, 100]
|
||||
|
||||
function clampIntervalTimelineHours(hours: number): number {
|
||||
return Math.min(720, Math.max(1, Math.ceil(hours)))
|
||||
}
|
||||
|
||||
function getIntervalTimelineHours(dateRange: DateRangeParams): number {
|
||||
switch (dateRange.preset) {
|
||||
case 'yesterday':
|
||||
return 48
|
||||
case 'last7days':
|
||||
return 24 * 7
|
||||
case 'last30days':
|
||||
return 24 * 30
|
||||
case 'last90days':
|
||||
return 24 * 30
|
||||
case 'today':
|
||||
return 24
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
if (dateRange.start_date && dateRange.end_date) {
|
||||
const start = new Date(`${dateRange.start_date}T00:00:00`)
|
||||
const end = new Date(`${dateRange.end_date}T23:59:59`)
|
||||
const diffMs = end.getTime() - start.getTime()
|
||||
if (!Number.isNaN(diffMs) && diffMs >= 0) {
|
||||
return clampIntervalTimelineHours(diffMs / (1000 * 60 * 60))
|
||||
}
|
||||
}
|
||||
|
||||
return 24
|
||||
}
|
||||
|
||||
function formatIntervalTimelineWindow(hours: number): string {
|
||||
if (hours === 24) return '最近24小时'
|
||||
if (hours % 24 === 0) return `最近${hours / 24}天`
|
||||
return `最近${hours}小时`
|
||||
}
|
||||
|
||||
// 筛选状态
|
||||
const filterSearch = ref('')
|
||||
const filterUser = ref('__all__')
|
||||
@@ -200,6 +241,11 @@ const {
|
||||
const activityHeatmapData = ref<ActivityHeatmap | null>(null)
|
||||
const isLoadingHeatmap = ref(false)
|
||||
const heatmapError = ref(false)
|
||||
const intervalTimelineHours = computed(() => getIntervalTimelineHours(timeRange.value))
|
||||
const intervalTimelineTitle = computed(() => {
|
||||
const baseTitle = isAdminPage.value ? '请求间隔时间线' : '我的请求间隔'
|
||||
return `${baseTitle}(${formatIntervalTimelineWindow(intervalTimelineHours.value)})`
|
||||
})
|
||||
const ADMIN_ANALYTICS_REFRESH_INTERVAL = 60000
|
||||
let adminAnalyticsRefreshInFlight: Promise<void> | null = null
|
||||
let lastAdminAnalyticsRefreshAt = 0
|
||||
@@ -254,13 +300,6 @@ async function refreshAdminAnalytics(options: { force?: boolean } = {}) {
|
||||
warning('统计数据加载失败,请刷新重试')
|
||||
}
|
||||
|
||||
try {
|
||||
await loadHeatmapData()
|
||||
hasSuccessfulRefresh = true
|
||||
} catch (error) {
|
||||
log.error('加载热力图数据失败:', error)
|
||||
}
|
||||
|
||||
if (hasSuccessfulRefresh) {
|
||||
lastAdminAnalyticsRefreshAt = Date.now()
|
||||
}
|
||||
@@ -632,6 +671,7 @@ onMounted(async () => {
|
||||
)
|
||||
void (async () => {
|
||||
await refreshAdminAnalytics({ force: true })
|
||||
await loadHeatmapData()
|
||||
await loadAdminUsers()
|
||||
})()
|
||||
} else {
|
||||
@@ -769,6 +809,7 @@ async function refreshData() {
|
||||
getCurrentFilters(),
|
||||
timeRange.value
|
||||
)
|
||||
// 热力图反映长期活跃分布,不跟随自动刷新链路一起重载。
|
||||
void refreshAdminAnalytics()
|
||||
return
|
||||
}
|
||||
@@ -791,6 +832,13 @@ function showRequestDetail(id: string) {
|
||||
detailModalOpen.value = true
|
||||
}
|
||||
|
||||
function prefetchRequestDetail(id: string) {
|
||||
if (!isAdminPage.value) return
|
||||
void dashboardApi.prefetchRequestDetail(id).catch(error => {
|
||||
log.debug('预取请求详情失败', error)
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
Reference in New Issue
Block a user