|
@@ -30,6 +30,7 @@ class BotManager {
|
|
|
|
|
|
this.initializeBot();
|
|
|
this.speedTestMode = 'concurrent'; // 默认测速模式
|
|
|
+ this.pendingAddSubscription = {}; // 新增:记录每个chatId的添加订阅状态
|
|
|
}
|
|
|
|
|
|
/**
|
|
@@ -183,8 +184,7 @@ class BotManager {
|
|
|
{ command: '/add_subscription', description: '添加订阅链接' },
|
|
|
{ command: '/remove_subscription', description: '删除订阅' },
|
|
|
{ command: '/update_subscriptions', description: '手动更新订阅' },
|
|
|
- { command: '/test_speed', description: '手动触发测速' },
|
|
|
- { command: '/set_speed_mode', description: '设置测速模式(concurrent/serial)' }
|
|
|
+ { command: '/speed', description: '测速(弹出模式选择按钮)' }
|
|
|
]);
|
|
|
}
|
|
|
|
|
@@ -234,14 +234,16 @@ class BotManager {
|
|
|
await this.handleUpdateSubscriptions(msg);
|
|
|
});
|
|
|
|
|
|
- // 处理 /test_speed 命令
|
|
|
- this.bot.onText(/\/test_speed/, async (msg) => {
|
|
|
- await this.handleTestSpeed(msg);
|
|
|
+ // 处理 /speed 命令
|
|
|
+ this.bot.onText(/\/speed/, async (msg) => {
|
|
|
+ await this.handleSpeed(msg);
|
|
|
});
|
|
|
-
|
|
|
- // 处理 /set_speed_mode 命令
|
|
|
- this.bot.onText(/\/set_speed_mode (.+)/, async (msg, match) => {
|
|
|
- await this.handleSetSpeedMode(msg, match[1]);
|
|
|
+ // 处理测速模式按钮回调
|
|
|
+ this.bot.on('callback_query', async (callbackQuery) => {
|
|
|
+ const data = callbackQuery.data;
|
|
|
+ if (data.startsWith('speed_mode_concurrent_') || data.startsWith('speed_mode_serial_')) {
|
|
|
+ await this.handleSpeedModeButton(callbackQuery, data);
|
|
|
+ }
|
|
|
});
|
|
|
|
|
|
// 处理普通消息(只处理订阅链接,不处理其他消息)
|
|
@@ -252,9 +254,32 @@ class BotManager {
|
|
|
// 如果是命令,不处理(由命令处理器处理)
|
|
|
if (msg.text.startsWith('/')) return;
|
|
|
|
|
|
- // 只处理订阅链接
|
|
|
+ const chatId = msg.chat.id;
|
|
|
+ // 新增:多步添加订阅流程
|
|
|
+ if (this.pendingAddSubscription[chatId]) {
|
|
|
+ const state = this.pendingAddSubscription[chatId];
|
|
|
+ if (state.step === 1) {
|
|
|
+ // 用户发送了名字,进入下一步
|
|
|
+ state.name = msg.text.trim();
|
|
|
+ state.step = 2;
|
|
|
+ await this.sendMessage(chatId, `✅ 名字已记录:*${state.name}*\n\n请发送订阅链接(支持 Clash、SS、Vmess、Trojan)`);
|
|
|
+ return;
|
|
|
+ } else if (state.step === 2) {
|
|
|
+ // 用户发送了链接,完成添加
|
|
|
+ const url = msg.text.trim();
|
|
|
+ if (!this.isSubscriptionUrl(url)) {
|
|
|
+ await this.sendMessage(chatId, '❌ 链接格式不正确,请重新发送订阅链接');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const name = state.name;
|
|
|
+ delete this.pendingAddSubscription[chatId];
|
|
|
+ await this.addSubscriptionWithName(chatId, name, url);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // 原有逻辑:只处理订阅链接
|
|
|
if (this.isSubscriptionUrl(msg.text)) {
|
|
|
- await this.addSubscription(msg.chat.id, msg.text);
|
|
|
+ await this.addSubscription(chatId, msg.text);
|
|
|
}
|
|
|
// 其他消息不回复
|
|
|
});
|
|
@@ -305,8 +330,10 @@ class BotManager {
|
|
|
• /update_subscriptions - 手动更新订阅
|
|
|
• /test_speed - 手动触发测速
|
|
|
|
|
|
+💡 *添加订阅方式:*
|
|
|
+• 发送 /add_subscription 按提示分步添加(先名字,后链接)
|
|
|
+
|
|
|
💡 *使用提示:*
|
|
|
-• 直接发送订阅链接即可添加订阅
|
|
|
• 使用 /remove_subscription 查看订阅列表
|
|
|
• 使用 /delete_1 删除第1个订阅
|
|
|
• 支持多种格式:Clash、Shadowsocks、Vmess等`;
|
|
@@ -459,15 +486,14 @@ class BotManager {
|
|
|
await this.sendMessage(chatId, '❌ 您没有权限使用此机器人');
|
|
|
return;
|
|
|
}
|
|
|
-
|
|
|
+ // 新增:进入等待名字状态
|
|
|
+ this.pendingAddSubscription[chatId] = { step: 1 };
|
|
|
await this.sendMessage(chatId,
|
|
|
`📥 *添加订阅*\n\n` +
|
|
|
- `请发送订阅链接,支持以下格式:\n` +
|
|
|
- `• Clash配置:\`https://example.com/clash.yaml\`\n` +
|
|
|
- `• Shadowsocks:\`ss://...\`\n` +
|
|
|
- `• Vmess:\`vmess://...\`\n` +
|
|
|
- `• Trojan:\`trojan://...\`\n\n` +
|
|
|
- `💡 直接发送链接即可添加订阅`
|
|
|
+ `请按照以下步骤添加订阅:\n\n` +
|
|
|
+ `1. 现在发送订阅名字\n` +
|
|
|
+ `2. 然后发送订阅链接\n\n` +
|
|
|
+ `💡 支持格式:Clash、Shadowsocks、Vmess、Trojan`
|
|
|
);
|
|
|
}
|
|
|
|
|
@@ -622,6 +648,71 @@ class BotManager {
|
|
|
await this.sendMessage(chatId, `✅ 已切换测速模式为: *${mode === 'concurrent' ? '并发连通性' : '串行精准延迟'}*`);
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 处理 /speed 命令,弹出所有订阅列表,每个订阅后有“并发测速/精准测速”按钮,显示当前模式。
|
|
|
+ */
|
|
|
+ async handleSpeed(msg) {
|
|
|
+ const chatId = msg.chat.id;
|
|
|
+ if (!this.isAuthorized(chatId)) {
|
|
|
+ await this.sendMessage(chatId, '❌ 您没有权限使用此机器人');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const { Subscription } = require('../models');
|
|
|
+ const subscriptions = await Subscription.findAll({ where: { isActive: true } });
|
|
|
+ if (subscriptions.length === 0) {
|
|
|
+ await this.sendMessage(chatId, '📭 暂无订阅,请先添加订阅');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ // 构建消息文本和按钮
|
|
|
+ let text = '请选择要设置测速模式的订阅:\n';
|
|
|
+ const inline_keyboard = [];
|
|
|
+ subscriptions.forEach((sub, idx) => {
|
|
|
+ let mode = 'concurrent';
|
|
|
+ if (sub.speedTestConfig && typeof sub.speedTestConfig === 'object' && sub.speedTestConfig.mode) {
|
|
|
+ mode = sub.speedTestConfig.mode;
|
|
|
+ }
|
|
|
+ const modeText = mode === 'concurrent' ? '并发测速' : '精准测速';
|
|
|
+ text += `\n*${sub.name || sub.id}* 当前: *${modeText}*`;
|
|
|
+ inline_keyboard.push([
|
|
|
+ { text: `🚀 并发测速${mode === 'concurrent' ? '(当前)' : ''}`, callback_data: `speed_mode_concurrent_${sub.id}` },
|
|
|
+ { text: `🎯 精准测速${mode === 'serial' ? '(当前)' : ''}`, callback_data: `speed_mode_serial_${sub.id}` }
|
|
|
+ ]);
|
|
|
+ });
|
|
|
+ const opts = {
|
|
|
+ reply_markup: { inline_keyboard },
|
|
|
+ parse_mode: 'Markdown'
|
|
|
+ };
|
|
|
+ await this.sendMessage(chatId, text, opts);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理测速模式按钮点击,切换模式并立即测速
|
|
|
+ */
|
|
|
+ async handleSpeedModeButton(callbackQuery, data) {
|
|
|
+ const chatId = callbackQuery.message.chat.id;
|
|
|
+ // 解析模式和订阅ID
|
|
|
+ const match = data.match(/^speed_mode_(concurrent|serial)_(\d+)$/);
|
|
|
+ if (!match) return;
|
|
|
+ const mode = match[1];
|
|
|
+ const subId = parseInt(match[2]);
|
|
|
+ const { Subscription } = require('../models');
|
|
|
+ const sub = await Subscription.findByPk(subId);
|
|
|
+ if (!sub) {
|
|
|
+ await this.sendMessage(chatId, '❌ 订阅不存在');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ // 更新 speedTestConfig
|
|
|
+ let config = sub.speedTestConfig && typeof sub.speedTestConfig === 'object' ? { ...sub.speedTestConfig } : {};
|
|
|
+ config.mode = mode;
|
|
|
+ await sub.update({ speedTestConfig: config });
|
|
|
+ const modeText = mode === 'concurrent' ? '并发测速' : '精准测速';
|
|
|
+ await this.bot.editMessageText(`已切换【${sub.name || sub.id}】的测速模式为:*${modeText}*`, {
|
|
|
+ chat_id: chatId,
|
|
|
+ message_id: callbackQuery.message.message_id,
|
|
|
+ parse_mode: 'Markdown'
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 检查是否是订阅链接
|
|
|
*/
|
|
@@ -635,7 +726,6 @@ class BotManager {
|
|
|
async addSubscription(chatId, url) {
|
|
|
try {
|
|
|
await this.sendMessage(chatId, '📥 正在添加订阅...');
|
|
|
-
|
|
|
// 创建新订阅
|
|
|
const subscription = await Subscription.create({
|
|
|
name: `订阅_${Date.now()}`,
|
|
@@ -643,130 +733,64 @@ class BotManager {
|
|
|
isActive: true,
|
|
|
nodeCount: 0
|
|
|
});
|
|
|
-
|
|
|
// 更新订阅
|
|
|
const result = await this.subscriptionManager.manualUpdateSubscription(subscription.id);
|
|
|
-
|
|
|
+ // manualUpdateSubscription后,重新查一次数据库,获取最新节点数
|
|
|
+ const updatedSub = await Subscription.findByPk(subscription.id);
|
|
|
+ const nodeCount = updatedSub && updatedSub.nodeCount != null ? updatedSub.nodeCount : 0;
|
|
|
if (result.error) {
|
|
|
await this.sendMessage(chatId, `❌ 添加订阅失败:${result.error}`);
|
|
|
return;
|
|
|
}
|
|
|
-
|
|
|
const message = `✅ *订阅添加成功*\n\n` +
|
|
|
`📡 订阅名称:*${subscription.name}*\n` +
|
|
|
`🔗 URL:\`${url}\`\n` +
|
|
|
- `📊 节点数:${result.actualNodeCount || 0}\n` +
|
|
|
+ `📊 节点数:${nodeCount}\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 });
|
|
|
+ logger.error('添加订阅失败', { error: error.message });
|
|
|
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) {
|
|
|
+ async addSubscriptionWithName(chatId, name, url) {
|
|
|
try {
|
|
|
- const subscriptions = await Subscription.findAll({
|
|
|
- where: { isActive: true },
|
|
|
- order: [['createdAt', 'DESC']]
|
|
|
+ await this.sendMessage(chatId, '📥 正在添加订阅...');
|
|
|
+ // 创建新订阅
|
|
|
+ const subscription = await Subscription.create({
|
|
|
+ name: name,
|
|
|
+ url: url,
|
|
|
+ isActive: true,
|
|
|
+ nodeCount: 0
|
|
|
});
|
|
|
-
|
|
|
- if (index < 1 || index > subscriptions.length) {
|
|
|
- await this.sendMessage(chatId, '❌ 无效的订阅编号');
|
|
|
+ // 更新订阅
|
|
|
+ const result = await this.subscriptionManager.manualUpdateSubscription(subscription.id);
|
|
|
+ // manualUpdateSubscription后,重新查一次数据库,获取最新节点数
|
|
|
+ const updatedSub = await Subscription.findByPk(subscription.id);
|
|
|
+ const nodeCount = updatedSub && updatedSub.nodeCount != null ? updatedSub.nodeCount : 0;
|
|
|
+ if (result.error) {
|
|
|
+ await this.sendMessage(chatId, `❌ 添加订阅失败:${result.error}`);
|
|
|
return;
|
|
|
}
|
|
|
-
|
|
|
- const subscription = subscriptions[index - 1];
|
|
|
-
|
|
|
- // 标记为非活跃
|
|
|
- await subscription.update({ isActive: false });
|
|
|
-
|
|
|
- const message = `🗑️ *订阅删除成功*\n\n` +
|
|
|
+ const message = `✅ *订阅添加成功*\n\n` +
|
|
|
`📡 订阅名称:*${subscription.name}*\n` +
|
|
|
- `🔗 URL:\`${subscription.url}\`\n` +
|
|
|
- `📊 节点数:${subscription.nodeCount || 0}`;
|
|
|
-
|
|
|
+ `🔗 URL:\`${url}\`\n` +
|
|
|
+ `📊 节点数:${nodeCount}\n` +
|
|
|
+ `➕ 新增:${result.added || 0} 个\n` +
|
|
|
+ `🔄 更新:${result.updated || 0} 个\n` +
|
|
|
+ `🗑️ 移除:${result.removed || 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机器人已停止');
|
|
|
+ logger.error('添加订阅失败', { error: error.message });
|
|
|
+ await this.sendMessage(chatId, '❌ 添加订阅失败:' + error.message);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-module.exports = BotManager;
|
|
|
+module.exports = BotManager;
|