/** * 统一代理工具 — 支持 HTTP/HTTPS/SOCKS5/SOCKS5h 协议 * * Node 20+ 原生 fetch() 使用 undici Dispatcher,但 socks-proxy-agent 不实现此接口。 * 解决方案:使用 http.Agent 接口 + http/https.request()。 */ let HttpsProxyAgent: any; let SocksProxyAgent: any; try { HttpsProxyAgent = require('https-proxy-agent').HttpsProxyAgent; } catch { try { HttpsProxyAgent = require('https-proxy-agent'); } catch {} } try { SocksProxyAgent = require('socks-proxy-agent').SocksProxyAgent; } catch { try { SocksProxyAgent = require('socks-proxy-agent'); } catch {} } /** Create an http.Agent for the given proxy URL (works with https.request) */ function createProxyAgent(proxyUrl: string): any | null { if (!proxyUrl || typeof proxyUrl !== 'string') return null; const trimmed = proxyUrl.trim(); if (!trimmed) return null; const lower = trimmed.toLowerCase(); try { if (lower.startsWith('socks5://') || lower.startsWith('socks5h://')) { if (!SocksProxyAgent) { console.warn('[Proxy] socks-proxy-agent not installed'); return null; } return new SocksProxyAgent(trimmed); } if (lower.startsWith('http://') || lower.startsWith('https://')) { if (!HttpsProxyAgent) { console.warn('[Proxy] No HTTP proxy agent available'); return null; } return new HttpsProxyAgent(trimmed); } // Unknown scheme — try as HTTP proxy if (HttpsProxyAgent) return new HttpsProxyAgent(trimmed); return null; } catch (err: any) { console.error(`[Proxy] Failed to create proxy agent: ${err.message}`); return null; } } /** * Fetch with proxy support. * Uses native fetch() when no proxy, or http/https.request() with agent when proxy is set. */ export async function proxiedFetch( url: string, init?: RequestInit, proxyUrl?: string, ): Promise { if (!proxyUrl) return fetch(url, init); const agent = createProxyAgent(proxyUrl); if (!agent) return fetch(url, init); const parsedUrl = new URL(url); const mod = parsedUrl.protocol === 'https:' ? require('https') : require('http'); return new Promise((resolve, reject) => { const headers: Record = {}; if (init?.headers) { const h = init.headers as any; if (h instanceof Headers) { h.forEach((v, k) => { headers[k] = v; }); } else if (typeof h === 'object') { Object.assign(headers, h); } } const options: any = { hostname: parsedUrl.hostname, port: parsedUrl.port || (parsedUrl.protocol === 'https:' ? 443 : 80), path: parsedUrl.pathname + parsedUrl.search, method: init?.method || 'GET', headers, agent, }; const req = mod.request(options, (res: any) => { const chunks: Buffer[] = []; res.on('data', (c: Buffer) => chunks.push(c)); res.on('end', () => { const body = Buffer.concat(chunks); resolve(new Response(body, { status: res.statusCode || 502, statusText: res.statusMessage || '', headers: new Headers(res.headers || {}), })); }); }); req.on('error', reject); if (init?.signal) { init.signal.addEventListener('abort', () => req.destroy()); } if (init?.body) { req.write( typeof init.body === 'string' ? init.body : init.body instanceof Buffer ? init.body : init.body instanceof ArrayBuffer ? Buffer.from(init.body) : Buffer.from(String(init.body)) ); } req.end(); }); } export async function testProxyConnection( proxyUrl: string, testUrl?: string, ): Promise<{ ok: boolean; latency: number; info: string }> { const target = testUrl || 'https://www.baidu.com'; const start = Date.now(); try { const res = await proxiedFetch(target, { signal: AbortSignal.timeout(10000), }, proxyUrl); const latency = Date.now() - start; return { ok: true, latency, info: `连接成功 (${res.status})` }; } catch (err: any) { return { ok: false, latency: Date.now() - start, info: `代理连接失败: ${err.message}` }; } } // Legacy compat — no longer returns dispatcher, kept for type compatibility export function createProxyDispatcher(proxyUrl: string): { agent?: any } | null { const agent = createProxyAgent(proxyUrl); return agent ? { agent } : null; }