diff --git a/apps/aether-gateway/src/handlers/admin/model/external_cache.rs b/apps/aether-gateway/src/handlers/admin/model/external_cache.rs
index 41a89e431..e2457f007 100644
--- a/apps/aether-gateway/src/handlers/admin/model/external_cache.rs
+++ b/apps/aether-gateway/src/handlers/admin/model/external_cache.rs
@@ -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
diff --git a/frontend/src/features/models/components/GlobalModelFormDialog.vue b/frontend/src/features/models/components/GlobalModelFormDialog.vue
index a8ad73510..39e1bf6dc 100644
--- a/frontend/src/features/models/components/GlobalModelFormDialog.vue
+++ b/frontend/src/features/models/components/GlobalModelFormDialog.vue
@@ -35,6 +35,35 @@
>
+
+
+
+
+
+
+ 外部模型目录加载失败
+
+
+ {{ catalogLoadError }}
+
+
+
+
(null)
// 模型列表相关
const loading = ref(false)
+const catalogLoadError = ref(null)
const searchQuery = ref('')
const allModelsCache = ref([]) // 全部模型(缓存)
const existingModelsCache = ref([])
@@ -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
+ }
}
// 打开对话框时加载数据
diff --git a/frontend/src/features/models/components/__tests__/GlobalModelFormDialog.prefill.spec.ts b/frontend/src/features/models/components/__tests__/GlobalModelFormDialog.prefill.spec.ts
index 7396f90d5..a99cef20d 100644
--- a/frontend/src/features/models/components/__tests__/GlobalModelFormDialog.prefill.spec.ts
+++ b/frontend/src/features/models/components/__tests__/GlobalModelFormDialog.prefill.spec.ts
@@ -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(
+ '[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()