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/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "qwen-alpha",
3
+ "version": "1.0.0",
4
+ "description": "Telegram bot for Qwen Code integration — AI-powered code review, bug detection, and code generation",
5
+ "main": "src/index.js",
6
+ "bin": {
7
+ "qwen-alpha": "./bin/qwen-alpha.js"
8
+ },
9
+ "scripts": {
10
+ "start": "node bin/qwen-alpha.js",
11
+ "test": "node --test tests/",
12
+ "lint": "eslint .",
13
+ "format": "prettier --write .",
14
+ "format:check": "prettier --check ."
15
+ },
16
+ "keywords": [
17
+ "telegram",
18
+ "bot",
19
+ "qwen",
20
+ "code",
21
+ "ai",
22
+ "cli",
23
+ "code-review",
24
+ "assistant"
25
+ ],
26
+ "author": "JeBance",
27
+ "license": "MIT",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "https://github.com/JeBance/QwenAlpha.git"
31
+ },
32
+ "bugs": {
33
+ "url": "https://github.com/JeBance/QwenAlpha/issues"
34
+ },
35
+ "homepage": "https://github.com/JeBance/QwenAlpha#readme",
36
+ "engines": {
37
+ "node": ">=18.0.0"
38
+ },
39
+ "dependencies": {
40
+ "telegraf": "^4.16.3",
41
+ "commander": "^11.1.0",
42
+ "dotenv": "^16.3.1",
43
+ "pino": "^8.17.2",
44
+ "pino-pretty": "^10.2.3"
45
+ },
46
+ "devDependencies": {
47
+ "eslint": "^8.56.0",
48
+ "prettier": "^3.1.1"
49
+ }
50
+ }
package/src/bot/bot.js ADDED
@@ -0,0 +1,81 @@
1
+ const { Telegraf } = require('telegraf');
2
+ const { message } = require('telegraf/filters');
3
+ const { logger } = require('../utils/logger');
4
+ const { loggingMiddleware } = require('./middleware/logging');
5
+ const { rateLimitMiddleware } = require('./middleware/rateLimit');
6
+ const { sessionMiddleware } = require('./middleware/session');
7
+ const { authMiddleware } = require('./middleware/auth');
8
+
9
+ // Handlers
10
+ const startHandler = require('./handlers/start');
11
+ const helpHandler = require('./handlers/help');
12
+ const resetHandler = require('./handlers/reset');
13
+ const statsHandler = require('./handlers/stats');
14
+ const settingsHandler = require('./handlers/settings');
15
+ const adminHandler = require('./handlers/admin');
16
+ const messageHandler = require('./handlers/message');
17
+ const fileHandler = require('./handlers/file');
18
+
19
+ /**
20
+ * Инициализация Telegraf бота
21
+ * @param {string} token - Telegram Bot API токен
22
+ * @param {Object} config - Конфигурация
23
+ * @returns {Promise<import('telegraf').Telegraf>} Бот
24
+ */
25
+ async function initBot(token, config = {}) {
26
+ const bot = new Telegraf(token, {
27
+ handlerTimeout: 30, // Таймаут обработки (секунды)
28
+ });
29
+
30
+ // Глобальная обработка ошибок
31
+ bot.catch((err, ctx) => {
32
+ logger.error({
33
+ err,
34
+ userId: ctx?.from?.id,
35
+ chatId: ctx?.chat?.id,
36
+ updateType: ctx?.updateType,
37
+ }, 'Bot error caught');
38
+
39
+ // Не показываем пользователю технические детали
40
+ if (ctx && ctx.reply) {
41
+ ctx.reply('⚠️ Произошла ошибка. Попробуйте позже.').catch(() => {});
42
+ }
43
+ });
44
+
45
+ // Middleware
46
+ bot.use(loggingMiddleware);
47
+ bot.use(rateLimitMiddleware);
48
+ bot.use(authMiddleware);
49
+ bot.use(sessionMiddleware);
50
+
51
+ // Commands
52
+ bot.command('start', startHandler);
53
+ bot.command('help', helpHandler);
54
+ bot.command('reset', resetHandler);
55
+ bot.command('stats', statsHandler);
56
+ bot.command('settings', settingsHandler);
57
+ bot.command('admin', adminHandler);
58
+
59
+ // Сообщения
60
+ bot.on(message('text'), messageHandler);
61
+ bot.on(message('document'), fileHandler);
62
+ bot.on(message('photo'), fileHandler);
63
+
64
+ // Упоминания бота (для групповых чатов)
65
+ bot.on('message', async (ctx, next) => {
66
+ const botInfo = await bot.telegram.getMe();
67
+ const botUsername = `@${botInfo.username}`;
68
+
69
+ if (ctx.message.text?.includes(botUsername)) {
70
+ return messageHandler(ctx);
71
+ }
72
+
73
+ return next();
74
+ });
75
+
76
+ logger.info('Bot initialized with middleware and handlers');
77
+
78
+ return bot;
79
+ }
80
+
81
+ module.exports = { initBot };
@@ -0,0 +1,231 @@
1
+ const adminService = require('../../services/db/admins');
2
+ const userService = require('../../services/db/users');
3
+ const sessionService = require('../../services/db/sessions');
4
+ const statsService = require('../../services/db/stats');
5
+ const { storeManager } = require('../../services/db');
6
+ const { logger } = require('../../utils/logger');
7
+
8
+ /**
9
+ * Обработчик команды /admin
10
+ * Панель администратора
11
+ * @param {import('telegraf').Context} ctx
12
+ */
13
+ async function adminHandler(ctx) {
14
+ const userId = ctx.state.userId;
15
+ const args = ctx.message?.text?.split(' ').slice(1) || [];
16
+ const command = args[0]?.toLowerCase();
17
+
18
+ // Проверка прав администратора
19
+ if (!ctx.state.isAdmin) {
20
+ await ctx.reply('⛔ Доступ запрещён. Требуются права администратора.');
21
+ logger.warn({ userId }, 'Non-admin tried to access /admin');
22
+ return;
23
+ }
24
+
25
+ // Без подкоманды — показать меню
26
+ if (!command) {
27
+ const admins = adminService.getAllAdmins();
28
+ const globalStats = statsService.getGlobal();
29
+
30
+ const menuText = `
31
+ 🛡 **Панель администратора**
32
+
33
+ **Администраторы:**
34
+ • Супер-админ: ${admins.super_admin}
35
+ • Обычные админы: ${admins.admins.length}
36
+
37
+ **Глобальная статистика:**
38
+ • Пользователей: ${globalStats.total_users}
39
+ • Активных за 24ч: ${globalStats.active_24h}
40
+ • Запросов сегодня: ${globalStats.requests_today}
41
+ • Ошибок за 24ч: ${globalStats.errors_24h}
42
+
43
+ ---
44
+ **Команды:**
45
+
46
+ **Управление админами:**
47
+ /admin add <user_id> — добавить админа
48
+ /admin remove <user_id> — удалить админа
49
+
50
+ **Управление пользователями:**
51
+ /admin ban <user_id> — забанить
52
+ /admin unban <user_id> — разбанить
53
+
54
+ **Сессии:**
55
+ /admin sessions list — список сессий
56
+ /admin sessions close <session_id> — закрыть
57
+ /admin sessions clear <chat_id> — очистить чат
58
+
59
+ **Настройки:**
60
+ /admin set <key> <value> — изменить настройку
61
+ /admin settings — показать настройки
62
+
63
+ **Статистика:**
64
+ /admin stats — подробная статистика
65
+ /admin broadcast <message> — рассылка всем
66
+ `.trim();
67
+
68
+ await ctx.reply(menuText, { parse_mode: 'Markdown' });
69
+ return;
70
+ }
71
+
72
+ // Обработка подкоманд
73
+ switch (command) {
74
+ case 'add': {
75
+ const targetId = parseInt(args[1], 10);
76
+ if (!targetId || isNaN(targetId)) {
77
+ await ctx.reply('❌ Usage: /admin add <user_id>');
78
+ return;
79
+ }
80
+
81
+ const success = adminService.addAdmin(targetId, userId);
82
+ if (success) {
83
+ await ctx.reply(`✅ Пользователь ${targetId} добавлен в админы.`);
84
+ logger.info({ userId, targetId }, 'Admin added');
85
+ } else {
86
+ await ctx.reply('❌ Не удалось добавить админа.');
87
+ }
88
+ break;
89
+ }
90
+
91
+ case 'remove': {
92
+ const targetId = parseInt(args[1], 10);
93
+ if (!targetId || isNaN(targetId)) {
94
+ await ctx.reply('❌ Usage: /admin remove <user_id>');
95
+ return;
96
+ }
97
+
98
+ const success = adminService.removeAdmin(targetId, userId);
99
+ if (success) {
100
+ await ctx.reply(`✅ Пользователь ${targetId} удалён из админов.`);
101
+ logger.info({ userId, targetId }, 'Admin removed');
102
+ } else {
103
+ await ctx.reply('❌ Не удалось удалить админа.');
104
+ }
105
+ break;
106
+ }
107
+
108
+ case 'ban': {
109
+ const targetId = parseInt(args[1], 10);
110
+ if (!targetId || isNaN(targetId)) {
111
+ await ctx.reply('❌ Usage: /admin ban <user_id>');
112
+ return;
113
+ }
114
+
115
+ const success = userService.ban(targetId);
116
+ if (success) {
117
+ await ctx.reply(`✅ Пользователь ${targetId} забанен.`);
118
+ logger.info({ userId, targetId }, 'User banned');
119
+ } else {
120
+ await ctx.reply('❌ Пользователь не найден.');
121
+ }
122
+ break;
123
+ }
124
+
125
+ case 'unban': {
126
+ const targetId = parseInt(args[1], 10);
127
+ if (!targetId || isNaN(targetId)) {
128
+ await ctx.reply('❌ Usage: /admin unban <user_id>');
129
+ return;
130
+ }
131
+
132
+ const success = userService.unban(targetId);
133
+ if (success) {
134
+ await ctx.reply(`✅ Пользователь ${targetId} разбанен.`);
135
+ logger.info({ userId, targetId }, 'User unbanned');
136
+ } else {
137
+ await ctx.reply('❌ Пользователь не найден.');
138
+ }
139
+ break;
140
+ }
141
+
142
+ case 'sessions': {
143
+ const subCommand = args[2];
144
+
145
+ if (subCommand === 'list') {
146
+ const allSessions = sessionService._store.getData();
147
+ const sessionCount = Object.keys(allSessions).length;
148
+ await ctx.reply(`📊 Всего сессий: ${sessionCount}`);
149
+ } else if (subCommand === 'clear') {
150
+ const chatId = parseInt(args[3], 10);
151
+ if (!chatId) {
152
+ await ctx.reply('❌ Usage: /admin sessions clear <chat_id>');
153
+ return;
154
+ }
155
+
156
+ const data = sessionService._store.getData();
157
+ delete data[`chat:${chatId}`];
158
+ sessionService._store.setData(data);
159
+
160
+ await ctx.reply(`✅ Сессии чата ${chatId} очищены.`);
161
+ logger.info({ userId, chatId }, 'Chat sessions cleared');
162
+ } else {
163
+ await ctx.reply('❌ Usage: /admin sessions list | clear <chat_id>');
164
+ }
165
+ break;
166
+ }
167
+
168
+ case 'set': {
169
+ const key = args[1];
170
+ const value = args[2];
171
+
172
+ if (!key || value === undefined) {
173
+ await ctx.reply('❌ Usage: /admin set <key> <value>');
174
+ return;
175
+ }
176
+
177
+ const settings = storeManager.get('settings');
178
+ const data = settings.getData();
179
+
180
+ // Преобразование значения
181
+ let parsedValue = value;
182
+ if (!isNaN(Number(value))) {
183
+ parsedValue = Number(value);
184
+ } else if (value.toLowerCase() === 'true') {
185
+ parsedValue = true;
186
+ } else if (value.toLowerCase() === 'false') {
187
+ parsedValue = false;
188
+ }
189
+
190
+ if (data.hasOwnProperty(key)) {
191
+ data[key] = parsedValue;
192
+ settings.setData(data);
193
+ await ctx.reply(`✅ Настройка '${key}' установлена в '${parsedValue}'.`);
194
+ logger.info({ userId, key, value: parsedValue }, 'Setting updated');
195
+ } else {
196
+ await ctx.reply(`❌ Настройка '${key}' не найдена.`);
197
+ }
198
+ break;
199
+ }
200
+
201
+ case 'settings': {
202
+ const settings = storeManager.get('settings').getData();
203
+ const settingsText = Object.entries(settings)
204
+ .map(([key, value]) => `• ${key}: ${value}`)
205
+ .join('\n');
206
+
207
+ await ctx.reply(`⚙️ **Настройки бота:**\n\n${settingsText}`, { parse_mode: 'Markdown' });
208
+ break;
209
+ }
210
+
211
+ case 'stats': {
212
+ const periodStats = statsService.getPeriod(7);
213
+ const statsText = `
214
+ 📊 **Статистика за 7 дней:**
215
+
216
+ **Запросы:** ${periodStats.total_requests}
217
+ **Ошибки:** ${periodStats.total_errors}
218
+ **Файлы:** ${periodStats.total_files}
219
+ **Сессии:** ${periodStats.total_sessions}
220
+ `.trim();
221
+
222
+ await ctx.reply(statsText, { parse_mode: 'Markdown' });
223
+ break;
224
+ }
225
+
226
+ default:
227
+ await ctx.reply('❌ Неизвестная команда. Используйте /admin для просмотра меню.');
228
+ }
229
+ }
230
+
231
+ module.exports = adminHandler;
@@ -0,0 +1,177 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const sessionService = require('../../services/db/sessions');
5
+ const statsService = require('../../services/db/stats');
6
+ const userService = require('../../services/db/users');
7
+ const { qwenService } = require('../../services/qwenService');
8
+ const config = require('../../config');
9
+ const { logger } = require('../../utils/logger');
10
+
11
+ /**
12
+ * Обработчик файлов (документы и фото)
13
+ * @param {import('telegraf').Context} ctx
14
+ */
15
+ async function fileHandler(ctx) {
16
+ const userId = ctx.state.userId;
17
+ const chatId = ctx.state.chatId;
18
+ const isPrivate = ctx.state.isPrivate;
19
+
20
+ // Получение файла из сообщения
21
+ let fileId;
22
+ let fileName = 'unknown';
23
+ let fileSize = 0;
24
+
25
+ if (ctx.message.document) {
26
+ fileId = ctx.message.document.file_id;
27
+ fileName = ctx.message.document.file_name || 'code.txt';
28
+ fileSize = ctx.message.document.file_size || 0;
29
+ } else if (ctx.message.photo && ctx.message.photo.length > 0) {
30
+ // Берём фото наилучшего качества
31
+ const photo = ctx.message.photo[ctx.message.photo.length - 1];
32
+ fileId = photo.file_id;
33
+ fileName = 'image.jpg';
34
+ fileSize = photo.file_size || 0;
35
+ } else {
36
+ return;
37
+ }
38
+
39
+ // Проверка размера файла
40
+ const maxFileSize = config.qwen.maxFileSize;
41
+ if (fileSize > maxFileSize) {
42
+ const maxMB = (maxFileSize / 1024 / 1024).toFixed(2);
43
+ await ctx.reply(
44
+ `❌ Файл слишком большой. Максимальный размер: ${maxMB}MB`,
45
+ { reply_parameters: { message_id: ctx.message.message_id } }
46
+ );
47
+ return;
48
+ }
49
+
50
+ // Загрузка индикатора
51
+ const loadingMsg = await ctx.reply('⏳ Скачиваю и анализирую файл...');
52
+
53
+ let tempFilePath = null;
54
+
55
+ try {
56
+ const startTime = Date.now();
57
+
58
+ // Скачивание файла
59
+ const fileLink = await ctx.telegram.getFileLink(fileId);
60
+ const response = await fetch(fileLink.href);
61
+
62
+ if (!response.ok) {
63
+ throw new Error('Failed to download file');
64
+ }
65
+
66
+ // Сохранение во временный файл
67
+ tempFilePath = path.join(os.tmpdir(), `qwen-alpha-${Date.now()}-${fileName}`);
68
+ const buffer = Buffer.from(await response.arrayBuffer());
69
+ fs.writeFileSync(tempFilePath, buffer);
70
+
71
+ // Чтение содержимого (для текстовых файлов)
72
+ let fileContent;
73
+ try {
74
+ fileContent = fs.readFileSync(tempFilePath, 'utf-8');
75
+ } catch (readError) {
76
+ throw new Error('Не удалось прочитать файл. Поддерживаются только текстовые файлы.');
77
+ }
78
+
79
+ // Проверка размера содержимого
80
+ if (fileContent.length > maxFileSize) {
81
+ await ctx.editMessageText(
82
+ `❌ Содержимое файла слишком большое. Максимум: ${maxFileSize} символов.`
83
+ );
84
+ return;
85
+ }
86
+
87
+ // Получение контекста из сессии
88
+ let contextMessages = [];
89
+ const session = ctx.state.session;
90
+
91
+ if (session) {
92
+ const replyToMessageId = ctx.message?.reply_to_message?.message_id;
93
+ if (replyToMessageId && session.message_tree[replyToMessageId]) {
94
+ contextMessages = sessionService.getMessageChain(session, replyToMessageId);
95
+ }
96
+ }
97
+
98
+ // Формирование промпта для анализа файла
99
+ const prompt = `Проанализируй этот файл (${fileName}):\n\n${fileContent}`;
100
+
101
+ // Запрос к Qwen
102
+ const result = await qwenService.analyzeCode(prompt, contextMessages);
103
+
104
+ const duration = Date.now() - startTime;
105
+ statsService.updateAvgResponseTime(duration);
106
+
107
+ // Отправка ответа (разбиваем на части если длинный)
108
+ const maxMessageLength = 4096;
109
+ const chunks = splitMessage(result, maxMessageLength);
110
+
111
+ for (let i = 0; i < chunks.length; i++) {
112
+ if (i === 0 && loadingMsg) {
113
+ await ctx.editMessageText(chunks[i], { parse_mode: 'Markdown' });
114
+ } else {
115
+ await ctx.reply(chunks[i], { parse_mode: 'Markdown' });
116
+ }
117
+ }
118
+
119
+ // Обновление статистики
120
+ statsService.incrementRequest();
121
+ statsService.incrementFile();
122
+ userService.incrementRequest(userId);
123
+ userService.updateStats(userId, {
124
+ total_files: (userService.getById(userId)?.stats?.total_files || 0) + 1,
125
+ });
126
+
127
+ logger.info({ userId, chatId, fileName, fileSize, duration }, 'File analyzed');
128
+
129
+ } catch (error) {
130
+ logger.error({ userId, chatId, fileName, error }, 'File analysis failed');
131
+
132
+ await ctx.editMessageText(
133
+ `❌ Ошибка при анализе файла: ${error.message}`
134
+ );
135
+
136
+ statsService.incrementError();
137
+ } finally {
138
+ // Очистка временного файла
139
+ if (tempFilePath && fs.existsSync(tempFilePath)) {
140
+ try {
141
+ fs.unlinkSync(tempFilePath);
142
+ } catch (e) {
143
+ logger.warn({ tempFilePath }, 'Failed to delete temp file');
144
+ }
145
+ }
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Разбиение длинного сообщения на части
151
+ * @param {string} text - Текст для разбиения
152
+ * @param {number} maxLength - Максимальная длина
153
+ * @returns {string[]} Массив частей
154
+ */
155
+ function splitMessage(text, maxLength) {
156
+ const chunks = [];
157
+ let currentChunk = '';
158
+
159
+ const lines = text.split('\n');
160
+
161
+ for (const line of lines) {
162
+ if (currentChunk.length + line.length + 1 > maxLength) {
163
+ chunks.push(currentChunk);
164
+ currentChunk = line;
165
+ } else {
166
+ currentChunk += (currentChunk ? '\n' : '') + line;
167
+ }
168
+ }
169
+
170
+ if (currentChunk) {
171
+ chunks.push(currentChunk);
172
+ }
173
+
174
+ return chunks;
175
+ }
176
+
177
+ module.exports = fileHandler;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Обработчик команды /help
3
+ * Список команд и информация о боте
4
+ * @param {import('telegraf').Context} ctx
5
+ */
6
+ async function helpHandler(ctx) {
7
+ const isAdmin = ctx.state.isAdmin;
8
+
9
+ const helpText = `
10
+ 📖 **Qwen Alpha — Команды бота**
11
+
12
+ **Основные команды:**
13
+ /start — Запуск бота и приветствие
14
+ /help — Эта справка
15
+ /reset — Сброс текущей сессии
16
+ /stats — Ваша статистика
17
+ /settings — Настройки бота
18
+
19
+ **Работа с кодом:**
20
+ • Отправьте код текстом — я проанализирую его
21
+ • Отправьте файл с кодом — я изучу и дам рекомендации
22
+ • В группах: /qwen <запрос> или @QwenAlphaRobot <запрос>
23
+ • Ответьте на сообщение бота — продолжим диалог
24
+
25
+ **Для групповых чатов:**
26
+ 1. Начните сессию: /qwen Как сделать аутентификацию?
27
+ 2. Продолжайте ответами на сообщения — я помню контекст
28
+ 3. Каждая тема — отдельное дерево сообщений
29
+
30
+ ${isAdmin ? '**Админ команды:**\n/admin — Панель администратора\n\n' : ''}
31
+ 🔗 **Репозиторий:** https://github.com/JeBance/QwenAlpha
32
+ 📝 **Документация:** https://github.com/JeBance/QwenAlpha#readme
33
+
34
+ **Технологии:**
35
+ • Qwen Code (headless режим)
36
+ • Telegraf (Node.js)
37
+ • JSON хранилище (~/.qwen-alpha/)
38
+ `.trim();
39
+
40
+ await ctx.reply(helpText, { parse_mode: 'Markdown' });
41
+ }
42
+
43
+ module.exports = helpHandler;