import { HttpsProxyAgent } from 'https-proxy-agent' import { Config } from '../config/simpleEnv.js' import { logger } from './logger.js' /** * 统一的HTTP客户端,支持proxy和重试 */ export class HttpClient { constructor() { this.defaultTimeout = 30000 this.defaultRetries = 3 this.defaultRetryDelay = 1000 this.initializeProxy() } static getInstance() { if (!HttpClient.instance) { HttpClient.instance = new HttpClient() } return HttpClient.instance } initializeProxy() { if (Config.proxy.isAnyConfigured()) { logger.info('HTTP代理系统已初始化') if (Config.proxy.isConfigured()) { logger.info(`全局代理: ${Config.proxy.protocol()}://${Config.proxy.host()}:${Config.proxy.port()}`) } if (Config.proxy.aster.isConfigured()) { logger.info( `Aster专用代理: ${Config.proxy.aster.protocol()}://${Config.proxy.aster.host()}:${Config.proxy.aster.port()}`, ) } } else { logger.info('HTTP代理未配置,使用直连') } } /** * 重新初始化代理 (当环境变量发生变化时调用) */ reinitializeProxy() { this.proxyAgent = undefined this.initializeProxy() } /** * 检查HTTP状态码是否应该重试 */ shouldRetry(status) { return status >= 500 || status === 429 || status === 408 || status === 0 } /** * 等待指定时间 */ sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)) } /** * 发送HTTP请求 */ async request(url, options = {}) { const { method = 'GET', headers = {}, body, timeout = this.defaultTimeout, signal, retries = this.defaultRetries, retryDelay = this.defaultRetryDelay, useProxy = true, exchange, } = options const maxRetries = Math.max(0, retries) let lastError // 请求配置 const fetchOptions = { method, headers: { 'User-Agent': 'Binance-Multi-Exchange-Client/1.0', ...headers, }, signal, } // 添加请求体 if (body && method !== 'GET') { if (typeof body === 'object') { fetchOptions.body = JSON.stringify(body) fetchOptions.headers = { 'Content-Type': 'application/json', ...fetchOptions.headers, } } else { fetchOptions.body = body } } // 添加代理(仅在Node.js环境中) if (useProxy && typeof global !== 'undefined') { const proxyUrl = Config.proxy.getUrl(exchange) if (proxyUrl) { const proxyAgent = new HttpsProxyAgent(proxyUrl) fetchOptions.agent = proxyAgent if (Config.isDev()) { logger.debug(`使用代理发送HTTP请求`, { exchange: exchange || 'global', proxyHost: exchange === 'aster' ? Config.proxy.aster.host() : Config.proxy.host(), method, url: url.length > 100 ? url.substring(0, 100) + '...' : url, }) } } } for (let attempt = 0; attempt <= maxRetries; attempt++) { try { // 创建超时控制 const controller = new AbortController() const timeoutId = setTimeout(() => controller.abort(), timeout) const requestSignal = signal || controller.signal const startTime = Date.now() const response = await fetch(url, { ...fetchOptions, signal: requestSignal, }) clearTimeout(timeoutId) const duration = Date.now() - startTime // 读取响应数据 const contentType = response.headers.get('content-type') || '' let data if (contentType.includes('application/json')) { data = await response.json() } else { data = await response.text() } // 记录请求日志 if (Config.isDev()) { logger.debug(`HTTP ${method} ${url}`, { status: response.status, duration: `${duration}ms`, attempt: attempt + 1, proxy: useProxy && this.proxyAgent ? 'enabled' : 'disabled', }) } // 检查是否需要重试 if (!response.ok && this.shouldRetry(response.status) && attempt < maxRetries) { const jitter = Math.random() * 500 // 0-500ms随机延迟 const delay = retryDelay * Math.pow(2, attempt) + jitter logger.warn(`HTTP请求失败,将在${Math.round(delay)}ms后重试`, { url, status: response.status, attempt: attempt + 1, maxRetries: maxRetries + 1, }) await this.sleep(delay) continue } return { status: response.status, statusText: response.statusText, ok: response.ok, data, headers: response.headers, } } catch (error) { lastError = error const isTimeout = error.name === 'AbortError' const isNetworkError = error.message?.includes('fetch') // 记录错误日志 logger.error(`HTTP请求异常`, { url, method, attempt: attempt + 1, error: error.message, isTimeout, isNetworkError, }) // 检查是否需要重试 if ((isTimeout || isNetworkError) && attempt < maxRetries) { const jitter = Math.random() * 500 const delay = retryDelay * Math.pow(2, attempt) + jitter logger.warn(`网络错误,将在${Math.round(delay)}ms后重试`, { url, attempt: attempt + 1, maxRetries: maxRetries + 1, }) await this.sleep(delay) continue } // 不能重试,抛出错误 throw error } } // 所有重试都失败了 throw lastError || new Error(`HTTP请求失败,已重试${maxRetries}次`) } /** * GET 请求 */ async get(url, options = {}) { return this.request(url, { ...options, method: 'GET' }) } /** * POST 请求 */ async post(url, body, options = {}) { return this.request(url, { ...options, method: 'POST', body }) } /** * PUT 请求 */ async put(url, body, options = {}) { return this.request(url, { ...options, method: 'PUT', body }) } /** * DELETE 请求 */ async delete(url, options = {}) { return this.request(url, { ...options, method: 'DELETE' }) } } /** * 全局HTTP客户端实例 */ export const httpClient = HttpClient.getInstance() /** * 便捷方法:使用代理的fetch */ export async function fetchWithProxy(url, options = {}) { return httpClient.request(url, options) } /** * 便捷方法:GET请求with proxy */ export async function getWithProxy(url, options = {}) { const response = await httpClient.get(url, options) if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`) } return response.data } /** * 便捷方法:POST请求with proxy */ export async function postWithProxy(url, body, options = {}) { const response = await httpClient.post(url, body, options) if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`) } return response.data }