diff --git a/frontend/src/features/providers/components/KeyAllowedModelsDialog.vue b/frontend/src/features/providers/components/KeyAllowedModelsDialog.vue index 39878ba..1507c22 100644 --- a/frontend/src/features/providers/components/KeyAllowedModelsDialog.vue +++ b/frontend/src/features/providers/components/KeyAllowedModelsDialog.vue @@ -187,6 +187,7 @@ import Button from '@/components/ui/button.vue' import Badge from '@/components/ui/badge.vue' import Checkbox from '@/components/ui/checkbox.vue' import { useToast } from '@/composables/useToast' +import { parseUpstreamModelError } from '@/utils/errorParser' import { adminApi } from '@/api/admin' import { importModelsFromUpstream, @@ -289,13 +290,18 @@ async function fetchUpstreamModels() { hasQueried.value = true // 如果有部分失败,显示警告提示 if (response.data.error) { - showError(`部分格式获取失败: ${response.data.error}`, '警告') + // 使用友好的错误解析 + showError(`部分格式获取失败: ${parseUpstreamModelError(response.data.error)}`, '警告') } } else { - errorMessage.value = response.data?.error || '获取上游模型失败' + // 使用友好的错误解析 + const rawError = response.data?.error || '获取上游模型失败' + errorMessage.value = parseUpstreamModelError(rawError) } } catch (err: any) { - errorMessage.value = err.response?.data?.detail || '获取上游模型失败' + // 使用友好的错误解析 + const rawError = err.response?.data?.detail || err.message || '获取上游模型失败' + errorMessage.value = parseUpstreamModelError(rawError) } finally { loading.value = false } diff --git a/frontend/src/features/providers/components/KeyAllowedModelsEditDialog.vue b/frontend/src/features/providers/components/KeyAllowedModelsEditDialog.vue index 243cbb4..f93115d 100644 --- a/frontend/src/features/providers/components/KeyAllowedModelsEditDialog.vue +++ b/frontend/src/features/providers/components/KeyAllowedModelsEditDialog.vue @@ -334,7 +334,7 @@ import { } from 'lucide-vue-next' import { Dialog, Button, Input, Checkbox, Badge } from '@/components/ui' import { useToast } from '@/composables/useToast' -import { parseApiError } from '@/utils/errorParser' +import { parseApiError, parseUpstreamModelError } from '@/utils/errorParser' import { updateProviderKey, API_FORMAT_LABELS, @@ -522,11 +522,17 @@ async function fetchUpstreamModels() { } collapsedGroups.value = allGroups } else { - showError(response.data?.error || '获取上游模型失败', '错误') + // 使用友好的错误解析 + const errorMsg = response.data?.error + ? parseUpstreamModelError(response.data.error) + : '获取上游模型失败' + showError(errorMsg, '获取上游模型失败') } } catch (err: any) { if (loadingCancelled) return - showError(err.response?.data?.detail || '获取上游模型失败', '错误') + // 使用友好的错误解析 + const rawError = err.response?.data?.detail || err.message || '获取上游模型失败' + showError(parseUpstreamModelError(rawError), '获取上游模型失败') } finally { fetchingUpstreamModels.value = false } diff --git a/frontend/src/features/providers/composables/useUpstreamModelsCache.ts b/frontend/src/features/providers/composables/useUpstreamModelsCache.ts index 2c4d5fe..67d8935 100644 --- a/frontend/src/features/providers/composables/useUpstreamModelsCache.ts +++ b/frontend/src/features/providers/composables/useUpstreamModelsCache.ts @@ -3,6 +3,7 @@ */ import { ref } from 'vue' import { adminApi } from '@/api/admin' +import { parseUpstreamModelError } from '@/utils/errorParser' import type { UpstreamModel } from '@/api/endpoints/types' // 扩展类型,包含可能的额外字段 @@ -63,10 +64,14 @@ export function useUpstreamModelsCache() { }) return { models: response.data.models } } else { - return { models: [], error: response.data?.error || '获取上游模型失败' } + // 使用友好的错误解析 + const rawError = response.data?.error || '获取上游模型失败' + return { models: [], error: parseUpstreamModelError(rawError) } } } catch (err: any) { - return { models: [], error: err.response?.data?.detail || '获取上游模型失败' } + // 使用友好的错误解析 + const rawError = err.response?.data?.detail || err.message || '获取上游模型失败' + return { models: [], error: parseUpstreamModelError(rawError) } } finally { loadingMap.value.set(providerId, false) pendingRequests.delete(providerId) diff --git a/frontend/src/utils/errorParser.ts b/frontend/src/utils/errorParser.ts index 6b80258..9beb59f 100644 --- a/frontend/src/utils/errorParser.ts +++ b/frontend/src/utils/errorParser.ts @@ -250,3 +250,115 @@ export function parseTestModelError(result: { return errorMsg } + +/** + * 解析上游模型查询错误信息 + * 将后端返回的原始错误信息(如 "HTTP 401: {json...}")转换为友好的错误提示 + * @param error 错误字符串,格式可能是 "HTTP {status}: {json_body}" 或其他 + * @returns 友好的错误信息 + */ +export function parseUpstreamModelError(error: string): string { + if (!error) return '获取上游模型失败' + + // 匹配 "HTTP {status}: {body}" 格式 + const httpMatch = error.match(/^HTTP\s+(\d+):\s*(.*)$/s) + if (httpMatch) { + const status = parseInt(httpMatch[1], 10) + const body = httpMatch[2] + + // 根据状态码生成友好消息 + let friendlyMsg = '' + if (status === 401) { + friendlyMsg = '密钥无效或已过期' + } else if (status === 403) { + friendlyMsg = '密钥权限不足' + } else if (status === 404) { + friendlyMsg = '模型列表接口不存在' + } else if (status === 429) { + friendlyMsg = '请求频率过高,请稍后重试' + } else if (status >= 500) { + friendlyMsg = '上游服务暂时不可用' + } + + // 尝试从 JSON body 中提取更详细的错误信息 + if (body) { + try { + const parsed = JSON.parse(body) + // 常见的错误格式: {error: {message: "..."}} 或 {error: "..."} 或 {message: "..."} + let detailMsg = '' + if (parsed.error?.message) { + detailMsg = parsed.error.message + } else if (typeof parsed.error === 'string') { + detailMsg = parsed.error + } else if (parsed.message) { + detailMsg = parsed.message + } else if (parsed.detail) { + detailMsg = typeof parsed.detail === 'string' ? parsed.detail : JSON.stringify(parsed.detail) + } + + // 如果提取到了详细消息,用它来丰富友好消息 + if (detailMsg) { + // 检查是否是 token/认证相关的错误 + const lowerMsg = detailMsg.toLowerCase() + if (lowerMsg.includes('invalid token') || lowerMsg.includes('invalid api key')) { + return '密钥无效,请检查密钥是否正确' + } + if (lowerMsg.includes('expired')) { + return '密钥已过期,请更新密钥' + } + if (lowerMsg.includes('quota') || lowerMsg.includes('exceeded')) { + return '配额已用尽或超出限制' + } + if (lowerMsg.includes('rate limit')) { + return '请求频率过高,请稍后重试' + } + // 没有匹配特定关键词,但有详细信息,使用它作为补充 + if (friendlyMsg) { + const truncated = detailMsg.length > 80 ? detailMsg.substring(0, 80) + '...' : detailMsg + return `${friendlyMsg}: ${truncated}` + } + // 没有友好消息,直接使用详细信息 + const truncated = detailMsg.length > 100 ? detailMsg.substring(0, 100) + '...' : detailMsg + return truncated + } + } catch { + // JSON 解析失败,忽略 + } + } + + // 返回友好消息,附加状态码 + if (friendlyMsg) { + return friendlyMsg + } + return `请求失败 (HTTP ${status})` + } + + // 检查是否是请求错误 + if (error.startsWith('Request error:')) { + const detail = error.replace('Request error:', '').trim() + if (detail.toLowerCase().includes('timeout')) { + return '请求超时,上游服务响应过慢' + } + if (detail.toLowerCase().includes('connection')) { + return '无法连接到上游服务' + } + return '网络请求失败' + } + + // 检查是否是未知 API 格式 + if (error.startsWith('Unknown API format:')) { + return '不支持的 API 格式' + } + + // 如果包含分号,可能是多个错误合并的,取第一个 + if (error.includes('; ')) { + const firstError = error.split('; ')[0] + return parseUpstreamModelError(firstError) + } + + // 默认返回原始错误(截断过长的部分) + if (error.length > 100) { + return error.substring(0, 100) + '...' + } + return error +}