mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
// 日期范围参数
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user