/** * 解析 API 错误响应,提取友好的错误信息 */ /** * Pydantic 验证错误项 */ interface ValidationError { loc: (string | number)[] msg: string type: string ctx?: Record } /** * 字段名称映射(中文化) */ const fieldNameMap: Record = { '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> = { '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: any, defaultMessage: string = '操作失败'): string { if (!err) return defaultMessage // 处理网络错误 if (!err.response) { 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.message) { return detail.message } // 尝试 JSON 序列化 try { return JSON.stringify(detail, null, 2) } catch { return defaultMessage } } return defaultMessage } /** * 解析并提取第一个错误信息(用于简短提示) */ export function parseApiErrorShort(err: any, defaultMessage: string = '操作失败'): string { const fullError = parseApiError(err, defaultMessage) // 如果有多行错误,只取第一行 const lines = fullError.split('\n') return lines[0] || defaultMessage }