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.
@@ -0,0 +1,342 @@
1
+ const { storeManager } = require('./index');
2
+ const { logger } = require('../../utils/logger');
3
+
4
+ /**
5
+ * Сервис для управления сессиями
6
+ * Поддерживает древовидную структуру для групповых чатов
7
+ */
8
+ class SessionService {
9
+ /**
10
+ * Получение хранилища сессий
11
+ * @private
12
+ */
13
+ get _store() {
14
+ return storeManager.get('sessions');
15
+ }
16
+
17
+ /**
18
+ * Генерация ключа сессии
19
+ * @param {number} userId - Telegram user ID
20
+ * @param {number} chatId - Telegram chat ID
21
+ * @returns {string} Ключ сессии
22
+ * @private
23
+ */
24
+ _getSessionKey(userId, chatId) {
25
+ // Личный чат: user:{userId}
26
+ // Групповой чат: chat:{chatId}:session:{sessionId}
27
+ if (userId === chatId || chatId > 0) {
28
+ return `user:${userId}`;
29
+ }
30
+ return `chat:${chatId}`;
31
+ }
32
+
33
+ /**
34
+ * Создание новой сессии
35
+ * @param {Object} options - Опции сессии
36
+ * @param {number} options.userId - Telegram user ID
37
+ * @param {number} options.chatId - Telegram chat ID
38
+ * @param {number} options.rootMessageId - ID корневого сообщения
39
+ * @param {string} [options.chatType] - Тип чата (private, group, supergroup)
40
+ * @param {string} [options.chatTitle] - Название чата (для групп)
41
+ * @returns {Object} Данные сессии
42
+ */
43
+ create({ userId, chatId, rootMessageId, chatType = 'private', chatTitle = null }) {
44
+ const data = this._store.getData();
45
+ const now = new Date();
46
+ const timeoutHours = storeManager.get('settings').getData().session_timeout_hours || 24;
47
+ const expiresAt = new Date(now.getTime() + timeoutHours * 60 * 60 * 1000);
48
+
49
+ const sessionKey = this._getSessionKey(userId, chatId);
50
+ const sessionId = `${sessionKey}:session:${now.getTime()}`;
51
+
52
+ const session = {
53
+ session_id: sessionId,
54
+ chat_id: chatId,
55
+ root_message_id: rootMessageId,
56
+ root_user_id: userId,
57
+ chat_type: chatType,
58
+ chat_title: chatTitle,
59
+ created_at: now.toISOString(),
60
+ expires_at: expiresAt.toISOString(),
61
+ status: 'active',
62
+ message_tree: {
63
+ [rootMessageId]: {
64
+ message_id: rootMessageId,
65
+ user_id: userId,
66
+ text: '',
67
+ type: 'root',
68
+ children: [],
69
+ },
70
+ },
71
+ context: {
72
+ summary: '',
73
+ keywords: [],
74
+ last_summary_at: null,
75
+ },
76
+ participants: [userId],
77
+ message_count: 1,
78
+ };
79
+
80
+ // Для групповых чатов - массив сессий
81
+ if (chatType !== 'private') {
82
+ if (!data[sessionKey]) {
83
+ data[sessionKey] = [];
84
+ }
85
+ data[sessionKey].push(session);
86
+ } else {
87
+ data[sessionKey] = session;
88
+ }
89
+
90
+ this._store.setData(data);
91
+ logger.info({ sessionId, userId, chatId }, 'Session created');
92
+
93
+ return session;
94
+ }
95
+
96
+ /**
97
+ * Получение сессии по ключу
98
+ * @param {string} sessionKey - Ключ сессии
99
+ * @returns {Object|null} Сессия или null
100
+ */
101
+ getByKey(sessionKey) {
102
+ const data = this._store.getData();
103
+ return data[sessionKey] || null;
104
+ }
105
+
106
+ /**
107
+ * Поиск сессии по ID сообщения (для reply в группах)
108
+ * @param {number} chatId - Telegram chat ID
109
+ * @param {number} messageId - ID сообщения
110
+ * @returns {Object|null} Сессия или null
111
+ */
112
+ findByMessage(chatId, messageId) {
113
+ const data = this._store.getData();
114
+ const chatKey = `chat:${chatId}`;
115
+
116
+ const sessions = data[chatKey];
117
+ if (!Array.isArray(sessions)) {
118
+ return null;
119
+ }
120
+
121
+ for (const session of sessions) {
122
+ if (session.message_tree && session.message_tree[messageId]) {
123
+ return session;
124
+ }
125
+ }
126
+
127
+ return null;
128
+ }
129
+
130
+ /**
131
+ * Добавление сообщения в дерево сессии
132
+ * @param {Object} options - Опции
133
+ * @param {string} options.sessionId - ID сессии
134
+ * @param {number} options.chatId - Telegram chat ID
135
+ * @param {number} options.messageId - ID сообщения
136
+ * @param {number|string} options.userId - Telegram user ID (или 'bot')
137
+ * @param {string} options.text - Текст сообщения
138
+ * @param {number} [options.parentId] - ID родительского сообщения
139
+ * @param {string} [options.type] - Тип сообщения
140
+ * @returns {Object|null} Обновлённая сессия или null
141
+ */
142
+ addMessage({ sessionId, chatId, messageId, userId, text, parentId = null, type = 'user_message' }) {
143
+ const data = this._store.getData();
144
+ const chatKey = `chat:${chatId}`;
145
+
146
+ let session;
147
+ let sessionIndex = -1;
148
+
149
+ // Поиск сессии
150
+ if (Array.isArray(data[chatKey])) {
151
+ sessionIndex = data[chatKey].findIndex(s => s.session_id === sessionId);
152
+ if (sessionIndex === -1) {
153
+ logger.warn({ sessionId, chatId }, 'Session not found');
154
+ return null;
155
+ }
156
+ session = data[chatKey][sessionIndex];
157
+ } else {
158
+ session = data[chatKey];
159
+ }
160
+
161
+ if (!session || session.status !== 'active') {
162
+ return null;
163
+ }
164
+
165
+ // Добавление сообщения в дерево
166
+ session.message_tree[messageId] = {
167
+ message_id: messageId,
168
+ user_id: userId,
169
+ text,
170
+ type,
171
+ parent_id: parentId,
172
+ children: [],
173
+ created_at: new Date().toISOString(),
174
+ };
175
+
176
+ // Обновление родительского узла
177
+ if (parentId && session.message_tree[parentId]) {
178
+ if (!session.message_tree[parentId].children) {
179
+ session.message_tree[parentId].children = [];
180
+ }
181
+ session.message_tree[parentId].children.push(messageId);
182
+ }
183
+
184
+ // Обновление участников
185
+ if (typeof userId === 'number' && !session.participants.includes(userId)) {
186
+ session.participants.push(userId);
187
+ }
188
+
189
+ session.message_count++;
190
+
191
+ // Сохранение
192
+ if (Array.isArray(data[chatKey])) {
193
+ data[chatKey][sessionIndex] = session;
194
+ } else {
195
+ data[chatKey] = session;
196
+ }
197
+
198
+ this._store.setData(data);
199
+ logger.debug({ sessionId, messageId }, 'Message added to session');
200
+
201
+ return session;
202
+ }
203
+
204
+ /**
205
+ * Получение цепочки сообщений от корня до указанного
206
+ * @param {Object} session - Сессия
207
+ * @param {number} messageId - ID сообщения
208
+ * @returns {Array} Массив сообщений от корня до текущего
209
+ */
210
+ getMessageChain(session, messageId) {
211
+ const chain = [];
212
+ let currentId = messageId;
213
+
214
+ while (currentId) {
215
+ const message = session.message_tree[currentId];
216
+ if (!message) break;
217
+
218
+ chain.unshift({
219
+ role: message.user_id === 'bot' ? 'assistant' : 'user',
220
+ content: message.text || '',
221
+ message_id: message.message_id,
222
+ });
223
+
224
+ currentId = message.parent_id;
225
+ }
226
+
227
+ return chain;
228
+ }
229
+
230
+ /**
231
+ * Обновление контекста сессии
232
+ * @param {string} sessionId - ID сессии
233
+ * @param {number} chatId - Telegram chat ID
234
+ * @param {Object} context - Новый контекст
235
+ * @returns {Object|null} Обновлённая сессия
236
+ */
237
+ updateContext(sessionId, chatId, context) {
238
+ const data = this._store.getData();
239
+ const chatKey = `chat:${chatId}`;
240
+
241
+ let session;
242
+ let sessionIndex = -1;
243
+
244
+ if (Array.isArray(data[chatKey])) {
245
+ sessionIndex = data[chatKey].findIndex(s => s.session_id === sessionId);
246
+ if (sessionIndex === -1) return null;
247
+ session = data[chatKey][sessionIndex];
248
+ } else {
249
+ session = data[chatKey];
250
+ }
251
+
252
+ session.context = { ...session.context, ...context };
253
+
254
+ if (Array.isArray(data[chatKey])) {
255
+ data[chatKey][sessionIndex] = session;
256
+ } else {
257
+ data[chatKey] = session;
258
+ }
259
+
260
+ this._store.setData(data);
261
+ return session;
262
+ }
263
+
264
+ /**
265
+ * Закрытие сессии
266
+ * @param {string} sessionId - ID сессии
267
+ * @param {number} chatId - Telegram chat ID
268
+ * @returns {boolean} Успешность
269
+ */
270
+ close(sessionId, chatId) {
271
+ const data = this._store.getData();
272
+ const chatKey = `chat:${chatId}`;
273
+
274
+ if (Array.isArray(data[chatKey])) {
275
+ const index = data[chatKey].findIndex(s => s.session_id === sessionId);
276
+ if (index === -1) return false;
277
+
278
+ data[chatKey][index].status = 'closed';
279
+ data[chatKey][index].closed_at = new Date().toISOString();
280
+ } else if (data[chatKey]?.session_id === sessionId) {
281
+ data[chatKey].status = 'closed';
282
+ data[chatKey].closed_at = new Date().toISOString();
283
+ } else {
284
+ return false;
285
+ }
286
+
287
+ this._store.setData(data);
288
+ logger.info({ sessionId, chatId }, 'Session closed');
289
+
290
+ return true;
291
+ }
292
+
293
+ /**
294
+ * Очистка просроченных сессий
295
+ * @returns {number} Количество удалённых сессий
296
+ */
297
+ cleanupExpired() {
298
+ const data = this._store.getData();
299
+ const now = new Date();
300
+ let removed = 0;
301
+
302
+ for (const [key, value] of Object.entries(data)) {
303
+ if (Array.isArray(value)) {
304
+ // Групповые сессии
305
+ const filtered = value.filter(session => {
306
+ const expiresAt = new Date(session.expires_at);
307
+ if (expiresAt < now) {
308
+ removed++;
309
+ return false;
310
+ }
311
+ return true;
312
+ });
313
+ data[key] = filtered;
314
+ } else if (value?.expires_at) {
315
+ // Личная сессия
316
+ const expiresAt = new Date(value.expires_at);
317
+ if (expiresAt < now) {
318
+ delete data[key];
319
+ removed++;
320
+ }
321
+ }
322
+ }
323
+
324
+ this._store.setData(data);
325
+ logger.info({ removed }, 'Expired sessions cleaned up');
326
+
327
+ return removed;
328
+ }
329
+
330
+ /**
331
+ * Получение активных сессий чата
332
+ * @param {number} chatId - Telegram chat ID
333
+ * @returns {Array} Массив сессий
334
+ */
335
+ getChatSessions(chatId) {
336
+ const data = this._store.getData();
337
+ const chatKey = `chat:${chatId}`;
338
+ return data[chatKey] || [];
339
+ }
340
+ }
341
+
342
+ module.exports = new SessionService();
@@ -0,0 +1,223 @@
1
+ const { storeManager } = require('./index');
2
+ const { logger } = require('../../utils/logger');
3
+
4
+ /**
5
+ * Сервис для управления статистикой
6
+ */
7
+ class StatsService {
8
+ /**
9
+ * Получение хранилища статистики
10
+ * @private
11
+ */
12
+ get _store() {
13
+ return storeManager.get('stats');
14
+ }
15
+
16
+ /**
17
+ * Получение сегодняшней даты в формате YYYY-MM-DD
18
+ * @private
19
+ */
20
+ get _today() {
21
+ return new Date().toISOString().split('T')[0];
22
+ }
23
+
24
+ /**
25
+ * Инициализация дня в статистике
26
+ * @private
27
+ */
28
+ _initDay() {
29
+ const data = this._store.getData();
30
+ const today = this._today;
31
+
32
+ if (!data.daily[today]) {
33
+ data.daily[today] = {
34
+ requests: 0,
35
+ users: 0,
36
+ errors: 0,
37
+ files: 0,
38
+ sessions_created: 0,
39
+ };
40
+ }
41
+
42
+ return data;
43
+ }
44
+
45
+ /**
46
+ * Инкремент запроса
47
+ */
48
+ incrementRequest() {
49
+ const data = this._initDay();
50
+
51
+ data.global.requests_today++;
52
+ data.daily[this._today].requests++;
53
+
54
+ this._store.setData(data);
55
+ }
56
+
57
+ /**
58
+ * Инкремент ошибки
59
+ */
60
+ incrementError() {
61
+ const data = this._initDay();
62
+
63
+ data.global.errors_24h++;
64
+ data.daily[this._today].errors++;
65
+
66
+ this._store.setData(data);
67
+ }
68
+
69
+ /**
70
+ * Инкремент анализа файла
71
+ */
72
+ incrementFile() {
73
+ const data = this._initDay();
74
+
75
+ data.daily[this._today].files++;
76
+
77
+ this._store.setData(data);
78
+ }
79
+
80
+ /**
81
+ * Инкремент создания сессии
82
+ */
83
+ incrementSessionCreated() {
84
+ const data = this._initDay();
85
+
86
+ data.daily[this._today].sessions_created++;
87
+
88
+ this._store.setData(data);
89
+ }
90
+
91
+ /**
92
+ * Обновление среднего времени ответа
93
+ * @param {number} responseTimeMs - Время ответа в мс
94
+ */
95
+ updateAvgResponseTime(responseTimeMs) {
96
+ const data = this._store.getData();
97
+ const currentAvg = data.global.avg_response_time_ms;
98
+ const totalRequests = data.global.requests_today;
99
+
100
+ // Скользящее среднее
101
+ const newAvg = totalRequests > 0
102
+ ? ((currentAvg * (totalRequests - 1)) + responseTimeMs) / totalRequests
103
+ : responseTimeMs;
104
+
105
+ data.global.avg_response_time_ms = Math.round(newAvg);
106
+
107
+ this._store.setData(data);
108
+ }
109
+
110
+ /**
111
+ * Обновление количества активных пользователей за 24ч
112
+ * @param {number} count - Количество пользователей
113
+ */
114
+ updateActiveUsers24h(count) {
115
+ const data = this._store.getData();
116
+ data.global.active_24h = count;
117
+ this._store.setData(data);
118
+ }
119
+
120
+ /**
121
+ * Обновление общего количества пользователей
122
+ * @param {number} count - Количество пользователей
123
+ */
124
+ updateTotalUsers(count) {
125
+ const data = this._store.getData();
126
+ data.global.total_users = count;
127
+ this._store.setData(data);
128
+ }
129
+
130
+ /**
131
+ * Получение глобальной статистики
132
+ * @returns {Object} Глобальная статистика
133
+ */
134
+ getGlobal() {
135
+ const data = this._store.getData();
136
+ return data.global;
137
+ }
138
+
139
+ /**
140
+ * Получение статистики за день
141
+ * @param {string} [date] - Дата в формате YYYY-MM-DD (по умолчанию сегодня)
142
+ * @returns {Object} Статистика за день
143
+ */
144
+ getDaily(date = this._today) {
145
+ const data = this._store.getData();
146
+ return data.daily[date] || {
147
+ requests: 0,
148
+ users: 0,
149
+ errors: 0,
150
+ files: 0,
151
+ sessions_created: 0,
152
+ };
153
+ }
154
+
155
+ /**
156
+ * Получение статистики за период
157
+ * @param {number} days - Количество дней
158
+ * @returns {Object} Статистика за период
159
+ */
160
+ getPeriod(days) {
161
+ const data = this._store.getData();
162
+ const result = {
163
+ total_requests: 0,
164
+ total_errors: 0,
165
+ total_files: 0,
166
+ total_sessions: 0,
167
+ daily: [],
168
+ };
169
+
170
+ for (let i = 0; i < days; i++) {
171
+ const date = new Date();
172
+ date.setDate(date.getDate() - i);
173
+ const dateStr = date.toISOString().split('T')[0];
174
+
175
+ const dayStats = data.daily[dateStr] || {
176
+ requests: 0,
177
+ errors: 0,
178
+ files: 0,
179
+ sessions_created: 0,
180
+ };
181
+
182
+ result.total_requests += dayStats.requests;
183
+ result.total_errors += dayStats.errors;
184
+ result.total_files += dayStats.files;
185
+ result.total_sessions += dayStats.sessions_created;
186
+
187
+ result.daily.push({
188
+ date: dateStr,
189
+ ...dayStats,
190
+ });
191
+ }
192
+
193
+ return result;
194
+ }
195
+
196
+ /**
197
+ * Сброс статистики за день (вызывается ежедневно)
198
+ */
199
+ resetDaily() {
200
+ const data = this._store.getData();
201
+
202
+ // Архивация старых данных (опционально)
203
+ // Очистка данных старше 30 дней
204
+ const thirtyDaysAgo = new Date();
205
+ thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
206
+ const cutoffDate = thirtyDaysAgo.toISOString().split('T')[0];
207
+
208
+ for (const date of Object.keys(data.daily)) {
209
+ if (date < cutoffDate) {
210
+ delete data.daily[date];
211
+ }
212
+ }
213
+
214
+ // Сброс счётчиков 24ч
215
+ data.global.errors_24h = 0;
216
+ data.global.active_24h = 0;
217
+
218
+ this._store.setData(data);
219
+ logger.info('Daily stats reset');
220
+ }
221
+ }
222
+
223
+ module.exports = new StatsService();