const fs = require('fs'); const http = require('http'); const https = require('https'); function getProtocol(url) { return url.startsWith('https') ? https : http; } function fetchJSON(url) { return new Promise((resolve, reject) => { getProtocol(url).get(url, (res) => { let data = ''; res.on('data', chunk => data += chunk); res.on('end', () => { try { resolve(JSON.parse(data)); } catch { reject(new Error(`JSON 解析失败: ${data.slice(0, 200)}`)); } }); }).on('error', reject); }); } function downloadFile(url, dest, showProgress = true) { return new Promise((resolve, reject) => { getProtocol(url).get(url, (res) => { if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { return downloadFile(res.headers.location, dest, showProgress).then(resolve).catch(reject); } if (res.statusCode !== 200) return reject(new Error(`HTTP ${res.statusCode}`)); const total = parseInt(res.headers['content-length'], 10); let downloaded = 0; let lastTime = Date.now(); let lastBytes = 0; const file = fs.createWriteStream(dest); res.on('data', (chunk) => { downloaded += chunk.length; if (!showProgress) return; const now = Date.now(); const elapsed = (now - lastTime) / 1000; if (elapsed < 0.3) return; const speed = (downloaded - lastBytes) / elapsed; lastTime = now; lastBytes = downloaded; const speedStr = speed >= 1048576 ? `${(speed / 1048576).toFixed(1)} MB/s` : `${(speed / 1024).toFixed(0)} KB/s`; if (total) { const pct = downloaded / total; const barWidth = 30; const filled = Math.round(pct * barWidth); const bar = '█'.repeat(filled) + '░'.repeat(barWidth - filled); const eta = speed > 0 ? Math.ceil((total - downloaded) / speed) : 0; const etaStr = eta > 60 ? `${Math.floor(eta / 60)}m${eta % 60}s` : `${eta}s`; process.stdout.write(`\r ⬇️ [${bar}] ${(pct * 100).toFixed(1)}% ${(downloaded / 1048576).toFixed(1)}/${(total / 1048576).toFixed(1)} MB ${speedStr} ETA ${etaStr} `); } else { process.stdout.write(`\r ⬇️ ${(downloaded / 1048576).toFixed(1)} MB ${speedStr} `); } }); res.pipe(file); file.on('finish', () => { file.close(); if (showProgress) console.log(); resolve(); }); file.on('error', (err) => { fs.unlink(dest, () => {}); reject(err); }); }).on('error', reject); }); } function downloadBuffer(url) { return new Promise((resolve, reject) => { getProtocol(url).get(url, (res) => { if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { return downloadBuffer(res.headers.location).then(resolve).catch(reject); } if (res.statusCode !== 200) return reject(new Error(`HTTP ${res.statusCode}`)); const chunks = []; res.on('data', chunk => chunks.push(chunk)); res.on('end', () => resolve(Buffer.concat(chunks))); }).on('error', reject); }); } module.exports = { fetchJSON, downloadFile, downloadBuffer };