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,215 @@
|
|
|
1
|
+
const { storeManager } = require('./index');
|
|
2
|
+
const { logger } = require('../../utils/logger');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Сервис для управления пользователями
|
|
6
|
+
*/
|
|
7
|
+
class UserService {
|
|
8
|
+
/**
|
|
9
|
+
* Получение хранилища пользователей
|
|
10
|
+
* @private
|
|
11
|
+
*/
|
|
12
|
+
get _store() {
|
|
13
|
+
return storeManager.get('users');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Получение пользователя по ID
|
|
18
|
+
* @param {number} userId - Telegram user ID
|
|
19
|
+
* @returns {Object|null} Данные пользователя или null
|
|
20
|
+
*/
|
|
21
|
+
getById(userId) {
|
|
22
|
+
const data = this._store.getData();
|
|
23
|
+
return data[userId] || null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Создание или обновление пользователя
|
|
28
|
+
* @param {Object} userData - Данные пользователя
|
|
29
|
+
* @param {number} userData.id - Telegram user ID
|
|
30
|
+
* @param {string} [userData.username] - Username
|
|
31
|
+
* @param {string} [userData.first_name] - First name
|
|
32
|
+
* @param {string} [userData.last_name] - Last name
|
|
33
|
+
* @returns {Object} Данные пользователя
|
|
34
|
+
*/
|
|
35
|
+
upsert(userData) {
|
|
36
|
+
const data = this._store.getData();
|
|
37
|
+
const now = new Date().toISOString();
|
|
38
|
+
|
|
39
|
+
const existingUser = data[userData.id];
|
|
40
|
+
|
|
41
|
+
data[userData.id] = {
|
|
42
|
+
id: userData.id,
|
|
43
|
+
username: userData.username || null,
|
|
44
|
+
first_name: userData.first_name || null,
|
|
45
|
+
last_name: userData.last_name || null,
|
|
46
|
+
created_at: existingUser?.created_at || now,
|
|
47
|
+
last_seen: now,
|
|
48
|
+
settings: {
|
|
49
|
+
model: 'qwen3-coder-plus',
|
|
50
|
+
language: 'ru',
|
|
51
|
+
notifications: true,
|
|
52
|
+
requests_per_hour: 60,
|
|
53
|
+
...existingUser?.settings,
|
|
54
|
+
},
|
|
55
|
+
stats: {
|
|
56
|
+
total_requests: existingUser?.stats?.total_requests || 0,
|
|
57
|
+
total_files: existingUser?.stats?.total_files || 0,
|
|
58
|
+
total_tokens: existingUser?.stats?.total_tokens || 0,
|
|
59
|
+
groups_used: existingUser?.stats?.groups_used || 0,
|
|
60
|
+
},
|
|
61
|
+
rate_limits: {
|
|
62
|
+
requests_today: existingUser?.rate_limits?.requests_today || 0,
|
|
63
|
+
last_request: existingUser?.rate_limits?.last_request || null,
|
|
64
|
+
},
|
|
65
|
+
is_banned: existingUser?.is_banned || false,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
this._store.setData(data);
|
|
69
|
+
logger.debug({ userId: userData.id }, 'User upserted');
|
|
70
|
+
|
|
71
|
+
return data[userData.id];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Обновление настроек пользователя
|
|
76
|
+
* @param {number} userId - Telegram user ID
|
|
77
|
+
* @param {Object} settings - Новые настройки
|
|
78
|
+
* @returns {Object} Обновлённые данные пользователя
|
|
79
|
+
*/
|
|
80
|
+
updateSettings(userId, settings) {
|
|
81
|
+
const data = this._store.getData();
|
|
82
|
+
|
|
83
|
+
if (!data[userId]) {
|
|
84
|
+
throw new Error(`User ${userId} not found`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
data[userId].settings = {
|
|
88
|
+
...data[userId].settings,
|
|
89
|
+
...settings,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
this._store.setData(data);
|
|
93
|
+
logger.debug({ userId, settings }, 'User settings updated');
|
|
94
|
+
|
|
95
|
+
return data[userId];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Бан пользователя
|
|
100
|
+
* @param {number} userId - Telegram user ID
|
|
101
|
+
* @returns {boolean} Успешность операции
|
|
102
|
+
*/
|
|
103
|
+
ban(userId) {
|
|
104
|
+
const data = this._store.getData();
|
|
105
|
+
|
|
106
|
+
if (!data[userId]) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
data[userId].is_banned = true;
|
|
111
|
+
this._store.setData(data);
|
|
112
|
+
logger.warn({ userId }, 'User banned');
|
|
113
|
+
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Разбан пользователя
|
|
119
|
+
* @param {number} userId - Telegram user ID
|
|
120
|
+
* @returns {boolean} Успешность операции
|
|
121
|
+
*/
|
|
122
|
+
unban(userId) {
|
|
123
|
+
const data = this._store.getData();
|
|
124
|
+
|
|
125
|
+
if (!data[userId]) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
data[userId].is_banned = false;
|
|
130
|
+
this._store.setData(data);
|
|
131
|
+
logger.info({ userId }, 'User unbanned');
|
|
132
|
+
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Обновление статистики пользователя
|
|
138
|
+
* @param {number} userId - Telegram user ID
|
|
139
|
+
* @param {Object} updates - Обновления статистики
|
|
140
|
+
* @returns {Object} Обновлённые данные пользователя
|
|
141
|
+
*/
|
|
142
|
+
updateStats(userId, updates) {
|
|
143
|
+
const data = this._store.getData();
|
|
144
|
+
|
|
145
|
+
if (!data[userId]) {
|
|
146
|
+
throw new Error(`User ${userId} not found`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
data[userId].stats = {
|
|
150
|
+
...data[userId].stats,
|
|
151
|
+
...updates,
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
this._store.setData(data);
|
|
155
|
+
return data[userId];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Increment запроса пользователя
|
|
160
|
+
* @param {number} userId - Telegram user ID
|
|
161
|
+
*/
|
|
162
|
+
incrementRequest(userId) {
|
|
163
|
+
const data = this._store.getData();
|
|
164
|
+
const today = new Date().toISOString().split('T')[0];
|
|
165
|
+
|
|
166
|
+
if (!data[userId]) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Сброс счётчика если новый день
|
|
171
|
+
const lastRequest = data[userId].rate_limits?.last_request;
|
|
172
|
+
if (lastRequest && lastRequest.split('T')[0] !== today) {
|
|
173
|
+
data[userId].rate_limits.requests_today = 0;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
data[userId].rate_limits = {
|
|
177
|
+
requests_today: (data[userId].rate_limits?.requests_today || 0) + 1,
|
|
178
|
+
last_request: new Date().toISOString(),
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
data[userId].stats.total_requests = (data[userId].stats?.total_requests || 0) + 1;
|
|
182
|
+
|
|
183
|
+
this._store.setData(data);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Получение всех пользователей
|
|
188
|
+
* @returns {Array} Массив пользователей
|
|
189
|
+
*/
|
|
190
|
+
getAll() {
|
|
191
|
+
const data = this._store.getData();
|
|
192
|
+
return Object.values(data);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Удаление пользователя
|
|
197
|
+
* @param {number} userId - Telegram user ID
|
|
198
|
+
* @returns {boolean} Успешность операции
|
|
199
|
+
*/
|
|
200
|
+
delete(userId) {
|
|
201
|
+
const data = this._store.getData();
|
|
202
|
+
|
|
203
|
+
if (!data[userId]) {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
delete data[userId];
|
|
208
|
+
this._store.setData(data);
|
|
209
|
+
logger.info({ userId }, 'User deleted');
|
|
210
|
+
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
module.exports = new UserService();
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
const { exec } = require('child_process');
|
|
2
|
+
const { promisify } = require('util');
|
|
3
|
+
const { logger } = require('../utils/logger');
|
|
4
|
+
const config = require('../config');
|
|
5
|
+
|
|
6
|
+
const execAsync = promisify(exec);
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Класс ошибки Qwen
|
|
10
|
+
*/
|
|
11
|
+
class QwenError extends Error {
|
|
12
|
+
constructor(message, cause = null, code = null) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = 'QwenError';
|
|
15
|
+
this.cause = cause;
|
|
16
|
+
this.code = code;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Сервис для работы с Qwen Code в headless режиме
|
|
22
|
+
*/
|
|
23
|
+
class QwenService {
|
|
24
|
+
/**
|
|
25
|
+
* Экранирование строки для shell
|
|
26
|
+
* @param {string} str - Строка для экранирования
|
|
27
|
+
* @returns {string} Экранированная строка
|
|
28
|
+
* @private
|
|
29
|
+
*/
|
|
30
|
+
_escapeShell(str) {
|
|
31
|
+
if (typeof str !== 'string') {
|
|
32
|
+
return '';
|
|
33
|
+
}
|
|
34
|
+
// Экранирование специальных символов
|
|
35
|
+
return str.replace(/'/g, "'\\''").replace(/"/g, '\\"').replace(/`/g, '\\`');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Проверка доступности Qwen Code
|
|
40
|
+
* @returns {Promise<boolean>} true если Qwen доступен
|
|
41
|
+
*/
|
|
42
|
+
async checkAvailability() {
|
|
43
|
+
try {
|
|
44
|
+
const { stdout } = await execAsync('qwen --version', {
|
|
45
|
+
timeout: 5000,
|
|
46
|
+
});
|
|
47
|
+
logger.info({ version: stdout.trim() }, 'Qwen Code available');
|
|
48
|
+
return true;
|
|
49
|
+
} catch (error) {
|
|
50
|
+
logger.warn({ error: error.message }, 'Qwen Code not available');
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Анализ кода через Qwen Code headless
|
|
57
|
+
* @param {string} code - Код для анализа
|
|
58
|
+
* @param {Array} [contextMessages] - Контекстные сообщения для истории диалога
|
|
59
|
+
* @returns {Promise<string>} Ответ от Qwen
|
|
60
|
+
* @throws {QwenError} Ошибка при анализе
|
|
61
|
+
*/
|
|
62
|
+
async analyzeCode(code, contextMessages = []) {
|
|
63
|
+
const startTime = Date.now();
|
|
64
|
+
|
|
65
|
+
// Проверка доступности Qwen
|
|
66
|
+
const isAvailable = await this.checkAvailability();
|
|
67
|
+
if (!isAvailable) {
|
|
68
|
+
throw new QwenError(
|
|
69
|
+
'Qwen Code не установлен. Установите: npm install -g @qwen-code/qwen-code',
|
|
70
|
+
null,
|
|
71
|
+
'QWEN_NOT_INSTALLED'
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Формирование промпта с контекстом
|
|
76
|
+
let fullPrompt = code;
|
|
77
|
+
|
|
78
|
+
if (contextMessages && contextMessages.length > 0) {
|
|
79
|
+
// Добавляем контекст диалога
|
|
80
|
+
const contextText = contextMessages
|
|
81
|
+
.map(msg => `${msg.role === 'assistant' ? 'Assistant' : 'User'}: ${msg.content}`)
|
|
82
|
+
.join('\n');
|
|
83
|
+
|
|
84
|
+
fullPrompt = `${contextText}\n\nUser: ${code}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Команда для Qwen
|
|
88
|
+
const escapedPrompt = this._escapeShell(fullPrompt);
|
|
89
|
+
const command = `echo '${escapedPrompt}' | qwen -p "Проанализируй код и дай рекомендации" -o json`;
|
|
90
|
+
|
|
91
|
+
logger.debug({ codeLength: code.length, contextLength: contextMessages.length }, 'Running Qwen analysis');
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const { stdout, stderr } = await execAsync(command, {
|
|
95
|
+
timeout: config.qwen.timeout,
|
|
96
|
+
maxBuffer: config.qwen.maxBuffer,
|
|
97
|
+
env: { ...process.env },
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Парсинг JSON ответа
|
|
101
|
+
const result = this._parseJsonResponse(stdout);
|
|
102
|
+
|
|
103
|
+
const duration = Date.now() - startTime;
|
|
104
|
+
logger.info({ duration, resultLength: result.length }, 'Qwen analysis completed');
|
|
105
|
+
|
|
106
|
+
return result;
|
|
107
|
+
|
|
108
|
+
} catch (error) {
|
|
109
|
+
logger.error({ error, stderr }, 'Qwen analysis failed');
|
|
110
|
+
|
|
111
|
+
// Обработка различных типов ошибок
|
|
112
|
+
if (error.killed && error.signal === 'SIGTERM') {
|
|
113
|
+
throw new QwenError(
|
|
114
|
+
'Анализ прерван по таймауту. Попробуйте с меньшим файлом.',
|
|
115
|
+
error,
|
|
116
|
+
'TIMEOUT'
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (error.message.includes('maxBuffer')) {
|
|
121
|
+
throw new QwenError(
|
|
122
|
+
'Ответ слишком большой. Попробуйте меньший фрагмент кода.',
|
|
123
|
+
error,
|
|
124
|
+
'BUFFER_EXCEEDED'
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
throw new QwenError(
|
|
129
|
+
`Ошибка Qwen Code: ${error.message}`,
|
|
130
|
+
error,
|
|
131
|
+
'QWEN_ERROR'
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Парсинг JSON ответа от Qwen
|
|
138
|
+
* @param {string} stdout - JSON строка от Qwen
|
|
139
|
+
* @returns {string} Извлечённый текст ответа
|
|
140
|
+
* @private
|
|
141
|
+
*/
|
|
142
|
+
_parseJsonResponse(stdout) {
|
|
143
|
+
try {
|
|
144
|
+
const messages = JSON.parse(stdout.trim());
|
|
145
|
+
|
|
146
|
+
if (!Array.isArray(messages)) {
|
|
147
|
+
logger.warn({ stdout }, 'Unexpected Qwen response format');
|
|
148
|
+
return stdout.trim();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Поиск сообщения от assistant
|
|
152
|
+
const assistantMessage = messages.find(m => m.type === 'assistant');
|
|
153
|
+
|
|
154
|
+
if (assistantMessage?.message?.content) {
|
|
155
|
+
const content = assistantMessage.message.content;
|
|
156
|
+
|
|
157
|
+
// Content может быть строкой или массивом
|
|
158
|
+
if (typeof content === 'string') {
|
|
159
|
+
return content;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (Array.isArray(content)) {
|
|
163
|
+
// Объединение текстовых частей
|
|
164
|
+
return content
|
|
165
|
+
.filter(part => part.type === 'text')
|
|
166
|
+
.map(part => part.text)
|
|
167
|
+
.join('\n');
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Поиск result сообщения
|
|
172
|
+
const resultMessage = messages.find(m => m.type === 'result');
|
|
173
|
+
if (resultMessage?.result) {
|
|
174
|
+
return resultMessage.result;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Fallback: возврат всего stdout
|
|
178
|
+
return stdout.trim();
|
|
179
|
+
|
|
180
|
+
} catch (parseError) {
|
|
181
|
+
logger.warn({ parseError, stdout }, 'Failed to parse Qwen JSON response');
|
|
182
|
+
return stdout.trim();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Генерация кода по описанию
|
|
188
|
+
* @param {string} description - Описание того, что нужно сгенерировать
|
|
189
|
+
* @param {string} [language] - Язык программирования
|
|
190
|
+
* @returns {Promise<string>} Сгенерированный код
|
|
191
|
+
*/
|
|
192
|
+
async generateCode(description, language = null) {
|
|
193
|
+
const prompt = language
|
|
194
|
+
? `Напиши код на ${language}: ${description}`
|
|
195
|
+
: `Напиши код: ${description}`;
|
|
196
|
+
|
|
197
|
+
return this.analyzeCode(prompt);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Code review кода
|
|
202
|
+
* @param {string} code - Код для ревью
|
|
203
|
+
* @param {string} [focus] - На чём сосредоточиться (security, performance, style)
|
|
204
|
+
* @returns {Promise<string>} Результат ревью
|
|
205
|
+
*/
|
|
206
|
+
async reviewCode(code, focus = null) {
|
|
207
|
+
let prompt = 'Сделай code review этого кода. Найди баги, уязвимости и предложи улучшения.\n\n';
|
|
208
|
+
|
|
209
|
+
if (focus) {
|
|
210
|
+
prompt += `Сосредоточься на: ${focus}.\n\n`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
prompt += code;
|
|
214
|
+
|
|
215
|
+
return this.analyzeCode(prompt);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Объяснение кода
|
|
220
|
+
* @param {string} code - Код для объяснения
|
|
221
|
+
* @returns {Promise<string>} Объяснение
|
|
222
|
+
*/
|
|
223
|
+
async explainCode(code) {
|
|
224
|
+
const prompt = `Объясни подробно, что делает этот код:\n\n${code}`;
|
|
225
|
+
return this.analyzeCode(prompt);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Рефакторинг кода
|
|
230
|
+
* @param {string} code - Код для рефакторинга
|
|
231
|
+
* @param {string} [goal] - Цель рефакторинга
|
|
232
|
+
* @returns {Promise<string>} Рефакторированный код
|
|
233
|
+
*/
|
|
234
|
+
async refactorCode(code, goal = null) {
|
|
235
|
+
let prompt = 'Рефактори этот код, улучши читаемость и производительность.\n\n';
|
|
236
|
+
|
|
237
|
+
if (goal) {
|
|
238
|
+
prompt += `Цель: ${goal}.\n\n`;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
prompt += code;
|
|
242
|
+
|
|
243
|
+
return this.analyzeCode(prompt);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Экспорт синглтона
|
|
248
|
+
const qwenService = new QwenService();
|
|
249
|
+
|
|
250
|
+
module.exports = {
|
|
251
|
+
QwenService,
|
|
252
|
+
qwenService,
|
|
253
|
+
QwenError,
|
|
254
|
+
};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
const pino = require('pino');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { DIRECTORIES, getLogFilePath } = require('./paths');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Создаёт транспорт для записи логов в файл с ежедневной ротацией
|
|
7
|
+
*/
|
|
8
|
+
function createFileTransport() {
|
|
9
|
+
return {
|
|
10
|
+
target: 'pino/file',
|
|
11
|
+
options: {
|
|
12
|
+
destination: getLogFilePath(),
|
|
13
|
+
mkdir: true,
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Создаёт транспорт для вывода в консоль (pretty)
|
|
20
|
+
*/
|
|
21
|
+
function createPrettyTransport() {
|
|
22
|
+
return {
|
|
23
|
+
target: 'pino-pretty',
|
|
24
|
+
options: {
|
|
25
|
+
colorize: true,
|
|
26
|
+
translateTime: 'SYS:standard',
|
|
27
|
+
ignore: 'pid,hostname',
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Получает уровень логирования из переменных окружения
|
|
34
|
+
* @returns {string} Уровень логирования (debug, info, warn, error)
|
|
35
|
+
*/
|
|
36
|
+
function getLogLevel() {
|
|
37
|
+
const level = process.env.LOG_LEVEL || 'info';
|
|
38
|
+
const validLevels = ['debug', 'info', 'warn', 'error', 'fatal', 'silent'];
|
|
39
|
+
return validLevels.includes(level) ? level : 'info';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Создаёт и конфигурирует логгер
|
|
44
|
+
* @returns {import('pino').Logger} Настроенный логгер Pino
|
|
45
|
+
*/
|
|
46
|
+
function createLogger() {
|
|
47
|
+
const level = getLogLevel();
|
|
48
|
+
|
|
49
|
+
return pino({
|
|
50
|
+
level,
|
|
51
|
+
formatters: {
|
|
52
|
+
level: (label) => ({ level: label }),
|
|
53
|
+
},
|
|
54
|
+
timestamp: pino.stdTimeFunctions.isoTime,
|
|
55
|
+
}, pino.multistream([
|
|
56
|
+
createFileTransport(),
|
|
57
|
+
{
|
|
58
|
+
stream: process.stdout,
|
|
59
|
+
level,
|
|
60
|
+
},
|
|
61
|
+
]));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Логгер с добавлением контекста
|
|
66
|
+
*/
|
|
67
|
+
class ContextLogger {
|
|
68
|
+
constructor(baseLogger, defaultContext = {}) {
|
|
69
|
+
this.baseLogger = baseLogger;
|
|
70
|
+
this.defaultContext = defaultContext;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
child(context) {
|
|
74
|
+
return new ContextLogger(
|
|
75
|
+
this.baseLogger.child(context),
|
|
76
|
+
{ ...this.defaultContext, ...context }
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
debug(msg, context = {}) {
|
|
81
|
+
this.baseLogger.debug({ ...this.defaultContext, ...context }, msg);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
info(msg, context = {}) {
|
|
85
|
+
this.baseLogger.info({ ...this.defaultContext, ...context }, msg);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
warn(msg, context = {}) {
|
|
89
|
+
this.baseLogger.warn({ ...this.defaultContext, ...context }, msg);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
error(msg, context = {}) {
|
|
93
|
+
this.baseLogger.error({ ...this.defaultContext, ...context }, msg);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
fatal(msg, context = {}) {
|
|
97
|
+
this.baseLogger.fatal({ ...this.defaultContext, ...context }, msg);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Создаём базовый логгер
|
|
102
|
+
const logger = createLogger();
|
|
103
|
+
|
|
104
|
+
// Обработчик для ежедневной ротации логов
|
|
105
|
+
function setupLogRotation() {
|
|
106
|
+
const now = new Date();
|
|
107
|
+
const tomorrow = new Date(now);
|
|
108
|
+
tomorrow.setDate(tomorrow.getDate() + 1);
|
|
109
|
+
tomorrow.setHours(0, 0, 0, 0);
|
|
110
|
+
|
|
111
|
+
const msUntilMidnight = tomorrow.getTime() - now.getTime();
|
|
112
|
+
|
|
113
|
+
// Планируем ротацию на полночь
|
|
114
|
+
setTimeout(() => {
|
|
115
|
+
logger.info('Rotating log file');
|
|
116
|
+
setupLogRotation(); // Планируем следующую ротацию
|
|
117
|
+
}, msUntilMidnight);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
setupLogRotation();
|
|
121
|
+
|
|
122
|
+
module.exports = {
|
|
123
|
+
logger,
|
|
124
|
+
ContextLogger,
|
|
125
|
+
createLogger,
|
|
126
|
+
getLogLevel,
|
|
127
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Базовая директория для хранения данных Qwen Alpha
|
|
6
|
+
* ~/.qwen-alpha/
|
|
7
|
+
*/
|
|
8
|
+
const QWEN_ALPHA_HOME = path.join(os.homedir(), '.qwen-alpha');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Директории для хранения данных
|
|
12
|
+
*/
|
|
13
|
+
const DIRECTORIES = {
|
|
14
|
+
/** База данных (JSON файлы) */
|
|
15
|
+
db: path.join(QWEN_ALPHA_HOME, 'db'),
|
|
16
|
+
/** Логи */
|
|
17
|
+
logs: path.join(QWEN_ALPHA_HOME, 'logs'),
|
|
18
|
+
/** Конфигурация */
|
|
19
|
+
config: path.join(QWEN_ALPHA_HOME, 'config'),
|
|
20
|
+
/** Временные файлы */
|
|
21
|
+
temp: path.join(QWEN_ALPHA_HOME, 'temp'),
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Файлы базы данных
|
|
26
|
+
*/
|
|
27
|
+
const DB_FILES = {
|
|
28
|
+
users: path.join(DIRECTORIES.db, 'users.json'),
|
|
29
|
+
sessions: path.join(DIRECTORIES.db, 'sessions.json'),
|
|
30
|
+
admins: path.join(DIRECTORIES.db, 'admins.json'),
|
|
31
|
+
stats: path.join(DIRECTORIES.db, 'stats.json'),
|
|
32
|
+
settings: path.join(DIRECTORIES.config, 'settings.json'),
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Лог файл (с ежедневной ротацией)
|
|
37
|
+
*/
|
|
38
|
+
function getLogFilePath(date = new Date()) {
|
|
39
|
+
const dateStr = date.toISOString().split('T')[0]; // YYYY-MM-DD
|
|
40
|
+
return path.join(DIRECTORIES.logs, `qwen-alpha-${dateStr}.log`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Инициализация директорий
|
|
45
|
+
* Создаёт все необходимые директории, если они не существуют
|
|
46
|
+
*/
|
|
47
|
+
function initDirectories() {
|
|
48
|
+
const fs = require('fs');
|
|
49
|
+
|
|
50
|
+
for (const dir of Object.values(DIRECTORIES)) {
|
|
51
|
+
if (!fs.existsSync(dir)) {
|
|
52
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = {
|
|
58
|
+
QWEN_ALPHA_HOME,
|
|
59
|
+
DIRECTORIES,
|
|
60
|
+
DB_FILES,
|
|
61
|
+
getLogFilePath,
|
|
62
|
+
initDirectories,
|
|
63
|
+
};
|