Files
Aether/frontend/src/utils/errorParser.ts
fawney19 dddb327885 refactor: 重构模型测试错误解析逻辑并修复用量统计变量引用
- 将 ModelsTab 和 ModelAliasesTab 中重复的错误解析逻辑提取到 errorParser.ts
- 添加 parseTestModelError 函数统一处理测试响应错误
- 为 testModel API 添加 TypeScript 类型定义 (TestModelRequest/TestModelResponse)
- 修复 endpoint_checker.py 中 usage_data 变量引用错误
2025-12-25 19:36:29 +08:00

247 lines
7.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 解析 API 错误响应,提取友好的错误信息
*/
import { isApiError } from '@/types/api-error'
/**
* Pydantic 验证错误项
*/
interface ValidationError {
loc: (string | number)[]
msg: string
type: string
ctx?: Record<string, unknown>
}
/**
* 字段名称映射(中文化)
*/
const fieldNameMap: Record<string, string> = {
'api_key': 'API 密钥',
'priority': '优先级',
'max_concurrent': '最大并发',
'rate_limit': '速率限制',
'daily_limit': '每日限制',
'monthly_limit': '每月限制',
'allowed_models': '允许的模型',
'note': '备注',
'is_active': '启用状态',
'endpoint_id': 'Endpoint ID',
'base_url': 'API 基础 URL',
'timeout': '超时时间',
'max_retries': '最大重试次数',
'weight': '权重',
'email': '邮箱',
'username': '用户名',
'password': '密码',
'name': '名称',
'display_name': '显示名称',
'description': '描述',
'website': '网站',
'provider_priority': '提供商优先级',
'billing_type': '计费类型',
'monthly_quota_usd': '月度配额',
'quota_reset_day': '配额重置日',
'quota_expires_at': '配额过期时间',
'rpm_limit': 'RPM 限制',
'cache_ttl_minutes': '缓存 TTL',
'max_probe_interval_minutes': '最大探测间隔',
}
/**
* 错误类型映射(中文化)
*/
const errorTypeMap: Record<string, (error: ValidationError) => string> = {
'string_too_short': (error) => {
const minLength = error.ctx?.min_length || 10
return `长度不能少于 ${minLength} 个字符`
},
'string_too_long': (error) => {
const maxLength = error.ctx?.max_length
return `长度不能超过 ${maxLength} 个字符`
},
'value_error.missing': () => '此字段为必填项',
'missing': () => '此字段为必填项',
'type_error.none.not_allowed': () => '此字段不能为空',
'value_error': (error) => error.msg,
'type_error.integer': () => '必须为整数',
'type_error.float': () => '必须为数字',
'value_error.number.not_ge': (error) => {
const limit = error.ctx?.limit_value
return limit !== undefined ? `不能小于 ${limit}` : '数值过小'
},
'value_error.number.not_le': (error) => {
const limit = error.ctx?.limit_value
return limit !== undefined ? `不能大于 ${limit}` : '数值过大'
},
'value_error.number.not_gt': (error) => {
const limit = error.ctx?.limit_value
return limit !== undefined ? `必须大于 ${limit}` : '数值过小'
},
'value_error.number.not_lt': (error) => {
const limit = error.ctx?.limit_value
return limit !== undefined ? `必须小于 ${limit}` : '数值过大'
},
'less_than_equal': (error) => {
const limit = error.ctx?.le
return limit !== undefined ? `不能大于 ${limit}` : '数值过大'
},
'greater_than_equal': (error) => {
const limit = error.ctx?.ge
return limit !== undefined ? `不能小于 ${limit}` : '数值过小'
},
'less_than': (error) => {
const limit = error.ctx?.lt
return limit !== undefined ? `必须小于 ${limit}` : '数值过大'
},
'greater_than': (error) => {
const limit = error.ctx?.gt
return limit !== undefined ? `必须大于 ${limit}` : '数值过小'
},
'value_error.email': () => '邮箱格式不正确',
'value_error.url': () => 'URL 格式不正确',
'type_error.bool': () => '必须为布尔值true/false',
'type_error.list': () => '必须为数组',
'type_error.dict': () => '必须为对象',
}
/**
* 获取字段的中文名称
*/
function getFieldName(loc: (string | number)[]): string {
if (!loc || loc.length === 0) return '字段'
const fieldPath = loc.filter(item => item !== 'body').join('.')
const fieldKey = String(loc[loc.length - 1])
return fieldNameMap[fieldKey] || fieldPath || '字段'
}
/**
* 格式化单个验证错误
*/
function formatValidationError(error: ValidationError): string {
const fieldName = getFieldName(error.loc)
const errorFormatter = errorTypeMap[error.type]
if (errorFormatter) {
const errorMsg = errorFormatter(error)
return `${fieldName}: ${errorMsg}`
}
// 默认格式
return `${fieldName}: ${error.msg}`
}
/**
* 解析 API 错误响应
* @param err 错误对象
* @param defaultMessage 默认错误信息
* @returns 格式化的错误信息
*/
export function parseApiError(err: unknown, defaultMessage: string = '操作失败'): string {
if (!err) return defaultMessage
// 处理网络错误
if (!isApiError(err) || !err.response) {
if (err instanceof Error) {
return err.message || defaultMessage
}
return '无法连接到服务器,请检查网络连接'
}
const detail = err.response?.data?.detail
// 如果没有 detail 字段
if (!detail) {
return err.response?.data?.message || err.message || defaultMessage
}
// 1. 处理 Pydantic 验证错误(数组格式)
if (Array.isArray(detail)) {
const errors = detail
.map((error: ValidationError) => formatValidationError(error))
.join('\n')
return errors || defaultMessage
}
// 2. 处理字符串错误
if (typeof detail === 'string') {
return detail
}
// 3. 处理对象错误
if (typeof detail === 'object') {
// 可能是自定义错误对象
if ((detail as Record<string, unknown>).message) {
return String((detail as Record<string, unknown>).message)
}
// 尝试 JSON 序列化
try {
return JSON.stringify(detail, null, 2)
} catch {
return defaultMessage
}
}
return defaultMessage
}
/**
* 解析并提取第一个错误信息(用于简短提示)
*/
export function parseApiErrorShort(err: unknown, defaultMessage: string = '操作失败'): string {
const fullError = parseApiError(err, defaultMessage)
// 如果有多行错误,只取第一行
const lines = fullError.split('\n')
return lines[0] || defaultMessage
}
/**
* 解析模型测试响应的错误信息
* @param result 测试响应结果
* @returns 格式化的错误信息
*/
export function parseTestModelError(result: {
error?: string
data?: {
response?: {
status_code?: number
error?: string | { message?: string }
}
}
}): string {
let errorMsg = result.error || '测试失败'
// 检查HTTP状态码错误
if (result.data?.response?.status_code) {
const status = result.data.response.status_code
if (status === 403) {
errorMsg = '认证失败: API密钥无效或客户端类型不被允许'
} else if (status === 401) {
errorMsg = '认证失败: API密钥无效或已过期'
} else if (status === 404) {
errorMsg = '模型不存在: 请检查模型名称是否正确'
} else if (status === 429) {
errorMsg = '请求频率过高: 请稍后重试'
} else if (status >= 500) {
errorMsg = `服务器错误: HTTP ${status}`
} else {
errorMsg = `请求失败: HTTP ${status}`
}
}
// 尝试从错误响应中提取更多信息
if (result.data?.response?.error) {
if (typeof result.data.response.error === 'string') {
errorMsg = result.data.response.error
} else if (result.data.response.error?.message) {
errorMsg = result.data.response.error.message
}
}
return errorMsg
}