Taio_O 1 долоо хоног өмнө
parent
commit
55527af16b

+ 28 - 0
check_subscriptions.js

@@ -0,0 +1,28 @@
+const { Subscription } = require('./src/models');
+
+async function checkSubscriptions() {
+  try {
+    const subscriptions = await Subscription.findAll();
+    console.log('所有订阅配置:');
+    console.log('='.repeat(80));
+    
+    if (subscriptions.length === 0) {
+      console.log('数据库中没有订阅配置');
+      return;
+    }
+    
+    subscriptions.forEach(sub => {
+      console.log(`ID: ${sub.id}`);
+      console.log(`名称: ${sub.name}`);
+      console.log(`URL: ${sub.url}`);
+      console.log(`活跃状态: ${sub.isActive}`);
+      console.log(`创建时间: ${sub.createdAt}`);
+      console.log(`更新时间: ${sub.updatedAt}`);
+      console.log('-'.repeat(40));
+    });
+  } catch (error) {
+    console.error('检查订阅配置失败:', error.message);
+  }
+}
+
+checkSubscriptions(); 

+ 18 - 0
debug.js

@@ -0,0 +1,18 @@
+// 调试Base64解码 - 使用正确的Base64内容
+const correctBase64Content = `c3M6Ly9ZMmhoWTJoaE1qQXRhV1YwWmkxd2IyeDVNVE13TlRveU16VmhOR05tTmkwNU5qWXpMVFEyTjJRdFlUWTJaaTA1WW1ZM1kySmtNekZtTVRZQOacgOaWsOWumOe9ke+8mnNvLnhmeHNzci5tZToxMDgwIyVFNiU5QyU4MCVFNiU5NiVCMCVFNSVBRSU5OCVFNyVCRCU5MSVFRiVCQyU5QXNvLnhmeHNzci5tZQ0Kc3M6Ly9ZMmhoWTJoaE1qQXRhV1YwWmkxd2IyeDVNVE13TlRveU16VmhOR05tTmkwNU5qWXpMVFEyTjJRdFlUWTJaaTA1WW1ZM1kySmtNekZtTVRZQHgg5Luj6KGo5L2/55So5YCN546HOjEwODAjeCUyMCVFNCVCQiVBMyVFOCVBMSVBOCVFNCVCRCVCRiVFNyU5NCVBOCVFNSU4MCU4RCVFNyU4RSU4Nw0Kc3M6Ly9ZMmhoWTJoaE1qQXRhV1YwWmkxd2IyeDVNVE13TlRveU16VmhOR05tTmkwNU5qWXpMVFEyTjJRdFlUWTJaaTA1WW1ZM1kySmtNekZtTVRZQOeUqOS4jeS6hu+8jOivt+abtOaWsOiuoumYhToxMDgwIyVFNyU5NCVBOCVFNCVCOCU4RCVFNCVCQSU4NiVFRiVCQyU4QyVFOCVBRiVCNyVFNiU5QiVCNCVFNiU5NiVCMCVFOCVBRSVBMiVFOSU5OCU4NQ0K`;
+
+console.log('正确的Base64内容:');
+console.log(correctBase64Content.substring(0, 100) + '...');
+console.log('\n' + '='.repeat(50) + '\n');
+
+const decoded = Buffer.from(correctBase64Content, 'base64').toString('utf8');
+console.log('解码后内容:');
+console.log(decoded);
+console.log('\n' + '='.repeat(50) + '\n');
+
+const lines = decoded.split('\n').filter(line => line.trim());
+console.log('过滤后的行数:', lines.length);
+lines.forEach((line, index) => {
+  console.log(`行 ${index + 1}: ${line.substring(0, 100)}...`);
+  console.log(`是否以ss://开头: ${line.startsWith('ss://')}`);
+}); 

+ 4 - 1
env.example

@@ -28,4 +28,7 @@ NOTIFICATION_FAILURE_THRESHOLD=3 # 连续失败次数阈值
 NOTIFICATION_RECOVERY_THRESHOLD=2 # 恢复通知阈值
 
 # 订阅配置
-SUBSCRIPTION_UPDATE_INTERVAL=3600000 # 订阅更新间隔(毫秒,默认1小时) 
+SUBSCRIPTION_UPDATE_INTERVAL=3600000 # 订阅更新间隔(毫秒,默认1小时)
+
+# 定时测速配置
+ENABLE_SCHEDULED_SPEED_TEST=true # 是否启用定时测速功能(true/false) 

+ 6 - 0
src/api/routes.js

@@ -7,6 +7,9 @@ const SubscriptionManager = require('../core/subscriptionManager');
 const { Node, TestResult, Notification } = require('../models');
 const logger = require('../utils/logger');
 
+// 引入订阅管理路由
+const subscriptionRoutes = require('./subscriptionRoutes');
+
 // 节点管理API
 router.get('/nodes', async (req, res) => {
   try {
@@ -518,4 +521,7 @@ router.get('/status', async (req, res) => {
   }
 });
 
+// 注册订阅管理路由
+router.use('/api', subscriptionRoutes);
+
 module.exports = router; 

+ 341 - 0
src/api/subscriptionRoutes.js

@@ -0,0 +1,341 @@
+const express = require('express');
+const { Subscription } = require('../models');
+const MultiSubscriptionManager = require('../core/multiSubscriptionManager');
+const logger = require('../utils/logger');
+
+const router = express.Router();
+const subscriptionManager = new MultiSubscriptionManager();
+
+/**
+ * 获取所有订阅列表
+ */
+router.get('/subscriptions', async (req, res) => {
+  try {
+    const subscriptions = await Subscription.findAll({
+      order: [['createdAt', 'DESC']]
+    });
+
+    res.json({
+      success: true,
+      data: subscriptions
+    });
+  } catch (error) {
+    logger.error('获取订阅列表失败', { error: error.message });
+    res.status(500).json({
+      success: false,
+      error: error.message
+    });
+  }
+});
+
+/**
+ * 获取单个订阅详情
+ */
+router.get('/subscriptions/:id', async (req, res) => {
+  try {
+    const subscription = await Subscription.findByPk(req.params.id);
+    
+    if (!subscription) {
+      return res.status(404).json({
+        success: false,
+        error: '订阅不存在'
+      });
+    }
+
+    res.json({
+      success: true,
+      data: subscription
+    });
+  } catch (error) {
+    logger.error('获取订阅详情失败', { error: error.message });
+    res.status(500).json({
+      success: false,
+      error: error.message
+    });
+  }
+});
+
+/**
+ * 创建新订阅
+ */
+router.post('/subscriptions', async (req, res) => {
+  try {
+    const { name, url, description, speedTestConfig, notifyConfig } = req.body;
+
+    if (!name || !url) {
+      return res.status(400).json({
+        success: false,
+        error: '订阅名称和URL不能为空'
+      });
+    }
+
+    // 检查URL是否已存在
+    const existingSubscription = await Subscription.findOne({
+      where: { url }
+    });
+
+    if (existingSubscription) {
+      return res.status(400).json({
+        success: false,
+        error: '该订阅URL已存在'
+      });
+    }
+
+    const subscription = await Subscription.create({
+      name,
+      url,
+      description,
+      speedTestConfig: speedTestConfig || {
+        testCount: 3,
+        timeout: 10000,
+        testUrls: ['https://www.google.com', 'https://www.youtube.com'],
+        speedTestEnabled: true
+      },
+      notifyConfig: notifyConfig || {
+        enabled: true,
+        notifyOnSpeedTest: true,
+        notifyOnNodeUpdate: true,
+        webhookUrl: '',
+        emailConfig: null
+      }
+    });
+
+    logger.info('创建新订阅成功', { 
+      subscriptionId: subscription.id,
+      subscriptionName: subscription.name 
+    });
+
+    res.status(201).json({
+      success: true,
+      data: subscription
+    });
+  } catch (error) {
+    logger.error('创建订阅失败', { error: error.message });
+    res.status(500).json({
+      success: false,
+      error: error.message
+    });
+  }
+});
+
+/**
+ * 更新订阅
+ */
+router.put('/subscriptions/:id', async (req, res) => {
+  try {
+    const subscription = await Subscription.findByPk(req.params.id);
+    
+    if (!subscription) {
+      return res.status(404).json({
+        success: false,
+        error: '订阅不存在'
+      });
+    }
+
+    const { name, url, description, speedTestConfig, notifyConfig, isActive } = req.body;
+
+    // 如果更新URL,检查是否与其他订阅冲突
+    if (url && url !== subscription.url) {
+      const existingSubscription = await Subscription.findOne({
+        where: { 
+          url,
+          id: { [require('sequelize').Op.ne]: req.params.id }
+        }
+      });
+
+      if (existingSubscription) {
+        return res.status(400).json({
+          success: false,
+          error: '该订阅URL已被其他订阅使用'
+        });
+      }
+    }
+
+    await subscription.update({
+      name: name || subscription.name,
+      url: url || subscription.url,
+      description: description !== undefined ? description : subscription.description,
+      speedTestConfig: speedTestConfig || subscription.speedTestConfig,
+      notifyConfig: notifyConfig || subscription.notifyConfig,
+      isActive: isActive !== undefined ? isActive : subscription.isActive
+    });
+
+    logger.info('更新订阅成功', { 
+      subscriptionId: subscription.id,
+      subscriptionName: subscription.name 
+    });
+
+    res.json({
+      success: true,
+      data: subscription
+    });
+  } catch (error) {
+    logger.error('更新订阅失败', { error: error.message });
+    res.status(500).json({
+      success: false,
+      error: error.message
+    });
+  }
+});
+
+/**
+ * 删除订阅
+ */
+router.delete('/subscriptions/:id', async (req, res) => {
+  try {
+    const subscription = await Subscription.findByPk(req.params.id);
+    
+    if (!subscription) {
+      return res.status(404).json({
+        success: false,
+        error: '订阅不存在'
+      });
+    }
+
+    await subscription.destroy();
+
+    logger.info('删除订阅成功', { 
+      subscriptionId: subscription.id,
+      subscriptionName: subscription.name 
+    });
+
+    res.json({
+      success: true,
+      message: '订阅删除成功'
+    });
+  } catch (error) {
+    logger.error('删除订阅失败', { error: error.message });
+    res.status(500).json({
+      success: false,
+      error: error.message
+    });
+  }
+});
+
+/**
+ * 手动更新所有订阅
+ */
+router.post('/subscriptions/update-all', async (req, res) => {
+  try {
+    const result = await subscriptionManager.manualUpdate();
+    
+    if (result.success) {
+      logger.info('手动更新所有订阅成功', { results: result.data });
+      res.json({
+        success: true,
+        data: result.data
+      });
+    } else {
+      logger.error('手动更新所有订阅失败', { error: result.error });
+      res.status(500).json({
+        success: false,
+        error: result.error
+      });
+    }
+  } catch (error) {
+    logger.error('手动更新所有订阅失败', { error: error.message });
+    res.status(500).json({
+      success: false,
+      error: error.message
+    });
+  }
+});
+
+/**
+ * 手动更新单个订阅
+ */
+router.post('/subscriptions/:id/update', async (req, res) => {
+  try {
+    const result = await subscriptionManager.manualUpdateSubscription(req.params.id);
+    
+    if (result.success) {
+      logger.info('手动更新订阅成功', { 
+        subscriptionId: req.params.id,
+        result: result.data 
+      });
+      res.json({
+        success: true,
+        data: result.data
+      });
+    } else {
+      logger.error('手动更新订阅失败', { 
+        subscriptionId: req.params.id,
+        error: result.error 
+      });
+      res.status(500).json({
+        success: false,
+        error: result.error
+      });
+    }
+  } catch (error) {
+    logger.error('手动更新订阅失败', { error: error.message });
+    res.status(500).json({
+      success: false,
+      error: error.message
+    });
+  }
+});
+
+/**
+ * 获取订阅状态
+ */
+router.get('/subscriptions/status', async (req, res) => {
+  try {
+    const status = await subscriptionManager.getStatus();
+    
+    res.json({
+      success: true,
+      data: status
+    });
+  } catch (error) {
+    logger.error('获取订阅状态失败', { error: error.message });
+    res.status(500).json({
+      success: false,
+      error: error.message
+    });
+  }
+});
+
+/**
+ * 启动自动更新
+ */
+router.post('/subscriptions/start-auto-update', async (req, res) => {
+  try {
+    subscriptionManager.startAutoUpdate();
+    
+    logger.info('启动订阅自动更新成功');
+    res.json({
+      success: true,
+      message: '订阅自动更新已启动'
+    });
+  } catch (error) {
+    logger.error('启动订阅自动更新失败', { error: error.message });
+    res.status(500).json({
+      success: false,
+      error: error.message
+    });
+  }
+});
+
+/**
+ * 停止自动更新
+ */
+router.post('/subscriptions/stop-auto-update', async (req, res) => {
+  try {
+    subscriptionManager.stopAutoUpdate();
+    
+    logger.info('停止订阅自动更新成功');
+    res.json({
+      success: true,
+      message: '订阅自动更新已停止'
+    });
+  } catch (error) {
+    logger.error('停止订阅自动更新失败', { error: error.message });
+    res.status(500).json({
+      success: false,
+      error: error.message
+    });
+  }
+});
+
+module.exports = router; 

+ 40 - 9
src/app.js

@@ -8,6 +8,7 @@ const logger = require('./utils/logger');
 const sequelize = require('./config/database');
 const routes = require('./api/routes');
 const Scheduler = require('./core/scheduler');
+const MultiSubscriptionManager = require('./core/multiSubscriptionManager');
 
 const app = express();
 const PORT = process.env.PORT || 3000;
@@ -115,15 +116,35 @@ async function startApp() {
     scheduler.start();
     app.set('scheduler', scheduler);
 
-    // 立即开始测速
-    setTimeout(async () => {
-      try {
-        logger.info('应用启动后立即开始测速');
-        await scheduler.runSpeedTest();
-      } catch (error) {
-        logger.error('启动测速失败', { error: error.message });
-      }
-    }, 3000); // 延迟3秒开始,确保应用完全启动
+    // 启动多订阅管理器
+    const subscriptionManager = new MultiSubscriptionManager();
+    subscriptionManager.startAutoUpdate();
+    
+    // 设置测速触发器
+    subscriptionManager.setSpeedTestTrigger(() => {
+      scheduler.runSpeedTest().catch(error => {
+        logger.error('订阅更新后自动测速失败', { error: error.message });
+      });
+    });
+    
+    app.set('subscriptionManager', subscriptionManager);
+
+    // 检查是否启用启动时立即测速
+    const enableStartupSpeedTest = process.env.ENABLE_SCHEDULED_SPEED_TEST !== 'false';
+    
+    if (enableStartupSpeedTest) {
+      // 立即开始测速
+      setTimeout(async () => {
+        try {
+          logger.info('应用启动后立即开始测速');
+          await scheduler.runSpeedTest();
+        } catch (error) {
+          logger.error('启动测速失败', { error: error.message });
+        }
+      }, 3000); // 延迟3秒开始,确保应用完全启动
+    } else {
+      logger.info('启动时立即测速功能已禁用');
+    }
 
     // 启动服务器
     app.listen(PORT, () => {
@@ -146,6 +167,11 @@ process.on('SIGTERM', async () => {
     scheduler.stop();
   }
   
+  const subscriptionManager = app.get('subscriptionManager');
+  if (subscriptionManager) {
+    subscriptionManager.stopAutoUpdate();
+  }
+  
   await sequelize.close();
   logger.info('应用已关闭');
   process.exit(0);
@@ -159,6 +185,11 @@ process.on('SIGINT', async () => {
     scheduler.stop();
   }
   
+  const subscriptionManager = app.get('subscriptionManager');
+  if (subscriptionManager) {
+    subscriptionManager.stopAutoUpdate();
+  }
+  
   await sequelize.close();
   logger.info('应用已关闭');
   process.exit(0);

+ 551 - 0
src/core/multiSubscriptionManager.js

@@ -0,0 +1,551 @@
+const axios = require('axios');
+const yaml = require('yaml');
+const logger = require('../utils/logger');
+const ClashParser = require('./clashParser');
+const { Node, Subscription } = require('../models');
+
+class MultiSubscriptionManager {
+  constructor() {
+    this.updateInterval = parseInt(process.env.SUBSCRIPTION_UPDATE_INTERVAL) || 3600000; // 默认1小时
+    this.updateTimer = null;
+  }
+
+  /**
+   * 获取所有活跃的订阅
+   */
+  async getActiveSubscriptions() {
+    try {
+      return await Subscription.findAll({
+        where: { isActive: true },
+        order: [['createdAt', 'ASC']]
+      });
+    } catch (error) {
+      logger.error('获取活跃订阅失败', { error: error.message });
+      return [];
+    }
+  }
+
+  /**
+   * 从订阅地址获取配置
+   */
+  async fetchSubscription(subscription) {
+    if (!subscription.url) {
+      throw new Error('订阅地址为空');
+    }
+
+    try {
+      const response = await axios.get(subscription.url, {
+        timeout: 30000,
+        headers: {
+          'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
+        }
+      });
+
+      if (!response.data) {
+        throw new Error('订阅地址返回空内容');
+      }
+
+      let config;
+      let rawContent = response.data;
+
+      // 1. 先尝试YAML解析
+      let yamlParsed = false;
+      try {
+        config = yaml.parse(rawContent);
+        if (config && typeof config === 'object' && Array.isArray(config.proxies)) {
+          logger.info('成功解析为YAML格式', { subscriptionId: subscription.id });
+          yamlParsed = true;
+        } else {
+          logger.info('YAML解析结果不是有效的订阅对象,进入Base64解码流程', { subscriptionId: subscription.id });
+          throw new Error('不是有效的YAML订阅');
+        }
+      } catch (error) {
+        if (!yamlParsed) logger.info('不是YAML格式,尝试Base64解码', { subscriptionId: subscription.id });
+        
+        // 2. 不是YAML,尝试整体Base64解码
+        let decoded;
+        try {
+          decoded = Buffer.from(rawContent, 'base64').toString('utf8');
+          logger.info('Base64解码成功,内容长度:', decoded.length, { subscriptionId: subscription.id });
+          
+          // 如果解码后包含ss://、vmess://、trojan://,说明是明文链接合集
+          if (/ss:\/\/|vmess:\/\/|trojan:\/\//.test(decoded)) {
+            logger.info('检测到代理链接,转换为Clash格式', { subscriptionId: subscription.id });
+            config = this.convertShadowsocksToClash(decoded);
+          } else {
+            logger.info('整体解码后不是代理链接,尝试多行Base64解码', { subscriptionId: subscription.id });
+            // 3. 如果整体解码后不是明文链接合集,尝试多行Base64(每行一个链接)
+            const lines = rawContent.split('\n').filter(line => line.trim());
+            logger.info('原始内容行数:', lines.length, { subscriptionId: subscription.id });
+            
+            let decodedLines = [];
+            for (const line of lines) {
+              try {
+                const d = Buffer.from(line, 'base64').toString('utf8');
+                if (/ss:\/\/|vmess:\/\/|trojan:\/\//.test(d)) {
+                  decodedLines.push(d);
+                  logger.debug('成功解码一行:', d.substring(0, 50) + '...', { subscriptionId: subscription.id });
+                }
+              } catch (e) { 
+                logger.debug('跳过无效的Base64行', { subscriptionId: subscription.id });
+                continue; 
+              }
+            }
+            
+            logger.info('成功解码的行数:', decodedLines.length, { subscriptionId: subscription.id });
+            if (decodedLines.length > 0) {
+              config = this.convertShadowsocksToClash(decodedLines.join('\n'));
+            } else {
+              throw new Error('无法解析订阅内容,既不是有效的YAML也不是有效的Base64编码');
+            }
+          }
+        } catch (decodeError) {
+          logger.error('Base64解码失败:', decodeError.message, { subscriptionId: subscription.id });
+          throw new Error('无法解析订阅内容,既不是有效的YAML也不是有效的Base64编码');
+        }
+      }
+
+      if (!config.proxies || !Array.isArray(config.proxies)) {
+        logger.error('订阅配置中没有找到有效的代理节点', {
+          hasProxies: !!config.proxies,
+          isArray: Array.isArray(config.proxies),
+          configKeys: Object.keys(config || {}),
+          subscriptionId: subscription.id
+        });
+        throw new Error('订阅配置中没有找到有效的代理节点');
+      }
+      
+      // 只在节点数量较多时显示前几个名称
+      const proxyNames = config.proxies.length > 10 
+        ? config.proxies.slice(0, 3).map(p => p.name)
+        : config.proxies.map(p => p.name);
+      
+      logger.info('订阅配置解析成功', {
+        proxyCount: config.proxies.length,
+        proxyNames: proxyNames,
+        subscriptionId: subscription.id
+      });
+
+      return config;
+    } catch (error) {
+      logger.error('获取订阅配置失败', { 
+        error: error.message, 
+        url: subscription.url,
+        subscriptionId: subscription.id
+      });
+      throw error;
+    }
+  }
+
+  /**
+   * 更新单个订阅的节点列表
+   */
+  async updateSubscriptionNodes(subscription) {
+    try {
+      logger.info('开始更新订阅节点', { 
+        subscriptionId: subscription.id,
+        subscriptionName: subscription.name 
+      });
+
+      // 获取订阅配置
+      const config = await this.fetchSubscription(subscription);
+      
+      // 解析节点
+      const parser = new ClashParser();
+      const newNodes = [];
+      
+      for (const proxy of config.proxies) {
+        const node = parser.parseProxy(proxy);
+        if (node) {
+          newNodes.push(node);
+        }
+      }
+
+      if (newNodes.length === 0) {
+        logger.warn('订阅中没有找到有效的节点', { subscriptionId: subscription.id });
+        return { updated: 0, added: 0, removed: 0 };
+      }
+
+      // 获取现有节点
+      const existingNodes = await Node.findAll({
+        where: { 
+          subscriptionId: subscription.id,
+          isActive: true 
+        }
+      });
+
+      const existingNodeMap = new Map();
+      existingNodes.forEach(node => {
+        const key = `${node.name}-${node.server}-${node.port}`;
+        existingNodeMap.set(key, node);
+      });
+
+      let added = 0;
+      let updated = 0;
+      let removed = 0;
+
+      // 批量处理节点更新
+      const updatePromises = [];
+      const createPromises = [];
+      const deactivatePromises = [];
+
+      // 处理新节点
+      for (const nodeData of newNodes) {
+        const key = `${nodeData.name}-${nodeData.server}-${nodeData.port}`;
+        const existingNode = existingNodeMap.get(key);
+
+        if (existingNode) {
+          // 批量更新现有节点
+          updatePromises.push(
+            existingNode.update({
+              ...nodeData,
+              subscriptionId: subscription.id,
+              updatedAt: new Date()
+            })
+          );
+          updated++;
+          existingNodeMap.delete(key);
+        } else {
+          // 批量创建新节点
+          createPromises.push(
+            Node.create({
+              ...nodeData,
+              subscriptionId: subscription.id,
+              isActive: true,
+              status: 'offline'
+            })
+          );
+          added++;
+        }
+      }
+
+      // 批量标记不再存在的节点为非活跃
+      for (const [key, node] of existingNodeMap) {
+        deactivatePromises.push(node.update({ isActive: false }));
+        removed++;
+      }
+
+      // 并行执行所有数据库操作
+      logger.info(`正在批量更新数据库,共${updatePromises.length + createPromises.length + deactivatePromises.length}个操作...`);
+      await Promise.all([...updatePromises, ...createPromises, ...deactivatePromises]);
+
+      // 更新订阅的节点数量
+      await subscription.update({
+        nodeCount: newNodes.length,
+        lastUpdateTime: new Date()
+      });
+
+      logger.info(`订阅节点更新完成 - 新增${added}个,更新${updated}个,移除${removed}个`, {
+        subscriptionId: subscription.id,
+        subscriptionName: subscription.name
+      });
+
+      // 如果有新增或更新的节点,触发测速
+      if (added > 0 || updated > 0) {
+        logger.info('检测到节点更新,准备触发测速...');
+        // 延迟3秒后触发测速,确保数据库操作完成
+        setTimeout(() => {
+          this.triggerSpeedTest();
+        }, 3000);
+      }
+
+      return { updated: newNodes.length, added, updated, removed };
+    } catch (error) {
+      logger.error('更新订阅节点失败', { 
+        error: error.message,
+        subscriptionId: subscription.id 
+      });
+      throw error;
+    }
+  }
+
+  /**
+   * 更新所有订阅的节点
+   */
+  async updateAllSubscriptions() {
+    try {
+      const subscriptions = await this.getActiveSubscriptions();
+      logger.info(`开始更新所有订阅,共${subscriptions.length}个活跃订阅`);
+
+      const results = [];
+      for (const subscription of subscriptions) {
+        try {
+          const result = await this.updateSubscriptionNodes(subscription);
+          results.push({
+            subscriptionId: subscription.id,
+            subscriptionName: subscription.name,
+            ...result
+          });
+        } catch (error) {
+          logger.error('更新订阅失败', {
+            subscriptionId: subscription.id,
+            subscriptionName: subscription.name,
+            error: error.message
+          });
+          results.push({
+            subscriptionId: subscription.id,
+            subscriptionName: subscription.name,
+            error: error.message
+          });
+        }
+      }
+
+      // 检查是否有新增或更新的节点,如果有则触发测速
+      const hasUpdates = results.some(result => 
+        result.added > 0 || result.updated > 0
+      );
+
+      if (hasUpdates) {
+        logger.info('检测到节点更新,准备触发测速...');
+        // 延迟3秒后触发测速,确保数据库操作完成
+        setTimeout(() => {
+          this.triggerSpeedTest();
+        }, 3000);
+      }
+
+      return results;
+    } catch (error) {
+      logger.error('更新所有订阅失败', { error: error.message });
+      throw error;
+    }
+  }
+
+  /**
+   * 启动自动更新
+   */
+  startAutoUpdate() {
+    logger.info('启动多订阅自动更新', { 
+      interval: `${this.updateInterval / 1000}秒` 
+    });
+
+    // 立即执行一次更新
+    this.updateAllSubscriptions().catch(error => {
+      logger.error('初始多订阅更新失败', { error: error.message });
+    });
+
+    // 设置定时更新
+    this.updateTimer = setInterval(() => {
+      this.updateAllSubscriptions().catch(error => {
+        logger.error('定时多订阅更新失败', { error: error.message });
+      });
+    }, this.updateInterval);
+  }
+
+  /**
+   * 停止自动更新
+   */
+  stopAutoUpdate() {
+    if (this.updateTimer) {
+      clearInterval(this.updateTimer);
+      this.updateTimer = null;
+      logger.info('多订阅自动更新已停止');
+    }
+  }
+
+  /**
+   * 手动更新所有订阅
+   */
+  async manualUpdate() {
+    try {
+      const results = await this.updateAllSubscriptions();
+      return {
+        success: true,
+        data: results
+      };
+    } catch (error) {
+      return {
+        success: false,
+        error: error.message
+      };
+    }
+  }
+
+  /**
+   * 手动更新单个订阅
+   */
+  async manualUpdateSubscription(subscriptionId) {
+    try {
+      const subscription = await Subscription.findByPk(subscriptionId);
+      if (!subscription) {
+        throw new Error('订阅不存在');
+      }
+
+      const result = await this.updateSubscriptionNodes(subscription);
+      return {
+        success: true,
+        data: {
+          subscriptionId: subscription.id,
+          subscriptionName: subscription.name,
+          ...result
+        }
+      };
+    } catch (error) {
+      return {
+        success: false,
+        error: error.message
+      };
+    }
+  }
+
+  /**
+   * 将Shadowsocks链接转换为Clash格式
+   */
+  convertShadowsocksToClash(shadowsocksContent) {
+    const lines = shadowsocksContent.split('\n').filter(line => line.trim());
+    const proxies = [];
+    
+    logger.info(`开始解析Shadowsocks内容,共${lines.length}行`);
+    
+    let ssCount = 0;
+    let vmessCount = 0;
+    let trojanCount = 0;
+    let errorCount = 0;
+    
+    for (const line of lines) {
+      if (line.startsWith('ss://')) {
+        ssCount++;
+        try {
+          const proxy = this.parseShadowsocksUrl(line);
+          if (proxy) {
+            proxies.push(proxy);
+          } else {
+            errorCount++;
+          }
+        } catch (error) {
+          errorCount++;
+        }
+      } else if (line.startsWith('vmess://')) {
+        vmessCount++;
+      } else if (line.startsWith('trojan://')) {
+        trojanCount++;
+      }
+    }
+    
+    logger.info(`解析完成 - SS: ${ssCount}个, VMess: ${vmessCount}个, Trojan: ${trojanCount}个, 成功解析: ${proxies.length}个, 失败: ${errorCount}个`);
+    
+    if (proxies.length === 0) {
+      logger.warn('没有找到任何有效的代理节点');
+    }
+    
+    return {
+      proxies: proxies,
+      proxyGroups: [
+        {
+          name: 'Proxy',
+          type: 'select',
+          proxies: proxies.map(p => p.name)
+        }
+      ]
+    };
+  }
+
+  /**
+   * 解析Shadowsocks URL
+   */
+  parseShadowsocksUrl(url) {
+    try {
+      // 移除ss://前缀
+      const base64Part = url.substring(5);
+      
+      // 分离服务器信息和备注
+      const parts = base64Part.split('#');
+      const serverInfo = parts[0];
+      const remark = parts[1] ? decodeURIComponent(parts[1]) : '';
+      
+      // 分离服务器地址和认证信息
+      const atIndex = serverInfo.lastIndexOf('@');
+      if (atIndex === -1) {
+        logger.warn('Shadowsocks URL格式错误,缺少@符号', { url: url.substring(0, 50) + '...' });
+        return null;
+      }
+      
+      const authPart = serverInfo.substring(0, atIndex);
+      const serverPart = serverInfo.substring(atIndex + 1);
+      
+      // 解析服务器地址和端口
+      const colonIndex = serverPart.lastIndexOf(':');
+      if (colonIndex === -1) {
+        logger.warn('Shadowsocks URL格式错误,缺少端口号', { serverPart });
+        return null;
+      }
+      
+      const server = serverPart.substring(0, colonIndex);
+      const port = parseInt(serverPart.substring(colonIndex + 1));
+      
+      if (isNaN(port) || port <= 0 || port > 65535) {
+        logger.warn('Shadowsocks URL端口号无效', { port: serverPart.substring(colonIndex + 1) });
+        return null;
+      }
+      
+      // 解析认证信息
+      const decodedAuth = Buffer.from(authPart, 'base64').toString('utf8');
+      const colonIndex2 = decodedAuth.indexOf(':');
+      if (colonIndex2 === -1) {
+        logger.warn('Shadowsocks认证信息格式错误', { decodedAuth });
+        return null;
+      }
+      
+      const method = decodedAuth.substring(0, colonIndex2);
+      const password = decodedAuth.substring(colonIndex2 + 1);
+      
+      // 清理服务器地址(移除可能的\r字符)
+      const cleanServer = server.replace(/\r/g, '');
+      const cleanRemark = remark.replace(/\r/g, '');
+      
+      return {
+        name: cleanRemark || `${cleanServer}:${port}`,
+        type: 'ss',
+        server: cleanServer,
+        port: port,
+        method: method,
+        password: password
+      };
+    } catch (error) {
+      logger.warn('解析Shadowsocks URL失败', { url: url.substring(0, 50) + '...', error: error.message });
+      return null;
+    }
+  }
+
+  /**
+   * 触发测速
+   */
+  triggerSpeedTest() {
+    try {
+      // 通过事件触发测速,避免直接依赖
+      if (this.onSpeedTestTrigger) {
+        this.onSpeedTestTrigger();
+      } else {
+        logger.info('测速触发器未设置,跳过自动测速');
+      }
+    } catch (error) {
+      logger.error('触发测速失败', { error: error.message });
+    }
+  }
+
+  /**
+   * 设置测速触发器
+   */
+  setSpeedTestTrigger(trigger) {
+    this.onSpeedTestTrigger = trigger;
+  }
+
+  /**
+   * 获取订阅状态
+   */
+  async getStatus() {
+    const subscriptions = await this.getActiveSubscriptions();
+    return {
+      activeSubscriptions: subscriptions.length,
+      updateInterval: this.updateInterval,
+      autoUpdateEnabled: !!this.updateTimer,
+      subscriptions: subscriptions.map(sub => ({
+        id: sub.id,
+        name: sub.name,
+        nodeCount: sub.nodeCount,
+        lastUpdateTime: sub.lastUpdateTime
+      }))
+    };
+  }
+}
+
+module.exports = MultiSubscriptionManager; 

+ 19 - 12
src/core/scheduler.js

@@ -2,14 +2,14 @@ const cron = require('node-cron');
 const logger = require('../utils/logger');
 const SpeedTester = require('./speedTester');
 const TelegramNotifier = require('./notifier');
-const SubscriptionManager = require('./subscriptionManager');
+const MultiSubscriptionManager = require('./multiSubscriptionManager');
 const { Node, TestResult } = require('../models');
 
 class Scheduler {
   constructor() {
     this.speedTester = new SpeedTester();
     this.notifier = new TelegramNotifier();
-    this.subscriptionManager = new SubscriptionManager();
+    this.subscriptionManager = new MultiSubscriptionManager();
     this.isRunning = false;
     this.testInterval = parseInt(process.env.SPEED_TEST_INTERVAL) || 10; // 分钟
     this.failureThreshold = parseInt(process.env.NOTIFICATION_FAILURE_THRESHOLD) || 3;
@@ -20,16 +20,23 @@ class Scheduler {
    * 启动调度器
    */
   start() {
-    logger.info(`启动定时任务调度器 - 定时测速间隔: ${this.testInterval}分钟`);
-
-    // 启用定时测速
-    const cronExpression = `*/${this.testInterval} * * * *`;
-    this.speedTestJob = cron.schedule(cronExpression, async () => {
-      await this.runSpeedTest();
-    }, {
-      scheduled: true,
-      timezone: 'Asia/Shanghai'
-    });
+    // 检查是否启用定时测速
+    const enableScheduledSpeedTest = process.env.ENABLE_SCHEDULED_SPEED_TEST !== 'false';
+    
+    if (enableScheduledSpeedTest) {
+      logger.info(`启动定时任务调度器 - 定时测速间隔: ${this.testInterval}分钟`);
+
+      // 启用定时测速
+      const cronExpression = `*/${this.testInterval} * * * *`;
+      this.speedTestJob = cron.schedule(cronExpression, async () => {
+        await this.runSpeedTest();
+      }, {
+        scheduled: true,
+        timezone: 'Asia/Shanghai'
+      });
+    } else {
+      logger.info('定时测速功能已禁用');
+    }
 
     // 每小时重试失败的通知
     this.notificationRetryJob = cron.schedule('0 * * * *', async () => {

+ 180 - 33
src/core/subscriptionManager.js

@@ -6,7 +6,7 @@ const { Node } = require('../models');
 
 class SubscriptionManager {
   constructor() {
-    this.subscriptionUrl = process.env.CLASH_SUBSCRIPTION_URL;
+    this.subscriptionUrl = process.env.CLASH_SUBSCRIPTION_URL || 'http://so.xfxssr.me/api/v1/client/subscribe?token=7854d59f38ac51700730b9e782c5160c';
     this.updateInterval = parseInt(process.env.SUBSCRIPTION_UPDATE_INTERVAL) || 3600000; // 默认1小时
   }
 
@@ -19,8 +19,6 @@ class SubscriptionManager {
     }
 
     try {
-      // 简化日志输出
-
       const response = await axios.get(this.subscriptionUrl, {
         timeout: 30000,
         headers: {
@@ -28,57 +26,85 @@ class SubscriptionManager {
         }
       });
 
-      // 检查响应内容
       if (!response.data) {
         throw new Error('订阅地址返回空内容');
       }
 
-      // 尝试解析为YAML
       let config;
       let rawContent = response.data;
-      
+
+      // 1. 先尝试YAML解析
+      let yamlParsed = false;
       try {
         config = yaml.parse(rawContent);
+        if (config && typeof config === 'object' && Array.isArray(config.proxies)) {
+          logger.info('成功解析为YAML格式');
+          yamlParsed = true;
+        } else {
+          logger.info('YAML解析结果不是有效的订阅对象,进入Base64解码流程');
+          throw new Error('不是有效的YAML订阅');
+        }
       } catch (error) {
-        // 如果不是YAML,尝试Base64解码
+        if (!yamlParsed) logger.info('不是YAML格式,尝试Base64解码');
+        // 2. 不是YAML,尝试整体Base64解码
+        let decoded;
         try {
-          const decoded = Buffer.from(rawContent, 'base64').toString('utf8');
-          config = yaml.parse(decoded);
-        } catch (decodeError) {
-          // 如果Base64解码也失败,尝试直接解析原始内容
-          // 有些订阅可能包含多个Base64编码的配置,用换行符分隔
-          const lines = rawContent.split('\n').filter(line => line.trim());
-          let decodedContent = '';
-          
-          for (const line of lines) {
-            try {
-              const decoded = Buffer.from(line, 'base64').toString('utf8');
-              decodedContent += decoded + '\n';
-            } catch (e) {
-              // 如果某行不是Base64,跳过
-              continue;
-            }
-          }
-          
-          if (decodedContent) {
-            config = yaml.parse(decodedContent);
+          decoded = Buffer.from(rawContent, 'base64').toString('utf8');
+          logger.info('Base64解码成功,内容长度:', decoded.length);
+          // 如果解码后包含ss://、vmess://、trojan://,说明是明文链接合集
+          if (/ss:\/\/|vmess:\/\/|trojan:\/\//.test(decoded)) {
+            logger.info('检测到代理链接,转换为Clash格式');
+            config = this.convertShadowsocksToClash(decoded);
           } else {
-          throw new Error('无法解析订阅内容,既不是有效的YAML也不是Base64编码');
+            logger.info('整体解码后不是代理链接,尝试多行Base64解码');
+            // 3. 如果整体解码后不是明文链接合集,尝试多行Base64(每行一个链接)
+            const lines = rawContent.split('\n').filter(line => line.trim());
+            logger.info('原始内容行数:', lines.length);
+            let decodedLines = [];
+            for (const line of lines) {
+              try {
+                const d = Buffer.from(line, 'base64').toString('utf8');
+                if (/ss:\/\/|vmess:\/\/|trojan:\/\//.test(d)) {
+                  decodedLines.push(d);
+                  logger.debug('成功解码一行:', d.substring(0, 50) + '...');
+                }
+              } catch (e) { 
+                logger.debug('跳过无效的Base64行');
+                continue; 
+              }
+            }
+            logger.info('成功解码的行数:', decodedLines.length);
+            if (decodedLines.length > 0) {
+              config = this.convertShadowsocksToClash(decodedLines.join('\n'));
+            } else {
+              throw new Error('无法解析订阅内容,既不是有效的YAML也不是有效的Base64编码');
+            }
           }
+        } catch (decodeError) {
+          logger.error('Base64解码失败:', decodeError.message);
+          throw new Error('无法解析订阅内容,既不是有效的YAML也不是有效的Base64编码');
         }
       }
 
       if (!config.proxies || !Array.isArray(config.proxies)) {
+        logger.error('订阅配置中没有找到有效的代理节点', {
+          hasProxies: !!config.proxies,
+          isArray: Array.isArray(config.proxies),
+          configKeys: Object.keys(config || {})
+        });
         throw new Error('订阅配置中没有找到有效的代理节点');
       }
-
-      // 简化日志输出
+      
+      logger.info('订阅配置解析成功', {
+        proxyCount: config.proxies.length,
+        proxyNames: config.proxies.slice(0, 3).map(p => p.name)
+      });
 
       return config;
     } catch (error) {
-      logger.error('获取订阅配置失败', { 
-        error: error.message, 
-        url: this.subscriptionUrl 
+      logger.error('获取订阅配置失败', {
+        error: error.message,
+        url: this.subscriptionUrl
       });
       throw error;
     }
@@ -219,6 +245,127 @@ class SubscriptionManager {
     }
   }
 
+  /**
+   * 将Shadowsocks链接转换为Clash格式
+   */
+  convertShadowsocksToClash(shadowsocksContent) {
+    const lines = shadowsocksContent.split('\n').filter(line => line.trim());
+    const proxies = [];
+    
+    logger.info(`开始解析Shadowsocks内容,共${lines.length}行`);
+    
+    let ssCount = 0;
+    let vmessCount = 0;
+    let trojanCount = 0;
+    
+    for (const line of lines) {
+      if (line.startsWith('ss://')) {
+        ssCount++;
+        try {
+          const proxy = this.parseShadowsocksUrl(line);
+          if (proxy) {
+            proxies.push(proxy);
+            logger.debug('成功解析SS节点', { name: proxy.name, server: proxy.server, port: proxy.port });
+          } else {
+            logger.warn('解析SS链接返回null', { line: line.substring(0, 50) + '...' });
+          }
+        } catch (error) {
+          logger.warn('解析SS链接失败', { line: line.substring(0, 50) + '...', error: error.message });
+        }
+      } else if (line.startsWith('vmess://')) {
+        vmessCount++;
+        logger.info('检测到VMess链接,暂不支持解析');
+      } else if (line.startsWith('trojan://')) {
+        trojanCount++;
+        logger.info('检测到Trojan链接,暂不支持解析');
+      }
+    }
+    
+    logger.info(`解析完成 - SS: ${ssCount}个, VMess: ${vmessCount}个, Trojan: ${trojanCount}个, 成功解析: ${proxies.length}个`);
+    
+    if (proxies.length === 0) {
+      logger.warn('没有找到任何有效的代理节点');
+    }
+    
+    return {
+      proxies: proxies,
+      proxyGroups: [
+        {
+          name: 'Proxy',
+          type: 'select',
+          proxies: proxies.map(p => p.name)
+        }
+      ]
+    };
+  }
+
+  /**
+   * 解析Shadowsocks URL
+   */
+  parseShadowsocksUrl(url) {
+    try {
+      // 移除ss://前缀
+      const base64Part = url.substring(5);
+      
+      // 分离服务器信息和备注
+      const parts = base64Part.split('#');
+      const serverInfo = parts[0];
+      const remark = parts[1] ? decodeURIComponent(parts[1]) : '';
+      
+      // 分离服务器地址和认证信息
+      const atIndex = serverInfo.lastIndexOf('@');
+      if (atIndex === -1) {
+        logger.warn('Shadowsocks URL格式错误,缺少@符号', { url: url.substring(0, 50) + '...' });
+        return null;
+      }
+      
+      const authPart = serverInfo.substring(0, atIndex);
+      const serverPart = serverInfo.substring(atIndex + 1);
+      
+      // 解析服务器地址和端口
+      const colonIndex = serverPart.lastIndexOf(':');
+      if (colonIndex === -1) {
+        logger.warn('Shadowsocks URL格式错误,缺少端口号', { serverPart });
+        return null;
+      }
+      
+      const server = serverPart.substring(0, colonIndex);
+      const port = parseInt(serverPart.substring(colonIndex + 1));
+      
+      if (isNaN(port) || port <= 0 || port > 65535) {
+        logger.warn('Shadowsocks URL端口号无效', { port: serverPart.substring(colonIndex + 1) });
+        return null;
+      }
+      
+      // 解析认证信息
+      const decodedAuth = Buffer.from(authPart, 'base64').toString('utf8');
+      const colonIndex2 = decodedAuth.indexOf(':');
+      if (colonIndex2 === -1) {
+        logger.warn('Shadowsocks认证信息格式错误', { decodedAuth });
+        return null;
+      }
+      
+      const method = decodedAuth.substring(0, colonIndex2);
+      const password = decodedAuth.substring(colonIndex2 + 1);
+      
+      // 清理服务器地址(移除可能的\r字符)
+      const cleanServer = server.replace(/\r/g, '');
+      const cleanRemark = remark.replace(/\r/g, '');
+      
+      return {
+        name: cleanRemark || `${cleanServer}:${port}`,
+        type: 'ss',
+        server: cleanServer,
+        port: port,
+        method: method,
+        password: password
+      };
+    } catch (error) {
+      logger.warn('解析Shadowsocks URL失败', { url: url.substring(0, 50) + '...', error: error.message });
+      return null;
+    }
+  }
+
   /**
    * 获取订阅状态
    */

+ 115 - 0
src/database/migrate_subscriptions.js

@@ -0,0 +1,115 @@
+const sequelize = require('../config/database');
+const { DataTypes } = require('sequelize');
+
+async function migrateSubscriptions() {
+  try {
+    console.log('开始创建订阅表...');
+    
+    // 创建订阅表
+    await sequelize.getQueryInterface().createTable('subscriptions', {
+      id: {
+        type: DataTypes.INTEGER,
+        primaryKey: true,
+        autoIncrement: true
+      },
+      name: {
+        type: DataTypes.STRING(100),
+        allowNull: false,
+        comment: '订阅名称'
+      },
+      url: {
+        type: DataTypes.STRING(500),
+        allowNull: false,
+        comment: '订阅链接'
+      },
+      description: {
+        type: DataTypes.TEXT,
+        allowNull: true,
+        comment: '订阅描述'
+      },
+      speedTestConfig: {
+        type: DataTypes.JSON,
+        allowNull: true,
+        comment: '测速配置'
+      },
+      notifyConfig: {
+        type: DataTypes.JSON,
+        allowNull: true,
+        comment: '通知配置'
+      },
+      isActive: {
+        type: DataTypes.BOOLEAN,
+        allowNull: false,
+        defaultValue: true,
+        comment: '是否启用'
+      },
+      lastUpdateTime: {
+        type: DataTypes.DATE,
+        allowNull: true,
+        comment: '最后更新时间'
+      },
+      nodeCount: {
+        type: DataTypes.INTEGER,
+        allowNull: false,
+        defaultValue: 0,
+        comment: '节点数量'
+      },
+      createdAt: {
+        type: DataTypes.DATE,
+        allowNull: false,
+        defaultValue: DataTypes.NOW
+      },
+      updatedAt: {
+        type: DataTypes.DATE,
+        allowNull: false,
+        defaultValue: DataTypes.NOW
+      }
+    });
+
+    // 创建索引
+    await sequelize.getQueryInterface().addIndex('subscriptions', ['isActive']);
+    await sequelize.getQueryInterface().addIndex('subscriptions', ['name']);
+
+    console.log('订阅表创建成功');
+
+    // 为nodes表添加subscriptionId字段
+    console.log('为nodes表添加subscriptionId字段...');
+    
+    try {
+      await sequelize.getQueryInterface().addColumn('nodes', 'subscriptionId', {
+        type: DataTypes.INTEGER,
+        allowNull: true,
+        comment: '所属订阅ID'
+      });
+      
+      await sequelize.getQueryInterface().addIndex('nodes', ['subscriptionId']);
+      console.log('nodes表subscriptionId字段添加成功');
+    } catch (error) {
+      if (error.message.includes('already exists')) {
+        console.log('subscriptionId字段已存在,跳过');
+      } else {
+        throw error;
+      }
+    }
+
+    console.log('数据库迁移完成');
+  } catch (error) {
+    console.error('数据库迁移失败:', error.message);
+    throw error;
+  }
+}
+
+// 如果直接运行此文件,执行迁移
+if (require.main === module) {
+  migrateSubscriptions()
+    .then(() => {
+      console.log('迁移完成');
+      process.exit(0);
+    })
+    .catch((error) => {
+      console.error('迁移失败:', error);
+      process.exit(1);
+    });
+}
+
+module.exports = migrateSubscriptions; 

+ 8 - 0
src/models/Node.js

@@ -116,6 +116,11 @@ const Node = sequelize.define('Node', {
     allowNull: false,
     defaultValue: 'offline',
     comment: '节点状态'
+  },
+  subscriptionId: {
+    type: DataTypes.INTEGER,
+    allowNull: true,
+    comment: '所属订阅ID'
   }
 }, {
   tableName: 'nodes',
@@ -129,6 +134,9 @@ const Node = sequelize.define('Node', {
     },
     {
       fields: ['isActive']
+    },
+    {
+      fields: ['subscriptionId']
     }
   ]
 });

+ 78 - 0
src/models/Subscription.js

@@ -0,0 +1,78 @@
+const { DataTypes } = require('sequelize');
+const sequelize = require('../config/database');
+
+const Subscription = sequelize.define('Subscription', {
+  id: {
+    type: DataTypes.INTEGER,
+    primaryKey: true,
+    autoIncrement: true
+  },
+  name: {
+    type: DataTypes.STRING(100),
+    allowNull: false,
+    comment: '订阅名称'
+  },
+  url: {
+    type: DataTypes.STRING(500),
+    allowNull: false,
+    comment: '订阅链接'
+  },
+  description: {
+    type: DataTypes.TEXT,
+    allowNull: true,
+    comment: '订阅描述'
+  },
+  speedTestConfig: {
+    type: DataTypes.JSON,
+    allowNull: true,
+    defaultValue: {
+      testCount: 3,
+      timeout: 10000,
+      testUrls: ['https://www.google.com', 'https://www.youtube.com'],
+      speedTestEnabled: true
+    },
+    comment: '测速配置'
+  },
+  notifyConfig: {
+    type: DataTypes.JSON,
+    allowNull: true,
+    defaultValue: {
+      enabled: true,
+      notifyOnSpeedTest: true,
+      notifyOnNodeUpdate: true,
+      webhookUrl: '',
+      emailConfig: null
+    },
+    comment: '通知配置'
+  },
+  isActive: {
+    type: DataTypes.BOOLEAN,
+    allowNull: false,
+    defaultValue: true,
+    comment: '是否启用'
+  },
+  lastUpdateTime: {
+    type: DataTypes.DATE,
+    allowNull: true,
+    comment: '最后更新时间'
+  },
+  nodeCount: {
+    type: DataTypes.INTEGER,
+    allowNull: false,
+    defaultValue: 0,
+    comment: '节点数量'
+  }
+}, {
+  tableName: 'subscriptions',
+  timestamps: true,
+  indexes: [
+    {
+      fields: ['isActive']
+    },
+    {
+      fields: ['name']
+    }
+  ]
+});
+
+module.exports = Subscription; 

+ 15 - 1
src/models/index.js

@@ -1,6 +1,7 @@
 const Node = require('./Node');
 const TestResult = require('./TestResult');
 const Notification = require('./Notification');
+const Subscription = require('./Subscription');
 
 // 定义模型关联关系
 Node.hasMany(TestResult, {
@@ -25,8 +26,21 @@ Notification.belongsTo(Node, {
   as: 'node'
 });
 
+// 订阅与节点的关联关系
+Subscription.hasMany(Node, {
+  foreignKey: 'subscriptionId',
+  as: 'nodes',
+  onDelete: 'CASCADE'
+});
+
+Node.belongsTo(Subscription, {
+  foreignKey: 'subscriptionId',
+  as: 'subscription'
+});
+
 module.exports = {
   Node,
   TestResult,
-  Notification
+  Notification,
+  Subscription
 }; 

+ 94 - 0
test_auto_speedtest.js

@@ -0,0 +1,94 @@
+const { Subscription, Node } = require('./src/models');
+const MultiSubscriptionManager = require('./src/core/multiSubscriptionManager');
+
+async function testAutoSpeedTest() {
+  try {
+    console.log('=== 自动测速功能测试 ===');
+    
+    // 创建测试订阅
+    const testSubscription = await Subscription.create({
+      name: '自动测速测试订阅',
+      url: 'http://so.xfxssr.me/api/v1/client/subscribe?token=7854d59f38ac51700730b9e782c5160c',
+      description: '用于测试自动测速功能的订阅',
+      speedTestConfig: {
+        testCount: 3,
+        timeout: 10000,
+        testUrls: ['https://www.google.com'],
+        speedTestEnabled: true
+      },
+      notifyConfig: {
+        enabled: true,
+        notifyOnSpeedTest: true,
+        notifyOnNodeUpdate: true,
+        webhookUrl: '',
+        emailConfig: null
+      }
+    });
+    
+    console.log('✅ 创建测试订阅成功');
+
+    // 创建多订阅管理器
+    const manager = new MultiSubscriptionManager();
+    
+    // 设置测速触发器(模拟)
+    let speedTestTriggered = false;
+    manager.setSpeedTestTrigger(() => {
+      console.log('🚀 测速触发器被调用!');
+      speedTestTriggered = true;
+    });
+
+    // 模拟一些测试节点
+    console.log('\n2. 创建测试节点...');
+    const testNodes = [
+      { name: '自动测速节点1', type: 'ss', server: 'auto1.example.com', port: 443, method: 'aes-256-gcm', password: 'password1', subscriptionId: testSubscription.id },
+      { name: '自动测速节点2', type: 'ss', server: 'auto2.example.com', port: 443, method: 'aes-256-gcm', password: 'password2', subscriptionId: testSubscription.id },
+      { name: '自动测速节点3', type: 'ss', server: 'auto3.example.com', port: 443, method: 'aes-256-gcm', password: 'password3', subscriptionId: testSubscription.id }
+    ];
+
+    for (const nodeData of testNodes) {
+      await Node.create({
+        ...nodeData,
+        isActive: true,
+        status: 'offline'
+      });
+    }
+
+    console.log(`✅ 创建了 ${testNodes.length} 个测试节点`);
+
+    // 更新订阅的节点数量
+    await testSubscription.update({
+      nodeCount: testNodes.length,
+      lastUpdateTime: new Date()
+    });
+
+    // 直接测试触发器功能
+    console.log('\n3. 直接测试测速触发器...');
+    manager.triggerSpeedTest();
+    
+    // 等待触发器执行
+    await new Promise(resolve => setTimeout(resolve, 1000));
+
+    // 检查触发器是否被调用
+    console.log('\n4. 检查触发器状态...');
+
+    if (speedTestTriggered) {
+      console.log('✅ 自动测速功能正常!');
+    } else {
+      console.log('❌ 自动测速功能未触发');
+    }
+
+    // 清理测试数据
+    console.log('\n5. 清理测试数据...');
+    await testSubscription.destroy();
+    console.log('✅ 测试数据清理完成');
+
+    console.log('\n=== 自动测速功能测试完成 ===');
+    
+  } catch (error) {
+    console.error('❌ 测试失败:', error.message);
+    console.error(error.stack);
+  }
+}
+
+// 运行测试
+testAutoSpeedTest(); 

+ 97 - 0
test_performance.js

@@ -0,0 +1,97 @@
+const { Subscription, Node } = require('./src/models');
+const MultiSubscriptionManager = require('./src/core/multiSubscriptionManager');
+
+async function testPerformance() {
+  try {
+    console.log('=== 性能测试 ===');
+    
+    // 创建测试订阅
+    const testSubscription = await Subscription.create({
+      name: '性能测试订阅',
+      url: 'http://so.xfxssr.me/api/v1/client/subscribe?token=7854d59f38ac51700730b9e782c5160c',
+      description: '用于测试性能的订阅',
+      speedTestConfig: {
+        testCount: 3,
+        timeout: 10000,
+        testUrls: ['https://www.google.com'],
+        speedTestEnabled: true
+      },
+      notifyConfig: {
+        enabled: true,
+        notifyOnSpeedTest: true,
+        notifyOnNodeUpdate: true,
+        webhookUrl: '',
+        emailConfig: null
+      }
+    });
+    
+    console.log('✅ 创建测试订阅成功');
+
+    // 创建大量测试节点
+    const nodeCount = 50;
+    console.log(`\n创建 ${nodeCount} 个测试节点...`);
+    
+    const testNodes = [];
+    for (let i = 1; i <= nodeCount; i++) {
+      testNodes.push({
+        name: `性能测试节点${i}`,
+        type: 'ss',
+        server: `server${i}.example.com`,
+        port: 443 + (i % 100),
+        method: 'aes-256-gcm',
+        password: `password${i}`,
+        subscriptionId: testSubscription.id,
+        isActive: true,
+        status: 'offline'
+      });
+    }
+
+    // 批量创建节点
+    const startTime = Date.now();
+    await Node.bulkCreate(testNodes);
+    const createTime = Date.now() - startTime;
+    
+    console.log(`✅ 批量创建 ${nodeCount} 个节点完成,耗时: ${createTime}ms`);
+
+    // 更新订阅节点数量
+    await testSubscription.update({
+      nodeCount: nodeCount,
+      lastUpdateTime: new Date()
+    });
+
+    // 测试多订阅管理器性能
+    console.log('\n测试多订阅管理器性能...');
+    const manager = new MultiSubscriptionManager();
+    
+    const managerStartTime = Date.now();
+    const activeSubscriptions = await manager.getActiveSubscriptions();
+    const managerTime = Date.now() - managerStartTime;
+    
+    console.log(`✅ 获取活跃订阅完成,耗时: ${managerTime}ms`);
+    console.log(`  活跃订阅数量: ${activeSubscriptions.length}`);
+
+    // 测试状态获取性能
+    const statusStartTime = Date.now();
+    const status = await manager.getStatus();
+    const statusTime = Date.now() - statusStartTime;
+    
+    console.log(`✅ 获取状态完成,耗时: ${statusTime}ms`);
+
+    // 清理测试数据
+    console.log('\n清理测试数据...');
+    await testSubscription.destroy();
+    console.log('✅ 测试数据清理完成');
+
+    console.log('\n=== 性能测试完成 ===');
+    console.log(`总结:`);
+    console.log(`  - 批量创建 ${nodeCount} 个节点: ${createTime}ms`);
+    console.log(`  - 获取活跃订阅: ${managerTime}ms`);
+    console.log(`  - 获取状态: ${statusTime}ms`);
+    
+  } catch (error) {
+    console.error('❌ 性能测试失败:', error.message);
+  }
+}
+
+// 运行测试
+testPerformance(); 

+ 45 - 0
update_subscription.js

@@ -0,0 +1,45 @@
+const { Subscription } = require('./src/models');
+
+async function updateSubscription() {
+  try {
+    // 查找测试订阅
+    const testSubscription = await Subscription.findOne({
+      where: { url: 'https://example.com/subscription' }
+    });
+    
+    if (testSubscription) {
+      console.log('找到测试订阅,正在更新为真实链接...');
+      
+      // 更新为真实订阅链接
+      await testSubscription.update({
+        url: 'http://so.xfxssr.me/api/v1/client/subscribe?token=7854d59f38ac51700730b9e782c5160c',
+        name: '真实订阅'
+      });
+      
+      console.log('订阅更新成功!');
+      console.log(`ID: ${testSubscription.id}`);
+      console.log(`新名称: ${testSubscription.name}`);
+      console.log(`新URL: ${testSubscription.url}`);
+    } else {
+      console.log('未找到测试订阅配置');
+      
+      // 如果没有找到,创建一个新的真实订阅
+      const newSubscription = await Subscription.create({
+        name: '真实订阅',
+        url: 'http://so.xfxssr.me/api/v1/client/subscribe?token=7854d59f38ac51700730b9e782c5160c',
+        isActive: true,
+        speedTestInterval: 3600000, // 1小时
+        notificationEnabled: true
+      });
+      
+      console.log('创建新的真实订阅配置成功!');
+      console.log(`ID: ${newSubscription.id}`);
+      console.log(`名称: ${newSubscription.name}`);
+      console.log(`URL: ${newSubscription.url}`);
+    }
+  } catch (error) {
+    console.error('更新订阅配置失败:', error.message);
+  }
+}
+
+updateSubscription();