From 6067a396c29ae995e6faeeb113141635fffb8a59 Mon Sep 17 00:00:00 2001 From: foxhui Date: Sun, 28 Dec 2025 22:22:22 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=88=9D=E5=A7=8B=E5=8C=96=E8=84=9A?= =?UTF-8?q?=E6=9C=AC=E5=B0=8F=E5=AE=BD=E5=B8=A6=E4=B8=8B=E8=BD=BD=E8=B6=85?= =?UTF-8?q?=E6=97=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/init.js | 333 ++++++++++++++++++++++++++++++++++-------------- 1 file changed, 238 insertions(+), 95 deletions(-) diff --git a/scripts/init.js b/scripts/init.js index f00b273..20ec123 100644 --- a/scripts/init.js +++ b/scripts/init.js @@ -13,11 +13,13 @@ import fs from 'fs'; import path from 'path'; import os from 'os'; +import https from 'https'; +import http from 'http'; import { fileURLToPath } from 'url'; -import { gotScraping } from 'got-scraping'; import compressing from 'compressing'; import { logger } from '../src/utils/logger.js'; import { select, input } from '@inquirer/prompts'; +import { anonymizeProxy, closeAnonymizedProxy } from 'proxy-chain'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const PROJECT_ROOT = path.join(__dirname, '..'); @@ -144,135 +146,276 @@ function validateABI(abi) { * 下载文件(带进度,流式,支持重试) * @param {string} url - 下载地址 * @param {string} destPath - 目标文件路径 - * @param {string|null} proxyUrl - 代理 URL + * @param {string|null} proxyUrl - 代理 URL(支持 http:// 和 socks5://) * @param {number} maxRetries - 最大重试次数 */ async function downloadFile(url, destPath, proxyUrl = null, maxRetries = 3) { - for (let attempt = 1; attempt <= maxRetries; attempt++) { + // 如果是 SOCKS5 代理,先转换为本地 HTTP 代理 + let effectiveProxyUrl = proxyUrl; + let anonymizedProxy = null; + + if (proxyUrl && proxyUrl.startsWith('socks5://')) { try { - if (attempt > 1) { - logger.info('初始化', `第 ${attempt}/${maxRetries} 次尝试下载...`); - // 删除之前失败的文件 - try { - if (fs.existsSync(destPath)) { - fs.unlinkSync(destPath); - } - } catch (e) { } - } else { - logger.info('初始化', `开始下载: ${url}`); - } - - await downloadFileOnce(url, destPath, proxyUrl); - return destPath; + logger.info('初始化', `检测到 SOCKS5 代理,正在转换为 HTTP 代理...`); + anonymizedProxy = await anonymizeProxy(proxyUrl); + effectiveProxyUrl = anonymizedProxy; + logger.info('初始化', `SOCKS5 代理已转换: ${anonymizedProxy}`); } catch (error) { - logger.error('初始化', `下载失败 (尝试 ${attempt}/${maxRetries}): ${error.message}`); + logger.error('初始化', `SOCKS5 代理转换失败: ${error.message}`); + throw error; + } + } - if (attempt === maxRetries) { - throw error; + try { + for (let attempt = 1; attempt <= maxRetries; attempt++) { + try { + if (attempt > 1) { + logger.info('初始化', `第 ${attempt}/${maxRetries} 次尝试下载...`); + // 删除之前失败的文件 + try { + if (fs.existsSync(destPath)) { + fs.unlinkSync(destPath); + } + } catch (e) { } + } else { + logger.info('初始化', `开始下载: ${url}`); + } + + await downloadFileOnce(url, destPath, effectiveProxyUrl); + return destPath; + } catch (error) { + logger.error('初始化', `下载失败 (尝试 ${attempt}/${maxRetries}): ${error.message}`); + + if (attempt === maxRetries) { + throw error; + } + + // 等待后重试(递增延迟) + const delay = attempt * 2000; + logger.info('初始化', `${delay / 1000} 秒后重试...`); + await new Promise(resolve => setTimeout(resolve, delay)); } - - // 等待后重试(递增延迟) - const delay = attempt * 2000; - logger.info('初始化', `${delay / 1000} 秒后重试...`); - await new Promise(resolve => setTimeout(resolve, delay)); + } + } finally { + // 清理 SOCKS5 代理资源 + if (anonymizedProxy) { + try { + await closeAnonymizedProxy(anonymizedProxy, true); + logger.debug('初始化', '已关闭临时代理桥接'); + } catch (e) { } } } } /** * 单次下载尝试(内部函数) + * 使用 Node.js 原生 http/https 模块,手动管理超时 + * 只有在指定时间内没有任何数据传输才会触发超时 */ async function downloadFileOnce(url, destPath, proxyUrl = null) { + const IDLE_TIMEOUT = 180000; // 3 分钟无数据传输才超时 + return new Promise((resolve, reject) => { - const options = { - http2: false, - timeout: { - request: 900000, // 总请求超时 15 分钟 - read: 180000 // 两次数据接收间隔超时 3 分钟 - }, - retry: { - limit: 0 - }, - headerGeneratorOptions: { - browsers: [], - devices: [], - locales: [], - operatingSystems: [] - }, + const urlObj = new URL(url); + const isHttps = urlObj.protocol === 'https:'; + + let requestOptions = { + hostname: urlObj.hostname, + port: urlObj.port || (isHttps ? 443 : 80), + path: urlObj.pathname + urlObj.search, + method: 'GET', headers: { - 'user-agent': 'Wget/1.21.4 (linux-gnu)', - 'accept': '*/*', - 'accept-encoding': 'identity', - 'connection': 'keep-alive' + 'User-Agent': 'Wget/1.21.4 (linux-gnu)', + 'Accept': '*/*', + 'Accept-Encoding': 'identity', + 'Connection': 'keep-alive' } }; - if (proxyUrl) { - options.proxyUrl = proxyUrl; + // 如果有 HTTP 代理(注意:这里只支持 HTTP 代理,SOCKS 需要额外处理) + let httpModule = isHttps ? https : http; + if (proxyUrl && proxyUrl.startsWith('http')) { + const proxyUrlObj = new URL(proxyUrl); + // 使用 CONNECT 隧道代理 + requestOptions = { + hostname: proxyUrlObj.hostname, + port: proxyUrlObj.port || 80, + method: 'CONNECT', + path: `${urlObj.hostname}:${urlObj.port || (isHttps ? 443 : 80)}`, + headers: { + 'Host': `${urlObj.hostname}:${urlObj.port || (isHttps ? 443 : 80)}` + } + }; + if (proxyUrlObj.username) { + const auth = Buffer.from(`${proxyUrlObj.username}:${proxyUrlObj.password || ''}`).toString('base64'); + requestOptions.headers['Proxy-Authorization'] = `Basic ${auth}`; + } + httpModule = http; // 代理连接始终是 HTTP } - const downloadStream = gotScraping.stream(url, options); const fileStream = fs.createWriteStream(destPath); - let downloadedSize = 0; let totalSize = 0; let lastLogTime = Date.now(); + let finished = false; + let idleTimer = null; + let req = null; - downloadStream.on('response', (response) => { - totalSize = parseInt(response.headers['content-length'] || '0', 10); - if (totalSize > 0) { - logger.info('初始化', `文件大小: ${(totalSize / 1024 / 1024).toFixed(2)} MB`); + const resetIdleTimer = () => { + if (idleTimer) clearTimeout(idleTimer); + idleTimer = setTimeout(() => { + if (!finished) { + const error = new Error(`下载超时: ${IDLE_TIMEOUT / 1000} 秒内没有收到任何数据`); + cleanup(); + reject(error); + } + }, IDLE_TIMEOUT); + }; + + const cleanup = () => { + finished = true; + if (idleTimer) clearTimeout(idleTimer); + if (req) { + try { req.destroy(); } catch (e) { } } - }); - - downloadStream.on('data', (chunk) => { - downloadedSize += chunk.length; - - // 每秒更新一次进度 - const now = Date.now(); - if (totalSize > 0 && now - lastLogTime > 1000) { - const percent = ((downloadedSize / totalSize) * 100).toFixed(1); - const downloadedMB = (downloadedSize / 1024 / 1024).toFixed(2); - const totalMB = (totalSize / 1024 / 1024).toFixed(2); - logger.info('初始化', `下载进度: ${percent}% (${downloadedMB}MB / ${totalMB}MB)`); - lastLogTime = now; - } - }); - - downloadStream.on('error', (error) => { fileStream.close(); - try { - fs.unlinkSync(destPath); - } catch (e) { } - reject(error); - }); + }; - fileStream.on('error', (error) => { - reject(error); - }); + const handleResponse = (res) => { + resetIdleTimer(); - fileStream.on('finish', () => { - const finalSize = (downloadedSize / 1024 / 1024).toFixed(2); - - // 验证下载完整性 - if (totalSize > 0 && downloadedSize !== totalSize) { - const errorMsg = `下载不完整: 预期 ${(totalSize / 1024 / 1024).toFixed(2)} MB, 实际 ${finalSize} MB`; - logger.error('初始化', errorMsg); - - // 清理损坏的文件 - try { - fs.unlinkSync(destPath); - } catch (e) { } - - reject(new Error(errorMsg)); + // 处理重定向 + if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { + cleanup(); + try { fs.unlinkSync(destPath); } catch (e) { } + logger.info('初始化', `重定向到: ${res.headers.location}`); + // 递归调用处理重定向 + downloadFileOnce(res.headers.location, destPath, proxyUrl) + .then(resolve) + .catch(reject); return; } - logger.info('初始化', `下载完成: ${finalSize} MB`); - resolve(destPath); - }); + if (res.statusCode !== 200) { + cleanup(); + try { fs.unlinkSync(destPath); } catch (e) { } + reject(new Error(`HTTP 错误: ${res.statusCode}`)); + return; + } - downloadStream.pipe(fileStream); + totalSize = parseInt(res.headers['content-length'] || '0', 10); + if (totalSize > 0) { + logger.info('初始化', `文件大小: ${(totalSize / 1024 / 1024).toFixed(2)} MB`); + } + + res.on('data', (chunk) => { + resetIdleTimer(); + downloadedSize += chunk.length; + + const now = Date.now(); + if (totalSize > 0 && now - lastLogTime > 100) { // 100ms 更新一次,更流畅 + const percent = ((downloadedSize / totalSize) * 100).toFixed(1); + const downloadedMB = (downloadedSize / 1024 / 1024).toFixed(2); + const totalMB = (totalSize / 1024 / 1024).toFixed(2); + // 使用 \r 回到行首,实现单行刷新 + process.stdout.write(`\r下载进度: ${percent}% (${downloadedMB}MB / ${totalMB}MB) `); + lastLogTime = now; + } + }); + + res.on('error', (error) => { + if (finished) return; + cleanup(); + try { fs.unlinkSync(destPath); } catch (e) { } + reject(error); + }); + + res.pipe(fileStream); + + fileStream.on('error', (error) => { + if (finished) return; + cleanup(); + reject(error); + }); + + fileStream.on('finish', () => { + if (finished) return; + finished = true; + if (idleTimer) clearTimeout(idleTimer); + + const finalSize = (downloadedSize / 1024 / 1024).toFixed(2); + + if (totalSize > 0 && downloadedSize !== totalSize) { + process.stdout.write('\n'); // 换行,避免与进度条混在一起 + const errorMsg = `下载不完整: 预期 ${(totalSize / 1024 / 1024).toFixed(2)} MB, 实际 ${finalSize} MB`; + logger.error('初始化', errorMsg); + try { fs.unlinkSync(destPath); } catch (e) { } + reject(new Error(errorMsg)); + return; + } + + process.stdout.write('\n'); // 换行,结束进度条 + logger.info('初始化', `下载完成: ${finalSize} MB`); + resolve(destPath); + }); + }; + + resetIdleTimer(); + + // 如果使用代理,先建立隧道 + if (proxyUrl && proxyUrl.startsWith('http')) { + req = http.request(requestOptions); + req.on('connect', (res, socket) => { + if (res.statusCode !== 200) { + cleanup(); + reject(new Error(`代理连接失败: ${res.statusCode}`)); + return; + } + + // 通过隧道发起真正的 HTTPS 请求 + const tunnelOptions = { + hostname: urlObj.hostname, + port: urlObj.port || (isHttps ? 443 : 80), + path: urlObj.pathname + urlObj.search, + method: 'GET', + headers: { + 'User-Agent': 'Wget/1.21.4 (linux-gnu)', + 'Accept': '*/*', + 'Accept-Encoding': 'identity', + 'Connection': 'keep-alive', + 'Host': urlObj.host + }, + socket: socket, + agent: false + }; + + const tunnelReq = (isHttps ? https : http).request(tunnelOptions, handleResponse); + tunnelReq.on('error', (error) => { + if (finished) return; + cleanup(); + try { fs.unlinkSync(destPath); } catch (e) { } + reject(error); + }); + tunnelReq.end(); + }); + req.on('error', (error) => { + if (finished) return; + cleanup(); + try { fs.unlinkSync(destPath); } catch (e) { } + reject(error); + }); + req.end(); + } else { + // 直连模式 + req = httpModule.request(requestOptions, handleResponse); + req.on('error', (error) => { + if (finished) return; + cleanup(); + try { fs.unlinkSync(destPath); } catch (e) { } + reject(error); + }); + req.end(); + } }); }