|
@@ -10,13 +10,14 @@ class SpeedTester {
|
|
|
this.testUrls = process.env.SPEED_TEST_URLS?.split(',') || [
|
|
|
'https://www.google.com',
|
|
|
'https://www.youtube.com',
|
|
|
- 'https://www.github.com'
|
|
|
+ 'https://www.github.com',
|
|
|
+ 'https://httpbin.org/get'
|
|
|
];
|
|
|
this.timeout = parseInt(process.env.SPEED_TEST_TIMEOUT) || 10000;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 测试单个节点
|
|
|
+ * 测试单个节点连通性
|
|
|
*/
|
|
|
async testNode(node) {
|
|
|
const startTime = Date.now();
|
|
@@ -25,8 +26,8 @@ class SpeedTester {
|
|
|
testTime: new Date(),
|
|
|
isSuccess: false,
|
|
|
latency: null,
|
|
|
- downloadSpeed: null,
|
|
|
- uploadSpeed: null,
|
|
|
+ downloadSpeed: null, // 保留字段但不测试
|
|
|
+ uploadSpeed: null, // 保留字段但不测试
|
|
|
packetLoss: null,
|
|
|
testUrl: null,
|
|
|
errorMessage: null,
|
|
@@ -36,7 +37,7 @@ class SpeedTester {
|
|
|
};
|
|
|
|
|
|
try {
|
|
|
- logger.info(`开始测试节点: ${node.name} (${node.type})`);
|
|
|
+ logger.info(`开始测试节点连通性: ${node.name} (${node.type})`);
|
|
|
|
|
|
// 创建代理配置
|
|
|
const proxyConfig = this.createProxyConfig(node);
|
|
@@ -44,7 +45,7 @@ class SpeedTester {
|
|
|
throw new Error(`不支持的代理类型: ${node.type}`);
|
|
|
}
|
|
|
|
|
|
- // 简化测试流程:只测试基本连接性
|
|
|
+ // 只测试基本连通性
|
|
|
const connectivityResult = await this.testBasicConnectivity(node, proxyConfig);
|
|
|
testResult.isSuccess = connectivityResult.success;
|
|
|
testResult.latency = connectivityResult.latency;
|
|
@@ -55,9 +56,9 @@ class SpeedTester {
|
|
|
testResult.testDuration = Date.now() - startTime;
|
|
|
|
|
|
if (testResult.isSuccess) {
|
|
|
- logger.info(`✅ 节点测试成功: ${node.name} - 延迟: ${testResult.latency}ms`);
|
|
|
+ logger.info(`✅ 节点连通性测试成功: ${node.name} - 延迟: ${testResult.latency}ms - IP: ${testResult.ipAddress}`);
|
|
|
} else {
|
|
|
- logger.warn(`❌ 节点测试失败: ${node.name} - ${testResult.errorMessage || '连接超时'}`);
|
|
|
+ logger.warn(`❌ 节点连通性测试失败: ${node.name} - ${testResult.errorMessage || '连接超时'}`);
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
@@ -111,7 +112,6 @@ class SpeedTester {
|
|
|
type: 'ss',
|
|
|
password: node.password,
|
|
|
method: node.cipher || node.method,
|
|
|
- // 尝试使用HTTP代理模式
|
|
|
auth: node.username && node.password ? {
|
|
|
username: node.username,
|
|
|
password: node.password
|
|
@@ -130,7 +130,6 @@ class SpeedTester {
|
|
|
method: node.cipher || node.method,
|
|
|
protocol: node.protocol,
|
|
|
obfs: node.obfs,
|
|
|
- // 尝试使用HTTP代理模式
|
|
|
auth: node.username && node.password ? {
|
|
|
username: node.username,
|
|
|
password: node.password
|
|
@@ -152,7 +151,6 @@ class SpeedTester {
|
|
|
sni: node.sni,
|
|
|
wsPath: node.wsPath,
|
|
|
wsHeaders: node.wsHeaders ? JSON.parse(node.wsHeaders) : {},
|
|
|
- // 尝试使用HTTP代理模式
|
|
|
auth: node.username && node.password ? {
|
|
|
username: node.username,
|
|
|
password: node.password
|
|
@@ -170,7 +168,6 @@ class SpeedTester {
|
|
|
password: node.password,
|
|
|
tls: node.tls,
|
|
|
sni: node.sni,
|
|
|
- // 尝试使用HTTP代理模式
|
|
|
auth: node.username && node.password ? {
|
|
|
username: node.username,
|
|
|
password: node.password
|
|
@@ -222,149 +219,95 @@ class SpeedTester {
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 测试延迟
|
|
|
- */
|
|
|
- async testLatency(node, proxyConfig) {
|
|
|
- try {
|
|
|
- const startTime = Date.now();
|
|
|
-
|
|
|
- // 使用HEAD请求测试延迟
|
|
|
- const response = await this.makeRequest(node, proxyConfig, {
|
|
|
- method: 'HEAD',
|
|
|
- url: this.testUrls[0],
|
|
|
- timeout: 5000
|
|
|
- });
|
|
|
-
|
|
|
- const latency = Date.now() - startTime;
|
|
|
-
|
|
|
- logger.debug(`延迟测试: ${node.name} - ${latency}ms`);
|
|
|
-
|
|
|
- return {
|
|
|
- latency: latency,
|
|
|
- packetLoss: 0 // 简化实现,实际需要多次测试计算丢包率
|
|
|
- };
|
|
|
- } catch (error) {
|
|
|
- logger.debug(`延迟测试失败: ${node.name} - ${error.message}`);
|
|
|
- return {
|
|
|
- latency: null,
|
|
|
- packetLoss: 100
|
|
|
- };
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 测试基本连接性(简化版本)
|
|
|
+ * 测试基本连通性
|
|
|
*/
|
|
|
async testBasicConnectivity(node, proxyConfig) {
|
|
|
const startTime = Date.now();
|
|
|
|
|
|
- try {
|
|
|
- // 尝试连接Google
|
|
|
- const response = await this.makeRequest(node, proxyConfig, {
|
|
|
- method: 'HEAD',
|
|
|
- url: 'https://www.google.com',
|
|
|
- timeout: 10000,
|
|
|
- maxRedirects: 3
|
|
|
- });
|
|
|
-
|
|
|
- const latency = Date.now() - startTime;
|
|
|
-
|
|
|
- return {
|
|
|
- success: true,
|
|
|
- latency: latency,
|
|
|
- url: 'https://www.google.com',
|
|
|
- ipAddress: this.extractIpAddress(response),
|
|
|
- location: this.extractLocation(response)
|
|
|
- };
|
|
|
- } catch (error) {
|
|
|
- // 如果Google失败,尝试其他URL
|
|
|
+ // 如果是直连测试
|
|
|
+ if (node.type === 'direct') {
|
|
|
+ return this.testDirectConnectivity();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 尝试多个测试URL
|
|
|
for (const url of this.testUrls) {
|
|
|
try {
|
|
|
+ logger.debug(`尝试连接: ${node.name} -> ${url}`);
|
|
|
+
|
|
|
const response = await this.makeRequest(node, proxyConfig, {
|
|
|
- method: 'HEAD',
|
|
|
+ method: 'HEAD',
|
|
|
url: url,
|
|
|
- timeout: 10000,
|
|
|
+ timeout: this.timeout,
|
|
|
maxRedirects: 3
|
|
|
});
|
|
|
|
|
|
- const latency = Date.now() - startTime;
|
|
|
-
|
|
|
+ const latency = Date.now() - startTime;
|
|
|
+
|
|
|
+ logger.debug(`连接成功: ${node.name} -> ${url} (${latency}ms)`);
|
|
|
+
|
|
|
return {
|
|
|
success: true,
|
|
|
- latency: latency,
|
|
|
+ latency: latency,
|
|
|
url: url,
|
|
|
ipAddress: this.extractIpAddress(response),
|
|
|
location: this.extractLocation(response)
|
|
|
};
|
|
|
- } catch (innerError) {
|
|
|
- logger.debug(`连接测试失败: ${node.name} - ${url} - ${innerError.message}`);
|
|
|
+ } catch (error) {
|
|
|
+ logger.debug(`连接失败: ${node.name} -> ${url} - ${error.message}`);
|
|
|
continue;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return {
|
|
|
success: false,
|
|
|
- latency: null,
|
|
|
+ latency: null,
|
|
|
url: null,
|
|
|
ipAddress: null,
|
|
|
location: null
|
|
|
};
|
|
|
- }
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 测试下载速度
|
|
|
+ * 测试直连连通性
|
|
|
*/
|
|
|
- async testDownloadSpeed(node, proxyConfig) {
|
|
|
- try {
|
|
|
- // 测试下载速度(使用更大的文件以获得更准确的结果)
|
|
|
- const downloadStart = Date.now();
|
|
|
- const downloadResponse = await this.makeRequest(node, proxyConfig, {
|
|
|
- method: 'GET',
|
|
|
- url: 'https://httpbin.org/bytes/1048576', // 1MB测试文件
|
|
|
- timeout: this.timeout,
|
|
|
- responseType: 'arraybuffer'
|
|
|
- });
|
|
|
-
|
|
|
- const downloadDuration = Date.now() - downloadStart;
|
|
|
- const downloadSize = downloadResponse.data.byteLength;
|
|
|
-
|
|
|
- // 计算下载速度 (bytes per second)
|
|
|
- const downloadSpeedBps = downloadSize / (downloadDuration / 1000);
|
|
|
-
|
|
|
- // 转换为Mbps
|
|
|
- const downloadSpeedMbps = (downloadSpeedBps * 8) / (1024 * 1024);
|
|
|
-
|
|
|
- // 测试上传速度(简化实现,使用POST请求)
|
|
|
- const uploadStart = Date.now();
|
|
|
- const testData = Buffer.alloc(1024 * 1024); // 1MB测试数据
|
|
|
- await this.makeRequest(node, proxyConfig, {
|
|
|
- method: 'POST',
|
|
|
- url: 'https://httpbin.org/post',
|
|
|
- data: testData,
|
|
|
- timeout: this.timeout,
|
|
|
- headers: {
|
|
|
- 'Content-Type': 'application/octet-stream'
|
|
|
- }
|
|
|
- });
|
|
|
-
|
|
|
- const uploadDuration = Date.now() - uploadStart;
|
|
|
- const uploadSpeedBps = testData.length / (uploadDuration / 1000);
|
|
|
- const uploadSpeedMbps = (uploadSpeedBps * 8) / (1024 * 1024);
|
|
|
-
|
|
|
- logger.debug(`速度测试: ${node.name} - 下载: ${Math.round(downloadSpeedMbps * 100) / 100}Mbps, 上传: ${Math.round(uploadSpeedMbps * 100) / 100}Mbps`);
|
|
|
+ async testDirectConnectivity() {
|
|
|
+ const startTime = Date.now();
|
|
|
+
|
|
|
+ for (const url of this.testUrls) {
|
|
|
+ try {
|
|
|
+ logger.debug(`尝试直连: ${url}`);
|
|
|
+
|
|
|
+ const response = await axios.get(url, {
|
|
|
+ timeout: this.timeout,
|
|
|
+ headers: {
|
|
|
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
|
|
|
+ }
|
|
|
+ });
|
|
|
|
|
|
- return {
|
|
|
- downloadSpeed: Math.round(downloadSpeedMbps * 100) / 100,
|
|
|
- uploadSpeed: Math.round(uploadSpeedMbps * 100) / 100
|
|
|
- };
|
|
|
- } catch (error) {
|
|
|
- logger.debug(`速度测试失败: ${node.name} - ${error.message}`);
|
|
|
- return {
|
|
|
- downloadSpeed: null,
|
|
|
- uploadSpeed: null
|
|
|
- };
|
|
|
+ const latency = Date.now() - startTime;
|
|
|
+
|
|
|
+ logger.debug(`直连成功: ${url} (${latency}ms)`);
|
|
|
+
|
|
|
+ return {
|
|
|
+ success: true,
|
|
|
+ latency: latency,
|
|
|
+ url: url,
|
|
|
+ ipAddress: this.extractIpAddress(response),
|
|
|
+ location: this.extractLocation(response)
|
|
|
+ };
|
|
|
+ } catch (error) {
|
|
|
+ logger.debug(`直连失败: ${url} - ${error.message}`);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
}
|
|
|
+
|
|
|
+ return {
|
|
|
+ success: false,
|
|
|
+ latency: null,
|
|
|
+ url: null,
|
|
|
+ ipAddress: null,
|
|
|
+ location: null
|
|
|
+ };
|
|
|
}
|
|
|
|
|
|
/**
|
|
@@ -393,7 +336,7 @@ class SpeedTester {
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 创建代理Agent(增强版本)
|
|
|
+ * 创建代理Agent
|
|
|
*/
|
|
|
createProxyAgent(node, proxyConfig) {
|
|
|
try {
|
|
@@ -403,14 +346,28 @@ class SpeedTester {
|
|
|
case 'http':
|
|
|
case 'https':
|
|
|
// HTTP/HTTPS代理
|
|
|
- const httpProxyUrl = `${proxyConfig.type}://${proxyConfig.host}:${proxyConfig.port}`;
|
|
|
+ let httpProxyUrl = `${proxyConfig.type}://${proxyConfig.host}:${proxyConfig.port}`;
|
|
|
+
|
|
|
+ // 如果有认证信息,添加到URL中
|
|
|
+ if (proxyConfig.auth) {
|
|
|
+ const { username, password } = proxyConfig.auth;
|
|
|
+ httpProxyUrl = `${proxyConfig.type}://${username}:${password}@${proxyConfig.host}:${proxyConfig.port}`;
|
|
|
+ }
|
|
|
+
|
|
|
const httpAgent = new HttpsProxyAgent(httpProxyUrl);
|
|
|
logger.debug(`HTTP代理创建成功: ${node.name} - ${httpProxyUrl}`);
|
|
|
return httpAgent;
|
|
|
|
|
|
case 'socks5':
|
|
|
// SOCKS5代理
|
|
|
- const socksProxyUrl = `socks5://${proxyConfig.host}:${proxyConfig.port}`;
|
|
|
+ let socksProxyUrl = `socks5://${proxyConfig.host}:${proxyConfig.port}`;
|
|
|
+
|
|
|
+ // 如果有认证信息,添加到URL中
|
|
|
+ if (proxyConfig.auth) {
|
|
|
+ const { username, password } = proxyConfig.auth;
|
|
|
+ socksProxyUrl = `socks5://${username}:${password}@${proxyConfig.host}:${proxyConfig.port}`;
|
|
|
+ }
|
|
|
+
|
|
|
const socksAgent = new SocksProxyAgent(socksProxyUrl);
|
|
|
logger.debug(`SOCKS5代理创建成功: ${node.name} - ${socksProxyUrl}`);
|
|
|
return socksAgent;
|
|
@@ -419,26 +376,19 @@ class SpeedTester {
|
|
|
case 'ssr':
|
|
|
case 'vmess':
|
|
|
case 'trojan':
|
|
|
- // 对于这些高级代理类型,我们尝试使用HTTP代理方式
|
|
|
- // 这需要代理服务器支持HTTP代理模式
|
|
|
- logger.info(`尝试HTTP代理模式: ${proxyConfig.type} - ${node.name}`);
|
|
|
+ // 对于这些高级代理类型,我们需要通过本地Clash代理来测试
|
|
|
+ // 因为Node.js没有直接的vmess/ss客户端库
|
|
|
+ logger.info(`通过本地Clash代理测试: ${proxyConfig.type} - ${node.name}`);
|
|
|
|
|
|
- // 构建代理URL
|
|
|
- let proxyUrl = `http://${proxyConfig.host}:${proxyConfig.port}`;
|
|
|
-
|
|
|
- // 如果有认证信息,添加到URL中
|
|
|
- if (proxyConfig.auth) {
|
|
|
- const { username, password } = proxyConfig.auth;
|
|
|
- proxyUrl = `http://${username}:${password}@${proxyConfig.host}:${proxyConfig.port}`;
|
|
|
- }
|
|
|
-
|
|
|
- const advancedAgent = new HttpsProxyAgent(proxyUrl);
|
|
|
- logger.debug(`高级代理创建成功: ${node.name} - ${proxyUrl}`);
|
|
|
- return advancedAgent;
|
|
|
+ // 使用本地Clash混合端口
|
|
|
+ const localProxyUrl = 'http://127.0.0.1:7897';
|
|
|
+ const localAgent = new HttpsProxyAgent(localProxyUrl);
|
|
|
+ logger.debug(`使用本地Clash代理: ${node.name} - ${localProxyUrl}`);
|
|
|
+ return localAgent;
|
|
|
|
|
|
default:
|
|
|
logger.warn(`不支持的代理类型: ${proxyConfig.type} - ${node.name}`);
|
|
|
- return null;
|
|
|
+ return null;
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
@@ -454,6 +404,7 @@ class SpeedTester {
|
|
|
// 从响应头中提取IP地址
|
|
|
return response.headers['x-forwarded-for'] ||
|
|
|
response.headers['x-real-ip'] ||
|
|
|
+ response.headers['cf-connecting-ip'] ||
|
|
|
'unknown';
|
|
|
}
|
|
|
|
|
@@ -462,7 +413,13 @@ class SpeedTester {
|
|
|
*/
|
|
|
extractLocation(response) {
|
|
|
// 从响应头中提取位置信息
|
|
|
- return response.headers['cf-ray'] ? 'Cloudflare' : 'unknown';
|
|
|
+ if (response.headers['cf-ray']) {
|
|
|
+ return 'Cloudflare';
|
|
|
+ }
|
|
|
+ if (response.headers['cf-ipcountry']) {
|
|
|
+ return response.headers['cf-ipcountry'];
|
|
|
+ }
|
|
|
+ return 'unknown';
|
|
|
}
|
|
|
|
|
|
/**
|
|
@@ -471,8 +428,9 @@ class SpeedTester {
|
|
|
async saveTestResult(testResult) {
|
|
|
try {
|
|
|
await TestResult.create(testResult);
|
|
|
+ logger.debug(`测试结果已保存: ${testResult.nodeId}`);
|
|
|
} catch (error) {
|
|
|
- logger.error('保存测试结果失败', { error: error.message, testResult });
|
|
|
+ logger.error(`保存测试结果失败: ${error.message}`);
|
|
|
}
|
|
|
}
|
|
|
|
|
@@ -480,25 +438,35 @@ class SpeedTester {
|
|
|
* 批量测试节点
|
|
|
*/
|
|
|
async testNodes(nodes) {
|
|
|
- const results = [];
|
|
|
+ logger.info(`开始批量测试 ${nodes.length} 个节点`);
|
|
|
|
|
|
- // 并发测试,但限制并发数
|
|
|
- const concurrency = 5;
|
|
|
- const chunks = this.chunkArray(nodes, concurrency);
|
|
|
+ const results = [];
|
|
|
+ const concurrency = parseInt(process.env.CONCURRENCY) || 5;
|
|
|
|
|
|
- for (const chunk of chunks) {
|
|
|
- const chunkPromises = chunk.map(node => this.testNode(node));
|
|
|
- const chunkResults = await Promise.allSettled(chunkPromises);
|
|
|
+ // 分批处理,控制并发数
|
|
|
+ for (let i = 0; i < nodes.length; i += concurrency) {
|
|
|
+ const batch = nodes.slice(i, i + concurrency);
|
|
|
+ const batchPromises = batch.map(node => this.testNode(node));
|
|
|
+
|
|
|
+ const batchResults = await Promise.allSettled(batchPromises);
|
|
|
|
|
|
- for (const result of chunkResults) {
|
|
|
+ batchResults.forEach((result, index) => {
|
|
|
if (result.status === 'fulfilled') {
|
|
|
results.push(result.value);
|
|
|
} else {
|
|
|
- logger.error('节点测试失败', { error: result.reason });
|
|
|
+ logger.error(`节点测试失败: ${batch[index].name} - ${result.reason}`);
|
|
|
}
|
|
|
+ });
|
|
|
+
|
|
|
+ // 批次间延迟,避免过于频繁的请求
|
|
|
+ if (i + concurrency < nodes.length) {
|
|
|
+ await new Promise(resolve => setTimeout(resolve, 1000));
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ const successCount = results.filter(r => r.isSuccess).length;
|
|
|
+ logger.info(`批量测试完成: ${successCount}/${nodes.length} 个节点连通`);
|
|
|
+
|
|
|
return results;
|
|
|
}
|
|
|
|