errorHandler.ts 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  1. /**
  2. * 错误处理工具
  3. *
  4. * 提供统一的错误处理和用户友好的错误提示
  5. */
  6. export interface ErrorDetail {
  7. code?: string;
  8. message: string;
  9. title?: string;
  10. solution?: string;
  11. actions?: Array<{
  12. label: string;
  13. onClick: () => void;
  14. }>;
  15. }
  16. /**
  17. * DashScope API 错误映射
  18. */
  19. const DASHSCOPE_ERROR_MAP: Record<string, ErrorDetail> = {
  20. 'App.AccessDenied': {
  21. title: 'API Key 权限不足',
  22. message: '您的 API Key 没有访问 Deep Research 的权限',
  23. solution: '请在阿里云百炼控制台开通 Deep Research 功能,或重新生成包含该权限的 API Key',
  24. },
  25. 'InvalidParameter': {
  26. title: '请求参数错误',
  27. message: '请求参数不正确',
  28. solution: '请检查输入内容是否符合要求,或联系技术支持',
  29. },
  30. 'QuotaExceeded': {
  31. title: 'API 调用配额已用尽',
  32. message: '您的 API 调用配额已达到上限',
  33. solution: '请充值或等待配额重置,也可以升级套餐获得更多配额',
  34. },
  35. 'Throttling': {
  36. title: '请求过于频繁',
  37. message: '您的请求速度超过了限制',
  38. solution: '请稍后再试,或联系客服提升限流阈值',
  39. },
  40. 'InternalError': {
  41. title: '服务内部错误',
  42. message: 'DashScope 服务遇到内部错误',
  43. solution: '请稍后重试,如果问题持续存在,请联系技术支持',
  44. },
  45. 'ServiceUnavailable': {
  46. title: '服务暂时不可用',
  47. message: 'DashScope 服务暂时无法访问',
  48. solution: '请稍后重试,或查看服务状态页面',
  49. },
  50. };
  51. /**
  52. * 通用错误映射
  53. */
  54. const COMMON_ERROR_MAP: Record<string, ErrorDetail> = {
  55. 'NetworkError': {
  56. title: '网络连接失败',
  57. message: '无法连接到服务器',
  58. solution: '请检查网络连接,或稍后重试',
  59. },
  60. 'Timeout': {
  61. title: '请求超时',
  62. message: '服务器响应超时',
  63. solution: '请检查网络连接,或稍后重试',
  64. },
  65. 'Unauthorized': {
  66. title: '未授权',
  67. message: '您的登录已过期',
  68. solution: '请重新登录',
  69. },
  70. 'Forbidden': {
  71. title: '访问被拒绝',
  72. message: '您没有权限执行此操作',
  73. solution: '请联系管理员获取权限',
  74. },
  75. 'NotFound': {
  76. title: '资源不存在',
  77. message: '请求的资源未找到',
  78. solution: '请检查请求是否正确',
  79. },
  80. };
  81. /**
  82. * 解析错误信息
  83. */
  84. export function parseError(error: any): ErrorDetail {
  85. // 如果是字符串,直接返回
  86. if (typeof error === 'string') {
  87. return {
  88. message: error,
  89. };
  90. }
  91. // 如果是 Error 对象
  92. if (error instanceof Error) {
  93. // 尝试解析错误消息中的错误代码
  94. const errorMessage = error.message;
  95. // axios 错误常包含 response 对象,优先解析
  96. // 处理 axios 风格的错误对象
  97. // @ts-ignore
  98. if ((error as any).response) {
  99. // @ts-ignore
  100. const resp = (error as any).response;
  101. const msg = resp.data?.message || resp.data?.detail;
  102. if (msg) {
  103. return {
  104. message: msg,
  105. };
  106. }
  107. }
  108. // 检查是否包含 DashScope 错误代码
  109. for (const [code, detail] of Object.entries(DASHSCOPE_ERROR_MAP)) {
  110. if (errorMessage.includes(code)) {
  111. return {
  112. code,
  113. ...detail,
  114. };
  115. }
  116. }
  117. // 检查是否包含通用错误
  118. for (const [code, detail] of Object.entries(COMMON_ERROR_MAP)) {
  119. if (errorMessage.includes(code) || errorMessage.toLowerCase().includes(code.toLowerCase())) {
  120. return {
  121. code,
  122. ...detail,
  123. };
  124. }
  125. }
  126. // 返回原始错误消息
  127. return {
  128. message: errorMessage,
  129. };
  130. }
  131. // 如果是对象,尝试提取错误信息
  132. if (typeof error === 'object' && error !== null) {
  133. // axios 风格的错误体可能包含 response
  134. // @ts-ignore
  135. if (error.response) {
  136. // @ts-ignore
  137. const resp = error.response;
  138. const msg = resp.data?.message || resp.data?.detail;
  139. if (msg) {
  140. return {
  141. message: msg,
  142. };
  143. }
  144. }
  145. const code = error.code || error.error_code;
  146. const message = error.message || error.error_message || error.msg;
  147. // 检查是否是已知的错误代码
  148. if (code && DASHSCOPE_ERROR_MAP[code]) {
  149. return {
  150. code,
  151. ...DASHSCOPE_ERROR_MAP[code],
  152. };
  153. }
  154. if (code && COMMON_ERROR_MAP[code]) {
  155. return {
  156. code,
  157. ...COMMON_ERROR_MAP[code],
  158. };
  159. }
  160. // 返回提取的信息
  161. return {
  162. code,
  163. message: message || '未知错误',
  164. };
  165. }
  166. // 默认错误
  167. return {
  168. message: '发生未知错误',
  169. };
  170. }
  171. /**
  172. * 格式化错误消息(用于 Toast 显示)
  173. */
  174. export function formatErrorMessage(error: any): string {
  175. const detail = parseError(error);
  176. if (detail.title) {
  177. return `${detail.title}: ${detail.message}`;
  178. }
  179. return detail.message;
  180. }
  181. /**
  182. * 获取错误的完整信息(用于详细展示)
  183. */
  184. export function getErrorDetail(error: any): ErrorDetail {
  185. return parseError(error);
  186. }
  187. /**
  188. * 判断是否是 API Key 权限错误
  189. */
  190. export function isApiKeyPermissionError(error: any): boolean {
  191. const detail = parseError(error);
  192. return detail.code === 'App.AccessDenied';
  193. }
  194. /**
  195. * 判断是否是网络错误
  196. */
  197. export function isNetworkError(error: any): boolean {
  198. const detail = parseError(error);
  199. return detail.code === 'NetworkError' ||
  200. detail.code === 'Timeout' ||
  201. detail.message.includes('network') ||
  202. detail.message.includes('timeout');
  203. }
  204. /**
  205. * 获取错误的建议操作
  206. */
  207. export function getErrorActions(error: any, navigate?: (path: string) => void): Array<{
  208. label: string;
  209. onClick: () => void;
  210. }> {
  211. const detail = parseError(error);
  212. const actions: Array<{ label: string; onClick: () => void }> = [];
  213. // API Key 权限错误
  214. if (detail.code === 'App.AccessDenied') {
  215. actions.push({
  216. label: '查看解决方案',
  217. onClick: () => {
  218. window.open('https://help.aliyun.com/zh/model-studio/', '_blank');
  219. },
  220. });
  221. if (navigate) {
  222. actions.push({
  223. label: '更新 API Key',
  224. onClick: () => navigate('/settings'),
  225. });
  226. }
  227. }
  228. // 配额用尽
  229. if (detail.code === 'QuotaExceeded') {
  230. actions.push({
  231. label: '查看配额',
  232. onClick: () => {
  233. window.open('https://bailian.console.aliyun.com/', '_blank');
  234. },
  235. });
  236. }
  237. return actions;
  238. }
  239. /**
  240. * 创建用户友好的错误对话框配置
  241. */
  242. export function createErrorDialog(error: any, navigate?: (path: string) => void) {
  243. const detail = getErrorDetail(error);
  244. const actions = getErrorActions(error, navigate);
  245. return {
  246. title: detail.title || '错误',
  247. message: detail.message,
  248. solution: detail.solution,
  249. actions,
  250. };
  251. }