fix(provider-mapping): 修复详情页映射延迟刷新并补齐 mapping-preview 缓存治理

- 前端:Provider 详情页在 key/模型关联/模型保存/映射保存后并行刷新 endpoints 与 mapping-preview
- 前端:为 mapping-preview 请求增加 requestId 保护,避免旧响应回写覆盖新状态
- 后端:Key.allowed_models 变更时按 provider 精准失效 mapping-preview 缓存
- 后端:GlobalModel 变更(含创建)时全量失效 mapping-preview 缓存
- 监控:在 Redis 缓存分类中新增 provider_mapping_preview(admin:providers:mapping-preview:*)

验证:
- frontend `npm run type-check`
- backend `python -m compileall`(相关文件)
- pytest 定向用例通过
This commit is contained in:
AAEE86
2026-03-11 09:58:25 +08:00
parent 6235c772ac
commit 4955166b85
4 changed files with 65 additions and 6 deletions

View File

@@ -1136,6 +1136,7 @@ const providerModels = ref<Model[]>([]) // Provider 级别的 models
const providerMappingPreview = ref<ProviderMappingPreviewResponse | null>(null) // 映射预览 const providerMappingPreview = ref<ProviderMappingPreviewResponse | null>(null) // 映射预览
let providerLoadRequestId = 0 let providerLoadRequestId = 0
let endpointsLoadRequestId = 0 let endpointsLoadRequestId = 0
let mappingPreviewLoadRequestId = 0
// 系统级格式转换配置 // 系统级格式转换配置
const systemFormatConversionEnabled = ref(false) const systemFormatConversionEnabled = ref(false)
@@ -1349,6 +1350,7 @@ watch(
// 使在途请求失效,避免关闭后旧响应回写 // 使在途请求失效,避免关闭后旧响应回写
providerLoadRequestId += 1 providerLoadRequestId += 1
endpointsLoadRequestId += 1 endpointsLoadRequestId += 1
mappingPreviewLoadRequestId += 1
// 停止倒计时定时器 // 停止倒计时定时器
stopCountdownTimer() stopCountdownTimer()
@@ -1926,7 +1928,7 @@ async function openAntigravityQuotaDialog(key: EndpointAPIKey) {
} }
async function handleKeyChanged() { async function handleKeyChanged() {
await loadEndpoints() await Promise.all([loadEndpoints(), loadMappingPreview()])
emit('refresh') emit('refresh')
// 添加/修改 key 后自动获取 Antigravity 配额(新 key 的 upstream_metadata 为空) // 添加/修改 key 后自动获取 Antigravity 配额(新 key 的 upstream_metadata 为空)
void autoRefreshQuotaInBackground() void autoRefreshQuotaInBackground()
@@ -2015,21 +2017,20 @@ function handleBatchAssign() {
// 处理批量关联完成 // 处理批量关联完成
async function handleBatchAssignChanged() { async function handleBatchAssignChanged() {
await loadEndpoints() await Promise.all([loadEndpoints(), loadMappingPreview()])
emit('refresh') emit('refresh')
} }
// 处理模型映射变更 // 处理模型映射变更
async function handleModelMappingChanged() { async function handleModelMappingChanged() {
void loadMappingPreview() await Promise.all([loadEndpoints(), loadMappingPreview()])
await loadEndpoints()
emit('refresh') emit('refresh')
} }
// 处理模型保存完成 // 处理模型保存完成
async function handleModelSaved() { async function handleModelSaved() {
editingModel.value = null editingModel.value = null
await loadEndpoints() await Promise.all([loadEndpoints(), loadMappingPreview()])
emit('refresh') emit('refresh')
} }
@@ -2719,9 +2720,13 @@ async function loadEndpoints() {
// 加载映射预览(独立于 loadEndpoints不阻塞首屏渲染 // 加载映射预览(独立于 loadEndpoints不阻塞首屏渲染
async function loadMappingPreview() { async function loadMappingPreview() {
if (!props.providerId) return if (!props.providerId) return
const requestId = ++mappingPreviewLoadRequestId
try { try {
providerMappingPreview.value = await getProviderMappingPreview(props.providerId) const preview = await getProviderMappingPreview(props.providerId)
if (requestId !== mappingPreviewLoadRequestId) return
providerMappingPreview.value = preview
} catch { } catch {
if (requestId !== mappingPreviewLoadRequestId) return
providerMappingPreview.value = null providerMappingPreview.value = null
} }
} }

View File

@@ -427,6 +427,12 @@ class AdminCreateGlobalModelAdapter(AdminApiAdapter):
logger.info(f"GlobalModel 已创建: id={global_model.id} name={global_model.name}") logger.info(f"GlobalModel 已创建: id={global_model.id} name={global_model.name}")
# 创建成功后失效缓存(避免 mapping-preview 在 TTL 内读到旧结果)
from src.services.cache.invalidation import get_cache_invalidation_service
cache_service = get_cache_invalidation_service()
await cache_service.on_global_model_changed(global_model.name, str(global_model.id))
return GlobalModelResponse.model_validate(global_model) return GlobalModelResponse.model_validate(global_model)

View File

@@ -1558,6 +1558,12 @@ _CACHE_CATEGORIES: list[tuple[str, str, str, str]] = [
"model:provider_global:*", "model:provider_global:*",
"Provider-GlobalModel 模型映射缓存", "Provider-GlobalModel 模型映射缓存",
), ),
(
"provider_mapping_preview",
"映射预览",
"admin:providers:mapping-preview:*",
"Provider 详情页 mapping-preview 缓存",
),
("global_model", "全局模型", "global_model:*", "GlobalModel 缓存ID/名称/解析)"), ("global_model", "全局模型", "global_model:*", "GlobalModel 缓存ID/名称/解析)"),
("models_list", "模型列表", "models:list:*", "/v1/models 端点模型列表缓存"), ("models_list", "模型列表", "models:list:*", "/v1/models 端点模型列表缓存"),
("user", "用户", "user:*", "用户信息缓存ID/Email"), ("user", "用户", "user:*", "用户信息缓存ID/Email"),

View File

@@ -6,10 +6,15 @@
from __future__ import annotations from __future__ import annotations
import hashlib
import json
from typing import Any from typing import Any
from src.core.logger import logger from src.core.logger import logger
_PROVIDER_MAPPING_PREVIEW_CACHE_KEY_PREFIX = "admin:providers:mapping-preview:global"
_PROVIDER_MAPPING_PREVIEW_CACHE_PATTERN = f"{_PROVIDER_MAPPING_PREVIEW_CACHE_KEY_PREFIX}:v:*"
class CacheInvalidationService: class CacheInvalidationService:
"""缓存失效服务""" """缓存失效服务"""
@@ -61,6 +66,12 @@ class CacheInvalidationService:
except Exception as e: except Exception as e:
logger.error(f"[CacheInvalidation] 失效 models list 缓存失败: {e}") logger.error(f"[CacheInvalidation] 失效 models list 缓存失败: {e}")
# 5. 清除 Provider 映射预览缓存GlobalModel 映射规则变化会影响所有 Provider
try:
await self._invalidate_all_provider_mapping_preview_cache()
except Exception as e:
logger.error(f"[CacheInvalidation] 失效 Provider 映射预览缓存失败: {e}")
def on_model_changed(self, provider_id: str, global_model_id: str) -> Any: def on_model_changed(self, provider_id: str, global_model_id: str) -> Any:
"""Model 变更时的缓存失效""" """Model 变更时的缓存失效"""
self._refresh_provider_cache(provider_id) self._refresh_provider_cache(provider_id)
@@ -78,6 +89,12 @@ class CacheInvalidationService:
logger.info(f"[CacheInvalidation] Key allowed_models 变更: provider_id={provider_id}") logger.info(f"[CacheInvalidation] Key allowed_models 变更: provider_id={provider_id}")
self._refresh_provider_cache(provider_id) self._refresh_provider_cache(provider_id)
# 清除该 Provider 的映射预览缓存(详情页模型映射依赖)
try:
await self._invalidate_provider_mapping_preview_cache(provider_id)
except Exception as e:
logger.error(f"[CacheInvalidation] 失效 Provider 映射预览缓存失败: {e}")
# 清除 /v1/models 列表缓存allowed_models 变更会影响模型可用性) # 清除 /v1/models 列表缓存allowed_models 变更会影响模型可用性)
from src.services.cache.model_list_cache import invalidate_models_list_cache from src.services.cache.model_list_cache import invalidate_models_list_cache
@@ -92,6 +109,31 @@ class CacheInvalidationService:
ModelMapperMiddleware.refresh_cache(provider_id) ModelMapperMiddleware.refresh_cache(provider_id)
@staticmethod
def _build_provider_mapping_preview_cache_key(provider_id: str) -> str:
"""构建指定 Provider 的 mapping-preview 缓存键。"""
raw = json.dumps(
{"provider_id": provider_id},
sort_keys=True,
ensure_ascii=False,
default=str,
)
vary_hash = hashlib.sha1(raw.encode("utf-8")).hexdigest()[:16]
return f"{_PROVIDER_MAPPING_PREVIEW_CACHE_KEY_PREFIX}:v:{vary_hash}"
async def _invalidate_provider_mapping_preview_cache(self, provider_id: str) -> None:
"""失效指定 Provider 的 mapping-preview 缓存。"""
from src.core.cache_service import CacheService
cache_key = self._build_provider_mapping_preview_cache_key(provider_id)
await CacheService.delete(cache_key)
async def _invalidate_all_provider_mapping_preview_cache(self) -> None:
"""失效所有 Provider 的 mapping-preview 缓存。"""
from src.core.cache_service import CacheService
await CacheService.delete_pattern(_PROVIDER_MAPPING_PREVIEW_CACHE_PATTERN)
def clear_all_caches(self) -> None: def clear_all_caches(self) -> None:
"""清空所有缓存""" """清空所有缓存"""
from src.services.model.mapper import ModelMapperMiddleware from src.services.model.mapper import ModelMapperMiddleware