|
@@ -78,7 +78,7 @@ class Scheduler {
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 运行速度测试
|
|
|
+ * 运行速度测试 - 按订阅分组
|
|
|
*/
|
|
|
async runSpeedTest() {
|
|
|
if (this.isRunning) {
|
|
@@ -90,63 +90,49 @@ class Scheduler {
|
|
|
const startTime = Date.now();
|
|
|
|
|
|
try {
|
|
|
- // 获取所有启用的节点
|
|
|
- const nodes = await Node.findAll({
|
|
|
+ // 获取所有活跃的订阅
|
|
|
+ const { Subscription } = require('../models');
|
|
|
+ const subscriptions = await Subscription.findAll({
|
|
|
where: { isActive: true },
|
|
|
- order: [['group', 'ASC'], ['name', 'ASC']]
|
|
|
+ order: [['createdAt', 'ASC']]
|
|
|
});
|
|
|
|
|
|
- if (nodes.length === 0) {
|
|
|
- logger.warn('没有找到启用的节点');
|
|
|
+ if (subscriptions.length === 0) {
|
|
|
+ logger.warn('没有找到活跃的订阅');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- // 批量测试节点
|
|
|
- const testResults = await this.speedTester.testNodes(nodes);
|
|
|
+ logger.info(`开始按订阅分组测试,共 ${subscriptions.length} 个订阅`);
|
|
|
|
|
|
- // 检查是否有高延迟节点需要重测
|
|
|
- const highLatencyNodes = [];
|
|
|
- const highLatencyResults = [];
|
|
|
-
|
|
|
- for (let i = 0; i < nodes.length; i++) {
|
|
|
- const result = testResults[i];
|
|
|
- if (result && result.isSuccess && result.latency && result.latency > 2000) {
|
|
|
- highLatencyNodes.push(nodes[i]);
|
|
|
- highLatencyResults.push(result);
|
|
|
+ const allResults = [];
|
|
|
+ const subscriptionResults = [];
|
|
|
+
|
|
|
+ // 按订阅分组测试
|
|
|
+ for (const subscription of subscriptions) {
|
|
|
+ try {
|
|
|
+ const subscriptionResult = await this.testSubscriptionNodes(subscription);
|
|
|
+ subscriptionResults.push(subscriptionResult);
|
|
|
+ allResults.push(...subscriptionResult.testResults);
|
|
|
+ } catch (error) {
|
|
|
+ logger.error(`订阅测试失败: ${subscription.name}`, { error: error.message });
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // 如果有高延迟节点,进行重测
|
|
|
- if (highLatencyNodes.length > 0) {
|
|
|
- logger.info(`重测 ${highLatencyNodes.length} 个高延迟节点`);
|
|
|
-
|
|
|
- // 重测高延迟节点
|
|
|
- const retestResults = await this.speedTester.testNodes(highLatencyNodes);
|
|
|
-
|
|
|
- // 更新测试结果,使用重测的结果
|
|
|
- for (let i = 0; i < highLatencyNodes.length; i++) {
|
|
|
- const node = highLatencyNodes[i];
|
|
|
- const retestResult = retestResults[i];
|
|
|
- const originalIndex = testResults.findIndex(r => r.nodeId === node.id);
|
|
|
-
|
|
|
- if (originalIndex !== -1 && retestResult) {
|
|
|
- // 如果重测结果更好,使用重测结果
|
|
|
- if (retestResult.isSuccess && retestResult.latency && retestResult.latency <= 2000) {
|
|
|
- testResults[originalIndex] = retestResult;
|
|
|
- }
|
|
|
- }
|
|
|
+ // 发送每个订阅的单独报告
|
|
|
+ for (const result of subscriptionResults) {
|
|
|
+ if (result.nodes.length > 0) {
|
|
|
+ await this.sendSubscriptionTestSummary(result);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // 处理测试结果
|
|
|
- await this.processTestResults(nodes, testResults);
|
|
|
-
|
|
|
- // 生成并发送摘要
|
|
|
- await this.sendTestSummary(nodes, testResults);
|
|
|
+ // 发送总体报告
|
|
|
+ await this.sendOverallTestSummary(subscriptionResults);
|
|
|
|
|
|
const duration = Date.now() - startTime;
|
|
|
- const successCount = testResults.filter(r => r.isSuccess).length;
|
|
|
- logger.info(`测速完成: ${successCount}/${nodes.length}个成功, 耗时${duration}ms`);
|
|
|
+ const totalNodes = allResults.length;
|
|
|
+ const successCount = allResults.filter(r => r.isSuccess).length;
|
|
|
+
|
|
|
+ logger.info(`分组测速完成: ${successCount}/${totalNodes}个成功, 耗时${duration}ms`);
|
|
|
|
|
|
} catch (error) {
|
|
|
logger.error('定时速度测试失败', { error: error.message });
|
|
@@ -162,6 +148,243 @@ class Scheduler {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 测试单个订阅的所有节点
|
|
|
+ */
|
|
|
+ async testSubscriptionNodes(subscription) {
|
|
|
+ logger.info(`开始测试订阅: ${subscription.name}`);
|
|
|
+
|
|
|
+ // 获取该订阅的所有活跃节点
|
|
|
+ const nodes = await Node.findAll({
|
|
|
+ where: {
|
|
|
+ subscriptionId: subscription.id,
|
|
|
+ isActive: true
|
|
|
+ },
|
|
|
+ order: [['name', 'ASC']]
|
|
|
+ });
|
|
|
+
|
|
|
+ if (nodes.length === 0) {
|
|
|
+ logger.warn(`订阅 ${subscription.name} 没有活跃节点`);
|
|
|
+ return {
|
|
|
+ subscription,
|
|
|
+ nodes: [],
|
|
|
+ testResults: [],
|
|
|
+ summary: {
|
|
|
+ totalNodes: 0,
|
|
|
+ onlineNodes: 0,
|
|
|
+ offlineNodes: 0,
|
|
|
+ successRate: 0,
|
|
|
+ averageLatency: null,
|
|
|
+ averageSpeed: null
|
|
|
+ }
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ logger.info(`订阅 ${subscription.name} 有 ${nodes.length} 个节点需要测试`);
|
|
|
+
|
|
|
+ // 批量测试节点
|
|
|
+ const testResults = await this.speedTester.testNodes(nodes);
|
|
|
+
|
|
|
+ // 检查是否有高延迟节点需要重测
|
|
|
+ const highLatencyNodes = [];
|
|
|
+ const highLatencyResults = [];
|
|
|
+
|
|
|
+ for (let i = 0; i < nodes.length; i++) {
|
|
|
+ const result = testResults[i];
|
|
|
+ if (result && result.isSuccess && result.latency && result.latency > 2000) {
|
|
|
+ highLatencyNodes.push(nodes[i]);
|
|
|
+ highLatencyResults.push(result);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果有高延迟节点,进行重测
|
|
|
+ if (highLatencyNodes.length > 0) {
|
|
|
+ logger.info(`订阅 ${subscription.name} 重测 ${highLatencyNodes.length} 个高延迟节点`);
|
|
|
+
|
|
|
+ // 重测高延迟节点
|
|
|
+ const retestResults = await this.speedTester.testNodes(highLatencyNodes);
|
|
|
+
|
|
|
+ // 更新测试结果,使用重测的结果
|
|
|
+ for (let i = 0; i < highLatencyNodes.length; i++) {
|
|
|
+ const node = highLatencyNodes[i];
|
|
|
+ const retestResult = retestResults[i];
|
|
|
+ const originalIndex = testResults.findIndex(r => r.nodeId === node.id);
|
|
|
+
|
|
|
+ if (originalIndex !== -1 && retestResult) {
|
|
|
+ // 如果重测结果更好,使用重测结果
|
|
|
+ if (retestResult.isSuccess && retestResult.latency && retestResult.latency <= 2000) {
|
|
|
+ testResults[originalIndex] = retestResult;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理测试结果
|
|
|
+ await this.processTestResults(nodes, testResults);
|
|
|
+
|
|
|
+ // 生成订阅摘要
|
|
|
+ const summary = this.generateSubscriptionSummary(nodes, testResults);
|
|
|
+
|
|
|
+ return {
|
|
|
+ subscription,
|
|
|
+ nodes,
|
|
|
+ testResults,
|
|
|
+ summary
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成订阅测试摘要
|
|
|
+ */
|
|
|
+ generateSubscriptionSummary(nodes, testResults) {
|
|
|
+ const totalNodes = nodes.length;
|
|
|
+ const onlineNodes = testResults.filter(r => r.isSuccess).length;
|
|
|
+ const offlineNodes = totalNodes - onlineNodes;
|
|
|
+ const successRate = totalNodes > 0 ? Math.round((onlineNodes / totalNodes) * 100) : 0;
|
|
|
+
|
|
|
+ // 计算平均延迟和速度
|
|
|
+ const successfulResults = testResults.filter(r => r.isSuccess && r.latency);
|
|
|
+ const averageLatency = successfulResults.length > 0
|
|
|
+ ? Math.round(successfulResults.reduce((sum, r) => sum + r.latency, 0) / successfulResults.length)
|
|
|
+ : null;
|
|
|
+
|
|
|
+ const speedResults = testResults.filter(r => r.isSuccess && r.downloadSpeed);
|
|
|
+ const averageSpeed = speedResults.length > 0
|
|
|
+ ? Math.round((speedResults.reduce((sum, r) => sum + r.downloadSpeed, 0) / speedResults.length) * 100) / 100
|
|
|
+ : null;
|
|
|
+
|
|
|
+ // 获取故障节点列表
|
|
|
+ const failedNodes = nodes.filter(node => {
|
|
|
+ const result = testResults.find(r => r.nodeId === node.id);
|
|
|
+ return result && !result.isSuccess;
|
|
|
+ }).slice(0, 5); // 最多显示5个
|
|
|
+
|
|
|
+ // 获取最佳节点列表
|
|
|
+ const bestNodes = testResults
|
|
|
+ .filter(r => r.isSuccess && r.latency)
|
|
|
+ .sort((a, b) => a.latency - b.latency)
|
|
|
+ .slice(0, 3)
|
|
|
+ .map(result => {
|
|
|
+ const node = nodes.find(n => n.id === result.nodeId);
|
|
|
+ return {
|
|
|
+ name: node ? node.name : 'Unknown',
|
|
|
+ latency: result.latency
|
|
|
+ };
|
|
|
+ });
|
|
|
+
|
|
|
+ return {
|
|
|
+ totalNodes,
|
|
|
+ onlineNodes,
|
|
|
+ offlineNodes,
|
|
|
+ successRate,
|
|
|
+ averageLatency,
|
|
|
+ averageSpeed,
|
|
|
+ failedNodes,
|
|
|
+ bestNodes,
|
|
|
+ testTime: new Date().toLocaleString('zh-CN')
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 发送订阅测试摘要
|
|
|
+ */
|
|
|
+ async sendSubscriptionTestSummary(subscriptionResult) {
|
|
|
+ const { subscription, summary } = subscriptionResult;
|
|
|
+
|
|
|
+ if (summary.totalNodes === 0) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const title = `📊 订阅测试报告: ${subscription.name}`;
|
|
|
+
|
|
|
+ const lines = [
|
|
|
+ `*订阅名称:* ${subscription.name}`,
|
|
|
+ `*测试时间:* ${summary.testTime}`,
|
|
|
+ `*节点总数:* ${summary.totalNodes}个`,
|
|
|
+ `*在线节点:* ${summary.onlineNodes}个`,
|
|
|
+ `*离线节点:* ${summary.offlineNodes}个`,
|
|
|
+ `*成功率:* ${summary.successRate}%`
|
|
|
+ ];
|
|
|
+
|
|
|
+ if (summary.averageLatency) {
|
|
|
+ lines.push(`*平均延迟:* ${summary.averageLatency}ms`);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (summary.averageSpeed) {
|
|
|
+ lines.push(`*平均速度:* ${summary.averageSpeed}Mbps`);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (summary.bestNodes.length > 0) {
|
|
|
+ lines.push(`\n🏆 *最佳节点:*`);
|
|
|
+ summary.bestNodes.forEach((node, index) => {
|
|
|
+ lines.push(`${index + 1}. ${node.name} - ${node.latency}ms`);
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ if (summary.failedNodes.length > 0) {
|
|
|
+ lines.push(`\n❌ *故障节点:*`);
|
|
|
+ summary.failedNodes.forEach((node, index) => {
|
|
|
+ lines.push(`${index + 1}. ${node.name}`);
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ const message = lines.join('\n');
|
|
|
+
|
|
|
+ await this.notifier.sendSubscriptionNotification(subscription, title, message, summary);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 发送总体测试摘要
|
|
|
+ */
|
|
|
+ async sendOverallTestSummary(subscriptionResults) {
|
|
|
+ const totalSubscriptions = subscriptionResults.length;
|
|
|
+ const totalNodes = subscriptionResults.reduce((sum, r) => sum + r.summary.totalNodes, 0);
|
|
|
+ const totalOnlineNodes = subscriptionResults.reduce((sum, r) => sum + r.summary.onlineNodes, 0);
|
|
|
+ const totalOfflineNodes = totalNodes - totalOnlineNodes;
|
|
|
+ const overallSuccessRate = totalNodes > 0 ? Math.round((totalOnlineNodes / totalNodes) * 100) : 0;
|
|
|
+
|
|
|
+ // 计算总体平均延迟
|
|
|
+ const allSuccessfulResults = subscriptionResults.flatMap(r =>
|
|
|
+ r.testResults.filter(result => result.isSuccess && result.latency)
|
|
|
+ );
|
|
|
+ const overallAverageLatency = allSuccessfulResults.length > 0
|
|
|
+ ? Math.round(allSuccessfulResults.reduce((sum, r) => sum + r.latency, 0) / allSuccessfulResults.length)
|
|
|
+ : null;
|
|
|
+
|
|
|
+ const title = '📈 总体测试报告';
|
|
|
+ const lines = [
|
|
|
+ `*测试时间:* ${new Date().toLocaleString('zh-CN')}`,
|
|
|
+ `*订阅数量:* ${totalSubscriptions}个`,
|
|
|
+ `*节点总数:* ${totalNodes}个`,
|
|
|
+ `*在线节点:* ${totalOnlineNodes}个`,
|
|
|
+ `*离线节点:* ${totalOfflineNodes}个`,
|
|
|
+ `*总体成功率:* ${overallSuccessRate}%`
|
|
|
+ ];
|
|
|
+
|
|
|
+ if (overallAverageLatency) {
|
|
|
+ lines.push(`*总体平均延迟:* ${overallAverageLatency}ms`);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 按订阅显示摘要
|
|
|
+ lines.push('\n📋 *各订阅状态:*');
|
|
|
+ subscriptionResults.forEach(result => {
|
|
|
+ const { subscription, summary } = result;
|
|
|
+ const status = summary.successRate >= 80 ? '🟢' : summary.successRate >= 50 ? '🟡' : '🔴';
|
|
|
+ lines.push(`${status} ${subscription.name}: ${summary.onlineNodes}/${summary.totalNodes} (${summary.successRate}%)`);
|
|
|
+ });
|
|
|
+
|
|
|
+ const message = lines.join('\n');
|
|
|
+
|
|
|
+ await this.notifier.sendSystemNotification(title, message, {
|
|
|
+ totalSubscriptions,
|
|
|
+ totalNodes,
|
|
|
+ totalOnlineNodes,
|
|
|
+ totalOfflineNodes,
|
|
|
+ overallSuccessRate,
|
|
|
+ overallAverageLatency
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 处理测试结果
|
|
|
*/
|
|
@@ -237,63 +460,6 @@ class Scheduler {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- /**
|
|
|
- * 发送测试摘要
|
|
|
- */
|
|
|
- async sendTestSummary(nodes, testResults) {
|
|
|
- const totalNodes = nodes.length;
|
|
|
- const onlineNodes = testResults.filter(r => r.isSuccess).length;
|
|
|
- const offlineNodes = totalNodes - onlineNodes;
|
|
|
- const successRate = totalNodes > 0 ? Math.round((onlineNodes / totalNodes) * 100) : 0;
|
|
|
-
|
|
|
- // 计算平均延迟和速度
|
|
|
- const successfulResults = testResults.filter(r => r.isSuccess && r.latency);
|
|
|
- const averageLatency = successfulResults.length > 0
|
|
|
- ? Math.round(successfulResults.reduce((sum, r) => sum + r.latency, 0) / successfulResults.length)
|
|
|
- : null;
|
|
|
-
|
|
|
- const speedResults = testResults.filter(r => r.isSuccess && r.downloadSpeed);
|
|
|
- const averageSpeed = speedResults.length > 0
|
|
|
- ? Math.round((speedResults.reduce((sum, r) => sum + r.downloadSpeed, 0) / speedResults.length) * 100) / 100
|
|
|
- : null;
|
|
|
-
|
|
|
- // 获取故障节点列表
|
|
|
- const failedNodes = nodes.filter(node => {
|
|
|
- const result = testResults.find(r => r.nodeId === node.id);
|
|
|
- return result && !result.isSuccess;
|
|
|
- }).slice(0, 10); // 最多显示10个
|
|
|
-
|
|
|
- // 获取最佳节点列表
|
|
|
- const bestNodes = testResults
|
|
|
- .filter(r => r.isSuccess && r.latency)
|
|
|
- .sort((a, b) => a.latency - b.latency)
|
|
|
- .slice(0, 5)
|
|
|
- .map(result => {
|
|
|
- const node = nodes.find(n => n.id === result.nodeId);
|
|
|
- return {
|
|
|
- name: node ? node.name : 'Unknown',
|
|
|
- latency: result.latency
|
|
|
- };
|
|
|
- });
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
- const summary = {
|
|
|
- totalNodes,
|
|
|
- onlineNodes,
|
|
|
- offlineNodes,
|
|
|
- successRate,
|
|
|
- averageLatency,
|
|
|
- averageSpeed,
|
|
|
- failedNodes,
|
|
|
- bestNodes,
|
|
|
- testTime: new Date().toLocaleString('zh-CN')
|
|
|
- };
|
|
|
-
|
|
|
- // 每次测速完成后都发送详细的测试总结报告
|
|
|
- await this.notifier.sendSummaryNotification(summary);
|
|
|
- }
|
|
|
-
|
|
|
/**
|
|
|
* 清理旧数据
|
|
|
*/
|