mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-08 12:10:19 +08:00
fix(models): surface external catalog failures promptly
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
// 打开对话框时加载数据
|
||||
|
||||
+32
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user