fix(models): surface external catalog failures promptly

This commit is contained in:
elky
2026-09-04 14:00:59 +08:00
parent 12571764bc
commit 499942e3e7
3 changed files with 99 additions and 12 deletions
@@ -25,7 +25,7 @@ const ADMIN_EXTERNAL_MODELS_OFFICIAL_PATH: &str = "/api.json";
pub(in crate::handlers::admin) const ADMIN_EXTERNAL_MODELS_PROXY_NODE_CONFIG_KEY: &str =
"external_models_proxy_node_id";
const ADMIN_EXTERNAL_MODELS_CONNECT_TIMEOUT_MS: u64 = 10_000;
const ADMIN_EXTERNAL_MODELS_TOTAL_TIMEOUT_MS: u64 = 300_000;
const ADMIN_EXTERNAL_MODELS_TOTAL_TIMEOUT_MS: u64 = 30_000;
const ADMIN_EXTERNAL_MODELS_RESPONSE_LIMIT_BYTES: usize = 8 * 1024 * 1024;
// Keep the cache envelope bounded independently of the upstream body limit.
// Normalization adds a small amount of metadata, while a corrupted/shared
@@ -35,6 +35,35 @@
>
<Loader2 class="w-5 h-5 animate-spin text-muted-foreground" />
</div>
<div
v-else-if="catalogLoadError"
class="flex flex-1 flex-col items-center justify-center gap-3 px-6 text-center"
role="alert"
data-testid="models-catalog-load-error"
>
<div class="flex h-9 w-9 items-center justify-center rounded-md border bg-muted/40 text-destructive">
<TriangleAlert class="h-4 w-4" />
</div>
<div class="max-w-md space-y-1">
<p class="text-sm font-medium">
外部模型目录加载失败
</p>
<p class="break-words text-xs text-muted-foreground">
{{ catalogLoadError }}
</p>
</div>
<Button
type="button"
variant="outline"
size="sm"
class="h-8 gap-1.5"
data-testid="models-catalog-retry"
@click="retryModelsCatalog"
>
<RefreshCw class="h-3.5 w-3.5" />
重试
</Button>
</div>
<template v-else>
<!-- 提供商 Logo 横向选择 -->
<div
@@ -771,7 +800,7 @@ import { ref, computed, nextTick, watch } from 'vue'
import {
Loader2, Layers, SquarePen,
Search, ChevronLeft, ChevronRight, Plus, Trash2, Check,
BrainCircuit, Eye, Wrench, Braces, Database, PackageOpen, RefreshCw
BrainCircuit, Eye, Wrench, Braces, Database, PackageOpen, RefreshCw, TriangleAlert
} from 'lucide-vue-next'
import {
Dialog, Button, Input, Label, Checkbox,
@@ -836,6 +865,7 @@ const basicInfoSection = ref<HTMLElement | null>(null)
// 模型列表相关
const loading = ref(false)
const catalogLoadError = ref<string | null>(null)
const searchQuery = ref('')
const allModelsCache = ref<ModelsDevModelItem[]>([]) // 全部模型(缓存)
const existingModelsCache = ref<GlobalModelResponse[]>([])
@@ -1300,19 +1330,44 @@ async function loadExistingModels() {
existingModelsCache.value = models
}
async function loadModelsCatalog() {
catalogLoadError.value = null
try {
allModelsCache.value = await getModelsDevList(false)
} catch (err: unknown) {
allModelsCache.value = []
catalogLoadError.value = parseApiError(err, '外部模型目录暂时不可用')
log.error('Failed to load online models:', err)
}
}
async function retryModelsCatalog() {
if (loading.value) return
loading.value = true
try {
await loadModelsCatalog()
if (!expandedProvider.value) {
expandedProvider.value = getDefaultProviderId(groupedModels.value)
}
} finally {
loading.value = false
}
}
// 加载在线目录和已有模型列表
async function loadModels() {
loading.value = true
await Promise.all([
allModelsCache.value.length > 0
? Promise.resolve()
: getModelsDevList(false)
.then(models => { allModelsCache.value = models })
.catch(err => log.error('Failed to load online models:', err)),
loadExistingModels()
.catch(err => log.error('Failed to load existing models:', err)),
])
loading.value = false
const shouldLoadCatalog = allModelsCache.value.length === 0
if (!shouldLoadCatalog) catalogLoadError.value = null
try {
await Promise.all([
shouldLoadCatalog ? loadModelsCatalog() : Promise.resolve(),
loadExistingModels()
.catch(err => log.error('Failed to load existing models:', err)),
])
} finally {
loading.value = false
}
}
// 打开对话框时加载数据
@@ -247,6 +247,38 @@ afterEach(() => {
})
describe('GlobalModelFormDialog preset replacement', () => {
it('shows a catalog error and retries instead of presenting a failed load as an empty catalog', async () => {
modelsDevMocks.getModelsDevList
.mockRejectedValueOnce({
response: {
status: 503,
data: { detail: 'External models catalog unavailable' },
},
})
.mockResolvedValueOnce([stalePreset, freshPreset])
mountDialog()
await settle()
const errorState = document.body.querySelector('[data-testid="models-catalog-load-error"]')
expect(errorState?.textContent).toContain('外部模型目录加载失败')
expect(errorState?.textContent).toContain('External models catalog unavailable')
expect(document.body.textContent).not.toContain('暂无可用模型')
expect(findExactButton('手动填写').disabled).toBe(false)
const retryButton = document.body.querySelector<HTMLButtonElement>(
'[data-testid="models-catalog-retry"]',
)
if (!retryButton) throw new Error('Missing catalog retry button')
retryButton.click()
await settle()
expect(modelsDevMocks.getModelsDevList).toHaveBeenCalledTimes(2)
expect(document.body.querySelector('[data-testid="models-catalog-load-error"]')).toBeNull()
expect(document.body.textContent).toContain('Stale Model')
expect(document.body.textContent).toContain('Fresh Model')
})
it('drops the previous draft and submits only the newly selected model preset', async () => {
mountDialog()
await settle()