Merge pull request #217 from AAEE86/1233

perf: 优化请求鉴权链路并批量化统计/调度查询
This commit is contained in:
fawney19
2026-03-10 23:31:28 +08:00
committed by GitHub
44 changed files with 2745 additions and 571 deletions

View File

@@ -46,6 +46,13 @@ interface Props {
const chartRef = ref<HTMLCanvasElement>()
let chart: ChartJS<'line'> | null = null
function buildChartOptions(): ChartOptions<'line'> {
return {
...defaultOptions,
...props.options
}
}
const defaultOptions: ChartOptions<'line'> = {
responsive: true,
maintainAspectRatio: false,
@@ -89,10 +96,7 @@ function createChart() {
chart = new ChartJS(chartRef.value, {
type: 'line',
data: props.data,
options: {
...defaultOptions,
...props.options
}
options: buildChartOptions()
})
}
@@ -115,15 +119,12 @@ onUnmounted(() => {
}
})
// 监听数据变化
watch(() => props.data, updateChart, { deep: true })
// 监听引用变化,避免深监听触发整图重算
watch(() => props.data, updateChart)
watch(() => props.options, () => {
if (chart) {
chart.options = {
...defaultOptions,
...props.options
}
chart.update()
chart.options = buildChartOptions()
chart.update('none')
}
}, { deep: true })
</script>
})
</script>

View File

@@ -119,6 +119,11 @@ let chart: ChartJS<'scatter'> | null = null
const crosshairY = ref<number | null>(null)
const gapInfoList = ref<GapInfo[]>([])
interface PreparedRenderData {
chartData: ChartData<'scatter'>
gaps: GapInfo[]
}
const crosshairStats = computed<CrosshairStats | null>(() => {
if (crosshairY.value === null || !props.data.datasets) return null
@@ -294,6 +299,22 @@ function transformData(data: ChartData<'scatter'>): ChartData<'scatter'> {
}
}
function prepareRenderData(): PreparedRenderData {
let dataToUse = props.data
let gaps: GapInfo[] = []
if (props.compressGaps) {
const compressedResult = compressTimeGaps(props.data)
dataToUse = compressedResult.data
gaps = compressedResult.gaps
}
return {
chartData: transformData(dataToUse),
gaps
}
}
// 格式化时长
function formatDuration(ms: number): string {
const hours = Math.floor(ms / (1000 * 60 * 60))
@@ -516,22 +537,12 @@ function handleMouseLeave() {
function createChart() {
if (!chartRef.value) return
let dataToUse = props.data
gapInfoList.value = []
// 如果启用间隙压缩
if (props.compressGaps) {
const { data: compressedData, gaps } = compressTimeGaps(props.data)
dataToUse = compressedData
gapInfoList.value = gaps
}
// 转换数据
const transformedData = transformData(dataToUse)
const { chartData, gaps } = prepareRenderData()
gapInfoList.value = gaps
chart = new ChartJS(chartRef.value, {
type: 'scatter',
data: transformedData,
data: chartData,
options: {
...defaultOptions,
...props.options
@@ -544,16 +555,9 @@ function createChart() {
function updateChart() {
if (chart) {
let dataToUse = props.data
gapInfoList.value = []
if (props.compressGaps) {
const { data: compressedData, gaps } = compressTimeGaps(props.data)
dataToUse = compressedData
gapInfoList.value = gaps
}
chart.data = transformData(dataToUse)
const { chartData, gaps } = prepareRenderData()
gapInfoList.value = gaps
chart.data = chartData
chart.update('none')
}
}
@@ -573,21 +577,22 @@ onUnmounted(() => {
}
})
watch(() => props.data, updateChart, { deep: true })
watch(() => props.compressGaps, () => {
if (chart) {
chart.destroy()
chart = null
}
createChart()
})
watch(
[
() => props.data,
() => props.compressGaps,
() => props.gapThreshold,
() => props.compressedGapSize
],
updateChart
)
watch(() => props.options, () => {
if (chart) {
chart.options = {
...defaultOptions,
...props.options
}
chart.update()
chart.update('none')
}
}, { deep: true })
})
</script>

View File

@@ -809,7 +809,7 @@ function getApiFormatTooltip(record: UsageRecord): string {
return record.api_format
}
// 获取实际使用的模型(优先 target_model其次 model_version
// 获取实际使用的模型(优先 target_model其次列表接口下发的 model_version
// 只有当实际模型与请求模型不同时才返回,用于显示映射箭头
function getActualModel(record: UsageRecord): string | null {
// 优先显示模型映射
@@ -817,8 +817,8 @@ function getActualModel(record: UsageRecord): string | null {
return record.target_model
}
// 其次显示 Provider 返回的实际版本(如 Gemini 的 modelVersion
if (record.request_metadata?.model_version && record.request_metadata.model_version !== record.model) {
return record.request_metadata.model_version
if (record.model_version && record.model_version !== record.model) {
return record.model_version
}
return null
}

View File

@@ -71,6 +71,7 @@ export interface UsageRecord {
rate_multiplier?: number
model: string
target_model?: string | null // 映射后的目标模型名(若无映射则为空)
model_version?: string | null // Provider 返回的实际模型版本(列表轻量字段)
api_format?: string
endpoint_api_format?: string // 端点原生格式
has_format_conversion?: boolean // 是否发生了格式转换
@@ -90,10 +91,6 @@ export interface UsageRecord {
created_at: string
has_fallback?: boolean
has_retry?: boolean
request_metadata?: {
model_version?: string // Provider 返回的实际模型版本(如 Gemini 的 modelVersion
[key: string]: unknown
}
}
// 日期范围参数

View File

@@ -392,7 +392,7 @@ function generateMockUsageRecords(count: number = 100) {
status,
created_at: createdAt.toISOString(),
has_fallback: Math.random() > 0.9,
request_metadata: model.provider === 'google' ? { model_version: 'gemini-3-pro-preview-2025-01' } : undefined
model_version: model.provider === 'google' ? 'gemini-3-pro-preview-2025-01' : undefined
})
}

View File

@@ -49,6 +49,7 @@ const currentPage = ref(1)
const pageSize = ref(20)
const currentTime = ref(Math.floor(Date.now() / 1000))
const isPageVisible = ref(typeof document === 'undefined' ? true : !document.hidden)
const nextExpireAt = ref<number | null>(null)
// ==================== 模型映射缓存 ====================
@@ -121,6 +122,8 @@ async function fetchAffinityList(keyword?: string) {
const response = await cacheApi.listAffinities(keyword)
affinityList.value = response.items
matchedUserId.value = response.matched_user_id ?? null
currentTime.value = Math.floor(Date.now() / 1000)
pruneExpiredAffinities(currentTime.value, true)
if (keyword && response.total === 0) {
showInfo('未找到匹配的缓存记录')
@@ -238,6 +241,38 @@ function handlePageChange() {
window.scrollTo({ top: 0, behavior: 'smooth' })
}
function recalculateNextExpireAt(now: number = currentTime.value) {
let nearestExpireAt: number | null = null
for (const item of affinityList.value) {
if (!item.expire_at || item.expire_at <= now) continue
if (nearestExpireAt === null || item.expire_at < nearestExpireAt) {
nearestExpireAt = item.expire_at
}
}
nextExpireAt.value = nearestExpireAt
}
function pruneExpiredAffinities(now: number, silent = false) {
const beforeCount = affinityList.value.length
const activeItems = affinityList.value.filter(
item => item.expire_at && item.expire_at > now
)
if (activeItems.length === beforeCount) {
recalculateNextExpireAt(now)
return
}
affinityList.value = activeItems
recalculateNextExpireAt(now)
if (!silent) {
showInfo(`${beforeCount - activeItems.length} 个缓存已自动过期移除`)
}
}
// ==================== 定时器管理 ====================
function startCountdown() {
@@ -247,14 +282,8 @@ function startCountdown() {
countdownTimer = setInterval(() => {
currentTime.value = Math.floor(Date.now() / 1000)
const beforeCount = affinityList.value.length
affinityList.value = affinityList.value.filter(
item => item.expire_at && item.expire_at > currentTime.value
)
if (beforeCount > affinityList.value.length) {
const removedCount = beforeCount - affinityList.value.length
showInfo(`${removedCount} 个缓存已自动过期移除`)
if (nextExpireAt.value !== null && currentTime.value >= nextExpireAt.value) {
pruneExpiredAffinities(currentTime.value)
}
}, 1000)
}
@@ -273,6 +302,9 @@ function handleVisibilityChange() {
return
}
currentTime.value = Math.floor(Date.now() / 1000)
if (nextExpireAt.value !== null && currentTime.value >= nextExpireAt.value) {
pruneExpiredAffinities(currentTime.value)
}
startCountdown()
}

View File

@@ -245,7 +245,7 @@ const activeRequestIds = computed(() => {
const hasActiveRequests = computed(() => activeRequestIds.value.length > 0)
// 自动刷新定时器
let autoRefreshTimer: ReturnType<typeof setInterval> | null = null
let autoRefreshTimer: ReturnType<typeof setTimeout> | null = null
let globalAutoRefreshTimer: ReturnType<typeof setInterval> | null = null
let refreshInFlight: Promise<void> | null = null
const AUTO_REFRESH_INTERVAL = 1000 // 1秒刷新一次用于活跃请求
@@ -271,8 +271,10 @@ async function pollActiveRequests() {
let shouldRefresh = false
const recordMap = new Map(currentRecords.value.map(record => [record.id, record]))
for (const update of requests) {
const record = currentRecords.value.find(r => r.id === update.id)
const record = recordMap.get(update.id)
if (!record) {
// 后端返回了未知的活跃请求,触发刷新以获取完整数据
shouldRefresh = true
@@ -339,17 +341,26 @@ async function pollActiveRequests() {
}
}
function scheduleNextAutoRefresh() {
if (autoRefreshTimer) return
if (!isPageVisible.value || !hasActiveRequests.value) return
autoRefreshTimer = setTimeout(async () => {
autoRefreshTimer = null
await pollActiveRequests()
scheduleNextAutoRefresh()
}, AUTO_REFRESH_INTERVAL)
}
// 启动自动刷新
function startAutoRefresh() {
if (!isPageVisible.value) return
if (autoRefreshTimer) return
autoRefreshTimer = setInterval(pollActiveRequests, AUTO_REFRESH_INTERVAL)
scheduleNextAutoRefresh()
}
// 停止自动刷新
function stopAutoRefresh() {
if (autoRefreshTimer) {
clearInterval(autoRefreshTimer)
clearTimeout(autoRefreshTimer)
autoRefreshTimer = null
}
}