mirror of
https://github.com/fawney19/Aether.git
synced 2026-01-11 20:18:30 +08:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94abab3260 | ||
|
|
76ed136228 | ||
|
|
8d8b20aa47 | ||
|
|
aec0326d40 |
@@ -22,7 +22,7 @@
|
||||
/>
|
||||
</Transition>
|
||||
|
||||
<div class="relative flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0 pointer-events-none">
|
||||
<div class="relative flex min-h-full items-end justify-center px-3 py-4 text-center sm:items-center sm:p-0 pointer-events-none">
|
||||
<!-- 对话框内容 -->
|
||||
<Transition
|
||||
enter-active-class="duration-300 ease-out"
|
||||
@@ -34,7 +34,7 @@
|
||||
>
|
||||
<div
|
||||
v-if="isOpen"
|
||||
class="relative transform rounded-lg bg-background text-left shadow-2xl transition-all sm:my-8 sm:w-full border border-border pointer-events-auto"
|
||||
class="relative transform rounded-lg bg-background text-left shadow-2xl transition-all w-full sm:my-8 sm:w-full border border-border pointer-events-auto"
|
||||
:style="{ zIndex: contentZIndex }"
|
||||
:class="maxWidthClass"
|
||||
@click.stop
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@
|
||||
:title="isEditMode ? '编辑密钥' : '添加密钥'"
|
||||
:description="isEditMode ? '修改 API 密钥配置' : '为提供商添加新的 API 密钥'"
|
||||
:icon="isEditMode ? SquarePen : Key"
|
||||
size="2xl"
|
||||
size="xl"
|
||||
@update:model-value="handleDialogUpdate"
|
||||
>
|
||||
<form
|
||||
@@ -80,7 +80,7 @@
|
||||
<!-- API 格式选择 -->
|
||||
<div v-if="sortedApiFormats.length > 0">
|
||||
<Label class="mb-1.5 block">支持的 API 格式 *</Label>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 gap-2">
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div
|
||||
v-for="format in sortedApiFormats"
|
||||
:key="format"
|
||||
|
||||
@@ -377,6 +377,7 @@ const props = defineProps<{
|
||||
providerApiFormats: string[]
|
||||
models: Model[]
|
||||
editingGroup?: AliasGroup | null
|
||||
preselectedModelId?: string | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -500,7 +501,7 @@ function initForm() {
|
||||
}
|
||||
} else {
|
||||
formData.value = {
|
||||
modelId: '',
|
||||
modelId: props.preselectedModelId || '',
|
||||
apiFormats: [],
|
||||
aliases: []
|
||||
}
|
||||
|
||||
@@ -168,26 +168,44 @@
|
||||
class="divide-y divide-border/40"
|
||||
>
|
||||
<div
|
||||
v-for="{ key, endpoint } in allKeys"
|
||||
v-for="({ key, endpoint }, index) in allKeys"
|
||||
:key="key.id"
|
||||
class="px-4 py-2.5 hover:bg-muted/30 transition-colors"
|
||||
class="px-4 py-2.5 hover:bg-muted/30 transition-colors group/item"
|
||||
:class="{
|
||||
'opacity-50': keyDragState.isDragging && keyDragState.draggedIndex === index,
|
||||
'bg-primary/5 border-l-2 border-l-primary': keyDragState.targetIndex === index && keyDragState.isDragging
|
||||
}"
|
||||
draggable="true"
|
||||
@dragstart="handleKeyDragStart($event, index)"
|
||||
@dragend="handleKeyDragEnd"
|
||||
@dragover="handleKeyDragOver($event, index)"
|
||||
@dragleave="handleKeyDragLeave"
|
||||
@drop="handleKeyDrop($event, index)"
|
||||
>
|
||||
<!-- 第一行:名称 + 状态 + 操作按钮 -->
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="flex items-center gap-2 flex-1 min-w-0">
|
||||
<span class="text-sm font-medium truncate">{{ key.name || '未命名密钥' }}</span>
|
||||
<span class="text-xs font-mono text-muted-foreground">
|
||||
{{ key.api_key_masked }}
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-5 w-5 shrink-0"
|
||||
title="复制密钥"
|
||||
@click.stop="copyFullKey(key)"
|
||||
>
|
||||
<Copy class="w-3 h-3" />
|
||||
</Button>
|
||||
<!-- 拖拽手柄 -->
|
||||
<div class="cursor-grab active:cursor-grabbing text-muted-foreground/30 group-hover/item:text-muted-foreground transition-colors shrink-0">
|
||||
<GripVertical class="w-4 h-4" />
|
||||
</div>
|
||||
<div class="flex flex-col min-w-0">
|
||||
<span class="text-sm font-medium truncate">{{ key.name || '未命名密钥' }}</span>
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="text-[11px] font-mono text-muted-foreground">
|
||||
{{ key.api_key_masked }}
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-4 w-4 shrink-0"
|
||||
title="复制密钥"
|
||||
@click.stop="copyFullKey(key)"
|
||||
>
|
||||
<Copy class="w-2.5 h-2.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Badge
|
||||
v-if="!key.is_active"
|
||||
variant="secondary"
|
||||
@@ -343,6 +361,7 @@
|
||||
@edit-model="handleEditModel"
|
||||
@delete-model="handleDeleteModel"
|
||||
@batch-assign="handleBatchAssign"
|
||||
@add-mapping="handleAddMapping"
|
||||
/>
|
||||
|
||||
<!-- 模型名称映射 -->
|
||||
@@ -570,7 +589,7 @@ const batchAssignDialogOpen = ref(false)
|
||||
// ModelAliasesTab 组件引用
|
||||
const modelAliasesTabRef = ref<InstanceType<typeof ModelAliasesTab> | null>(null)
|
||||
|
||||
// 拖动排序相关状态
|
||||
// 拖动排序相关状态(旧的端点级别拖拽,保留以兼容)
|
||||
const dragState = ref({
|
||||
isDragging: false,
|
||||
draggedKeyId: null as string | null,
|
||||
@@ -578,6 +597,13 @@ const dragState = ref({
|
||||
dragEndpointId: null as string | null
|
||||
})
|
||||
|
||||
// 密钥列表拖拽排序状态
|
||||
const keyDragState = ref({
|
||||
isDragging: false,
|
||||
draggedIndex: null as number | null,
|
||||
targetIndex: null as number | null
|
||||
})
|
||||
|
||||
// 点击编辑优先级相关状态
|
||||
const editingPriorityKey = ref<string | null>(null)
|
||||
const editingPriorityValue = ref<number>(0)
|
||||
@@ -930,6 +956,11 @@ function handleBatchAssign() {
|
||||
batchAssignDialogOpen.value = true
|
||||
}
|
||||
|
||||
// 处理添加映射(从 ModelsTab 触发)
|
||||
function handleAddMapping(model: Model) {
|
||||
modelAliasesTabRef.value?.openAddDialogForModel(model.id)
|
||||
}
|
||||
|
||||
// 处理批量关联完成
|
||||
async function handleBatchAssignChanged() {
|
||||
await loadProvider()
|
||||
@@ -1154,6 +1185,92 @@ async function savePriority(key: EndpointAPIKey) {
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 密钥列表拖拽排序 =====
|
||||
function handleKeyDragStart(event: DragEvent, index: number) {
|
||||
keyDragState.value.isDragging = true
|
||||
keyDragState.value.draggedIndex = index
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.effectAllowed = 'move'
|
||||
event.dataTransfer.setData('text/plain', String(index))
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeyDragEnd() {
|
||||
keyDragState.value.isDragging = false
|
||||
keyDragState.value.draggedIndex = null
|
||||
keyDragState.value.targetIndex = null
|
||||
}
|
||||
|
||||
function handleKeyDragOver(event: DragEvent, index: number) {
|
||||
event.preventDefault()
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.dropEffect = 'move'
|
||||
}
|
||||
if (keyDragState.value.draggedIndex !== index) {
|
||||
keyDragState.value.targetIndex = index
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeyDragLeave() {
|
||||
keyDragState.value.targetIndex = null
|
||||
}
|
||||
|
||||
async function handleKeyDrop(event: DragEvent, targetIndex: number) {
|
||||
event.preventDefault()
|
||||
|
||||
const draggedIndex = keyDragState.value.draggedIndex
|
||||
if (draggedIndex === null || draggedIndex === targetIndex) {
|
||||
handleKeyDragEnd()
|
||||
return
|
||||
}
|
||||
|
||||
const keys = allKeys.value.map(item => item.key)
|
||||
if (draggedIndex < 0 || draggedIndex >= keys.length || targetIndex < 0 || targetIndex >= keys.length) {
|
||||
handleKeyDragEnd()
|
||||
return
|
||||
}
|
||||
|
||||
const draggedKey = keys[draggedIndex]
|
||||
const targetKey = keys[targetIndex]
|
||||
const draggedPriority = draggedKey.internal_priority ?? 0
|
||||
const targetPriority = targetKey.internal_priority ?? 0
|
||||
|
||||
// 如果是同组内拖拽(同优先级),忽略操作
|
||||
if (draggedPriority === targetPriority) {
|
||||
handleKeyDragEnd()
|
||||
return
|
||||
}
|
||||
|
||||
// 检查目标 key 是否属于一个"组"(除了被拖拽的 key,还有其他 key 与目标同优先级)
|
||||
// 组的定义:2 个及以上同优先级的 key
|
||||
const keysAtTargetPriority = keys.filter(k =>
|
||||
k.id !== draggedKey.id && (k.internal_priority ?? 0) === targetPriority
|
||||
)
|
||||
// 如果有 2 个及以上 key 在目标优先级(不含被拖拽的),说明目标在组内
|
||||
const targetIsInGroup = keysAtTargetPriority.length >= 2
|
||||
|
||||
handleKeyDragEnd()
|
||||
|
||||
try {
|
||||
if (targetIsInGroup) {
|
||||
// 目标在组内,被拖拽的 key 加入该组
|
||||
await updateProviderKey(draggedKey.id, { internal_priority: targetPriority })
|
||||
} else {
|
||||
// 目标是单独的(或只有目标自己),交换优先级
|
||||
await Promise.all([
|
||||
updateProviderKey(draggedKey.id, { internal_priority: targetPriority }),
|
||||
updateProviderKey(targetKey.id, { internal_priority: draggedPriority })
|
||||
])
|
||||
}
|
||||
showSuccess('优先级已更新')
|
||||
await loadEndpoints()
|
||||
emit('refresh')
|
||||
} catch (err: any) {
|
||||
showError(err.response?.data?.detail || '更新优先级失败', '错误')
|
||||
await loadEndpoints()
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化探测时间
|
||||
function formatProbeTime(probeTime: string): string {
|
||||
if (!probeTime) return '-'
|
||||
|
||||
@@ -168,6 +168,7 @@
|
||||
:provider-api-formats="providerApiFormats"
|
||||
:models="models"
|
||||
:editing-group="editingGroup"
|
||||
:preselected-model-id="preselectedModelId"
|
||||
@saved="onDialogSaved"
|
||||
/>
|
||||
|
||||
@@ -219,6 +220,7 @@ const deleteConfirmOpen = ref(false)
|
||||
const editingGroup = ref<AliasGroup | null>(null)
|
||||
const deletingGroup = ref<AliasGroup | null>(null)
|
||||
const testingMapping = ref<string | null>(null)
|
||||
const preselectedModelId = ref<string | null>(null)
|
||||
|
||||
// 列表展开状态
|
||||
const expandedAliasGroups = ref<Set<string>>(new Set())
|
||||
@@ -311,12 +313,21 @@ function toggleAliasGroupExpand(groupKey: string) {
|
||||
// 打开添加对话框
|
||||
function openAddDialog() {
|
||||
editingGroup.value = null
|
||||
preselectedModelId.value = null
|
||||
dialogOpen.value = true
|
||||
}
|
||||
|
||||
// 打开添加对话框并预选模型(供外部调用)
|
||||
function openAddDialogForModel(modelId: string) {
|
||||
editingGroup.value = null
|
||||
preselectedModelId.value = modelId
|
||||
dialogOpen.value = true
|
||||
}
|
||||
|
||||
// 编辑分组
|
||||
function editGroup(group: AliasGroup) {
|
||||
editingGroup.value = group
|
||||
preselectedModelId.value = null
|
||||
dialogOpen.value = true
|
||||
}
|
||||
|
||||
@@ -416,8 +427,9 @@ onMounted(() => {
|
||||
}
|
||||
})
|
||||
|
||||
// 暴露给父组件,用于检测是否有弹窗打开
|
||||
// 暴露给父组件
|
||||
defineExpose({
|
||||
dialogOpen: computed(() => dialogOpen.value || deleteConfirmOpen.value)
|
||||
dialogOpen: computed(() => dialogOpen.value || deleteConfirmOpen.value),
|
||||
openAddDialogForModel
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -34,16 +34,13 @@
|
||||
<table class="w-full text-sm table-fixed">
|
||||
<thead class="bg-muted/50 text-xs uppercase tracking-wide text-muted-foreground">
|
||||
<tr>
|
||||
<th class="text-left px-4 py-3 font-semibold w-[40%]">
|
||||
<th class="text-left px-4 py-3 font-semibold w-[45%]">
|
||||
模型
|
||||
</th>
|
||||
<th class="text-left px-4 py-3 font-semibold w-[15%]">
|
||||
能力
|
||||
</th>
|
||||
<th class="text-left px-4 py-3 font-semibold w-[25%]">
|
||||
<th class="text-left px-4 py-3 font-semibold w-[30%]">
|
||||
价格 ($/M)
|
||||
</th>
|
||||
<th class="text-center px-4 py-3 font-semibold w-[20%]">
|
||||
<th class="text-center px-4 py-3 font-semibold w-[25%]">
|
||||
操作
|
||||
</th>
|
||||
</tr>
|
||||
@@ -80,42 +77,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="align-top px-4 py-3">
|
||||
<div
|
||||
v-if="hasAnyCapability(model)"
|
||||
class="grid grid-cols-3 gap-1 w-fit"
|
||||
>
|
||||
<Zap
|
||||
v-if="model.effective_supports_streaming ?? model.supports_streaming"
|
||||
class="w-4 h-4 text-muted-foreground"
|
||||
title="流式输出"
|
||||
/>
|
||||
<Image
|
||||
v-if="model.effective_supports_image_generation ?? model.supports_image_generation"
|
||||
class="w-4 h-4 text-muted-foreground"
|
||||
title="图像生成"
|
||||
/>
|
||||
<Eye
|
||||
v-if="model.effective_supports_vision ?? model.supports_vision"
|
||||
class="w-4 h-4 text-muted-foreground"
|
||||
title="视觉理解"
|
||||
/>
|
||||
<Wrench
|
||||
v-if="model.effective_supports_function_calling ?? model.supports_function_calling"
|
||||
class="w-4 h-4 text-muted-foreground"
|
||||
title="工具调用"
|
||||
/>
|
||||
<Brain
|
||||
v-if="model.effective_supports_extended_thinking ?? model.supports_extended_thinking"
|
||||
class="w-4 h-4 text-muted-foreground"
|
||||
title="深度思考"
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
v-else
|
||||
class="text-xs text-muted-foreground"
|
||||
>—</span>
|
||||
</td>
|
||||
<td class="align-top px-4 py-3 text-xs whitespace-nowrap">
|
||||
<div
|
||||
class="grid gap-1"
|
||||
@@ -148,7 +109,7 @@
|
||||
${{ formatPrice(model.effective_price_per_request ?? model.price_per_request) }}/次
|
||||
</span>
|
||||
</template>
|
||||
<!-- 无计费配置 -->
|
||||
<!-- 无计<EFBFBD><EFBFBD>配置 -->
|
||||
<template v-if="!hasTokenPricing(model) && !hasRequestPricing(model)">
|
||||
<span class="text-muted-foreground">—</span>
|
||||
</template>
|
||||
@@ -156,6 +117,15 @@
|
||||
</td>
|
||||
<td class="align-top px-4 py-3">
|
||||
<div class="flex justify-center gap-1.5">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
title="添加映射"
|
||||
@click="addMapping(model)"
|
||||
>
|
||||
<Link class="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@@ -209,7 +179,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { Box, Edit, Trash2, Layers, Eye, Wrench, Zap, Brain, Power, Copy, Image } from 'lucide-vue-next'
|
||||
import { Box, Edit, Trash2, Layers, Power, Copy, Link } from 'lucide-vue-next'
|
||||
import Card from '@/components/ui/card.vue'
|
||||
import Button from '@/components/ui/button.vue'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
@@ -225,6 +195,7 @@ const emit = defineEmits<{
|
||||
'editModel': [model: Model]
|
||||
'deleteModel': [model: Model]
|
||||
'batchAssign': []
|
||||
'addMapping': [model: Model]
|
||||
}>()
|
||||
|
||||
const { error: showError, success: showSuccess } = useToast()
|
||||
@@ -276,17 +247,6 @@ function formatPrice(price: number | null | undefined): string {
|
||||
return price.toFixed(4)
|
||||
}
|
||||
|
||||
// 检查模型是否有任何能力
|
||||
function hasAnyCapability(model: Model): boolean {
|
||||
return !!(
|
||||
(model.effective_supports_vision ?? model.supports_vision) ||
|
||||
(model.effective_supports_function_calling ?? model.supports_function_calling) ||
|
||||
(model.effective_supports_streaming ?? model.supports_streaming) ||
|
||||
(model.effective_supports_extended_thinking ?? model.supports_extended_thinking) ||
|
||||
(model.effective_supports_image_generation ?? model.supports_image_generation)
|
||||
)
|
||||
}
|
||||
|
||||
// 检查是否有按 Token 计费
|
||||
function hasTokenPricing(model: Model): boolean {
|
||||
const inputPrice = model.effective_input_price
|
||||
@@ -345,7 +305,7 @@ function getStatusTitle(model: Model): string {
|
||||
return '活跃但不可用'
|
||||
}
|
||||
|
||||
// 编辑模型
|
||||
// <EFBFBD><EFBFBD>辑模型
|
||||
function editModel(model: Model) {
|
||||
emit('editModel', model)
|
||||
}
|
||||
@@ -355,6 +315,11 @@ function deleteModel(model: Model) {
|
||||
emit('deleteModel', model)
|
||||
}
|
||||
|
||||
// 添加映射
|
||||
function addMapping(model: Model) {
|
||||
emit('addMapping', model)
|
||||
}
|
||||
|
||||
// 打开批量关联对话框
|
||||
function openBatchAssignDialog() {
|
||||
emit('batchAssign')
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user