2 次代碼提交 fc652adb70 ... 90fa5d311c

作者 SHA1 備註 提交日期
  Taio_O 90fa5d311c 其他类型的节点识别,分组测试和发送订阅报告 1 周之前
  Taio_O 8a4de49dbf 初步连接机器人 1 周之前
共有 11 個文件被更改,包括 1411 次插入107 次删除
  1. 155 0
      BOT_USAGE.md
  2. 2 0
      env.example
  3. 5 0
      public/index.html
  4. 1 0
      public/subscription-test-results.html
  5. 146 0
      src/api/routes.js
  6. 30 0
      src/app.js
  7. 631 0
      src/core/botManager.js
  8. 151 4
      src/core/multiSubscriptionManager.js
  9. 23 2
      src/core/notifier.js
  10. 266 100
      src/core/scheduler.js
  11. 1 1
      src/models/Notification.js

+ 155 - 0
BOT_USAGE.md

@@ -0,0 +1,155 @@
+# Telegram机器人使用说明
+
+## 🤖 机器人功能
+
+这个Telegram机器人可以帮助您通过聊天界面管理订阅链接和监控节点状态。
+
+## 📋 配置要求
+
+### 1. 创建Telegram机器人
+1. 在Telegram中搜索 `@BotFather`
+2. 发送 `/newbot` 命令
+3. 按提示设置机器人名称和用户名
+4. 获取Bot Token(类似:`123456789:ABCdefGHIjklMNOpqrsTUVwxyz`)
+
+### 2. 获取Chat ID
+1. 将机器人添加到您的聊天或群组
+2. 发送任意消息给机器人
+3. 访问:`https://api.telegram.org/bot<YOUR_BOT_TOKEN>/getUpdates`
+4. 找到 `chat.id` 字段的值
+
+### 3. 配置环境变量
+在 `.env` 文件中添加:
+```bash
+TELEGRAM_BOT_TOKEN=你的机器人Token
+TELEGRAM_CHAT_ID=你的Chat ID
+# 支持多个用户,用逗号分隔
+# TELEGRAM_CHAT_ID=123456789,987654321
+```
+
+## 🚀 启动机器人
+
+1. 确保配置正确后,启动应用:
+```bash
+node start.js
+```
+
+2. 机器人会自动启动并开始监听消息
+
+## 📱 可用命令
+
+### 基础命令
+- `/start` - 开始使用机器人,显示欢迎信息
+- `/help` - 显示帮助信息和使用说明
+- `/status` - 查看系统状态和运行信息
+
+### 订阅管理
+- `/subscriptions` - 查看所有订阅列表
+- `/add_subscription` - 添加订阅链接
+- `/remove_subscription` - 查看删除选项
+- `/delete_1` - 删除第1个订阅
+- `/delete_2` - 删除第2个订阅(以此类推)
+- `/update_subscriptions` - 手动更新所有订阅
+
+### 测速功能
+- `/test_speed` - 手动触发测速(开发中)
+
+## 💡 使用技巧
+
+### 1. 直接添加订阅
+直接发送订阅链接给机器人即可添加:
+```
+https://example.com/subscription
+ss://user:pass@server:port
+vmess://...
+trojan://...
+```
+
+### 2. 删除订阅
+1. 发送 `/remove_subscription` 查看订阅列表
+2. 使用 `/delete_1` 删除第1个订阅
+3. 使用 `/delete_2` 删除第2个订阅,以此类推
+
+### 3. 查看状态
+发送 `/status` 查看:
+- 订阅数量和节点数量
+- 自动更新状态
+- 测速功能状态
+- 系统运行时间
+
+## 🔧 支持格式
+
+机器人支持以下订阅格式:
+- **Clash配置**:`https://example.com/clash.yaml`
+- **Shadowsocks**:`ss://user:pass@server:port`
+- **Vmess**:`vmess://...`
+- **Trojan**:`trojan://...`
+- **Base64编码**:自动解码
+
+## 🛡️ 安全特性
+
+- **权限控制**:只有配置的Chat ID才能使用机器人
+- **输入验证**:自动验证订阅链接格式
+- **错误处理**:友好的错误提示信息
+- **日志记录**:所有操作都有详细日志
+
+## 📊 功能特点
+
+### 自动管理
+- 自动解析订阅内容
+- 自动去重节点
+- 自动更新节点信息
+- 自动触发测速
+
+### 实时监控
+- 节点状态监控
+- 延迟和速度测试
+- 故障节点通知
+- 恢复节点通知
+
+### 便捷操作
+- 一键添加订阅
+- 一键删除订阅
+- 一键更新订阅
+- 一键查看状态
+
+## 🔍 故障排除
+
+### 机器人无响应
+1. 检查Bot Token是否正确
+2. 检查Chat ID是否正确
+3. 检查网络连接
+4. 查看应用日志
+
+### 权限被拒绝
+1. 确认Chat ID在允许列表中
+2. 检查环境变量配置
+3. 重启应用
+
+### 订阅添加失败
+1. 检查订阅链接格式
+2. 确认链接可访问
+3. 查看错误日志
+
+## 📝 日志查看
+
+机器人操作日志会记录在应用日志中:
+```bash
+# 查看实时日志
+tail -f logs/app.log
+
+# 查看机器人相关日志
+grep "bot" logs/app.log
+```
+
+## 🔄 更新说明
+
+机器人功能会持续更新,新功能包括:
+- 手动测速功能
+- 节点排序功能
+- 批量操作功能
+- 更丰富的通知
+
+---
+
+💡 **提示**:机器人功能需要正确的Telegram配置才能正常工作。如果遇到问题,请检查配置和网络连接。 

+ 2 - 0
env.example

@@ -8,6 +8,8 @@ DB_PASSWORD=BjKhwBRAGSfKc4e6
 # Telegram机器人配置
 TELEGRAM_BOT_TOKEN=7804040067:AAFeV7WeCWHUJTJzLUnN83C0Wid0pGSj6NQ
 TELEGRAM_CHAT_ID=-4840979728
+# 支持多个聊天ID,用逗号分隔
+# TELEGRAM_CHAT_ID=-4840979728,123456789
 
 # 应用配置
 PORT=3000

+ 5 - 0
public/index.html

@@ -46,6 +46,11 @@
                             <i class="bi bi-gear"></i> 系统设置
                         </a>
                     </li>
+                    <li class="nav-item">
+                        <a class="nav-link" href="subscription-test-results.html" target="_blank">
+                            <i class="bi bi-collection"></i> 订阅测试
+                        </a>
+                    </li>
                 </ul>
                 <div class="navbar-nav">
                     <span class="navbar-text" id="status-indicator">

+ 1 - 0
public/subscription-test-results.html

@@ -0,0 +1 @@
+ 

+ 146 - 0
src/api/routes.js

@@ -179,6 +179,152 @@ router.post('/import/clash', async (req, res) => {
   }
 });
 
+// 获取按订阅分组的测试结果
+router.get('/subscriptions/test-results', async (req, res) => {
+  try {
+    const { Subscription } = require('../models');
+    
+    // 获取所有活跃订阅
+    const subscriptions = await Subscription.findAll({
+      where: { isActive: true },
+      order: [['createdAt', 'ASC']]
+    });
+
+    const results = [];
+
+    for (const subscription of subscriptions) {
+      // 获取该订阅的节点
+      const nodes = await Node.findAll({
+        where: { 
+          subscriptionId: subscription.id,
+          isActive: true 
+        },
+        order: [['name', 'ASC']]
+      });
+
+      if (nodes.length === 0) {
+        results.push({
+          subscription: {
+            id: subscription.id,
+            name: subscription.name,
+            nodeCount: 0
+          },
+          nodes: [],
+          summary: {
+            totalNodes: 0,
+            onlineNodes: 0,
+            offlineNodes: 0,
+            successRate: 0,
+            averageLatency: null,
+            averageSpeed: null
+          }
+        });
+        continue;
+      }
+
+      // 获取最近的测试结果
+      const testResults = await TestResult.findAll({
+        where: { nodeId: nodes.map(n => n.id) },
+        order: [['testTime', 'DESC']],
+        limit: nodes.length
+      });
+
+      // 按节点分组最新的测试结果
+      const latestResults = new Map();
+      testResults.forEach(result => {
+        if (!latestResults.has(result.nodeId)) {
+          latestResults.set(result.nodeId, result);
+        }
+      });
+
+      // 计算摘要
+      const onlineNodes = nodes.filter(node => {
+        const result = latestResults.get(node.id);
+        return result && result.isSuccess;
+      });
+
+      const offlineNodes = nodes.filter(node => {
+        const result = latestResults.get(node.id);
+        return !result || !result.isSuccess;
+      });
+
+      const successRate = nodes.length > 0 ? Math.round((onlineNodes.length / nodes.length) * 100) : 0;
+
+      // 计算平均延迟
+      const successfulResults = Array.from(latestResults.values()).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 bestNodes = Array.from(latestResults.values())
+        .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
+          };
+        });
+
+      results.push({
+        subscription: {
+          id: subscription.id,
+          name: subscription.name,
+          nodeCount: nodes.length
+        },
+        nodes: nodes.map(node => {
+          const result = latestResults.get(node.id);
+          return {
+            id: node.id,
+            name: node.name,
+            type: node.type,
+            server: node.server,
+            port: node.port,
+            status: node.status,
+            lastTestTime: node.lastTestTime,
+            lastTestResult: node.lastTestResult,
+            averageLatency: node.averageLatency,
+            failureCount: node.failureCount,
+            testResult: result ? {
+              isSuccess: result.isSuccess,
+              latency: result.latency,
+              testTime: result.testTime,
+              errorMessage: result.errorMessage
+            } : null
+          };
+        }),
+        summary: {
+          totalNodes: nodes.length,
+          onlineNodes: onlineNodes.length,
+          offlineNodes: offlineNodes.length,
+          successRate,
+          averageLatency,
+          bestNodes
+        }
+      });
+    }
+
+    res.json({
+      success: true,
+      data: {
+        subscriptions: results,
+        totalSubscriptions: results.length,
+        totalNodes: results.reduce((sum, r) => sum + r.summary.totalNodes, 0),
+        totalOnlineNodes: results.reduce((sum, r) => sum + r.summary.onlineNodes, 0),
+        overallSuccessRate: results.length > 0 
+          ? Math.round(results.reduce((sum, r) => sum + r.summary.successRate, 0) / results.length)
+          : 0
+      }
+    });
+  } catch (error) {
+    logger.error('获取订阅测试结果失败', { error: error.message });
+    res.status(500).json({ success: false, error: error.message });
+  }
+});
+
 // 订阅管理API
 router.post('/subscription/update', async (req, res) => {
   try {

+ 30 - 0
src/app.js

@@ -9,6 +9,7 @@ const sequelize = require('./config/database');
 const routes = require('./api/routes');
 const Scheduler = require('./core/scheduler');
 const MultiSubscriptionManager = require('./core/multiSubscriptionManager');
+const BotManager = require('./core/botManager');
 
 const app = express();
 const PORT = process.env.PORT || 3000;
@@ -100,6 +101,21 @@ app.use('*', (req, res) => {
   });
 });
 
+// 处理未捕获的Promise拒绝
+process.on('unhandledRejection', (reason, promise) => {
+  logger.error('未处理的Promise拒绝', {
+    reason: {
+      code: reason.code,
+      message: reason.message
+    },
+    promise: {
+      isFulfilled: promise.isFulfilled,
+      isRejected: promise.isRejected,
+      rejectionReason: promise.rejectionReason
+    }
+  });
+});
+
 // 启动应用
 async function startApp() {
   try {
@@ -129,6 +145,10 @@ async function startApp() {
     
     app.set('subscriptionManager', subscriptionManager);
 
+    // 启动Telegram机器人管理器
+    const botManager = new BotManager();
+    app.set('botManager', botManager);
+
     // 检查是否启用启动时立即测速
     const enableStartupSpeedTest = process.env.ENABLE_SCHEDULED_SPEED_TEST !== 'false';
     
@@ -172,6 +192,11 @@ process.on('SIGTERM', async () => {
     subscriptionManager.stopAutoUpdate();
   }
   
+  const botManager = app.get('botManager');
+  if (botManager) {
+    botManager.stop();
+  }
+  
   await sequelize.close();
   logger.info('应用已关闭');
   process.exit(0);
@@ -190,6 +215,11 @@ process.on('SIGINT', async () => {
     subscriptionManager.stopAutoUpdate();
   }
   
+  const botManager = app.get('botManager');
+  if (botManager) {
+    botManager.stop();
+  }
+  
   await sequelize.close();
   logger.info('应用已关闭');
   process.exit(0);

+ 631 - 0
src/core/botManager.js

@@ -0,0 +1,631 @@
+const TelegramBot = require('node-telegram-bot-api');
+const logger = require('../utils/logger');
+const { Subscription } = require('../models');
+const MultiSubscriptionManager = require('./multiSubscriptionManager');
+
+class BotManager {
+  constructor() {
+    this.botToken = process.env.TELEGRAM_BOT_TOKEN;
+    this.allowedChatIds = process.env.TELEGRAM_CHAT_ID?.split(',').map(id => id.trim()) || [];
+    this.subscriptionManager = new MultiSubscriptionManager();
+    
+    // 可选的代理配置
+    this.proxyConfig = null;
+    if (process.env.TELEGRAM_PROXY_URL) {
+      this.proxyConfig = {
+        host: process.env.TELEGRAM_PROXY_HOST,
+        port: parseInt(process.env.TELEGRAM_PROXY_PORT) || 1080,
+        protocol: process.env.TELEGRAM_PROXY_PROTOCOL || 'http'
+      };
+    }
+    
+    if (!this.botToken) {
+      logger.warn('Telegram Bot Token未配置,机器人功能将被禁用');
+      this.enabled = false;
+      return;
+    }
+
+    try {
+      const botOptions = {
+        polling: true,
+        request: {
+          timeout: 30000, // 增加超时时间到30秒
+          connect_timeout: 30000
+        }
+      };
+      
+      // 如果配置了代理,添加代理设置
+      if (this.proxyConfig) {
+        const { HttpsProxyAgent } = require('https-proxy-agent');
+        const proxyUrl = `${this.proxyConfig.protocol}://${this.proxyConfig.host}:${this.proxyConfig.port}`;
+        botOptions.request.httpsAgent = new HttpsProxyAgent(proxyUrl);
+        logger.info(`使用代理连接Telegram: ${proxyUrl}`);
+      } else {
+        botOptions.request.agent = false; // 禁用代理,避免网络问题
+      }
+      
+      this.bot = new TelegramBot(this.botToken, botOptions);
+      
+      // 添加错误处理
+      this.bot.on('polling_error', (error) => {
+        logger.warn('Telegram轮询错误,尝试重连...', { 
+          error: error.message,
+          code: error.code 
+        });
+        
+        // 如果是网络超时,尝试重新启动轮询
+        if (error.code === 'EFATAL' || error.message.includes('ESOCKETTIMEDOUT')) {
+          setTimeout(() => {
+            try {
+              this.bot.stopPolling();
+              setTimeout(() => {
+                this.bot.startPolling().catch(reconnectError => {
+                  logger.error('Telegram机器人重连失败', { error: reconnectError.message });
+                });
+                logger.info('Telegram机器人重连成功');
+              }, 5000);
+            } catch (reconnectError) {
+              logger.error('Telegram机器人重连失败', { error: reconnectError.message });
+            }
+          }, 10000);
+        }
+      });
+      
+      this.enabled = true;
+      this.setupCommands();
+      this.setupMessageHandlers();
+      logger.info('Telegram机器人管理器初始化成功');
+    } catch (error) {
+      logger.error('Telegram机器人管理器初始化失败', { error: error.message });
+      this.enabled = false;
+    }
+  }
+
+  /**
+   * 设置机器人命令
+   */
+  setupCommands() {
+    if (!this.enabled) return;
+
+    this.bot.setMyCommands([
+      { command: '/start', description: '开始使用机器人' },
+      { command: '/help', description: '显示帮助信息' },
+      { command: '/status', description: '查看系统状态' },
+      { command: '/subscriptions', description: '查看所有订阅' },
+      { command: '/add_subscription', description: '添加订阅链接' },
+      { command: '/remove_subscription', description: '删除订阅' },
+      { command: '/update_subscriptions', description: '手动更新订阅' },
+      { command: '/test_speed', description: '手动触发测速' }
+    ]);
+  }
+
+  /**
+   * 设置消息处理器
+   */
+  setupMessageHandlers() {
+    if (!this.enabled) return;
+
+    // 处理 /start 命令
+    this.bot.onText(/\/start/, async (msg) => {
+      await this.handleStart(msg);
+    });
+
+    // 处理 /help 命令
+    this.bot.onText(/\/help/, async (msg) => {
+      await this.handleHelp(msg);
+    });
+
+    // 处理 /status 命令
+    this.bot.onText(/\/status/, async (msg) => {
+      await this.handleStatus(msg);
+    });
+
+    // 处理 /subscriptions 命令
+    this.bot.onText(/\/subscriptions/, async (msg) => {
+      await this.handleSubscriptions(msg);
+    });
+
+    // 处理 /add_subscription 命令
+    this.bot.onText(/\/add_subscription/, async (msg) => {
+      await this.handleAddSubscription(msg);
+    });
+
+    // 处理 /remove_subscription 命令
+    this.bot.onText(/\/remove_subscription/, async (msg) => {
+      await this.handleRemoveSubscription(msg);
+    });
+
+    // 处理删除订阅的具体命令
+    this.bot.onText(/\/delete_(\d+)/, async (msg, match) => {
+      await this.handleDeleteSubscription(msg, parseInt(match[1]));
+    });
+
+    // 处理 /update_subscriptions 命令
+    this.bot.onText(/\/update_subscriptions/, async (msg) => {
+      await this.handleUpdateSubscriptions(msg);
+    });
+
+    // 处理 /test_speed 命令
+    this.bot.onText(/\/test_speed/, async (msg) => {
+      await this.handleTestSpeed(msg);
+    });
+
+    // 处理普通消息(只处理订阅链接,不处理其他消息)
+    this.bot.on('message', async (msg) => {
+      // 只处理文本消息
+      if (!msg.text) return;
+      
+      // 如果是命令,不处理(由命令处理器处理)
+      if (msg.text.startsWith('/')) return;
+      
+      // 只处理订阅链接
+      if (this.isSubscriptionUrl(msg.text)) {
+        await this.addSubscription(msg.chat.id, msg.text);
+      }
+      // 其他消息不回复
+    });
+  }
+
+  /**
+   * 检查用户权限
+   */
+  isAuthorized(chatId) {
+    return this.allowedChatIds.includes(chatId.toString());
+  }
+
+  /**
+   * 发送消息
+   */
+  async sendMessage(chatId, message, options = {}) {
+    try {
+      return await this.bot.sendMessage(chatId, message, {
+        parse_mode: 'Markdown',
+        disable_web_page_preview: true,
+        ...options
+      });
+    } catch (error) {
+      logger.error('发送Telegram消息失败', { error: error.message, chatId });
+      throw error;
+    }
+  }
+
+  /**
+   * 处理 /start 命令
+   */
+  async handleStart(msg) {
+    const chatId = msg.chat.id;
+    
+    if (!this.isAuthorized(chatId)) {
+      await this.sendMessage(chatId, '❌ 您没有权限使用此机器人');
+      return;
+    }
+
+    const message = `🤖 *欢迎使用测速机器人!*
+
+📋 *可用命令:*
+• /help - 显示帮助信息
+• /status - 查看系统状态
+• /subscriptions - 查看所有订阅
+• /add_subscription - 添加订阅链接
+• /remove_subscription - 删除订阅
+• /update_subscriptions - 手动更新订阅
+• /test_speed - 手动触发测速
+
+💡 *使用提示:*
+• 直接发送订阅链接即可添加订阅
+• 使用 /remove_subscription 查看订阅列表
+• 使用 /delete_1 删除第1个订阅
+• 支持多种格式:Clash、Shadowsocks、Vmess等`;
+
+    await this.sendMessage(chatId, message);
+  }
+
+  /**
+   * 处理 /help 命令
+   */
+  async handleHelp(msg) {
+    const chatId = msg.chat.id;
+    
+    if (!this.isAuthorized(chatId)) {
+      await this.sendMessage(chatId, '❌ 您没有权限使用此机器人');
+      return;
+    }
+
+    const message = `📖 *帮助信息*
+
+🔧 *订阅管理:*
+• 直接发送订阅链接即可添加
+• 支持格式:Clash、Shadowsocks、Vmess、Trojan
+• 系统会自动解析并去重
+
+⚡ *测速功能:*
+• 自动定时测速(默认每10分钟)
+• 支持手动触发测速
+• 节点故障自动通知
+
+📊 *状态监控:*
+• 实时监控节点状态
+• 延迟和速度测试
+• 故障节点自动标记
+
+🔔 *通知功能:*
+• 节点故障通知
+• 节点恢复通知
+• 测试摘要报告
+
+💡 *使用示例:*
+• 发送:\`https://example.com/subscription\`
+• 发送:\`ss://...\`
+• 发送:\`vmess://...\`
+• 删除:\`/delete_1\` 删除第1个订阅`;
+
+    await this.sendMessage(chatId, message);
+  }
+
+  /**
+   * 处理 /status 命令
+   */
+  async handleStatus(msg) {
+    const chatId = msg.chat.id;
+    
+    if (!this.isAuthorized(chatId)) {
+      await this.sendMessage(chatId, '❌ 您没有权限使用此机器人');
+      return;
+    }
+
+    try {
+      const status = await this.subscriptionManager.getStatus();
+      const subscriptions = await Subscription.findAll({
+        where: { isActive: true }
+      });
+
+      const totalNodes = subscriptions.reduce((sum, sub) => sum + (sub.nodeCount || 0), 0);
+      
+      const message = `📊 *系统状态*
+
+📡 *订阅信息:*
+• 活跃订阅:${subscriptions.length} 个
+• 总节点数:${totalNodes} 个
+
+🔄 *更新状态:*
+• 自动更新:${status.autoUpdateEnabled ? '✅ 启用' : '❌ 禁用'}
+• 更新间隔:${status.updateInterval || '未设置'} 秒
+• 最后更新:${status.lastUpdateTime || '未更新'}
+
+⚡ *测速状态:*
+• 测速触发器:${status.speedTestTrigger ? '✅ 已设置' : '❌ 未设置'}
+• 定时测速:${process.env.ENABLE_SCHEDULED_SPEED_TEST !== 'false' ? '✅ 启用' : '❌ 禁用'}
+
+🛠️ *系统信息:*
+• 运行时间:${this.getUptime()}
+• 内存使用:${this.getMemoryUsage()}`;
+
+      await this.sendMessage(chatId, message);
+    } catch (error) {
+      logger.error('获取状态失败', { error: error.message });
+      await this.sendMessage(chatId, '❌ 获取系统状态失败');
+    }
+  }
+
+  /**
+   * 处理 /subscriptions 命令
+   */
+  async handleSubscriptions(msg) {
+    const chatId = msg.chat.id;
+    
+    if (!this.isAuthorized(chatId)) {
+      await this.sendMessage(chatId, '❌ 您没有权限使用此机器人');
+      return;
+    }
+
+    try {
+      const subscriptions = await Subscription.findAll({
+        where: { isActive: true },
+        order: [['createdAt', 'DESC']]
+      });
+
+      if (subscriptions.length === 0) {
+        await this.sendMessage(chatId, '📭 暂无订阅,请使用 /add_subscription 添加订阅');
+        return;
+      }
+
+      let message = `📡 *订阅列表*\n\n`;
+      
+      subscriptions.forEach((sub, index) => {
+        const status = sub.isActive ? '✅' : '❌';
+        const lastUpdate = sub.lastUpdateTime 
+          ? new Date(sub.lastUpdateTime).toLocaleString('zh-CN')
+          : '未更新';
+        
+        message += `${index + 1}. ${status} *${sub.name}*\n`;
+        message += `   📊 节点数:${sub.nodeCount || 0}\n`;
+        message += `   🔗 URL:\`${sub.url}\`\n`;
+        message += `   ⏰ 最后更新:${lastUpdate}\n\n`;
+      });
+
+      message += `💡 *操作提示:*\n`;
+      message += `• 使用 /remove_subscription 删除订阅\n`;
+      message += `• 使用 /update_subscriptions 手动更新\n`;
+      message += `• 直接发送新链接添加订阅`;
+
+      await this.sendMessage(chatId, message);
+    } catch (error) {
+      logger.error('获取订阅列表失败', { error: error.message });
+      await this.sendMessage(chatId, '❌ 获取订阅列表失败');
+    }
+  }
+
+  /**
+   * 处理 /add_subscription 命令
+   */
+  async handleAddSubscription(msg) {
+    const chatId = msg.chat.id;
+    
+    if (!this.isAuthorized(chatId)) {
+      await this.sendMessage(chatId, '❌ 您没有权限使用此机器人');
+      return;
+    }
+
+    await this.sendMessage(chatId, 
+      `📥 *添加订阅*\n\n` +
+      `请发送订阅链接,支持以下格式:\n` +
+      `• Clash配置:\`https://example.com/clash.yaml\`\n` +
+      `• Shadowsocks:\`ss://...\`\n` +
+      `• Vmess:\`vmess://...\`\n` +
+      `• Trojan:\`trojan://...\`\n\n` +
+      `💡 直接发送链接即可添加订阅`
+    );
+  }
+
+  /**
+   * 处理 /remove_subscription 命令
+   */
+  async handleRemoveSubscription(msg) {
+    const chatId = msg.chat.id;
+    
+    if (!this.isAuthorized(chatId)) {
+      await this.sendMessage(chatId, '❌ 您没有权限使用此机器人');
+      return;
+    }
+
+    try {
+      const subscriptions = await Subscription.findAll({
+        where: { isActive: true },
+        order: [['createdAt', 'DESC']]
+      });
+
+      if (subscriptions.length === 0) {
+        await this.sendMessage(chatId, '📭 暂无订阅可删除');
+        return;
+      }
+
+      let message = `🗑️ *删除订阅*\n\n`;
+      message += `请选择要删除的订阅:\n\n`;
+      
+      subscriptions.forEach((sub, index) => {
+        message += `${index + 1}. *${sub.name}*\n`;
+        message += `   📊 节点数:${sub.nodeCount || 0}\n`;
+        message += `   🔗 \`${sub.url}\`\n\n`;
+      });
+
+      message += `💡 使用 /delete_1 删除第1个订阅\n`;
+      message += `💡 使用 /delete_2 删除第2个订阅\n`;
+      message += `💡 以此类推...`;
+
+      await this.sendMessage(chatId, message);
+    } catch (error) {
+      logger.error('获取订阅列表失败', { error: error.message });
+      await this.sendMessage(chatId, '❌ 获取订阅列表失败');
+    }
+  }
+
+  /**
+   * 处理 /update_subscriptions 命令
+   */
+  async handleUpdateSubscriptions(msg) {
+    const chatId = msg.chat.id;
+    
+    if (!this.isAuthorized(chatId)) {
+      await this.sendMessage(chatId, '❌ 您没有权限使用此机器人');
+      return;
+    }
+
+    try {
+      await this.sendMessage(chatId, '🔄 开始手动更新订阅...');
+      
+      const results = await this.subscriptionManager.manualUpdate();
+      
+      let message = `✅ *订阅更新完成*\n\n`;
+      
+      results.forEach(result => {
+        if (result.error) {
+          message += `❌ *${result.subscriptionName}*\n`;
+          message += `   错误:${result.error}\n\n`;
+        } else {
+          message += `✅ *${result.subscriptionName}*\n`;
+          message += `   新增:${result.added} 个\n`;
+          message += `   更新:${result.updated} 个\n`;
+          message += `   移除:${result.removed} 个\n\n`;
+        }
+      });
+
+      await this.sendMessage(chatId, message);
+    } catch (error) {
+      logger.error('手动更新订阅失败', { error: error.message });
+      await this.sendMessage(chatId, '❌ 更新订阅失败:' + error.message);
+    }
+  }
+
+  /**
+   * 处理 /test_speed 命令
+   */
+  async handleTestSpeed(msg) {
+    const chatId = msg.chat.id;
+    
+    if (!this.isAuthorized(chatId)) {
+      await this.sendMessage(chatId, '❌ 您没有权限使用此机器人');
+      return;
+    }
+
+    try {
+      await this.sendMessage(chatId, '⚡ 开始手动测速...');
+      
+      // 这里需要调用测速功能
+      // 暂时发送一个简单的消息
+      await this.sendMessage(chatId, '✅ 测速功能开发中,请稍后...');
+    } catch (error) {
+      logger.error('手动测速失败', { error: error.message });
+      await this.sendMessage(chatId, '❌ 测速失败:' + error.message);
+    }
+  }
+
+
+
+  /**
+   * 检查是否是订阅链接
+   */
+  isSubscriptionUrl(text) {
+    return /^(https?:\/\/|ss:\/\/|vmess:\/\/|trojan:\/\/)/.test(text);
+  }
+
+  /**
+   * 添加订阅
+   */
+  async addSubscription(chatId, url) {
+    try {
+      await this.sendMessage(chatId, '📥 正在添加订阅...');
+      
+      // 创建新订阅
+      const subscription = await Subscription.create({
+        name: `订阅_${Date.now()}`,
+        url: url,
+        isActive: true,
+        nodeCount: 0
+      });
+
+      // 更新订阅
+      const result = await this.subscriptionManager.manualUpdateSubscription(subscription.id);
+      
+      if (result.error) {
+        await this.sendMessage(chatId, `❌ 添加订阅失败:${result.error}`);
+        return;
+      }
+
+      const message = `✅ *订阅添加成功*\n\n` +
+        `📡 订阅名称:*${subscription.name}*\n` +
+        `🔗 URL:\`${url}\`\n` +
+        `📊 节点数:${result.actualNodeCount || 0}\n` +
+        `➕ 新增:${result.added || 0} 个\n` +
+        `🔄 更新:${result.updated || 0} 个\n` +
+        `🗑️ 移除:${result.removed || 0} 个`;
+
+      await this.sendMessage(chatId, message);
+    } catch (error) {
+      logger.error('添加订阅失败', { error: error.message, url });
+      await this.sendMessage(chatId, '❌ 添加订阅失败:' + error.message);
+    }
+  }
+
+  /**
+   * 处理删除订阅命令
+   */
+  async handleDeleteSubscription(msg, index) {
+    const chatId = msg.chat.id;
+    
+    if (!this.isAuthorized(chatId)) {
+      await this.sendMessage(chatId, '❌ 您没有权限使用此机器人');
+      return;
+    }
+
+    try {
+      const subscriptions = await Subscription.findAll({
+        where: { isActive: true },
+        order: [['createdAt', 'DESC']]
+      });
+
+      if (index < 1 || index > subscriptions.length) {
+        await this.sendMessage(chatId, '❌ 无效的订阅编号');
+        return;
+      }
+
+      const subscription = subscriptions[index - 1];
+      
+      // 标记为非活跃
+      await subscription.update({ isActive: false });
+      
+      const message = `🗑️ *订阅删除成功*\n\n` +
+        `📡 订阅名称:*${subscription.name}*\n` +
+        `🔗 URL:\`${subscription.url}\`\n` +
+        `📊 节点数:${subscription.nodeCount || 0}`;
+
+      await this.sendMessage(chatId, message);
+    } catch (error) {
+      logger.error('删除订阅失败', { error: error.message, index });
+      await this.sendMessage(chatId, '❌ 删除订阅失败:' + error.message);
+    }
+  }
+
+  /**
+   * 根据索引删除订阅(保留兼容性)
+   */
+  async removeSubscriptionByIndex(chatId, index) {
+    try {
+      const subscriptions = await Subscription.findAll({
+        where: { isActive: true },
+        order: [['createdAt', 'DESC']]
+      });
+
+      if (index < 1 || index > subscriptions.length) {
+        await this.sendMessage(chatId, '❌ 无效的订阅编号');
+        return;
+      }
+
+      const subscription = subscriptions[index - 1];
+      
+      // 标记为非活跃
+      await subscription.update({ isActive: false });
+      
+      const message = `🗑️ *订阅删除成功*\n\n` +
+        `📡 订阅名称:*${subscription.name}*\n` +
+        `🔗 URL:\`${subscription.url}\`\n` +
+        `📊 节点数:${subscription.nodeCount || 0}`;
+
+      await this.sendMessage(chatId, message);
+    } catch (error) {
+      logger.error('删除订阅失败', { error: error.message, index });
+      await this.sendMessage(chatId, '❌ 删除订阅失败:' + error.message);
+    }
+  }
+
+  /**
+   * 获取运行时间
+   */
+  getUptime() {
+    const uptime = process.uptime();
+    const hours = Math.floor(uptime / 3600);
+    const minutes = Math.floor((uptime % 3600) / 60);
+    return `${hours}小时${minutes}分钟`;
+  }
+
+  /**
+   * 获取内存使用情况
+   */
+  getMemoryUsage() {
+    const usage = process.memoryUsage();
+    const used = Math.round(usage.heapUsed / 1024 / 1024);
+    const total = Math.round(usage.heapTotal / 1024 / 1024);
+    return `${used}MB / ${total}MB`;
+  }
+
+  /**
+   * 停止机器人
+   */
+  stop() {
+    if (this.enabled && this.bot) {
+      this.bot.stopPolling();
+      logger.info('Telegram机器人已停止');
+    }
+  }
+}
+
+module.exports = BotManager; 

+ 151 - 4
src/core/multiSubscriptionManager.js

@@ -280,8 +280,10 @@ class MultiSubscriptionManager {
 
       return { updated, added, removed, actualNodeCount };
     } catch (error) {
-      // 回滚事务
-      await transaction.rollback();
+      // 检查事务状态,只有在事务仍然活跃时才回滚
+      if (transaction && !transaction.finished) {
+        await transaction.rollback();
+      }
       logger.error('更新订阅节点失败', { 
         error: error.message,
         subscriptionId: subscription.id 
@@ -446,7 +448,7 @@ class MultiSubscriptionManager {
     const proxies = [];
     const proxyKeys = new Set(); // 用于去重
     
-          // 开始解析Shadowsocks内容
+    logger.info('开始解析代理链接内容');
     
     let ssCount = 0;
     let vmessCount = 0;
@@ -479,12 +481,52 @@ class MultiSubscriptionManager {
         }
       } else if (line.startsWith('vmess://')) {
         vmessCount++;
+        try {
+          const proxy = this.parseVmessUrl(line);
+          if (proxy) {
+            const key = `${proxy.name}-${proxy.server}-${proxy.port}`;
+            
+            // 检查是否重复
+            if (proxyKeys.has(key)) {
+              duplicateCount++;
+              logger.debug(`跳过重复的VMess节点: ${proxy.name} (${proxy.server}:${proxy.port})`);
+              continue;
+            }
+            
+            proxyKeys.add(key);
+            proxies.push(proxy);
+          } else {
+            errorCount++;
+          }
+        } catch (error) {
+          errorCount++;
+        }
       } else if (line.startsWith('trojan://')) {
         trojanCount++;
+        try {
+          const proxy = this.parseTrojanUrl(line);
+          if (proxy) {
+            const key = `${proxy.name}-${proxy.server}-${proxy.port}`;
+            
+            // 检查是否重复
+            if (proxyKeys.has(key)) {
+              duplicateCount++;
+              logger.debug(`跳过重复的Trojan节点: ${proxy.name} (${proxy.server}:${proxy.port})`);
+              continue;
+            }
+            
+            proxyKeys.add(key);
+            proxies.push(proxy);
+          } else {
+            errorCount++;
+          }
+        } catch (error) {
+          errorCount++;
+        }
       }
     }
     
-          // 解析完成 - SS: ${ssCount}个, VMess: ${vmessCount}个, Trojan: ${trojanCount}个
+    logger.info(`解析完成 - SS: ${ssCount}个, VMess: ${vmessCount}个, Trojan: ${trojanCount}个, 成功解析: ${proxies.length}个, 错误: ${errorCount}个, 重复: ${duplicateCount}个`);
     
     if (proxies.length === 0) {
       logger.warn('没有找到任何有效的代理节点');
@@ -569,6 +611,111 @@ class MultiSubscriptionManager {
     }
   }
 
+  /**
+   * 解析VMess URL
+   */
+  parseVmessUrl(url) {
+    try {
+      // 移除vmess://前缀
+      const base64Part = url.substring(8);
+      
+      // Base64解码
+      const decoded = Buffer.from(base64Part, 'base64').toString('utf8');
+      
+      // 解析JSON格式的VMess配置
+      const config = JSON.parse(decoded);
+      
+      if (!config.add || !config.port || !config.id) {
+        logger.warn('VMess配置缺少必要字段', { config });
+        return null;
+      }
+      
+      // 清理服务器地址(移除可能的\r字符)
+      const cleanServer = config.add.replace(/\r/g, '');
+      const cleanRemark = (config.ps || '').replace(/\r/g, '');
+      
+      return {
+        name: cleanRemark || `${cleanServer}:${config.port}`,
+        type: 'vmess',
+        server: cleanServer,
+        port: parseInt(config.port),
+        uuid: config.id,
+        alterId: parseInt(config.aid) || 0,
+        network: config.net || 'tcp',
+        tls: config.tls === 'tls',
+        sni: config.sni || config.host || cleanServer,
+        wsPath: config.path || '/',
+        wsHeaders: config.host ? JSON.stringify({ Host: config.host }) : null
+      };
+    } catch (error) {
+      logger.warn('解析VMess URL失败', { url: url.substring(0, 50) + '...', error: error.message });
+      return null;
+    }
+  }
+
+  /**
+   * 解析Trojan URL
+   */
+  parseTrojanUrl(url) {
+    try {
+      // 移除trojan://前缀
+      const urlPart = url.substring(9);
+      
+      // 分离密码和服务器信息
+      const atIndex = urlPart.lastIndexOf('@');
+      if (atIndex === -1) {
+        logger.warn('Trojan URL格式错误,缺少@符号', { url: url.substring(0, 50) + '...' });
+        return null;
+      }
+      
+      const password = urlPart.substring(0, atIndex);
+      const serverPart = urlPart.substring(atIndex + 1);
+      
+      // 分离服务器地址、端口和备注
+      const hashIndex = serverPart.indexOf('#');
+      let serverInfo, remark;
+      
+      if (hashIndex !== -1) {
+        serverInfo = serverPart.substring(0, hashIndex);
+        remark = decodeURIComponent(serverPart.substring(hashIndex + 1));
+      } else {
+        serverInfo = serverPart;
+        remark = '';
+      }
+      
+      // 解析服务器地址和端口
+      const colonIndex = serverInfo.lastIndexOf(':');
+      if (colonIndex === -1) {
+        logger.warn('Trojan URL格式错误,缺少端口号', { serverInfo });
+        return null;
+      }
+      
+      const server = serverInfo.substring(0, colonIndex);
+      const port = parseInt(serverInfo.substring(colonIndex + 1));
+      
+      if (isNaN(port) || port <= 0 || port > 65535) {
+        logger.warn('Trojan URL端口号无效', { port: serverInfo.substring(colonIndex + 1) });
+        return null;
+      }
+      
+      // 清理服务器地址(移除可能的\r字符)
+      const cleanServer = server.replace(/\r/g, '');
+      const cleanRemark = remark.replace(/\r/g, '');
+      
+      return {
+        name: cleanRemark || `${cleanServer}:${port}`,
+        type: 'trojan',
+        server: cleanServer,
+        port: port,
+        password: password,
+        sni: cleanServer // Trojan默认使用服务器地址作为SNI
+      };
+    } catch (error) {
+      logger.warn('解析Trojan URL失败', { url: url.substring(0, 50) + '...', error: error.message });
+      return null;
+    }
+  }
+
   /**
    * 触发测速
    */

+ 23 - 2
src/core/notifier.js

@@ -75,6 +75,25 @@ class TelegramNotifier {
     });
   }
 
+  /**
+   * 发送订阅测试通知
+   */
+  async sendSubscriptionNotification(subscription, title, message, summary) {
+    if (!this.enabled) return;
+
+    await this.sendNotification({
+      type: 'subscription',
+      title,
+      message,
+      subscriptionId: subscription.id,
+      metadata: {
+        subscriptionName: subscription.name,
+        subscriptionId: subscription.id,
+        summary: summary
+      }
+    });
+  }
+
   /**
    * 发送系统通知
    */
@@ -85,8 +104,10 @@ class TelegramNotifier {
       type: 'system',
       title,
       message,
-      nodeId: null,
-      metadata
+      metadata: {
+        ...metadata,
+        timestamp: new Date().toISOString()
+      }
     });
   }
 

+ 266 - 100
src/core/scheduler.js

@@ -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);
-  }
-
   /**
    * 清理旧数据
    */

+ 1 - 1
src/models/Notification.js

@@ -13,7 +13,7 @@ const Notification = sequelize.define('Notification', {
     comment: '节点ID (可选,系统通知为null)'
   },
   type: {
-    type: DataTypes.ENUM('failure', 'recovery', 'system', 'summary'),
+    type: DataTypes.ENUM('failure', 'recovery', 'system', 'summary', 'subscription'),
     allowNull: false,
     comment: '通知类型'
   },