mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
refactor: 将上游模型缓存从前端迁移到后端 Redis
- 后端:定时任务刷新时将上游模型写入 Redis 缓存 - 后端:provider_query 优先从缓存读取,支持 force_refresh 参数 - 后端:auto_fetch_models 开启时改为同步获取,确保前端能立即看到数据 - 前端:移除本地缓存,只保留并发请求去重逻辑 - 前端:KeyAllowedModelsEditDialog 添加刷新上游模型按钮
This commit is contained in:
@@ -219,6 +219,7 @@ export interface ProviderModelsQueryResponse {
|
||||
api_format?: string
|
||||
}>
|
||||
error?: string
|
||||
from_cache?: boolean
|
||||
}
|
||||
provider: {
|
||||
id: string
|
||||
@@ -478,10 +479,10 @@ export const adminApi = {
|
||||
},
|
||||
|
||||
// 查询 Provider 可用模型(从上游 API 获取)
|
||||
async queryProviderModels(providerId: string, apiKeyId?: string): Promise<ProviderModelsQueryResponse> {
|
||||
async queryProviderModels(providerId: string, apiKeyId?: string, forceRefresh = false): Promise<ProviderModelsQueryResponse> {
|
||||
const response = await apiClient.post<ProviderModelsQueryResponse>(
|
||||
'/api/admin/provider-query/models',
|
||||
{ provider_id: providerId, api_key_id: apiKeyId }
|
||||
{ provider_id: providerId, api_key_id: apiKeyId, force_refresh: forceRefresh }
|
||||
)
|
||||
return response.data
|
||||
},
|
||||
|
||||
@@ -257,7 +257,7 @@ const emit = defineEmits<{
|
||||
'changed': []
|
||||
}>()
|
||||
|
||||
const { fetchModels: fetchCachedModels, clearCache, getCachedModels } = useUpstreamModelsCache()
|
||||
const { fetchModels: fetchCachedModels } = useUpstreamModelsCache()
|
||||
|
||||
const { error: showError, success } = useToast()
|
||||
const { confirmWarning } = useConfirm()
|
||||
@@ -633,24 +633,8 @@ async function loadData() {
|
||||
// 同步全局模型选择状态
|
||||
syncGlobalModelSelection()
|
||||
|
||||
// 检查缓存
|
||||
const cachedModels = getCachedModels(props.providerId)
|
||||
if (cachedModels && cachedModels.length > 0) {
|
||||
upstreamModels.value = cachedModels
|
||||
upstreamModelsLoaded.value = true
|
||||
// 同步上游模型选择状态
|
||||
syncUpstreamModelSelection()
|
||||
// 有多个分组时全部折叠
|
||||
const allGroups = new Set(['global'])
|
||||
for (const model of cachedModels) {
|
||||
if (model.api_format) {
|
||||
allGroups.add(model.api_format)
|
||||
}
|
||||
}
|
||||
collapsedGroups.value = allGroups
|
||||
} else {
|
||||
collapsedGroups.value = new Set()
|
||||
}
|
||||
// 初始折叠状态
|
||||
collapsedGroups.value = new Set()
|
||||
}
|
||||
|
||||
// 加载全局模型列表
|
||||
@@ -677,13 +661,9 @@ async function loadExistingModels() {
|
||||
|
||||
// 从提供商获取模型
|
||||
async function fetchUpstreamModels(forceRefresh = false) {
|
||||
if (forceRefresh) {
|
||||
clearCache(props.providerId)
|
||||
}
|
||||
|
||||
try {
|
||||
fetchingUpstreamModels.value = true
|
||||
const result = await fetchCachedModels(props.providerId, forceRefresh)
|
||||
const result = await fetchCachedModels(props.providerId, undefined, forceRefresh)
|
||||
if (result) {
|
||||
if (result.error) {
|
||||
showError(result.error, '错误')
|
||||
|
||||
@@ -40,10 +40,23 @@
|
||||
>
|
||||
已选 {{ selectedModels.length }} 个
|
||||
</span>
|
||||
<Loader2
|
||||
v-if="fetchingUpstreamModels"
|
||||
class="w-4 h-4 animate-spin text-muted-foreground shrink-0"
|
||||
/>
|
||||
<!-- 刷新上游模型按钮 -->
|
||||
<button
|
||||
type="button"
|
||||
class="h-6 w-6 flex items-center justify-center rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors shrink-0"
|
||||
:disabled="fetchingUpstreamModels"
|
||||
title="刷新上游模型"
|
||||
@click="refreshUpstreamModels"
|
||||
>
|
||||
<RefreshCw
|
||||
v-if="!fetchingUpstreamModels"
|
||||
class="w-3.5 h-3.5"
|
||||
/>
|
||||
<Loader2
|
||||
v-else
|
||||
class="w-3.5 h-3.5 animate-spin"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 分组列表 -->
|
||||
@@ -318,7 +331,8 @@ import {
|
||||
Check,
|
||||
ChevronDown,
|
||||
Lock,
|
||||
LockOpen
|
||||
LockOpen,
|
||||
RefreshCw
|
||||
} from 'lucide-vue-next'
|
||||
import { Dialog, Button, Input } from '@/components/ui'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
@@ -627,11 +641,11 @@ async function loadGlobalModels() {
|
||||
}
|
||||
|
||||
// 从提供商获取模型(使用缓存)
|
||||
async function fetchUpstreamModels() {
|
||||
async function fetchUpstreamModels(forceRefresh = false) {
|
||||
if (!props.providerId || !props.apiKey) return
|
||||
try {
|
||||
fetchingUpstreamModels.value = true
|
||||
const result = await fetchCachedModels(props.providerId, props.apiKey.id)
|
||||
const result = await fetchCachedModels(props.providerId, props.apiKey.id, forceRefresh)
|
||||
if (loadingCancelled) return
|
||||
if (result.models.length > 0) {
|
||||
upstreamModels.value = result.models
|
||||
@@ -647,6 +661,14 @@ async function fetchUpstreamModels() {
|
||||
}
|
||||
}
|
||||
|
||||
// 手动刷新上游模型(强制跳过缓存)
|
||||
async function refreshUpstreamModels() {
|
||||
await fetchUpstreamModels(true)
|
||||
if (upstreamModels.value.length > 0) {
|
||||
success('上游模型已刷新')
|
||||
}
|
||||
}
|
||||
|
||||
// 解析 allowed_models
|
||||
function parseAllowedModels(allowed: AllowedModels): string[] {
|
||||
if (allowed === null || allowed === undefined) {
|
||||
|
||||
@@ -1,25 +1,16 @@
|
||||
/**
|
||||
* 上游模型缓存 - 共享缓存,避免重复请求
|
||||
* 上游模型获取服务
|
||||
*
|
||||
* 缓存已移至后端(Redis),前端只保留并发请求去重,避免同时发多个相同请求。
|
||||
*/
|
||||
import { ref } from 'vue'
|
||||
import { adminApi } from '@/api/admin'
|
||||
import { parseUpstreamModelError } from '@/utils/errorParser'
|
||||
import type { UpstreamModel } from '@/api/endpoints/types'
|
||||
|
||||
// 扩展类型,包含可能的额外字段
|
||||
export type { UpstreamModel }
|
||||
|
||||
interface CacheEntry {
|
||||
models: UpstreamModel[]
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
type FetchResult = { models: UpstreamModel[]; error?: string }
|
||||
|
||||
// 全局缓存(模块级别,所有组件共享)
|
||||
// 支持两种 key: providerId 或 providerId:apiKeyId
|
||||
const cache = new Map<string, CacheEntry>()
|
||||
const CACHE_TTL = 5 * 60 * 1000 // 5分钟
|
||||
type FetchResult = { models: UpstreamModel[]; error?: string; fromCache?: boolean }
|
||||
|
||||
// 进行中的请求(用于去重并发请求)
|
||||
const pendingRequests = new Map<string, Promise<FetchResult>>()
|
||||
@@ -28,9 +19,9 @@ const pendingRequests = new Map<string, Promise<FetchResult>>()
|
||||
const loadingMap = ref<Map<string, boolean>>(new Map())
|
||||
|
||||
/**
|
||||
* 生成缓存 key
|
||||
* 生成请求 key
|
||||
*/
|
||||
function getCacheKey(providerId: string, apiKeyId?: string): string {
|
||||
function getRequestKey(providerId: string, apiKeyId?: string): string {
|
||||
return apiKeyId ? `${providerId}:${apiKeyId}` : providerId
|
||||
}
|
||||
|
||||
@@ -39,93 +30,59 @@ export function useUpstreamModelsCache() {
|
||||
* 获取上游模型列表
|
||||
* @param providerId 提供商ID
|
||||
* @param apiKeyId 可选的 API Key ID(用于获取特定 Key 支持的模型)
|
||||
* @param forceRefresh 是否强制刷新
|
||||
* @returns 模型列表或 null(如果请求失败)
|
||||
* @param forceRefresh 是否强制刷新(跳过后端缓存)
|
||||
* @returns 模型列表或错误信息
|
||||
*/
|
||||
async function fetchModels(
|
||||
providerId: string,
|
||||
apiKeyId?: string,
|
||||
forceRefresh = false
|
||||
): Promise<FetchResult> {
|
||||
const cacheKey = getCacheKey(providerId, apiKeyId)
|
||||
const requestKey = getRequestKey(providerId, apiKeyId)
|
||||
|
||||
// 检查缓存
|
||||
if (!forceRefresh) {
|
||||
const cached = cache.get(cacheKey)
|
||||
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
|
||||
return { models: cached.models }
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否有进行中的请求(非强制刷新时复用)
|
||||
if (!forceRefresh && pendingRequests.has(cacheKey)) {
|
||||
return pendingRequests.get(cacheKey)!
|
||||
// 强制刷新时不复用进行中的请求
|
||||
if (!forceRefresh && pendingRequests.has(requestKey)) {
|
||||
return pendingRequests.get(requestKey)!
|
||||
}
|
||||
|
||||
// 创建新请求
|
||||
const requestPromise = (async (): Promise<FetchResult> => {
|
||||
try {
|
||||
loadingMap.value.set(cacheKey, true)
|
||||
const response = await adminApi.queryProviderModels(providerId, apiKeyId)
|
||||
loadingMap.value.set(requestKey, true)
|
||||
const response = await adminApi.queryProviderModels(providerId, apiKeyId, forceRefresh)
|
||||
|
||||
if (response.success && response.data?.models) {
|
||||
// 存入缓存
|
||||
cache.set(cacheKey, {
|
||||
return {
|
||||
models: response.data.models,
|
||||
timestamp: Date.now()
|
||||
})
|
||||
return { models: response.data.models }
|
||||
fromCache: response.data.from_cache
|
||||
}
|
||||
} else {
|
||||
// 使用友好的错误解析
|
||||
const rawError = response.data?.error || '获取上游模型失败'
|
||||
return { models: [], error: parseUpstreamModelError(rawError) }
|
||||
}
|
||||
} catch (err: any) {
|
||||
// 使用友好的错误解析
|
||||
const rawError = err.response?.data?.detail || err.message || '获取上游模型失败'
|
||||
return { models: [], error: parseUpstreamModelError(rawError) }
|
||||
} finally {
|
||||
loadingMap.value.set(cacheKey, false)
|
||||
pendingRequests.delete(cacheKey)
|
||||
loadingMap.value.set(requestKey, false)
|
||||
pendingRequests.delete(requestKey)
|
||||
}
|
||||
})()
|
||||
|
||||
pendingRequests.set(cacheKey, requestPromise)
|
||||
pendingRequests.set(requestKey, requestPromise)
|
||||
return requestPromise
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存的模型(不发起请求)
|
||||
*/
|
||||
function getCachedModels(providerId: string, apiKeyId?: string): UpstreamModel[] | null {
|
||||
const cacheKey = getCacheKey(providerId, apiKeyId)
|
||||
const cached = cache.get(cacheKey)
|
||||
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
|
||||
return cached.models
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除指定提供商/Key的缓存
|
||||
*/
|
||||
function clearCache(providerId: string, apiKeyId?: string) {
|
||||
const cacheKey = getCacheKey(providerId, apiKeyId)
|
||||
cache.delete(cacheKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否正在加载
|
||||
*/
|
||||
function isLoading(providerId: string, apiKeyId?: string): boolean {
|
||||
const cacheKey = getCacheKey(providerId, apiKeyId)
|
||||
return loadingMap.value.get(cacheKey) || false
|
||||
const requestKey = getRequestKey(providerId, apiKeyId)
|
||||
return loadingMap.value.get(requestKey) || false
|
||||
}
|
||||
|
||||
return {
|
||||
fetchModels,
|
||||
getCachedModels,
|
||||
clearCache,
|
||||
isLoading,
|
||||
loadingMap
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user