blockmine 1.22.0 → 1.23.1

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.
Files changed (108) hide show
  1. package/.claude/agents/code-architect.md +34 -0
  2. package/.claude/agents/code-explorer.md +51 -0
  3. package/.claude/agents/code-reviewer.md +46 -0
  4. package/.claude/commands/feature-dev.md +125 -0
  5. package/.claude/settings.json +5 -1
  6. package/.claude/settings.local.json +12 -1
  7. package/.claude/skills/frontend-design/SKILL.md +42 -0
  8. package/CHANGELOG.md +32 -1
  9. package/README.md +302 -152
  10. package/backend/package-lock.json +681 -9
  11. package/backend/package.json +8 -0
  12. package/backend/prisma/migrations/20251116111851_add_execution_trace/migration.sql +22 -0
  13. package/backend/prisma/migrations/20251120154914_add_panel_api_keys/migration.sql +21 -0
  14. package/backend/prisma/migrations/20251121110241_add_proxy_table/migration.sql +45 -0
  15. package/backend/prisma/schema.prisma +70 -1
  16. package/backend/src/__tests__/services/BotLifecycleService.test.js +9 -4
  17. package/backend/src/ai/plugin-assistant-system-prompt.md +788 -0
  18. package/backend/src/api/middleware/auth.js +27 -0
  19. package/backend/src/api/middleware/botAccess.js +7 -3
  20. package/backend/src/api/middleware/panelApiAuth.js +135 -0
  21. package/backend/src/api/routes/aiAssistant.js +995 -0
  22. package/backend/src/api/routes/auth.js +90 -54
  23. package/backend/src/api/routes/botCommands.js +107 -0
  24. package/backend/src/api/routes/botGroups.js +165 -0
  25. package/backend/src/api/routes/botHistory.js +108 -0
  26. package/backend/src/api/routes/botPermissions.js +99 -0
  27. package/backend/src/api/routes/botStatus.js +36 -0
  28. package/backend/src/api/routes/botUsers.js +162 -0
  29. package/backend/src/api/routes/bots.js +108 -59
  30. package/backend/src/api/routes/eventGraphs.js +4 -1
  31. package/backend/src/api/routes/logs.js +13 -3
  32. package/backend/src/api/routes/panel.js +3 -3
  33. package/backend/src/api/routes/panelApiKeys.js +179 -0
  34. package/backend/src/api/routes/pluginIde.js +1715 -135
  35. package/backend/src/api/routes/plugins.js +170 -13
  36. package/backend/src/api/routes/proxies.js +130 -0
  37. package/backend/src/api/routes/search.js +4 -0
  38. package/backend/src/api/routes/servers.js +20 -3
  39. package/backend/src/api/routes/settings.js +5 -0
  40. package/backend/src/api/routes/system.js +3 -3
  41. package/backend/src/api/routes/traces.js +131 -0
  42. package/backend/src/config/debug.config.js +36 -0
  43. package/backend/src/core/BotHistoryStore.js +180 -0
  44. package/backend/src/core/BotManager.js +14 -4
  45. package/backend/src/core/BotProcess.js +1517 -1092
  46. package/backend/src/core/EventGraphManager.js +194 -280
  47. package/backend/src/core/GraphExecutionEngine.js +1004 -321
  48. package/backend/src/core/MessageQueue.js +12 -6
  49. package/backend/src/core/PluginLoader.js +99 -5
  50. package/backend/src/core/PluginManager.js +74 -13
  51. package/backend/src/core/TaskScheduler.js +1 -1
  52. package/backend/src/core/commands/whois.js +1 -1
  53. package/backend/src/core/node-registries/actions.js +72 -2
  54. package/backend/src/core/node-registries/arrays.js +18 -0
  55. package/backend/src/core/node-registries/data.js +1 -1
  56. package/backend/src/core/node-registries/events.js +14 -0
  57. package/backend/src/core/node-registries/logic.js +17 -0
  58. package/backend/src/core/node-registries/strings.js +34 -0
  59. package/backend/src/core/node-registries/type.js +25 -0
  60. package/backend/src/core/nodes/actions/bot_look_at.js +1 -1
  61. package/backend/src/core/nodes/actions/create_command.js +189 -0
  62. package/backend/src/core/nodes/actions/delete_command.js +92 -0
  63. package/backend/src/core/nodes/actions/http_request.js +23 -4
  64. package/backend/src/core/nodes/actions/send_message.js +2 -12
  65. package/backend/src/core/nodes/actions/update_command.js +133 -0
  66. package/backend/src/core/nodes/arrays/join.js +28 -0
  67. package/backend/src/core/nodes/data/cast.js +2 -1
  68. package/backend/src/core/nodes/data/string_literal.js +2 -13
  69. package/backend/src/core/nodes/logic/not.js +22 -0
  70. package/backend/src/core/nodes/strings/starts_with.js +1 -1
  71. package/backend/src/core/nodes/strings/to_lower.js +22 -0
  72. package/backend/src/core/nodes/strings/to_upper.js +22 -0
  73. package/backend/src/core/nodes/type/to_string.js +32 -0
  74. package/backend/src/core/services/BotLifecycleService.js +835 -596
  75. package/backend/src/core/services/CommandExecutionService.js +430 -351
  76. package/backend/src/core/services/DebugSessionManager.js +347 -0
  77. package/backend/src/core/services/GraphCollaborationManager.js +501 -0
  78. package/backend/src/core/services/MinecraftBotManager.js +259 -0
  79. package/backend/src/core/services/MinecraftViewerService.js +216 -0
  80. package/backend/src/core/services/TraceCollectorService.js +545 -0
  81. package/backend/src/core/system/RuntimeCommandRegistry.js +116 -0
  82. package/backend/src/core/system/Transport.js +0 -4
  83. package/backend/src/core/validation/nodeSchemas.js +6 -6
  84. package/backend/src/real-time/botApi/handlers/graphHandlers.js +2 -2
  85. package/backend/src/real-time/botApi/handlers/graphWebSocketHandlers.js +1 -1
  86. package/backend/src/real-time/botApi/utils.js +11 -0
  87. package/backend/src/real-time/panelNamespace.js +387 -0
  88. package/backend/src/real-time/presence.js +7 -2
  89. package/backend/src/real-time/socketHandler.js +395 -4
  90. package/backend/src/server.js +18 -0
  91. package/frontend/dist/assets/index-DqzDkFsP.js +11210 -0
  92. package/frontend/dist/assets/index-t6K1u4OV.css +32 -0
  93. package/frontend/dist/index.html +2 -2
  94. package/frontend/package-lock.json +9437 -0
  95. package/frontend/package.json +8 -0
  96. package/package.json +2 -2
  97. package/screen/console.png +0 -0
  98. package/screen/dashboard.png +0 -0
  99. package/screen/graph_collabe.png +0 -0
  100. package/screen/graph_live_debug.png +0 -0
  101. package/screen/management_command.png +0 -0
  102. package/screen/node_debug_trace.png +0 -0
  103. package/screen/plugin_/320/276/320/261/320/267/320/276/321/200.png +0 -0
  104. package/screen/websocket.png +0 -0
  105. package/screen//320/275/320/260/321/201/321/202/321/200/320/276/320/271/320/272/320/270_/320/276/321/202/320/264/320/265/320/273/321/214/320/275/321/213/321/205_/320/272/320/276/320/274/320/260/320/275/320/264_/320/272/320/260/320/266/320/264/321/203_/320/272/320/276/320/274/320/260/320/275/320/273/320/264/321/203_/320/274/320/276/320/266/320/275/320/276_/320/275/320/260/321/201/321/202/321/200/320/260/320/270/320/262/320/260/321/202/321/214.png +0 -0
  106. package/screen//320/277/320/273/320/260/320/275/320/270/321/200/320/276/320/262/321/211/320/270/320/272_/320/274/320/276/320/266/320/275/320/276_/320/267/320/260/320/264/320/260/320/262/320/260/321/202/321/214_/320/264/320/265/320/271/321/201/321/202/320/262/320/270/321/217_/320/277/320/276_/320/262/321/200/320/265/320/274/320/265/320/275/320/270.png +0 -0
  107. package/frontend/dist/assets/index-CfTo92bP.css +0 -1
  108. package/frontend/dist/assets/index-CiFD5X9Z.js +0 -8344
@@ -1,596 +1,835 @@
1
- const DependencyService = require('../DependencyService');
2
- const { decrypt } = require('../utils/crypto');
3
- const UserService = require('../UserService');
4
-
5
- class BotLifecycleService {
6
- constructor({
7
- botRepository,
8
- pluginRepository,
9
- commandRepository,
10
- permissionRepository,
11
- botProcessManager,
12
- cacheManager,
13
- resourceMonitorService,
14
- telemetryService,
15
- eventGraphManager,
16
- commandExecutionService,
17
- logger
18
- }) {
19
- this.botRepository = botRepository;
20
- this.pluginRepository = pluginRepository;
21
- this.commandRepository = commandRepository;
22
- this.permissionRepository = permissionRepository;
23
- this.processManager = botProcessManager;
24
- this.cache = cacheManager;
25
- this.resourceMonitor = resourceMonitorService;
26
- this.telemetry = telemetryService;
27
- this.eventGraphManager = eventGraphManager;
28
- this.commandExecutionService = commandExecutionService;
29
- this.logger = logger;
30
-
31
- this.logCache = new Map();
32
- this.crashCounters = new Map();
33
- }
34
-
35
- async startBot(botConfig) {
36
- const botId = botConfig.id;
37
-
38
- if (this.processManager.isRunning(botId)) {
39
- this.appendLog(botId, `[SYSTEM-ERROR] Попытка повторного запуска. Запуск отменен.`);
40
- return { success: false, message: 'Бот уже запущен или запускается.' };
41
- }
42
-
43
- await this._syncSystemPermissions(botId);
44
- await this.loadConfigForBot(botId);
45
- this.logCache.set(botId, []);
46
- this.emitStatusUpdate(botId, 'starting', '');
47
-
48
- const allPluginsForBot = await this.pluginRepository.findEnabledByBotId(botId);
49
- const { sortedPlugins, hasCriticalIssues, pluginInfo } = DependencyService.resolveDependencies(allPluginsForBot, allPluginsForBot);
50
-
51
- if (hasCriticalIssues) {
52
- this.appendLog(botId, '[DependencyManager] Обнаружены критические проблемы с зависимостями, запуск отменен.');
53
-
54
- const criticalIssueTypes = new Set(['missing_dependency', 'version_mismatch', 'circular_dependency']);
55
-
56
- for (const pluginId in pluginInfo) {
57
- const info = pluginInfo[pluginId];
58
- if (info.issues.length === 0) continue;
59
-
60
- const criticalIssues = info.issues.filter(issue => criticalIssueTypes.has(issue.type));
61
-
62
- if (criticalIssues.length > 0) {
63
- this.appendLog(botId, `* Плагин "${info.name}":`);
64
- for (const issue of criticalIssues) {
65
- this.appendLog(botId, ` - ${issue.message}`);
66
- }
67
- }
68
- }
69
-
70
- this.emitStatusUpdate(botId, 'stopped', 'Ошибка зависимостей плагинов.');
71
- return { success: false, message: 'Критические ошибки в зависимостях плагинов.' };
72
- }
73
-
74
- const decryptedConfig = { ...botConfig };
75
- if (decryptedConfig.password) decryptedConfig.password = decrypt(decryptedConfig.password);
76
- if (decryptedConfig.proxyPassword) decryptedConfig.proxyPassword = decrypt(decryptedConfig.proxyPassword);
77
- if (decryptedConfig.proxyUsername) decryptedConfig.proxyUsername = decryptedConfig.proxyUsername.trim();
78
-
79
- const fullBotConfig = { ...decryptedConfig, plugins: sortedPlugins };
80
-
81
- const child = await this.processManager.spawn(botConfig, fullBotConfig);
82
-
83
- child.api = {
84
- sendMessage: (type, message, username) => {
85
- if (!child.killed) {
86
- child.send({ type: 'chat', payload: { message, chatType: type, username } });
87
- }
88
- },
89
- sendLog: (message) => {
90
- this.appendLog(botId, message);
91
- }
92
- };
93
-
94
- // Регистрируем обработчики сообщений от child process
95
- this._setupChildProcessHandlers(child, botConfig);
96
-
97
- child.send({ type: 'start', config: fullBotConfig });
98
-
99
- await this.eventGraphManager.loadGraphsForBot(botId);
100
-
101
- this.telemetry.triggerHeartbeat();
102
- this.emitStatusUpdate(botId, 'starting');
103
-
104
- return child;
105
- }
106
-
107
- async stopBot(botId) {
108
- const child = this.processManager.getProcess(botId);
109
- if (child) {
110
- this.eventGraphManager.unloadGraphsForBot(botId);
111
-
112
- child.send({ type: 'stop' });
113
-
114
- // Принудительное завершение через 5 секунд
115
- setTimeout(() => {
116
- if (!child.killed) {
117
- this.logger.warn({ botId }, 'Принудительное завершение процесса');
118
- try {
119
- child.kill('SIGKILL');
120
- } catch (error) {
121
- this.logger.error({ botId, error }, 'Ошибка принудительного завершения');
122
- }
123
- }
124
- }, 5000);
125
-
126
- this.cache.clearBotCache(botId);
127
- return { success: true };
128
- }
129
- return { success: false, message: 'Бот не найден или уже остановлен' };
130
- }
131
-
132
- async restartBot(botId) {
133
- const botConfig = this.processManager.getProcess(botId)?.botConfig;
134
- if (!botConfig) {
135
- throw new Error('Bot configuration not found');
136
- }
137
-
138
- await this.stopBot(botId);
139
-
140
- // Ждём завершения процесса
141
- await new Promise(resolve => setTimeout(resolve, 1000));
142
-
143
- return this.startBot(botConfig);
144
- }
145
-
146
- _setupChildProcessHandlers(child, botConfig) {
147
- const botId = botConfig.id;
148
-
149
- child.on('message', async (message) => {
150
- try {
151
- switch (message.type) {
152
- case 'event':
153
- await this._handleEventMessage(botId, message);
154
- break;
155
- case 'plugin:data':
156
- this._handlePluginDataMessage(botId, message);
157
- break;
158
- case 'send_websocket_message':
159
- this._handleWebSocketMessage(message);
160
- break;
161
- case 'log':
162
- this.appendLog(botId, message.content);
163
- break;
164
- case 'status':
165
- this.emitStatusUpdate(botId, message.status);
166
- break;
167
- case 'bot_ready':
168
- this._handleBotReady(botId);
169
- break;
170
- case 'validate_and_run_command':
171
- if (this.commandExecutionService) {
172
- const botConfig = child.botConfig;
173
- if (botConfig) {
174
- await this.commandExecutionService.handleCommandValidation(botConfig, message);
175
- }
176
- }
177
- break;
178
- case 'request_user_action':
179
- await this._handleUserAction(botId, child, message);
180
- break;
181
- case 'get_player_list_response':
182
- this.processManager.resolvePlayerListRequest(message.requestId, message.payload.players);
183
- break;
184
- case 'get_nearby_entities_response':
185
- this.processManager.resolveNearbyEntitiesRequest(message.requestId, message.payload.entities);
186
- break;
187
- case 'execute_command_response':
188
- this.processManager.resolveCommandRequest(message.requestId, message.result, message.error);
189
- break;
190
- case 'register_command':
191
- await this._handleCommandRegistration(botId, message.commandConfig);
192
- break;
193
- }
194
- } catch (error) {
195
- this.appendLog(botId, `[SYSTEM-ERROR] Критическая ошибка в обработчике: ${error.stack}`);
196
- this.logger.error({ botId, error }, 'Критическая ошибка в обработчике сообщений');
197
- }
198
- });
199
-
200
- child.on('error', (err) => this.appendLog(botId, `[PROCESS FATAL] ${err.stack}`));
201
- child.stdout.on('data', (data) => console.log(data.toString()));
202
- child.stderr.on('data', (data) => this.appendLog(botId, `[STDERR] ${data.toString()}`));
203
-
204
- child.on('exit', (code, signal) => {
205
- this._handleProcessExit(botId, botConfig, code, signal);
206
- });
207
- }
208
-
209
- async _handleEventMessage(botId, message) {
210
- if (message.eventType === 'raw_message') {
211
- try {
212
- const { getIO } = require('../../real-time/socketHandler');
213
- const { broadcastToApiClients } = require('../../real-time/botApi');
214
- broadcastToApiClients(getIO(), botId, 'chat:raw_message', {
215
- raw_message: message.args.rawText || message.args.raw_message,
216
- json: message.args.json
217
- });
218
- } catch (e) { /* Socket.IO может быть не инициализирован */ }
219
- }
220
-
221
- if (this.eventGraphManager) {
222
- this.eventGraphManager.handleEvent(botId, message.eventType, message.args);
223
- }
224
- }
225
-
226
- _handlePluginDataMessage(botId, message) {
227
- const { plugin: pluginName, payload } = message;
228
- const pluginSubscribers = this.processManager.getPluginSubscribers(botId, pluginName);
229
-
230
- if (pluginSubscribers && pluginSubscribers.size > 0) {
231
- pluginSubscribers.forEach(socket => {
232
- socket.emit('plugin:ui:dataUpdate', payload);
233
- });
234
- }
235
- }
236
-
237
- _handleWebSocketMessage(message) {
238
- const { getIO } = require('../../real-time/socketHandler');
239
- const { botId, message: msg } = message.payload;
240
- getIO().to(`bot_${botId}`).emit('bot:message', { message: msg });
241
- }
242
-
243
- _handleBotReady(botId) {
244
- this.emitStatusUpdate(botId, 'running', 'Бот успешно подключился к серверу.');
245
- this.crashCounters.delete(botId);
246
-
247
- try {
248
- const { getIO } = require('../../real-time/socketHandler');
249
- const { broadcastBotStatus } = require('../../real-time/botApi');
250
- broadcastBotStatus(getIO(), botId, true);
251
- } catch (e) { /* Socket.IO может быть не инициализирован */ }
252
- }
253
-
254
- async _handleCommandRegistration(botId, commandConfig) {
255
- if (this.commandExecutionService) {
256
- await this.commandExecutionService.handleCommandRegistration(botId, commandConfig);
257
- // this.logger.debug({ botId, commandName: commandConfig.name }, 'Команда зарегистрирована');
258
- } else {
259
- this.logger.warn({ botId }, 'CommandExecutionService не доступен для регистрации команды');
260
- }
261
- }
262
-
263
- async _handleUserAction(botId, child, message) {
264
- const { requestId, payload } = message;
265
- const { targetUsername, action, data } = payload;
266
-
267
- try {
268
- const botConfig = child.botConfig;
269
- const user = await UserService.getUser(targetUsername, botId, botConfig);
270
- if (!user) throw new Error(`Пользователь ${targetUsername} не найден.`);
271
-
272
- let result;
273
-
274
- switch (action) {
275
- case 'addGroup':
276
- result = await user.addGroup(data.group);
277
- break;
278
- case 'removeGroup':
279
- result = await user.removeGroup(data.group);
280
- break;
281
- case 'getGroups':
282
- result = user.groups ? user.groups.map(g => g.group.name) : [];
283
- break;
284
- case 'getPermissions':
285
- result = Array.from(user.permissionsSet);
286
- break;
287
- case 'isBlacklisted':
288
- result = user.isBlacklisted;
289
- break;
290
- case 'setBlacklisted':
291
- result = await user.setBlacklist(data.value);
292
- break;
293
- default:
294
- throw new Error(`Неизвестное действие: ${action}`);
295
- }
296
-
297
- child.send({ type: 'user_action_response', requestId, payload: result });
298
- } catch (error) {
299
- this.logger.error({ botId, action, username: targetUsername, error }, 'Ошибка действия пользователя');
300
- child.send({ type: 'user_action_response', requestId, error: error.message });
301
- }
302
- }
303
-
304
- _handleProcessExit(botId, botConfig, code, signal) {
305
- this.processManager.remove(botId);
306
- this.resourceMonitor.clearResourceUsage(botId);
307
- this.cache.clearBotCache(botId);
308
-
309
- this.emitStatusUpdate(botId, 'stopped', `Процесс завершился с кодом ${code} (сигнал: ${signal || 'none'}).`);
310
-
311
- try {
312
- const { getIO } = require('../../real-time/socketHandler');
313
- const { broadcastBotStatus } = require('../../real-time/botApi');
314
- broadcastBotStatus(getIO(), botId, false);
315
- } catch (e) { /* Socket.IO может быть не инициализирован */ }
316
-
317
- // Автоперезапуск при критических ошибках
318
- if (code === 1) {
319
- this._handleCrashRestart(botId, botConfig);
320
- }
321
- }
322
-
323
- _handleCrashRestart(botId, botConfig) {
324
- const MAX_RESTART_ATTEMPTS = 5;
325
- const RESTART_COOLDOWN = 60000;
326
-
327
- const counter = this.crashCounters.get(botId) || { count: 0, firstCrash: Date.now() };
328
- const timeSinceFirstCrash = Date.now() - counter.firstCrash;
329
-
330
- if (timeSinceFirstCrash > RESTART_COOLDOWN) {
331
- counter.count = 0;
332
- counter.firstCrash = Date.now();
333
- }
334
-
335
- counter.count++;
336
- this.crashCounters.set(botId, counter);
337
-
338
- if (counter.count >= MAX_RESTART_ATTEMPTS) {
339
- this.logger.warn({ botId, attempts: counter.count }, 'Автоперезапуск остановлен');
340
- this.appendLog(botId, `[SYSTEM] Обнаружено ${counter.count} критических ошибок подряд.`);
341
- this.appendLog(botId, `[SYSTEM] Исправьте проблему и запустите бота вручную.`);
342
- this.crashCounters.delete(botId);
343
- return;
344
- }
345
-
346
- this.logger.info({ botId, attempt: counter.count, max: MAX_RESTART_ATTEMPTS }, 'Перезапуск через 5 секунд');
347
- this.appendLog(botId, `[SYSTEM] Обнаружена критическая ошибка, перезапуск через 5 секунд... (попытка ${counter.count}/${MAX_RESTART_ATTEMPTS})`);
348
-
349
- setTimeout(() => {
350
- this.logger.info({ botId }, 'Выполняется перезапуск');
351
- this.startBot(botConfig);
352
- }, 5000);
353
- }
354
-
355
- async loadConfigForBot(botId) {
356
- this.logger.info({ botId }, 'Загрузка конфигурации');
357
-
358
- try {
359
- const [commands, permissions] = await Promise.all([
360
- this.commandRepository.findByBotId(botId),
361
- this.permissionRepository.findByBotId(botId),
362
- ]);
363
-
364
- const config = {
365
- commands: new Map(commands.map(cmd => [cmd.name, cmd])),
366
- permissionsById: new Map(permissions.map(p => [p.id, p])),
367
- commandAliases: new Map()
368
- };
369
-
370
- for (const cmd of commands) {
371
- const aliases = JSON.parse(cmd.aliases || '[]');
372
- for (const alias of aliases) {
373
- config.commandAliases.set(alias, cmd.name);
374
- }
375
- }
376
-
377
- this.cache.setBotConfig(botId, config);
378
- this.logger.info({ botId }, 'Конфигурация загружена');
379
- return config;
380
- } catch (error) {
381
- this.logger.error({ botId, error }, 'Ошибка загрузки конфигурации');
382
- throw new Error(`Failed to load/cache bot configuration for botId ${botId}: ${error.message}`);
383
- }
384
- }
385
-
386
- invalidateConfigCache(botId) {
387
- if (this.cache.getBotConfig(botId)) {
388
- this.cache.deleteBotConfig(botId);
389
- this.logger.debug({ botId }, 'Кеш конфигурации инвалидирован');
390
- }
391
- }
392
-
393
- reloadBotConfigInRealTime(botId) {
394
- const { getIO } = require('../../real-time/socketHandler');
395
- this.invalidateConfigCache(botId);
396
-
397
- if (this.processManager.sendMessage(botId, { type: 'config:reload' })) {
398
- this.logger.info({ botId }, 'Отправлен config:reload');
399
- getIO().emit('bot:config_reloaded', { botId });
400
- }
401
- }
402
-
403
- async _syncSystemPermissions(botId) {
404
- const systemPermissions = [
405
- { name: "admin.*", description: "Все права администратора", owner: "system" },
406
- { name: "admin.cooldown.bypass", description: "Обход кулдауна для админ-команд", owner: "system" },
407
- { name: "user.*", description: "Все права обычного пользователя", owner: "system" },
408
- { name: "user.say", description: "Доступ к простым командам", owner: "system" },
409
- { name: "user.cooldown.bypass", description: "Обход кулдауна для юзер-команд", owner: "system" },
410
- ];
411
-
412
- this.logger.debug({ botId }, 'Синхронизация системных прав');
413
-
414
- try {
415
- for (const perm of systemPermissions) {
416
- const existing = await this.permissionRepository.findByName(botId, perm.name);
417
- if (existing) {
418
- // Обновляем описание если изменилось
419
- if (existing.description !== perm.description) {
420
- await this.permissionRepository.update(existing.id, {
421
- description: perm.description
422
- });
423
- }
424
- } else {
425
- // Создаем новое системное право
426
- await this.permissionRepository.create({
427
- botId,
428
- name: perm.name,
429
- description: perm.description,
430
- owner: perm.owner,
431
- });
432
- }
433
- }
434
- this.logger.debug({ botId }, 'Системные права синхронизированы');
435
- } catch (error) {
436
- this.logger.error({ botId, error }, 'Ошибка синхронизации системных прав');
437
- }
438
- }
439
-
440
- appendLog(botId, logContent) {
441
- const { getIO } = require('../../real-time/socketHandler');
442
- const logEntry = {
443
- id: Date.now() + Math.random(),
444
- content: logContent,
445
- };
446
-
447
- const currentLogs = this.logCache.get(botId) || [];
448
- const newLogs = [...currentLogs.slice(-199), logEntry];
449
- this.logCache.set(botId, newLogs);
450
-
451
- getIO().emit('bot:log', { botId, log: logEntry });
452
- }
453
-
454
- getBotLogs(botId) {
455
- return this.logCache.get(botId) || [];
456
- }
457
-
458
- emitStatusUpdate(botId, status, message = null) {
459
- const { getIO } = require('../../real-time/socketHandler');
460
- if (message) this.appendLog(botId, `[SYSTEM] ${message}`);
461
- getIO().emit('bot:status', { botId, status, message });
462
- }
463
-
464
- getFullState() {
465
- const processes = this.processManager.getAllProcesses();
466
- const statuses = {};
467
- for (const [id, child] of processes.entries()) {
468
- statuses[id] = child.killed ? 'stopped' : 'running';
469
- }
470
-
471
- const logs = {};
472
- for (const [botId, logArray] of this.logCache.entries()) {
473
- logs[botId] = logArray;
474
- }
475
-
476
- return { statuses, logs };
477
- }
478
-
479
- isBotRunning(botId) {
480
- return this.processManager.isRunning(botId);
481
- }
482
-
483
- sendMessageToBot(botId, message, chatType = 'command', username = null) {
484
- const child = this.processManager.getProcess(botId);
485
- if (child && child.api) {
486
- child.api.sendMessage(chatType, message, username);
487
- return { success: true };
488
- }
489
- return { success: false, message: 'Бот не найден или не запущен' };
490
- }
491
-
492
- lookAt(botId, position) {
493
- if (this.processManager.sendMessage(botId, { type: 'action', name: 'lookAt', payload: { position } })) {
494
- return { success: true };
495
- }
496
- this.logger.error({ botId }, 'Не удалось выполнить lookAt');
497
- return { success: false };
498
- }
499
-
500
- async reloadPlugins(botId) {
501
- if (this.processManager.sendMessage(botId, { type: 'plugins:reload' })) {
502
- this.logger.info({ botId }, 'Отправлен plugins:reload');
503
- const { getIO } = require('../../real-time/socketHandler');
504
- getIO().emit('bot:plugins_reloaded', { botId });
505
- return { success: true, message: 'Команда на перезагрузку плагинов отправлена.' };
506
- }
507
- return { success: false, message: 'Бот не запущен.' };
508
- }
509
-
510
- sendServerCommandToBot(botId, command) {
511
- this.processManager.sendMessage(botId, { type: 'server_command', payload: { command } });
512
- }
513
-
514
- async getPlayerList(botId) {
515
- const PLAYER_LIST_CACHE_TTL = 2000;
516
-
517
- if (!this.processManager.isRunning(botId)) {
518
- return [];
519
- }
520
-
521
- const cachedPlayers = this.cache.getPlayerList(botId);
522
- if (cachedPlayers) {
523
- return cachedPlayers;
524
- }
525
-
526
- return new Promise((resolve) => {
527
- const { v4: uuidv4 } = require('uuid');
528
- const requestId = uuidv4();
529
-
530
- const timeout = setTimeout(() => {
531
- resolve([]);
532
- }, 5000);
533
-
534
- this.processManager.addPlayerListRequest(requestId, {
535
- resolve: (playerList) => {
536
- clearTimeout(timeout);
537
- this.cache.setPlayerList(botId, playerList);
538
- resolve(playerList);
539
- },
540
- timeout
541
- });
542
-
543
- this.processManager.sendMessage(botId, { type: 'system:get_player_list', requestId });
544
- });
545
- }
546
-
547
- async getNearbyEntities(botId, position = null, radius = 32) {
548
- if (!this.processManager.isRunning(botId)) {
549
- return [];
550
- }
551
-
552
- return new Promise((resolve) => {
553
- const { v4: uuidv4 } = require('uuid');
554
- const requestId = uuidv4();
555
-
556
- const timeout = setTimeout(() => {
557
- resolve([]);
558
- }, 5000);
559
-
560
- this.processManager.addNearbyEntitiesRequest(requestId, {
561
- resolve: (entities) => {
562
- clearTimeout(timeout);
563
- resolve(entities);
564
- },
565
- timeout
566
- });
567
-
568
- this.processManager.sendMessage(botId, {
569
- type: 'system:get_nearby_entities',
570
- requestId,
571
- payload: { position, radius }
572
- });
573
- });
574
- }
575
-
576
- invalidateUserCache(botId, username) {
577
- UserService.clearCache(username, botId);
578
- this.processManager.sendMessage(botId, { type: 'invalidate_user_cache', username });
579
- return { success: true };
580
- }
581
-
582
- invalidateAllUserCache(botId) {
583
- for (const [cacheKey] of UserService.cache.entries()) {
584
- if (cacheKey.startsWith(`${botId}:`)) {
585
- UserService.cache.delete(cacheKey);
586
- }
587
- }
588
-
589
- this.logger.info({ botId }, 'Кеш пользователей очищен');
590
- this.processManager.sendMessage(botId, { type: 'invalidate_all_user_cache' });
591
-
592
- return { success: true };
593
- }
594
- }
595
-
596
- module.exports = BotLifecycleService;
1
+ const DependencyService = require('../DependencyService');
2
+ const { decrypt } = require('../utils/crypto');
3
+ const UserService = require('../UserService');
4
+ const PermissionManager = require('../PermissionManager');
5
+
6
+ class BotLifecycleService {
7
+ constructor({
8
+ botRepository,
9
+ pluginRepository,
10
+ commandRepository,
11
+ permissionRepository,
12
+ botProcessManager,
13
+ cacheManager,
14
+ resourceMonitorService,
15
+ telemetryService,
16
+ eventGraphManager,
17
+ commandExecutionService,
18
+ logger
19
+ }) {
20
+ this.botRepository = botRepository;
21
+ this.pluginRepository = pluginRepository;
22
+ this.commandRepository = commandRepository;
23
+ this.permissionRepository = permissionRepository;
24
+ this.processManager = botProcessManager;
25
+ this.cache = cacheManager;
26
+ this.resourceMonitor = resourceMonitorService;
27
+ this.telemetry = telemetryService;
28
+ this.eventGraphManager = eventGraphManager;
29
+ this.commandExecutionService = commandExecutionService;
30
+ this.logger = logger;
31
+
32
+ this.logCache = new Map();
33
+ this.crashCounters = new Map();
34
+ }
35
+
36
+ async startBot(botConfig) {
37
+ const botId = botConfig.id;
38
+
39
+ if (this.processManager.isRunning(botId)) {
40
+ this.appendLog(botId, `[SYSTEM-ERROR] Попытка повторного запуска. Запуск отменен.`);
41
+ return { success: false, message: 'Бот уже запущен или запускается.' };
42
+ }
43
+
44
+ await this._syncSystemPermissions(botId);
45
+ await this.loadConfigForBot(botId);
46
+ this.logCache.set(botId, []);
47
+ this.emitStatusUpdate(botId, 'starting', '');
48
+
49
+ const allPluginsForBot = await this.pluginRepository.findEnabledByBotId(botId);
50
+ const { sortedPlugins, hasCriticalIssues, pluginInfo } = DependencyService.resolveDependencies(allPluginsForBot, allPluginsForBot);
51
+
52
+ if (hasCriticalIssues) {
53
+ this.appendLog(botId, '[DependencyManager] Обнаружены критические проблемы с зависимостями, запуск отменен.');
54
+
55
+ const criticalIssueTypes = new Set(['missing_dependency', 'version_mismatch', 'circular_dependency']);
56
+
57
+ for (const pluginId in pluginInfo) {
58
+ const info = pluginInfo[pluginId];
59
+ if (info.issues.length === 0) continue;
60
+
61
+ const criticalIssues = info.issues.filter(issue => criticalIssueTypes.has(issue.type));
62
+
63
+ if (criticalIssues.length > 0) {
64
+ this.appendLog(botId, `* Плагин "${info.name}":`);
65
+ for (const issue of criticalIssues) {
66
+ this.appendLog(botId, ` - ${issue.message}`);
67
+ }
68
+ }
69
+ }
70
+
71
+ this.emitStatusUpdate(botId, 'stopped', 'Ошибка зависимостей плагинов.');
72
+ return { success: false, message: 'Критические ошибки в зависимостях плагинов.' };
73
+ }
74
+
75
+ const decryptedConfig = { ...botConfig };
76
+
77
+ if (decryptedConfig.proxy) {
78
+ decryptedConfig.proxyHost = decryptedConfig.proxy.host;
79
+ decryptedConfig.proxyPort = decryptedConfig.proxy.port;
80
+ decryptedConfig.proxyUsername = decryptedConfig.proxy.username;
81
+ decryptedConfig.proxyPassword = decryptedConfig.proxy.password;
82
+ }
83
+
84
+ if (decryptedConfig.password) decryptedConfig.password = decrypt(decryptedConfig.password);
85
+ if (decryptedConfig.proxyPassword) decryptedConfig.proxyPassword = decrypt(decryptedConfig.proxyPassword);
86
+ if (decryptedConfig.proxyUsername) decryptedConfig.proxyUsername = decryptedConfig.proxyUsername.trim();
87
+
88
+ const fullBotConfig = { ...decryptedConfig, plugins: sortedPlugins };
89
+
90
+ const child = await this.processManager.spawn(botConfig, fullBotConfig);
91
+
92
+ child.api = {
93
+ sendMessage: (type, message, username) => {
94
+ if (!child.killed) {
95
+ child.send({ type: 'chat', payload: { message, chatType: type, username } });
96
+ }
97
+ },
98
+ sendLog: (message) => {
99
+ this.appendLog(botId, message);
100
+ }
101
+ };
102
+
103
+ // Регистрируем обработчики сообщений от child process
104
+ this._setupChildProcessHandlers(child, botConfig);
105
+
106
+ child.send({ type: 'start', config: fullBotConfig });
107
+
108
+ await this.eventGraphManager.loadGraphsForBot(botId);
109
+
110
+ this.telemetry.triggerHeartbeat();
111
+ this.emitStatusUpdate(botId, 'starting');
112
+
113
+ return child;
114
+ }
115
+
116
+ async stopBot(botId) {
117
+ const child = this.processManager.getProcess(botId);
118
+ if (child) {
119
+ this.eventGraphManager.unloadGraphsForBot(botId);
120
+
121
+ // Очищаем traces для этого бота
122
+ const { getTraceCollector } = require('./TraceCollectorService');
123
+ const traceCollector = getTraceCollector();
124
+ traceCollector.clearForBot(botId);
125
+
126
+ child.send({ type: 'stop' });
127
+
128
+ // Принудительное завершение через 5 секунд
129
+ setTimeout(() => {
130
+ if (!child.killed) {
131
+ this.logger.warn({ botId }, 'Принудительное завершение процесса');
132
+ try {
133
+ child.kill('SIGKILL');
134
+ } catch (error) {
135
+ this.logger.error({ botId, error }, 'Ошибка принудительного завершения');
136
+ }
137
+ }
138
+ }, 5000);
139
+
140
+ this.cache.clearBotCache(botId);
141
+ return { success: true };
142
+ }
143
+ return { success: false, message: 'Бот не найден или уже остановлен' };
144
+ }
145
+
146
+ getChildProcess(botId) {
147
+ return this.processManager.getProcess(botId);
148
+ }
149
+
150
+ async restartBot(botId) {
151
+ const botConfig = this.processManager.getProcess(botId)?.botConfig;
152
+ if (!botConfig) {
153
+ throw new Error('Bot configuration not found');
154
+ }
155
+
156
+ await this.stopBot(botId);
157
+
158
+ // Ждём завершения процесса
159
+ await new Promise(resolve => setTimeout(resolve, 1000));
160
+
161
+ return this.startBot(botConfig);
162
+ }
163
+
164
+ _setupChildProcessHandlers(child, botConfig) {
165
+ const botId = botConfig.id;
166
+
167
+ child.on('message', async (message) => {
168
+ try {
169
+ switch (message.type) {
170
+ case 'event':
171
+ await this._handleEventMessage(botId, message);
172
+ break;
173
+ case 'plugin:data':
174
+ this._handlePluginDataMessage(botId, message);
175
+ break;
176
+ case 'send_websocket_message':
177
+ this._handleWebSocketMessage(message);
178
+ break;
179
+ case 'log':
180
+ this.appendLog(botId, message.content);
181
+ break;
182
+ case 'plugin-log':
183
+ this._handlePluginLog(message.log);
184
+ break;
185
+ case 'status':
186
+ this.emitStatusUpdate(botId, message.status);
187
+ break;
188
+ case 'bot_ready':
189
+ this._handleBotReady(botId);
190
+ break;
191
+ case 'validate_and_run_command':
192
+ if (this.commandExecutionService) {
193
+ const botConfig = child.botConfig;
194
+ if (botConfig) {
195
+ await this.commandExecutionService.handleCommandValidation(botConfig, message);
196
+ }
197
+ }
198
+ break;
199
+ case 'request_user_action':
200
+ await this._handleUserAction(botId, child, message);
201
+ break;
202
+ case 'get_player_list_response':
203
+ this.processManager.resolvePlayerListRequest(message.requestId, message.payload.players);
204
+ break;
205
+ case 'get_nearby_entities_response':
206
+ this.processManager.resolveNearbyEntitiesRequest(message.requestId, message.payload.entities);
207
+ break;
208
+ case 'execute_command_response':
209
+ this.processManager.resolveCommandRequest(message.requestId, message.result, message.error);
210
+ break;
211
+ case 'register_command':
212
+ await this._handleCommandRegistration(botId, message.commandConfig);
213
+ break;
214
+ case 'register_permissions':
215
+ await this._handlePermissionsRegistration(botId, message);
216
+ break;
217
+ case 'register_group':
218
+ await this._handleGroupRegistration(botId, message);
219
+ break;
220
+ case 'add_permissions_to_group':
221
+ await this._handleAddPermissionsToGroup(botId, message);
222
+ break;
223
+ case 'trace:completed':
224
+ await this._handleTraceCompleted(botId, message.trace);
225
+ break;
226
+ case 'debug:check_breakpoint':
227
+ await this._handleDebugBreakpointCheck(botId, child, message);
228
+ break;
229
+ case 'debug:check_step_mode':
230
+ await this._handleDebugStepModeCheck(botId, child, message);
231
+ break;
232
+ }
233
+ } catch (error) {
234
+ this.appendLog(botId, `[SYSTEM-ERROR] Критическая ошибка в обработчике: ${error.stack}`);
235
+ this.logger.error({ botId, error }, 'Критическая ошибка в обработчике сообщений');
236
+ }
237
+ });
238
+
239
+ child.on('error', (err) => this.appendLog(botId, `[PROCESS FATAL] ${err.stack}`));
240
+ child.stdout.on('data', (data) => console.log(data.toString()));
241
+ child.stderr.on('data', (data) => this.appendLog(botId, `[STDERR] ${data.toString()}`));
242
+
243
+ child.on('exit', (code, signal) => {
244
+ this._handleProcessExit(botId, botConfig, code, signal);
245
+ });
246
+ }
247
+
248
+ async _handleEventMessage(botId, message) {
249
+ if (message.eventType === 'raw_message') {
250
+ try {
251
+ const { getIOSafe } = require('../../real-time/socketHandler');
252
+ const { broadcastToApiClients } = require('../../real-time/botApi');
253
+ broadcastToApiClients(getIOSafe(), botId, 'chat:raw_message', {
254
+ raw_message: message.args.rawText || message.args.raw_message,
255
+ json: message.args.json
256
+ });
257
+ } catch (e) { /* Socket.IO может быть не инициализирован */ }
258
+ }
259
+
260
+ try {
261
+ const { broadcastToPanelNamespace } = require('../../real-time/panelNamespace');
262
+ broadcastToPanelNamespace(botId, 'bot:event', {
263
+ botId,
264
+ eventType: message.eventType,
265
+ data: message.args || {},
266
+ timestamp: new Date().toISOString()
267
+ });
268
+ } catch (e) { /* Socket.IO может быть не инициализирован */ }
269
+
270
+ if (this.eventGraphManager) {
271
+ this.eventGraphManager.handleEvent(botId, message.eventType, message.args);
272
+ }
273
+ }
274
+
275
+ _handlePluginDataMessage(botId, message) {
276
+ const { plugin: pluginName, payload } = message;
277
+ const pluginSubscribers = this.processManager.getPluginSubscribers(botId, pluginName);
278
+
279
+ if (pluginSubscribers && pluginSubscribers.size > 0) {
280
+ pluginSubscribers.forEach(socket => {
281
+ socket.emit('plugin:ui:dataUpdate', payload);
282
+ });
283
+ }
284
+ }
285
+
286
+ _handlePluginLog(logData) {
287
+ const { getIOSafe, addPluginLogToBuffer } = require('../../real-time/socketHandler');
288
+ const { botId, pluginName } = logData;
289
+
290
+ // Добавляем лог в буфер
291
+ addPluginLogToBuffer(botId, pluginName, logData);
292
+
293
+ // Отправляем через Socket.IO в комнату плагина
294
+ const io = getIOSafe();
295
+ if (io) {
296
+ const room = `plugin-logs:${botId}:${pluginName}`;
297
+ io.to(room).emit('plugin-log', logData);
298
+ }
299
+ }
300
+
301
+ _handleWebSocketMessage(message) {
302
+ const { getIOSafe } = require('../../real-time/socketHandler');
303
+ const { botId, message: msg } = message.payload;
304
+ getIOSafe().to(`bot_${botId}`).emit('bot:message', { message: msg });
305
+ }
306
+
307
+ _handleBotReady(botId) {
308
+ this.emitStatusUpdate(botId, 'running', 'Бот успешно подключился к серверу.');
309
+ this.crashCounters.delete(botId);
310
+
311
+ try {
312
+ const { getIOSafe } = require('../../real-time/socketHandler');
313
+ const { broadcastBotStatus } = require('../../real-time/botApi');
314
+ broadcastBotStatus(getIOSafe(), botId, true);
315
+ } catch (e) { /* Socket.IO может быть не инициализирован */ }
316
+
317
+ // Триггерим событие запуска бота
318
+ if (this.eventGraphManager) {
319
+ this.eventGraphManager.handleEvent(botId, 'botStartup', {});
320
+ }
321
+ }
322
+
323
+ async _handleCommandRegistration(botId, commandConfig) {
324
+ if (this.commandExecutionService) {
325
+ await this.commandExecutionService.handleCommandRegistration(botId, commandConfig);
326
+ // this.logger.debug({ botId, commandName: commandConfig.name }, 'Команда зарегистрирована');
327
+ } else {
328
+ this.logger.warn({ botId }, 'CommandExecutionService не доступен для регистрации команды');
329
+ }
330
+ }
331
+
332
+ async _handlePermissionsRegistration(botId, message) {
333
+ try {
334
+ await PermissionManager.registerPermissions(botId, message.permissions);
335
+ this.logger.debug({ botId, count: message.permissions.length }, 'Права зарегистрированы');
336
+ } catch (error) {
337
+ this.logger.error({ botId, error }, 'Ошибка регистрации прав');
338
+ }
339
+ }
340
+
341
+ async _handleGroupRegistration(botId, message) {
342
+ try {
343
+ await PermissionManager.registerGroup(botId, message.groupConfig);
344
+ this.logger.debug({ botId, groupName: message.groupConfig.name }, 'Группа зарегистрирована');
345
+ } catch (error) {
346
+ this.logger.error({ botId, error }, 'Ошибка регистрации группы');
347
+ }
348
+ }
349
+
350
+ async _handleAddPermissionsToGroup(botId, message) {
351
+ try {
352
+ await PermissionManager.addPermissionsToGroup(botId, message.groupName, message.permissionNames);
353
+ this.logger.debug({ botId, groupName: message.groupName, count: message.permissionNames.length }, 'Права добавлены в группу');
354
+ } catch (error) {
355
+ this.logger.error({ botId, error }, 'Ошибка добавления прав в группу');
356
+ }
357
+ }
358
+
359
+ async _handleUserAction(botId, child, message) {
360
+ const { requestId, payload } = message;
361
+ const { targetUsername, action, data } = payload;
362
+
363
+ try {
364
+ const botConfig = child.botConfig;
365
+ const user = await UserService.getUser(targetUsername, botId, botConfig);
366
+ if (!user) throw new Error(`Пользователь ${targetUsername} не найден.`);
367
+
368
+ let result;
369
+
370
+ switch (action) {
371
+ case 'addGroup':
372
+ result = await user.addGroup(data.group);
373
+ break;
374
+ case 'removeGroup':
375
+ result = await user.removeGroup(data.group);
376
+ break;
377
+ case 'getGroups':
378
+ result = user.groups ? user.groups.map(g => g.group.name) : [];
379
+ break;
380
+ case 'getPermissions':
381
+ result = Array.from(user.permissionsSet);
382
+ break;
383
+ case 'isBlacklisted':
384
+ result = user.isBlacklisted;
385
+ break;
386
+ case 'setBlacklisted':
387
+ result = await user.setBlacklist(data.value);
388
+ break;
389
+ default:
390
+ throw new Error(`Неизвестное действие: ${action}`);
391
+ }
392
+
393
+ child.send({ type: 'user_action_response', requestId, payload: result });
394
+ } catch (error) {
395
+ this.logger.error({ botId, action, username: targetUsername, error }, 'Ошибка действия пользователя');
396
+ child.send({ type: 'user_action_response', requestId, error: error.message });
397
+ }
398
+ }
399
+
400
+ _handleProcessExit(botId, botConfig, code, signal) {
401
+ this.processManager.remove(botId);
402
+ this.resourceMonitor.clearResourceUsage(botId);
403
+ this.cache.clearBotCache(botId);
404
+
405
+ this.emitStatusUpdate(botId, 'stopped', `Процесс завершился с кодом ${code} (сигнал: ${signal || 'none'}).`);
406
+
407
+ try {
408
+ const { getIOSafe } = require('../../real-time/socketHandler');
409
+ const { broadcastBotStatus } = require('../../real-time/botApi');
410
+ broadcastBotStatus(getIOSafe(), botId, false);
411
+ } catch (e) { /* Socket.IO может быть не инициализирован */ }
412
+
413
+ // Автоперезапуск при критических ошибках
414
+ if (code === 1) {
415
+ this._handleCrashRestart(botId, botConfig);
416
+ }
417
+ }
418
+
419
+ _handleCrashRestart(botId, botConfig) {
420
+ const MAX_RESTART_ATTEMPTS = 5;
421
+ const RESTART_COOLDOWN = 60000;
422
+
423
+ const counter = this.crashCounters.get(botId) || { count: 0, firstCrash: Date.now() };
424
+ const timeSinceFirstCrash = Date.now() - counter.firstCrash;
425
+
426
+ if (timeSinceFirstCrash > RESTART_COOLDOWN) {
427
+ counter.count = 0;
428
+ counter.firstCrash = Date.now();
429
+ }
430
+
431
+ counter.count++;
432
+ this.crashCounters.set(botId, counter);
433
+
434
+ if (counter.count >= MAX_RESTART_ATTEMPTS) {
435
+ this.logger.warn({ botId, attempts: counter.count }, 'Автоперезапуск остановлен');
436
+ this.appendLog(botId, `[SYSTEM] Обнаружено ${counter.count} критических ошибок подряд.`);
437
+ this.appendLog(botId, `[SYSTEM] Исправьте проблему и запустите бота вручную.`);
438
+ this.crashCounters.delete(botId);
439
+ return;
440
+ }
441
+
442
+ this.logger.info({ botId, attempt: counter.count, max: MAX_RESTART_ATTEMPTS }, 'Перезапуск через 5 секунд');
443
+ this.appendLog(botId, `[SYSTEM] Обнаружена критическая ошибка, перезапуск через 5 секунд... (попытка ${counter.count}/${MAX_RESTART_ATTEMPTS})`);
444
+
445
+ setTimeout(() => {
446
+ this.logger.info({ botId }, 'Выполняется перезапуск');
447
+ this.startBot(botConfig);
448
+ }, 5000);
449
+ }
450
+
451
+ async loadConfigForBot(botId) {
452
+ this.logger.info({ botId }, 'Загрузка конфигурации');
453
+
454
+ try {
455
+ const [commands, permissions] = await Promise.all([
456
+ this.commandRepository.findByBotId(botId),
457
+ this.permissionRepository.findByBotId(botId),
458
+ ]);
459
+
460
+ const config = {
461
+ commands: new Map(commands.map(cmd => [cmd.name, cmd])),
462
+ permissionsById: new Map(permissions.map(p => [p.id, p])),
463
+ commandAliases: new Map()
464
+ };
465
+
466
+ for (const cmd of commands) {
467
+ const aliases = JSON.parse(cmd.aliases || '[]');
468
+ for (const alias of aliases) {
469
+ config.commandAliases.set(alias, cmd.name);
470
+ }
471
+ }
472
+
473
+ this.cache.setBotConfig(botId, config);
474
+ this.logger.info({ botId }, 'Конфигурация загружена');
475
+ return config;
476
+ } catch (error) {
477
+ this.logger.error({ botId, error }, 'Ошибка загрузки конфигурации');
478
+ throw new Error(`Failed to load/cache bot configuration for botId ${botId}: ${error.message}`);
479
+ }
480
+ }
481
+
482
+ invalidateConfigCache(botId) {
483
+ if (this.cache.getBotConfig(botId)) {
484
+ this.cache.deleteBotConfig(botId);
485
+ this.logger.debug({ botId }, 'Кеш конфигурации инвалидирован');
486
+ }
487
+ }
488
+
489
+ reloadBotConfigInRealTime(botId) {
490
+ const { getIOSafe } = require('../../real-time/socketHandler');
491
+ this.invalidateConfigCache(botId);
492
+
493
+ if (this.processManager.sendMessage(botId, { type: 'config:reload' })) {
494
+ this.logger.info({ botId }, 'Отправлен config:reload');
495
+ getIOSafe().emit('bot:config_reloaded', { botId });
496
+ }
497
+ }
498
+
499
+ async _syncSystemPermissions(botId) {
500
+ const systemPermissions = [
501
+ { name: "admin.*", description: "Все права администратора", owner: "system" },
502
+ { name: "admin.cooldown.bypass", description: "Обход кулдауна для админ-команд", owner: "system" },
503
+ { name: "user.*", description: "Все права обычного пользователя", owner: "system" },
504
+ { name: "user.say", description: "Доступ к простым командам", owner: "system" },
505
+ { name: "user.cooldown.bypass", description: "Обход кулдауна для юзер-команд", owner: "system" },
506
+ ];
507
+
508
+ this.logger.debug({ botId }, 'Синхронизация системных прав');
509
+
510
+ try {
511
+ for (const perm of systemPermissions) {
512
+ const existing = await this.permissionRepository.findByName(botId, perm.name);
513
+ if (existing) {
514
+ // Обновляем описание если изменилось
515
+ if (existing.description !== perm.description) {
516
+ await this.permissionRepository.update(existing.id, {
517
+ description: perm.description
518
+ });
519
+ }
520
+ } else {
521
+ // Создаем новое системное право
522
+ await this.permissionRepository.create({
523
+ botId,
524
+ name: perm.name,
525
+ description: perm.description,
526
+ owner: perm.owner,
527
+ });
528
+ }
529
+ }
530
+ this.logger.debug({ botId }, 'Системные права синхронизированы');
531
+ } catch (error) {
532
+ this.logger.error({ botId, error }, 'Ошибка синхронизации системных прав');
533
+ }
534
+ }
535
+
536
+ appendLog(botId, logContent) {
537
+ const { getIOSafe } = require('../../real-time/socketHandler');
538
+ const logEntry = {
539
+ id: Date.now() + Math.random(),
540
+ content: logContent,
541
+ };
542
+
543
+ const currentLogs = this.logCache.get(botId) || [];
544
+ const newLogs = [...currentLogs.slice(-199), logEntry];
545
+ this.logCache.set(botId, newLogs);
546
+
547
+ getIOSafe().emit('bot:log', { botId, log: logEntry });
548
+ }
549
+
550
+ getBotLogs(botId) {
551
+ return this.logCache.get(botId) || [];
552
+ }
553
+
554
+ emitStatusUpdate(botId, status, message = null) {
555
+ const { getIOSafe, broadcastToPanelNamespace } = require('../../real-time/socketHandler');
556
+ if (message) this.appendLog(botId, `[SYSTEM] ${message}`);
557
+
558
+ getIOSafe().emit('bot:status', { botId, status, message });
559
+
560
+ broadcastToPanelNamespace(getIOSafe(), 'bots:status', {
561
+ botId,
562
+ status,
563
+ message,
564
+ timestamp: new Date().toISOString()
565
+ });
566
+ }
567
+
568
+ getFullState() {
569
+ const processes = this.processManager.getAllProcesses();
570
+ const statuses = {};
571
+ for (const [id, child] of processes.entries()) {
572
+ statuses[id] = child.killed ? 'stopped' : 'running';
573
+ }
574
+
575
+ const logs = {};
576
+ for (const [botId, logArray] of this.logCache.entries()) {
577
+ logs[botId] = logArray;
578
+ }
579
+
580
+ return { statuses, logs };
581
+ }
582
+
583
+ isBotRunning(botId) {
584
+ return this.processManager.isRunning(botId);
585
+ }
586
+
587
+ sendMessageToBot(botId, message, chatType = 'command', username = null) {
588
+ const child = this.processManager.getProcess(botId);
589
+ if (child && child.api) {
590
+ child.api.sendMessage(chatType, message, username);
591
+ return { success: true };
592
+ }
593
+ return { success: false, message: 'Бот не найден или не запущен' };
594
+ }
595
+
596
+ lookAt(botId, position) {
597
+ if (this.processManager.sendMessage(botId, { type: 'action', name: 'lookAt', payload: { position } })) {
598
+ return { success: true };
599
+ }
600
+ this.logger.error({ botId }, 'Не удалось выполнить lookAt');
601
+ return { success: false };
602
+ }
603
+
604
+ async reloadPlugins(botId) {
605
+ if (this.processManager.sendMessage(botId, { type: 'plugins:reload' })) {
606
+ this.logger.info({ botId }, 'Отправлен plugins:reload');
607
+ const { getIOSafe } = require('../../real-time/socketHandler');
608
+ getIOSafe().emit('bot:plugins_reloaded', { botId });
609
+ return { success: true, message: 'Команда на перезагрузку плагинов отправлена.' };
610
+ }
611
+ return { success: false, message: 'Бот не запущен.' };
612
+ }
613
+
614
+ sendServerCommandToBot(botId, command) {
615
+ this.processManager.sendMessage(botId, { type: 'server_command', payload: { command } });
616
+ }
617
+
618
+ async getPlayerList(botId) {
619
+ const PLAYER_LIST_CACHE_TTL = 2000;
620
+
621
+ if (!this.processManager.isRunning(botId)) {
622
+ return [];
623
+ }
624
+
625
+ const cachedPlayers = this.cache.getPlayerList(botId);
626
+ if (cachedPlayers) {
627
+ return cachedPlayers;
628
+ }
629
+
630
+ return new Promise((resolve) => {
631
+ const { v4: uuidv4 } = require('uuid');
632
+ const requestId = uuidv4();
633
+
634
+ const timeout = setTimeout(() => {
635
+ resolve([]);
636
+ }, 5000);
637
+
638
+ this.processManager.addPlayerListRequest(requestId, {
639
+ resolve: (playerList) => {
640
+ clearTimeout(timeout);
641
+ this.cache.setPlayerList(botId, playerList);
642
+ resolve(playerList);
643
+ },
644
+ timeout
645
+ });
646
+
647
+ this.processManager.sendMessage(botId, { type: 'system:get_player_list', requestId });
648
+ });
649
+ }
650
+
651
+ async getNearbyEntities(botId, position = null, radius = 32) {
652
+ if (!this.processManager.isRunning(botId)) {
653
+ return [];
654
+ }
655
+
656
+ return new Promise((resolve) => {
657
+ const { v4: uuidv4 } = require('uuid');
658
+ const requestId = uuidv4();
659
+
660
+ const timeout = setTimeout(() => {
661
+ resolve([]);
662
+ }, 5000);
663
+
664
+ this.processManager.addNearbyEntitiesRequest(requestId, {
665
+ resolve: (entities) => {
666
+ clearTimeout(timeout);
667
+ resolve(entities);
668
+ },
669
+ timeout
670
+ });
671
+
672
+ this.processManager.sendMessage(botId, {
673
+ type: 'system:get_nearby_entities',
674
+ requestId,
675
+ payload: { position, radius }
676
+ });
677
+ });
678
+ }
679
+
680
+ invalidateUserCache(botId, username) {
681
+ UserService.clearCache(username, botId);
682
+ this.processManager.sendMessage(botId, { type: 'invalidate_user_cache', username });
683
+ return { success: true };
684
+ }
685
+
686
+ invalidateAllUserCache(botId) {
687
+ for (const [cacheKey] of UserService.cache.entries()) {
688
+ if (cacheKey.startsWith(`${botId}:`)) {
689
+ UserService.cache.delete(cacheKey);
690
+ }
691
+ }
692
+
693
+ this.logger.info({ botId }, 'Кеш пользователей очищен');
694
+ this.processManager.sendMessage(botId, { type: 'invalidate_all_user_cache' });
695
+
696
+ return { success: true };
697
+ }
698
+
699
+ async _handleTraceCompleted(botId, trace) {
700
+ try {
701
+ const { getTraceCollector } = require('../services/TraceCollectorService');
702
+ const traceCollector = getTraceCollector();
703
+
704
+ // Сохраняем трассировку в главном TraceCollectorService
705
+ await traceCollector._storeCompletedTrace(trace);
706
+ } catch (error) {
707
+ this.logger.error({ botId, error }, 'Ошибка обработки завершённой трассировки');
708
+ }
709
+ }
710
+
711
+ async _handleDebugBreakpointCheck(botId, child, message) {
712
+ const { requestId, payload } = message;
713
+ const { graphId, nodeId, nodeType, inputs, executedSteps, context } = payload;
714
+
715
+ try {
716
+ const { getGlobalDebugManager } = require('../services/DebugSessionManager');
717
+ const debugManager = getGlobalDebugManager();
718
+
719
+ const debugState = debugManager.get(graphId);
720
+ if (!debugState) {
721
+ // Нет debug сессии для этого графа - просто продолжаем выполнение
722
+ child.send({
723
+ type: 'debug:breakpoint_response',
724
+ requestId,
725
+ overrides: null
726
+ });
727
+ return;
728
+ }
729
+
730
+ const breakpoint = debugState.breakpoints.get(nodeId);
731
+ if (!breakpoint || !breakpoint.enabled) {
732
+ // Нет брейкпоинта для этой ноды или он отключен
733
+ child.send({
734
+ type: 'debug:breakpoint_response',
735
+ requestId,
736
+ overrides: null
737
+ });
738
+ return;
739
+ }
740
+
741
+ // Проверяем условие брейкпоинта (пока всегда срабатывает)
742
+ // TODO: добавить evaluateBreakpointCondition
743
+
744
+ breakpoint.hitCount++;
745
+
746
+ // Приостанавливаем выполнение и ждём действий от пользователя
747
+ const overrides = await debugState.pause({
748
+ nodeId,
749
+ nodeType,
750
+ inputs,
751
+ executedSteps,
752
+ context,
753
+ breakpoint: {
754
+ condition: breakpoint.condition,
755
+ hitCount: breakpoint.hitCount
756
+ }
757
+ });
758
+
759
+ // Отправляем результат обратно в дочерний процесс
760
+ child.send({
761
+ type: 'debug:breakpoint_response',
762
+ requestId,
763
+ overrides: overrides || null
764
+ });
765
+
766
+ } catch (error) {
767
+ this.logger.error({ botId, error }, 'Ошибка обработки debug breakpoint check');
768
+ // В случае ошибки отправляем null чтобы продолжить выполнение
769
+ child.send({
770
+ type: 'debug:breakpoint_response',
771
+ requestId,
772
+ overrides: null
773
+ });
774
+ }
775
+ }
776
+
777
+ async _handleDebugStepModeCheck(botId, child, message) {
778
+ const { requestId, payload } = message;
779
+ const { graphId, nodeId, nodeType, inputs, executedSteps, context } = payload;
780
+
781
+ try {
782
+ const { getGlobalDebugManager } = require('../services/DebugSessionManager');
783
+ const debugManager = getGlobalDebugManager();
784
+
785
+ const debugState = debugManager.get(graphId);
786
+ if (!debugState) {
787
+ // Нет debug сессии - продолжаем выполнение
788
+ child.send({
789
+ type: 'debug:breakpoint_response', // Используем тот же тип ответа
790
+ requestId,
791
+ overrides: null
792
+ });
793
+ return;
794
+ }
795
+
796
+ // Проверяем, нужно ли остановиться в step mode
797
+ if (!debugState.shouldStepPause(nodeId)) {
798
+ // Step mode не активен или не нужно останавливаться на этой ноде
799
+ child.send({
800
+ type: 'debug:breakpoint_response',
801
+ requestId,
802
+ overrides: null
803
+ });
804
+ return;
805
+ }
806
+
807
+ // Приостанавливаем выполнение и ждём действий от пользователя
808
+ const overrides = await debugState.pause({
809
+ nodeId,
810
+ nodeType,
811
+ inputs,
812
+ executedSteps,
813
+ context
814
+ });
815
+
816
+ // Отправляем результат обратно в дочерний процесс
817
+ child.send({
818
+ type: 'debug:breakpoint_response',
819
+ requestId,
820
+ overrides: overrides || null
821
+ });
822
+
823
+ } catch (error) {
824
+ this.logger.error({ botId, error }, 'Ошибка обработки debug step mode check');
825
+ // В случае ошибки отправляем null чтобы продолжить выполнение
826
+ child.send({
827
+ type: 'debug:breakpoint_response',
828
+ requestId,
829
+ overrides: null
830
+ });
831
+ }
832
+ }
833
+ }
834
+
835
+ module.exports = BotLifecycleService;