AdapterErrorHandler.ts 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. /**
  2. * 统一交易所适配器错误处理工具
  3. */
  4. export interface AdapterError {
  5. code: string
  6. message: string
  7. exchange: string
  8. originalError?: any
  9. retryable: boolean
  10. severity: 'low' | 'medium' | 'high' | 'critical'
  11. }
  12. export class AdapterErrorHandler {
  13. /**
  14. * 标准化错误处理
  15. */
  16. static handleError(exchange: string, error: any, method: string): AdapterError {
  17. const originalMessage = this.extractErrorMessage(error)
  18. const code = this.extractErrorCode(error, exchange)
  19. // 根据错误码判断是否可重试和严重程度
  20. const { retryable, severity } = this.analyzeError(code, exchange)
  21. const adapterError: AdapterError = {
  22. code,
  23. message: `[${exchange}:${method}] ${originalMessage}`,
  24. exchange,
  25. originalError: error,
  26. retryable,
  27. severity,
  28. }
  29. // 记录日志
  30. this.logError(adapterError, method)
  31. return adapterError
  32. }
  33. /**
  34. * 提取错误信息
  35. */
  36. private static extractErrorMessage(error: any): string {
  37. if (typeof error === 'string') return error
  38. if (error?.message) return error.message
  39. if (error?.msg) return error.msg
  40. if (error?.reason) return error.reason
  41. return '未知错误'
  42. }
  43. /**
  44. * 提取错误代码
  45. */
  46. private static extractErrorCode(error: any, exchange: string): string {
  47. // Binance 风格错误码
  48. if (error?.code) return String(error.code)
  49. // HTTP 状态码
  50. if (error?.status) return `HTTP_${error.status}`
  51. if (error?.response?.status) return `HTTP_${error.response.status}`
  52. // Pacifica 特殊情况
  53. if (exchange === 'pacifica' && error?.message?.includes('403')) return 'HTTP_403'
  54. // Aster 特殊情况
  55. if (exchange === 'aster' && error?.message?.includes('-2019')) return 'INSUFFICIENT_MARGIN'
  56. return 'UNKNOWN_ERROR'
  57. }
  58. /**
  59. * 分析错误属性
  60. */
  61. private static analyzeError(
  62. code: string,
  63. exchange: string,
  64. ): { retryable: boolean; severity: 'low' | 'medium' | 'high' | 'critical' } {
  65. // 通用 HTTP 错误
  66. if (code.startsWith('HTTP_')) {
  67. const statusCode = parseInt(code.replace('HTTP_', ''))
  68. if (statusCode >= 500) return { retryable: true, severity: 'high' }
  69. if (statusCode === 429) return { retryable: true, severity: 'medium' }
  70. if (statusCode === 403) return { retryable: false, severity: 'medium' }
  71. if (statusCode === 401) return { retryable: false, severity: 'high' }
  72. return { retryable: false, severity: 'medium' }
  73. }
  74. // 交易所特定错误
  75. if (exchange === 'binance') {
  76. return this.analyzeBinanceError(code)
  77. } else if (exchange === 'aster') {
  78. return this.analyzeAsterError(code)
  79. } else if (exchange === 'pacifica') {
  80. return this.analyzePacificaError(code)
  81. }
  82. return { retryable: false, severity: 'medium' }
  83. }
  84. /**
  85. * 分析币安错误
  86. */
  87. private static analyzeBinanceError(code: string): {
  88. retryable: boolean
  89. severity: 'low' | 'medium' | 'high' | 'critical'
  90. } {
  91. const retryableCodes = ['-1001', '-1021', '-2013', '-2015'] // 网络、时间同步等
  92. const criticalCodes = ['-1022', '-2010', '-4028'] // API密钥、余额不足等
  93. const marginCodes = ['-2019', '-4061'] // 保证金相关
  94. if (retryableCodes.includes(code)) return { retryable: true, severity: 'medium' }
  95. if (criticalCodes.includes(code)) return { retryable: false, severity: 'critical' }
  96. if (marginCodes.includes(code)) return { retryable: false, severity: 'high' }
  97. return { retryable: false, severity: 'medium' }
  98. }
  99. /**
  100. * 分析Aster错误
  101. */
  102. private static analyzeAsterError(code: string): {
  103. retryable: boolean
  104. severity: 'low' | 'medium' | 'high' | 'critical'
  105. } {
  106. if (code === 'INSUFFICIENT_MARGIN') return { retryable: false, severity: 'high' }
  107. if (code === 'missing_user_signer_privateKey') return { retryable: false, severity: 'critical' }
  108. if (code === 'listenKeyExpired') return { retryable: true, severity: 'medium' }
  109. return { retryable: false, severity: 'medium' }
  110. }
  111. /**
  112. * 分析Pacifica错误
  113. */
  114. private static analyzePacificaError(code: string): {
  115. retryable: boolean
  116. severity: 'low' | 'medium' | 'high' | 'critical'
  117. } {
  118. if (code === 'HTTP_403') return { retryable: false, severity: 'medium' } // CDN/白名单
  119. if (code === 'orderbook_not_found') return { retryable: true, severity: 'medium' }
  120. return { retryable: false, severity: 'medium' }
  121. }
  122. /**
  123. * 记录错误日志
  124. */
  125. private static logError(error: AdapterError, method: string) {
  126. const level =
  127. error.severity === 'critical'
  128. ? 'error'
  129. : error.severity === 'high'
  130. ? 'error'
  131. : error.severity === 'medium'
  132. ? 'warn'
  133. : 'info'
  134. const logMessage = `${error.message} [代码: ${error.code}, 可重试: ${error.retryable}]`
  135. // 这里可以集成实际的日志系统
  136. if (level === 'error') {
  137. console.error(`❌ ${logMessage}`)
  138. } else if (level === 'warn') {
  139. console.warn(`⚠️ ${logMessage}`)
  140. } else {
  141. console.info(`ℹ️ ${logMessage}`)
  142. }
  143. }
  144. /**
  145. * 检查是否应该重试
  146. */
  147. static shouldRetry(error: AdapterError, attemptCount: number, maxAttempts = 3): boolean {
  148. return error.retryable && attemptCount < maxAttempts
  149. }
  150. /**
  151. * 计算重试延迟(指数退避)
  152. */
  153. static getRetryDelay(attemptCount: number, baseDelayMs = 1000, maxDelayMs = 10000): number {
  154. const delay = Math.min(baseDelayMs * Math.pow(2, attemptCount - 1), maxDelayMs)
  155. // 添加随机抖动避免雷群效应
  156. return delay + Math.random() * 1000
  157. }
  158. }
  159. /**
  160. * 装饰器:为适配器方法添加错误处理和重试逻辑
  161. */
  162. export function withErrorHandling(exchange: string, maxRetries = 2) {
  163. return function (target: any, propertyName: string, descriptor: PropertyDescriptor) {
  164. const method = descriptor.value
  165. descriptor.value = async function (...args: any[]) {
  166. let lastError: AdapterError | null = null
  167. for (let attempt = 1; attempt <= maxRetries + 1; attempt++) {
  168. try {
  169. return await method.apply(this, args)
  170. } catch (error) {
  171. const adapterError = AdapterErrorHandler.handleError(exchange, error, propertyName)
  172. lastError = adapterError
  173. if (!AdapterErrorHandler.shouldRetry(adapterError, attempt, maxRetries + 1)) {
  174. throw adapterError
  175. }
  176. const delay = AdapterErrorHandler.getRetryDelay(attempt)
  177. console.info(`🔄 [${exchange}:${propertyName}] 重试第 ${attempt} 次,${delay}ms 后重试`)
  178. await new Promise(resolve => setTimeout(resolve, delay))
  179. }
  180. }
  181. throw lastError
  182. }
  183. }
  184. }