qwen-alpha 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.js +21 -0
- package/.nvmrc +1 -0
- package/.prettierrc +9 -0
- package/LICENSE +21 -0
- package/PUBLISH.md +252 -0
- package/README.md +282 -0
- package/bin/qwen-alpha.js +95 -0
- package/package.json +50 -0
- package/src/bot/bot.js +81 -0
- package/src/bot/handlers/admin.js +231 -0
- package/src/bot/handlers/file.js +177 -0
- package/src/bot/handlers/help.js +43 -0
- package/src/bot/handlers/message.js +132 -0
- package/src/bot/handlers/reset.js +53 -0
- package/src/bot/handlers/settings.js +39 -0
- package/src/bot/handlers/start.js +65 -0
- package/src/bot/handlers/stats.js +76 -0
- package/src/bot/middleware/auth.js +70 -0
- package/src/bot/middleware/logging.js +55 -0
- package/src/bot/middleware/rateLimit.js +74 -0
- package/src/bot/middleware/session.js +63 -0
- package/src/config/index.js +57 -0
- package/src/index.js +86 -0
- package/src/services/db/admins.js +146 -0
- package/src/services/db/index.js +177 -0
- package/src/services/db/sessions.js +342 -0
- package/src/services/db/stats.js +223 -0
- package/src/services/db/users.js +215 -0
- package/src/services/qwenService.js +254 -0
- package/src/utils/logger.js +127 -0
- package/src/utils/paths.js +63 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
const sessionService = require('../../services/db/sessions');
|
|
2
|
+
const statsService = require('../../services/db/stats');
|
|
3
|
+
const { qwenService } = require('../../services/qwenService');
|
|
4
|
+
const { logger } = require('../../utils/logger');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Обработчик текстовых сообщений
|
|
8
|
+
* @param {import('telegraf').Context} ctx
|
|
9
|
+
*/
|
|
10
|
+
async function messageHandler(ctx) {
|
|
11
|
+
const userId = ctx.state.userId;
|
|
12
|
+
const chatId = ctx.state.chatId;
|
|
13
|
+
const isPrivate = ctx.state.isPrivate;
|
|
14
|
+
const text = ctx.message?.text;
|
|
15
|
+
|
|
16
|
+
if (!text) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Проверка на команду /qwen в группах
|
|
21
|
+
const isQwenCommand = text.startsWith('/qwen') || text.startsWith('/qwen@');
|
|
22
|
+
const isBotMention = text.includes('@QwenAlphaRobot') || text.includes(`@${ctx.botInfo?.username}`);
|
|
23
|
+
const isReplyToBot = ctx.message?.reply_to_message?.from?.is_bot;
|
|
24
|
+
|
|
25
|
+
// В личных чатах обрабатываем все сообщения
|
|
26
|
+
// В группах только /qwen, упоминания бота, или reply на бота
|
|
27
|
+
if (!isPrivate && !isQwenCommand && !isBotMention && !isReplyToBot) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Очистка команды из текста
|
|
32
|
+
let prompt = text;
|
|
33
|
+
if (isQwenCommand) {
|
|
34
|
+
prompt = text.replace(/^\/qwen(@\w+)?\s*/, '').trim();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Если пусто после очистки команды
|
|
38
|
+
if (!prompt && isQwenCommand) {
|
|
39
|
+
await ctx.reply('❌ Пожалуйста, укажите запрос после /qwen');
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Загрузка индикатора
|
|
44
|
+
const loadingMsg = isPrivate
|
|
45
|
+
? await ctx.reply('⏳ Думаю...')
|
|
46
|
+
: null;
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const startTime = Date.now();
|
|
50
|
+
|
|
51
|
+
// Получение контекста из сессии
|
|
52
|
+
let contextMessages = [];
|
|
53
|
+
let session = ctx.state.session;
|
|
54
|
+
|
|
55
|
+
if (session) {
|
|
56
|
+
// Если это reply на сообщение в сессии
|
|
57
|
+
const replyToMessageId = ctx.message?.reply_to_message?.message_id;
|
|
58
|
+
|
|
59
|
+
if (replyToMessageId && session.message_tree[replyToMessageId]) {
|
|
60
|
+
// Получаем цепочку сообщений
|
|
61
|
+
contextMessages = sessionService.getMessageChain(session, replyToMessageId);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Запрос к Qwen
|
|
66
|
+
const result = await qwenService.analyzeCode(prompt, contextMessages);
|
|
67
|
+
|
|
68
|
+
const duration = Date.now() - startTime;
|
|
69
|
+
statsService.updateAvgResponseTime(duration);
|
|
70
|
+
|
|
71
|
+
// Форматирование ответа
|
|
72
|
+
let responseText = result;
|
|
73
|
+
|
|
74
|
+
// В группах добавляем упоминание если это первый ответ
|
|
75
|
+
if (!isPrivate && ctx.message?.reply_to_message) {
|
|
76
|
+
const originalUser = ctx.message.reply_to_message.from;
|
|
77
|
+
if (originalUser?.username) {
|
|
78
|
+
responseText = `@${originalUser.username} ${responseText}`;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Отправка ответа
|
|
83
|
+
if (loadingMsg) {
|
|
84
|
+
await ctx.editMessageText(responseText, { parse_mode: 'Markdown' });
|
|
85
|
+
} else {
|
|
86
|
+
// В группах отвечаем reply на исходное сообщение
|
|
87
|
+
await ctx.reply(responseText, {
|
|
88
|
+
parse_mode: 'Markdown',
|
|
89
|
+
reply_parameters: { message_id: ctx.message.message_id },
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Добавление сообщений в сессию
|
|
94
|
+
if (session) {
|
|
95
|
+
// Добавляем сообщение пользователя
|
|
96
|
+
sessionService.addMessage({
|
|
97
|
+
sessionId: session.session_id,
|
|
98
|
+
chatId,
|
|
99
|
+
messageId: ctx.message.message_id,
|
|
100
|
+
userId,
|
|
101
|
+
text: prompt,
|
|
102
|
+
type: 'user_question',
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Добавляем ответ бота (будет добавлен после отправки)
|
|
106
|
+
// sessionService.addMessage({...})
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Обновление статистики
|
|
110
|
+
statsService.incrementRequest();
|
|
111
|
+
userService.incrementRequest(userId);
|
|
112
|
+
|
|
113
|
+
logger.info({ userId, chatId, duration, isPrivate }, 'Message processed');
|
|
114
|
+
|
|
115
|
+
} catch (error) {
|
|
116
|
+
logger.error({ userId, chatId, error }, 'Message processing failed');
|
|
117
|
+
|
|
118
|
+
if (loadingMsg) {
|
|
119
|
+
await ctx.editMessageText('❌ Ошибка при обработке запроса. Попробуйте позже.');
|
|
120
|
+
} else {
|
|
121
|
+
await ctx.reply('❌ Ошибка при обработке запроса. Попробуйте позже.', {
|
|
122
|
+
reply_parameters: { message_id: ctx.message.message_id },
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
statsService.incrementError();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const userService = require('../../services/db/users');
|
|
131
|
+
|
|
132
|
+
module.exports = messageHandler;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const sessionService = require('../../services/db/sessions');
|
|
2
|
+
const statsService = require('../../services/db/stats');
|
|
3
|
+
const { logger } = require('../../utils/logger');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Обработчик команды /reset
|
|
7
|
+
* Сброс текущей сессии пользователя
|
|
8
|
+
* @param {import('telegraf').Context} ctx
|
|
9
|
+
*/
|
|
10
|
+
async function resetHandler(ctx) {
|
|
11
|
+
const userId = ctx.state.userId;
|
|
12
|
+
const chatId = ctx.state.chatId;
|
|
13
|
+
const isPrivate = ctx.state.isPrivate;
|
|
14
|
+
|
|
15
|
+
if (isPrivate) {
|
|
16
|
+
// Личный чат — сброс сессии пользователя
|
|
17
|
+
const sessionKey = `user:${userId}`;
|
|
18
|
+
const session = sessionService.getByKey(sessionKey);
|
|
19
|
+
|
|
20
|
+
if (session) {
|
|
21
|
+
sessionService.close(session.session_id, chatId);
|
|
22
|
+
logger.info({ userId, sessionId: session.session_id }, 'Session reset by user');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Создание новой сессии
|
|
26
|
+
const newSession = sessionService.create({
|
|
27
|
+
userId,
|
|
28
|
+
chatId,
|
|
29
|
+
rootMessageId: ctx.message.message_id,
|
|
30
|
+
chatType: 'private',
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
statsService.incrementSessionCreated();
|
|
34
|
+
|
|
35
|
+
await ctx.reply(
|
|
36
|
+
'🔄 **Сессия сброшена**\n\n' +
|
|
37
|
+
'Контекст предыдущего диалога очищен. Начнём с чистого листа!',
|
|
38
|
+
{ parse_mode: 'Markdown' }
|
|
39
|
+
);
|
|
40
|
+
} else {
|
|
41
|
+
// Групповой чат — информация
|
|
42
|
+
await ctx.reply(
|
|
43
|
+
'⚠️ **Сброс сессии в группах**\n\n' +
|
|
44
|
+
'В групповых чатах команда /reset не работает.\n\n' +
|
|
45
|
+
'Чтобы начать новую тему, просто отправьте:\n' +
|
|
46
|
+
' `/qwen <ваш запрос>`\n\n' +
|
|
47
|
+
'Каждая тема — отдельная сессия с собственным контекстом.',
|
|
48
|
+
{ parse_mode: 'Markdown' }
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = resetHandler;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const userService = require('../../services/db/users');
|
|
2
|
+
const { storeManager } = require('../../services/db');
|
|
3
|
+
const { logger } = require('../../utils/logger');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Обработчик команды /settings
|
|
7
|
+
* Показывает и позволяет изменить настройки
|
|
8
|
+
* @param {import('telegraf').Context} ctx
|
|
9
|
+
*/
|
|
10
|
+
async function settingsHandler(ctx) {
|
|
11
|
+
const userId = ctx.state.userId;
|
|
12
|
+
const user = userService.getById(userId);
|
|
13
|
+
const globalSettings = storeManager.get('settings').getData();
|
|
14
|
+
|
|
15
|
+
const settingsText = `
|
|
16
|
+
⚙️ **Ваши настройки**
|
|
17
|
+
|
|
18
|
+
**Модель:** ${user?.settings?.model || 'qwen3-coder-plus'}
|
|
19
|
+
**Язык:** ${user?.settings?.language === 'ru' ? '🇷🇺 Русский' : '🇬🇧 English'}
|
|
20
|
+
**Уведомления:** ${user?.settings?.notifications ? '✅ Вкл' : '❌ Выкл'}
|
|
21
|
+
**Лимит запросов/час:** ${user?.settings?.requests_per_hour || 60}
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
🌍 **Глобальные настройки бота:**
|
|
25
|
+
**Таймаут сессии:** ${globalSettings.session_timeout_hours}ч
|
|
26
|
+
**Макс. размер файла:** ${globalSettings.max_file_size_mb}MB
|
|
27
|
+
**Режим в группах:** ${globalSettings.group_mode}
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
**Изменение настроек:**
|
|
31
|
+
/settings model <название> — сменить модель
|
|
32
|
+
/settings language <ru|en> — сменить язык
|
|
33
|
+
/settings notifications <on|off> — уведомления
|
|
34
|
+
`.trim();
|
|
35
|
+
|
|
36
|
+
await ctx.reply(settingsText, { parse_mode: 'Markdown' });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = settingsHandler;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const adminService = require('../../services/db/admins');
|
|
2
|
+
const sessionService = require('../../services/db/sessions');
|
|
3
|
+
const statsService = require('../../services/db/stats');
|
|
4
|
+
const { logger } = require('../../utils/logger');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Обработчик команды /start
|
|
8
|
+
* Приветствие и регистрация пользователя
|
|
9
|
+
* @param {import('telegraf').Context} ctx
|
|
10
|
+
*/
|
|
11
|
+
async function startHandler(ctx) {
|
|
12
|
+
const userId = ctx.from.id;
|
|
13
|
+
const chatId = ctx.chat.id;
|
|
14
|
+
const isPrivate = chatId > 0 || userId === chatId;
|
|
15
|
+
|
|
16
|
+
// Регистрация супер-админа если первый
|
|
17
|
+
const isNewSuperAdmin = ctx.state.isSuperAdmin;
|
|
18
|
+
|
|
19
|
+
if (isNewSuperAdmin) {
|
|
20
|
+
logger.info({ userId }, 'Super admin registered via /start');
|
|
21
|
+
|
|
22
|
+
await ctx.reply(
|
|
23
|
+
'🎉 **Поздравляю! Вы — первый пользователь и супер-администратор Qwen Alpha!**\n\n' +
|
|
24
|
+
'Теперь вы можете:\n' +
|
|
25
|
+
'• Управлять другими администраторами через /admin\n' +
|
|
26
|
+
'• Настраивать бота через /settings\n' +
|
|
27
|
+
'• Просматривать статистику через /stats\n\n' +
|
|
28
|
+
'📚 Документация: https://github.com/JeBance/QwenAlpha\n\n' +
|
|
29
|
+
'Для начала работы отправьте мне код или напишите /help',
|
|
30
|
+
{ parse_mode: 'Markdown' }
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Обычное приветствие
|
|
37
|
+
const welcomeMessages = [
|
|
38
|
+
'👋 Привет! Я Qwen Alpha — AI ассистент для работы с кодом.',
|
|
39
|
+
'🔍 Я умею:\n' +
|
|
40
|
+
' • Code Review и поиск багов\n' +
|
|
41
|
+
' • Генерация кода по описанию\n' +
|
|
42
|
+
' • Объяснение сложных участков\n' +
|
|
43
|
+
' • Рефакторинг и оптимизация\n\n' +
|
|
44
|
+
'📤 Отправь мне код текстом или файлом, и я проанализирую его!\n\n' +
|
|
45
|
+
'ℹ️ /help — список команд\n' +
|
|
46
|
+
'📊 /stats — ваша статистика\n' +
|
|
47
|
+
'⚙️ /settings — настройки',
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
await ctx.reply(welcomeMessages.join('\n'));
|
|
51
|
+
|
|
52
|
+
// Создание сессии для личного чата если нет активной
|
|
53
|
+
if (isPrivate && !ctx.state.session) {
|
|
54
|
+
const session = sessionService.create({
|
|
55
|
+
userId,
|
|
56
|
+
chatId,
|
|
57
|
+
rootMessageId: ctx.message.message_id,
|
|
58
|
+
chatType: 'private',
|
|
59
|
+
});
|
|
60
|
+
ctx.state.session = session;
|
|
61
|
+
statsService.incrementSessionCreated();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = startHandler;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
const userService = require('../../services/db/users');
|
|
2
|
+
const statsService = require('../../services/db/stats');
|
|
3
|
+
const sessionService = require('../../services/db/sessions');
|
|
4
|
+
const { logger } = require('../../utils/logger');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Обработчик команды /stats
|
|
8
|
+
* Показывает статистику пользователя или чата
|
|
9
|
+
* @param {import('telegraf').Context} ctx
|
|
10
|
+
*/
|
|
11
|
+
async function statsHandler(ctx) {
|
|
12
|
+
const userId = ctx.state.userId;
|
|
13
|
+
const chatId = ctx.state.chatId;
|
|
14
|
+
const isPrivate = ctx.state.isPrivate;
|
|
15
|
+
|
|
16
|
+
if (isPrivate) {
|
|
17
|
+
// Личная статистика
|
|
18
|
+
const user = userService.getById(userId);
|
|
19
|
+
const globalStats = statsService.getGlobal();
|
|
20
|
+
const todayStats = statsService.getDaily();
|
|
21
|
+
|
|
22
|
+
const statsText = `
|
|
23
|
+
📊 **Ваша статистика**
|
|
24
|
+
|
|
25
|
+
**Запросы:**
|
|
26
|
+
• Сегодня: ${user?.stats?.total_requests || 0}
|
|
27
|
+
• Всего: ${user?.stats?.total_requests || 0}
|
|
28
|
+
|
|
29
|
+
**Файлы:**
|
|
30
|
+
• Проанализировано: ${user?.stats?.total_files || 0}
|
|
31
|
+
|
|
32
|
+
**Токены:**
|
|
33
|
+
• Использовано: ~${user?.stats?.total_tokens?.toLocaleString() || 0}
|
|
34
|
+
|
|
35
|
+
**Группы:**
|
|
36
|
+
• Использовано чатов: ${user?.stats?.groups_used || 0}
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
🌍 **Глобальная статистика:**
|
|
40
|
+
• Пользователей: ${globalStats.total_users}
|
|
41
|
+
• Активных за 24ч: ${globalStats.active_24h}
|
|
42
|
+
• Запросов сегодня: ${globalStats.requests_today}
|
|
43
|
+
• Среднее время ответа: ${globalStats.avg_response_time_ms}мс
|
|
44
|
+
`.trim();
|
|
45
|
+
|
|
46
|
+
await ctx.reply(statsText, { parse_mode: 'Markdown' });
|
|
47
|
+
} else {
|
|
48
|
+
// Статистика чата
|
|
49
|
+
const sessions = sessionService.getChatSessions(chatId);
|
|
50
|
+
const activeSessions = sessions.filter(s => s.status === 'active');
|
|
51
|
+
const totalMessages = sessions.reduce((sum, s) => sum + (s.message_count || 0), 0);
|
|
52
|
+
const participants = new Set(sessions.flatMap(s => s.participants || []));
|
|
53
|
+
|
|
54
|
+
const chatStatsText = `
|
|
55
|
+
📊 **Статистика чата**
|
|
56
|
+
|
|
57
|
+
**Сессии:**
|
|
58
|
+
• Активных: ${activeSessions.length}
|
|
59
|
+
• Всего: ${sessions.length}
|
|
60
|
+
|
|
61
|
+
**Активность:**
|
|
62
|
+
• Сообщений в сессиях: ${totalMessages}
|
|
63
|
+
• Участников: ${participants.size}
|
|
64
|
+
|
|
65
|
+
**Сессии:**
|
|
66
|
+
${activeSessions.slice(0, 5).map(s => {
|
|
67
|
+
const timeLeft = Math.max(0, Math.floor((new Date(s.expires_at) - new Date()) / 3600000));
|
|
68
|
+
return `• Тема от ${s.root_user_id} (осталось ${timeLeft}ч)`;
|
|
69
|
+
}).join('\n') || 'Нет активных сессий'}
|
|
70
|
+
`.trim();
|
|
71
|
+
|
|
72
|
+
await ctx.reply(chatStatsText, { parse_mode: 'Markdown' });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = statsHandler;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const userService = require('../../services/db/users');
|
|
2
|
+
const adminService = require('../../services/db/admins');
|
|
3
|
+
const { logger } = require('../../utils/logger');
|
|
4
|
+
const config = require('../../config');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Middleware для аутентификации и авторизации пользователей
|
|
8
|
+
* @param {import('telegraf').Context} ctx
|
|
9
|
+
* @param {Function} next
|
|
10
|
+
*/
|
|
11
|
+
async function authMiddleware(ctx, next) {
|
|
12
|
+
const userId = ctx.from?.id;
|
|
13
|
+
|
|
14
|
+
if (!userId) {
|
|
15
|
+
return next();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Проверка whitelist (если настроен)
|
|
19
|
+
if (config.bot.allowedUsers.length > 0) {
|
|
20
|
+
if (!config.bot.allowedUsers.includes(userId)) {
|
|
21
|
+
logger.warn({ userId }, 'User not in whitelist');
|
|
22
|
+
await ctx.reply('⛔ Доступ запрещён. Вы не в списке разрешённых пользователей.');
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Получение или создание пользователя
|
|
28
|
+
let user = userService.getById(userId);
|
|
29
|
+
|
|
30
|
+
if (!user) {
|
|
31
|
+
user = userService.upsert({
|
|
32
|
+
id: userId,
|
|
33
|
+
username: ctx.from.username,
|
|
34
|
+
first_name: ctx.from.first_name,
|
|
35
|
+
last_name: ctx.from.last_name,
|
|
36
|
+
});
|
|
37
|
+
logger.info({ userId, username: ctx.from.username }, 'New user registered');
|
|
38
|
+
} else {
|
|
39
|
+
// Обновление last_seen и имени
|
|
40
|
+
userService.upsert({
|
|
41
|
+
id: userId,
|
|
42
|
+
username: ctx.from.username,
|
|
43
|
+
first_name: ctx.from.first_name,
|
|
44
|
+
last_name: ctx.from.last_name,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Проверка бана
|
|
49
|
+
if (user.is_banned) {
|
|
50
|
+
logger.warn({ userId }, 'Banned user attempted access');
|
|
51
|
+
await ctx.reply('⛔ Ваш аккаунт заблокирован.');
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Регистрация супер-админа (первый пользователь)
|
|
56
|
+
const isNewSuperAdmin = adminService.registerSuperAdmin(userId);
|
|
57
|
+
if (isNewSuperAdmin) {
|
|
58
|
+
logger.info({ userId }, 'First user registered as super admin');
|
|
59
|
+
ctx.state.isSuperAdmin = true;
|
|
60
|
+
} else {
|
|
61
|
+
ctx.state.isSuperAdmin = adminService.isSuperAdmin(userId);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
ctx.state.isAdmin = adminService.isAdmin(userId);
|
|
65
|
+
ctx.state.user = user;
|
|
66
|
+
|
|
67
|
+
return next();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = { authMiddleware };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const { logger } = require('../../utils/logger');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Middleware для логирования входящих запросов
|
|
5
|
+
* Добавляет correlation ID для трассировки
|
|
6
|
+
* @param {import('telegraf').Context} ctx
|
|
7
|
+
* @param {Function} next
|
|
8
|
+
*/
|
|
9
|
+
async function loggingMiddleware(ctx, next) {
|
|
10
|
+
const correlationId = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
11
|
+
ctx.state.correlationId = correlationId;
|
|
12
|
+
|
|
13
|
+
const logContext = {
|
|
14
|
+
correlationId,
|
|
15
|
+
userId: ctx.from?.id,
|
|
16
|
+
chatId: ctx.chat?.id,
|
|
17
|
+
chatType: ctx.chat?.type,
|
|
18
|
+
updateType: ctx.updateType,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// Добавляем дополнительную информацию для разных типов обновлений
|
|
22
|
+
if (ctx.message) {
|
|
23
|
+
logContext.messageId = ctx.message.message_id;
|
|
24
|
+
logContext.hasText = !!ctx.message.text;
|
|
25
|
+
logContext.hasDocument = !!ctx.message.document;
|
|
26
|
+
logContext.hasPhoto = !!ctx.message.photo;
|
|
27
|
+
logContext.replyToMessageId = ctx.message.reply_to_message?.message_id;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
logger.info(logContext, 'Incoming update');
|
|
31
|
+
|
|
32
|
+
const startTime = Date.now();
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
await next();
|
|
36
|
+
const duration = Date.now() - startTime;
|
|
37
|
+
|
|
38
|
+
logger.info({
|
|
39
|
+
correlationId,
|
|
40
|
+
duration,
|
|
41
|
+
}, 'Update processed');
|
|
42
|
+
} catch (error) {
|
|
43
|
+
const duration = Date.now() - startTime;
|
|
44
|
+
|
|
45
|
+
logger.error({
|
|
46
|
+
correlationId,
|
|
47
|
+
duration,
|
|
48
|
+
error: error.message,
|
|
49
|
+
}, 'Update failed');
|
|
50
|
+
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = { loggingMiddleware };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
const { logger } = require('../../utils/logger');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Хранилище запросов пользователей в памяти
|
|
5
|
+
* Ключ: user_id, Значение: массив временных меток
|
|
6
|
+
*/
|
|
7
|
+
const userRequests = new Map();
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Middleware для rate limiting
|
|
11
|
+
* Ограничивает количество запросов от пользователя в единицу времени
|
|
12
|
+
* @param {import('telegraf').Context} ctx
|
|
13
|
+
* @param {Function} next
|
|
14
|
+
*/
|
|
15
|
+
async function rateLimitMiddleware(ctx, next) {
|
|
16
|
+
const userId = ctx.from?.id;
|
|
17
|
+
|
|
18
|
+
if (!userId) {
|
|
19
|
+
return next();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const now = Date.now();
|
|
23
|
+
const windowMs = 60000; // 1 минута
|
|
24
|
+
const maxRequests = 10; // 10 запросов в минуту
|
|
25
|
+
|
|
26
|
+
if (!userRequests.has(userId)) {
|
|
27
|
+
userRequests.set(userId, []);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const requests = userRequests.get(userId);
|
|
31
|
+
|
|
32
|
+
// Очищаем старые запросы за пределами окна
|
|
33
|
+
const recentRequests = requests.filter(time => now - time < windowMs);
|
|
34
|
+
|
|
35
|
+
if (recentRequests.length >= maxRequests) {
|
|
36
|
+
const oldestRequest = Math.min(...recentRequests);
|
|
37
|
+
const waitTime = Math.ceil((windowMs - (now - oldestRequest)) / 1000);
|
|
38
|
+
|
|
39
|
+
logger.warn({ userId, waitTime }, 'Rate limit exceeded');
|
|
40
|
+
|
|
41
|
+
await ctx.reply(
|
|
42
|
+
`⚠️ Слишком много запросов. Пожалуйста, подождите ${waitTime} сек.`
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Добавляем текущий запрос
|
|
49
|
+
recentRequests.push(now);
|
|
50
|
+
userRequests.set(userId, recentRequests);
|
|
51
|
+
|
|
52
|
+
return next();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Сброс rate limiting для пользователя
|
|
57
|
+
* @param {number} userId
|
|
58
|
+
*/
|
|
59
|
+
function resetRateLimit(userId) {
|
|
60
|
+
userRequests.delete(userId);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Очистка всех rate limit данных
|
|
65
|
+
*/
|
|
66
|
+
function clearRateLimits() {
|
|
67
|
+
userRequests.clear();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = {
|
|
71
|
+
rateLimitMiddleware,
|
|
72
|
+
resetRateLimit,
|
|
73
|
+
clearRateLimits,
|
|
74
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const sessionService = require('../../services/db/sessions');
|
|
2
|
+
const { logger } = require('../../utils/logger');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Middleware для управления сессиями
|
|
6
|
+
* Автоматически очищает просроченные сессии раз в час
|
|
7
|
+
* @param {import('telegraf').Context} ctx
|
|
8
|
+
* @param {Function} next
|
|
9
|
+
*/
|
|
10
|
+
async function sessionMiddleware(ctx, next) {
|
|
11
|
+
const userId = ctx.from?.id;
|
|
12
|
+
const chatId = ctx.chat?.id;
|
|
13
|
+
|
|
14
|
+
if (!userId || !chatId) {
|
|
15
|
+
return next();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Очистка просроченных сессий (раз в час)
|
|
19
|
+
const lastCleanup = ctx.botInfo?.last_cleanup || 0;
|
|
20
|
+
const now = Date.now();
|
|
21
|
+
|
|
22
|
+
if (now - lastCleanup > 3600000) { // 1 час
|
|
23
|
+
const removed = sessionService.cleanupExpired();
|
|
24
|
+
logger.info({ removed }, 'Expired sessions cleaned up');
|
|
25
|
+
ctx.botInfo = { ...ctx.botInfo, last_cleanup: now };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Поиск активной сессии для текущего чата
|
|
29
|
+
const isPrivate = chatId > 0 || userId === chatId;
|
|
30
|
+
|
|
31
|
+
if (isPrivate) {
|
|
32
|
+
// Личный чат - одна сессия на пользователя
|
|
33
|
+
const sessionKey = `user:${userId}`;
|
|
34
|
+
const session = sessionService.getByKey(sessionKey);
|
|
35
|
+
|
|
36
|
+
if (session && session.status === 'active') {
|
|
37
|
+
ctx.state.session = session;
|
|
38
|
+
ctx.state.sessionKey = sessionKey;
|
|
39
|
+
}
|
|
40
|
+
} else {
|
|
41
|
+
// Групповой чат - поиск сессии по reply
|
|
42
|
+
const replyToMessageId = ctx.message?.reply_to_message?.message_id;
|
|
43
|
+
|
|
44
|
+
if (replyToMessageId) {
|
|
45
|
+
// Поиск сессии по сообщению, на которое ответили
|
|
46
|
+
const session = sessionService.findByMessage(chatId, replyToMessageId);
|
|
47
|
+
|
|
48
|
+
if (session) {
|
|
49
|
+
ctx.state.session = session;
|
|
50
|
+
ctx.state.sessionKey = `chat:${chatId}`;
|
|
51
|
+
ctx.state.replyToSession = true;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
ctx.state.isPrivate = isPrivate;
|
|
57
|
+
ctx.state.userId = userId;
|
|
58
|
+
ctx.state.chatId = chatId;
|
|
59
|
+
|
|
60
|
+
return next();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = { sessionMiddleware };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
require('dotenv').config();
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Конфигурация приложения
|
|
5
|
+
* Загружает значения из переменных окружения с fallback на значения по умолчанию
|
|
6
|
+
*/
|
|
7
|
+
module.exports = {
|
|
8
|
+
/** Настройки бота */
|
|
9
|
+
bot: {
|
|
10
|
+
/** Telegram Bot API токен */
|
|
11
|
+
token: process.env.BOT_TOKEN,
|
|
12
|
+
/** Уровень логирования */
|
|
13
|
+
logLevel: process.env.LOG_LEVEL || 'info',
|
|
14
|
+
/** Whitelist пользователей (опционально) */
|
|
15
|
+
allowedUsers: process.env.ALLOWED_USERS
|
|
16
|
+
? process.env.ALLOWED_USERS.split(',').map(Number).filter(Boolean)
|
|
17
|
+
: [],
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
/** Настройки Qwen Code */
|
|
21
|
+
qwen: {
|
|
22
|
+
/** Таймаут выполнения команды (мс) */
|
|
23
|
+
timeout: parseInt(process.env.QWEN_TIMEOUT, 10) || 60000,
|
|
24
|
+
/** Максимальный размер буфера (байты) */
|
|
25
|
+
maxBuffer: parseInt(process.env.QWEN_MAX_BUFFER, 10) || 2 * 1024 * 1024, // 2MB
|
|
26
|
+
/** Максимальный размер файла для анализа (байты) */
|
|
27
|
+
maxFileSize: 2 * 1024 * 1024, // 2MB
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
/** Rate limiting */
|
|
31
|
+
rateLimit: {
|
|
32
|
+
/** Окно времени (мс) */
|
|
33
|
+
windowMs: parseInt(process.env.RATE_LIMIT_WINDOW, 10) || 60000, // 1 минута
|
|
34
|
+
/** Максимум запросов в окно */
|
|
35
|
+
maxRequests: parseInt(process.env.RATE_LIMIT_MAX, 10) || 10,
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
/** Сессии */
|
|
39
|
+
session: {
|
|
40
|
+
/** Срок жизни сессии (часы) */
|
|
41
|
+
timeoutHours: 24,
|
|
42
|
+
/** Максимум сообщений в сессии */
|
|
43
|
+
maxMessages: 1000,
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
/** Логирование */
|
|
47
|
+
logging: {
|
|
48
|
+
/** Ротация логов (дни) */
|
|
49
|
+
rotationDays: 1,
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
/** Пути */
|
|
53
|
+
paths: {
|
|
54
|
+
/** Домашняя директория ~/.qwen-alpha */
|
|
55
|
+
home: null, // Заполняется при инициализации
|
|
56
|
+
},
|
|
57
|
+
};
|