blockmine 1.4.7 → 1.5.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.
Files changed (32) hide show
  1. package/backend/package.json +5 -0
  2. package/backend/prisma/migrations/20250627144030_add_visual_editor_fields/migration.sql +26 -0
  3. package/backend/prisma/migrations/20250628113254_add_event_graphs/migration.sql +14 -0
  4. package/backend/prisma/migrations/20250628122517_added_eventgraph_name/migration.sql +26 -0
  5. package/backend/prisma/migrations/20250628122710_complex_events/migration.sql +36 -0
  6. package/backend/prisma/migrations/migration_lock.toml +2 -2
  7. package/backend/prisma/schema.prisma +45 -14
  8. package/backend/src/api/routes/bots.js +530 -286
  9. package/backend/src/api/routes/eventGraphs.js +375 -0
  10. package/backend/src/api/routes/plugins.js +5 -3
  11. package/backend/src/core/BotManager.js +298 -171
  12. package/backend/src/core/BotProcess.js +312 -44
  13. package/backend/src/core/EventGraphManager.js +164 -0
  14. package/backend/src/core/GraphExecutionEngine.js +706 -0
  15. package/backend/src/core/NodeRegistry.js +888 -0
  16. package/backend/src/core/PluginManager.js +12 -2
  17. package/backend/src/core/UserService.js +15 -2
  18. package/backend/src/core/services.js +12 -0
  19. package/backend/src/core/system/CommandManager.js +2 -0
  20. package/backend/src/lib/logger.js +15 -0
  21. package/backend/src/server.js +12 -4
  22. package/frontend/dist/assets/index-CY4JKfFL.js +8203 -0
  23. package/frontend/dist/assets/index-DC4RjP6E.css +1 -0
  24. package/frontend/dist/index.html +2 -2
  25. package/frontend/package.json +4 -0
  26. package/image/1.png +0 -0
  27. package/image/2.png +0 -0
  28. package/image/3.png +0 -0
  29. package/package.json +7 -2
  30. package/test_visual_command.json +9 -0
  31. package/frontend/dist/assets/index-BGh31hwx.js +0 -8179
  32. package/frontend/dist/assets/index-CKAIPNvH.css +0 -1
@@ -2,12 +2,14 @@ const express = require('express');
2
2
  const { PrismaClient } = require('@prisma/client');
3
3
  const path = require('path');
4
4
  const fs = require('fs/promises');
5
- const BotManager = require('../../core/BotManager');
6
- const PluginManager = require('../../core/PluginManager');
5
+ const { botManager, pluginManager } = require('../../core/services');
7
6
  const UserService = require('../../core/UserService');
8
7
  const commandManager = require('../../core/system/CommandManager');
8
+ const NodeRegistry = require('../../core/NodeRegistry');
9
9
  const { authenticate, authorize } = require('../middleware/auth');
10
10
  const { encrypt } = require('../../core/utils/crypto');
11
+ const { randomUUID } = require('crypto');
12
+ const eventGraphsRouter = require('./eventGraphs');
11
13
 
12
14
  const multer = require('multer');
13
15
  const archiver = require('archiver');
@@ -16,11 +18,10 @@ const AdmZip = require('adm-zip');
16
18
  const prisma = new PrismaClient();
17
19
  const upload = multer({ storage: multer.memoryStorage() });
18
20
 
19
-
20
21
  const router = express.Router();
21
22
 
22
-
23
23
  router.use(authenticate);
24
+ router.use('/:botId/event-graphs', eventGraphsRouter);
24
25
 
25
26
  async function setupDefaultPermissionsForBot(botId, prismaClient = prisma) {
26
27
  const initialData = {
@@ -58,7 +59,6 @@ async function setupDefaultPermissionsForBot(botId, prismaClient = prisma) {
58
59
  console.log(`[Setup] Для бота ID ${botId} созданы группы и права по умолчанию.`);
59
60
  }
60
61
 
61
-
62
62
  router.get('/', authorize('bot:list'), async (req, res) => {
63
63
  try {
64
64
  const bots = await prisma.bot.findMany({ include: { server: true }, orderBy: { createdAt: 'asc' } });
@@ -71,7 +71,7 @@ router.get('/', authorize('bot:list'), async (req, res) => {
71
71
 
72
72
  router.get('/state', authorize('bot:list'), (req, res) => {
73
73
  try {
74
- const state = BotManager.getFullState();
74
+ const state = botManager.getFullState();
75
75
  res.json(state);
76
76
  } catch (error) { res.status(500).json({ error: 'Не удалось получить состояние ботов' }); }
77
77
  });
@@ -104,10 +104,8 @@ router.post('/', authorize('bot:create'), async (req, res) => {
104
104
 
105
105
  router.put('/:id', authorize('bot:update'), async (req, res) => {
106
106
  try {
107
- const botId = parseInt(req.params.id, 10);
108
-
109
107
  const {
110
- username, password, prefix, serverId, note,
108
+ username, password, prefix, serverId, note, owners,
111
109
  proxyHost, proxyPort, proxyUsername, proxyPassword
112
110
  } = req.body;
113
111
 
@@ -115,6 +113,7 @@ router.put('/:id', authorize('bot:update'), async (req, res) => {
115
113
  username,
116
114
  prefix,
117
115
  note,
116
+ owners,
118
117
  proxyHost,
119
118
  proxyPort: proxyPort ? parseInt(proxyPort, 10) : null,
120
119
  proxyUsername,
@@ -127,10 +126,6 @@ router.put('/:id', authorize('bot:update'), async (req, res) => {
127
126
  dataToUpdate.proxyPassword = encrypt(proxyPassword);
128
127
  }
129
128
 
130
- if (proxyPassword) {
131
- dataToUpdate.proxyPassword = proxyPassword;
132
- }
133
-
134
129
  if (serverId) {
135
130
  dataToUpdate.serverId = parseInt(serverId, 10);
136
131
  }
@@ -140,43 +135,51 @@ router.put('/:id', authorize('bot:update'), async (req, res) => {
140
135
  dataToUpdate = { ...rest, server: { connect: { id: sId } } };
141
136
  }
142
137
 
143
- const updatedBot = await prisma.bot.update({
144
- where: { id: botId },
145
- data: dataToUpdate,
146
- include: { server: true }
138
+ const botId = parseInt(req.params.id, 10);
139
+ if (isNaN(botId)) {
140
+ return res.status(400).json({ message: 'Неверный ID бота.' });
141
+ }
142
+
143
+ const updatedBot = await prisma.bot.update({
144
+ where: { id: botId },
145
+ data: dataToUpdate,
146
+ include: {
147
+ server: true
148
+ }
147
149
  });
148
-
150
+
151
+ const botManager = req.app.get('botManager');
152
+ botManager.reloadBotConfigInRealTime(botId);
153
+
149
154
  res.json(updatedBot);
150
155
  } catch (error) {
151
- console.error("Update bot error:", error);
152
- if (error.code === 'P2002') return res.status(409).json({ error: 'Бот с таким именем уже существует' });
153
- res.status(500).json({ error: 'Не удалось обновить бота' });
156
+ console.error('Error updating bot:', error);
157
+ res.status(500).json({ message: `Не удалось обновить бота: ${error.message}` });
154
158
  }
155
159
  });
156
160
 
157
161
  router.delete('/:id', authorize('bot:delete'), async (req, res) => {
158
162
  try {
159
163
  const botId = parseInt(req.params.id, 10);
160
- if (BotManager.bots.has(botId)) return res.status(400).json({ error: 'Нельзя удалить запущенного бота' });
164
+ if (botManager.bots.has(botId)) return res.status(400).json({ error: 'Нельзя удалить запущенного бота' });
161
165
  await prisma.bot.delete({ where: { id: botId } });
162
166
  res.status(204).send();
163
167
  } catch (error) { res.status(500).json({ error: 'Не удалось удалить бота' }); }
164
168
  });
165
169
 
166
-
167
170
  router.post('/:id/start', authorize('bot:start_stop'), async (req, res) => {
168
171
  try {
169
172
  const botId = parseInt(req.params.id, 10);
170
173
  const botConfig = await prisma.bot.findUnique({ where: { id: botId }, include: { server: true } });
171
174
  if (!botConfig) return res.status(404).json({ error: 'Бот не найден' });
172
- const result = await BotManager.startBot(botConfig);
175
+ const result = await botManager.startBot(botConfig);
173
176
  res.json(result);
174
177
  } catch (error) { res.status(500).json({ error: 'Ошибка при запуске бота: ' + error.message }); }
175
178
  });
176
179
 
177
180
  router.post('/:id/stop', authorize('bot:start_stop'), (req, res) => {
178
181
  const botId = parseInt(req.params.id, 10);
179
- const result = BotManager.stopBot(botId);
182
+ const result = botManager.stopBot(botId);
180
183
  res.json(result);
181
184
  });
182
185
 
@@ -185,200 +188,23 @@ router.post('/:id/chat', authorize('bot:interact'), (req, res) => {
185
188
  const botId = parseInt(req.params.id, 10);
186
189
  const { message } = req.body;
187
190
  if (!message) return res.status(400).json({ error: 'Сообщение не может быть пустым' });
188
- const result = BotManager.sendMessageToBot(botId, message);
191
+ const result = botManager.sendMessageToBot(botId, message);
189
192
  if (result.success) res.json({ success: true });
190
193
  else res.status(404).json(result);
191
194
  } catch (error) { res.status(500).json({ error: 'Внутренняя ошибка сервера: ' + error.message }); }
192
195
  });
193
196
 
194
- router.get('/:id/export', authorize('bot:export'), async (req, res) => {
195
- try {
196
- const botId = parseInt(req.params.id, 10);
197
- const {
198
- includeCommands = 'true',
199
- includePermissions = 'true',
200
- includePluginFiles = 'true'
201
- } = req.query;
202
-
203
- const bot = await prisma.bot.findUnique({
204
- where: { id: botId },
205
- include: { server: true, installedPlugins: true },
206
- });
207
-
208
- if (!bot) {
209
- return res.status(404).json({ error: 'Бот не найден' });
210
- }
211
-
212
- const archive = archiver('zip', { zlib: { level: 9 } });
213
- res.setHeader('Content-Disposition', `attachment; filename="bot-${bot.username}-export.zip"`);
214
- res.setHeader('Content-Type', 'application/zip');
215
- archive.pipe(res);
216
-
217
- const exportMetadata = {
218
- version: '1.2-configurable',
219
- bot: {
220
- username: bot.username,
221
- prefix: bot.prefix,
222
- note: bot.note,
223
- server: { host: bot.server.host, port: bot.server.port, version: bot.server.version },
224
- proxy: { host: bot.proxyHost, port: bot.proxyPort },
225
- },
226
- plugins: bot.installedPlugins.map(p => ({
227
- name: p.name,
228
- folderName: path.basename(p.path),
229
- isEnabled: p.isEnabled,
230
- settings: p.settings,
231
- sourceUri: p.sourceType === 'GITHUB' ? p.sourceUri : null,
232
- })),
233
- };
234
-
235
- if (includeCommands === 'true') {
236
- exportMetadata.commands = await prisma.command.findMany({ where: { botId } });
237
- }
238
- if (includePermissions === 'true') {
239
- exportMetadata.permissions = await prisma.permission.findMany({ where: { botId } });
240
- exportMetadata.groups = await prisma.group.findMany({ where: { botId }, include: { permissions: { select: { permission: { select: { name: true } } } } } });
241
- exportMetadata.users = await prisma.user.findMany({ where: { botId }, include: { groups: { select: { group: { select: { name: true } } } } } });
242
- }
243
-
244
- archive.append(JSON.stringify(exportMetadata, null, 2), { name: 'bot_export.json' });
245
-
246
- if (includePluginFiles === 'true') {
247
- for (const plugin of bot.installedPlugins) {
248
- const pluginFolderName = path.basename(plugin.path);
249
- try {
250
- await fs.access(plugin.path);
251
- archive.directory(plugin.path, `plugins/${pluginFolderName}`);
252
- } catch (error) {
253
- console.warn(`[Export] Директория плагина ${plugin.name} (${plugin.path}) не найдена, пропускаем.`);
254
- }
255
- }
256
- }
257
-
258
- await archive.finalize();
259
-
260
- } catch (error) {
261
- console.error("Bot export error:", error);
262
- res.status(500).json({ error: 'Не удалось экспортировать бота.' });
263
- }
264
- });
265
-
266
- router.post('/import', upload.single('file'), authorize('bot:import'), async (req, res) => {
267
- if (!req.file) {
268
- return res.status(400).send('Файл не загружен.');
269
- }
270
- if (path.extname(req.file.originalname).toLowerCase() !== '.zip') return res.status(400).json({ error: 'Неверный формат файла. Ожидался ZIP-архив.' });
271
-
272
- const zip = new AdmZip(req.file.buffer);
273
- const metadataEntry = zip.getEntry('bot_export.json');
274
- if (!metadataEntry) return res.status(400).json({ error: 'Файл bot_export.json не найден в архиве.' });
275
-
276
- const importMetadata = JSON.parse(metadataEntry.getData().toString('utf-8'));
277
- const { bot: botData, plugins: pluginsInfo, commands, permissions, groups, users } = importMetadata;
278
197
 
279
- const existingBot = await prisma.bot.findUnique({ where: { username: botData.username } });
280
- if (existingBot) return res.status(409).json({ error: `Бот с именем "${botData.username}" уже существует.` });
281
-
282
- let newBotId;
283
- let botStorageDir;
198
+ router.get('/servers', authorize('bot:list'), async (req, res) => {
284
199
  try {
285
- await prisma.$transaction(async (tx) => {
286
- let server = await tx.server.findFirst({ where: { host: botData.server.host, port: botData.server.port } });
287
- if (!server) server = await tx.server.create({ data: { name: botData.server.host, host: botData.server.host, port: botData.server.port, version: botData.server.version } });
288
- const newBot = await tx.bot.create({ data: { username: botData.username, prefix: botData.prefix, note: botData.note, serverId: server.id, proxyHost: botData.proxy?.host, proxyPort: botData.proxy?.port } });
289
- newBotId = newBot.id;
290
-
291
- if (permissions && groups && users) {
292
- const permMap = new Map();
293
- for (const perm of permissions) {
294
- const newPerm = await tx.permission.create({ data: { botId: newBotId, name: perm.name, description: perm.description, owner: perm.owner } });
295
- permMap.set(perm.name, newPerm.id);
296
- }
297
-
298
- const groupMap = new Map();
299
- for (const group of groups) {
300
- const newGroup = await tx.group.create({
301
- data: {
302
- botId: newBotId,
303
- name: group.name,
304
- owner: group.owner,
305
- permissions: { create: group.permissions.map(p => ({ permissionId: permMap.get(p.permission.name) })).filter(p => p.permissionId) }
306
- }
307
- });
308
- groupMap.set(group.name, newGroup.id);
309
- }
310
-
311
- for (const user of users) {
312
- await tx.user.create({
313
- data: {
314
- botId: newBotId,
315
- username: user.username,
316
- isBlacklisted: user.isBlacklisted,
317
- groups: { create: user.groups.map(g => ({ groupId: groupMap.get(g.group.name) })).filter(g => g.groupId) }
318
- }
319
- });
320
- }
321
- } else {
322
- await setupDefaultPermissionsForBot(newBotId, tx);
323
- }
324
-
325
- if (commands) {
326
- for (const cmd of commands) {
327
- const permName = permissions?.find(p => p.id === cmd.permissionId)?.name;
328
- const permId = permName ? (await tx.permission.findUnique({where: {botId_name: {botId: newBotId, name: permName}}}))?.id : null;
329
-
330
- await tx.command.create({
331
- data: {
332
- botId: newBotId,
333
- name: cmd.name,
334
- isEnabled: cmd.isEnabled,
335
- cooldown: cmd.cooldown,
336
- aliases: cmd.aliases,
337
- description: cmd.description,
338
- owner: cmd.owner,
339
- permissionId: permId,
340
- allowedChatTypes: cmd.allowedChatTypes,
341
- }
342
- });
343
- }
344
- }
345
-
346
- botStorageDir = path.resolve(__dirname, `../../../storage/plugins/bot_${newBotId}`);
347
- await fs.mkdir(botStorageDir, { recursive: true });
348
- const pluginsDirInZip = zip.getEntry('plugins/');
349
-
350
- if (pluginsDirInZip && pluginsDirInZip.isDirectory) {
351
- zip.extractEntryTo('plugins/', botStorageDir, true, true);
352
- for (const pInfo of pluginsInfo) {
353
- const targetPath = path.join(botStorageDir, 'plugins', pInfo.folderName);
354
- const packageJson = JSON.parse(await fs.readFile(path.join(targetPath, 'package.json'), 'utf-8'));
355
- await tx.installedPlugin.create({
356
- data: { botId: newBotId, name: pInfo.name, version: packageJson.version, description: packageJson.description, path: targetPath, sourceType: 'IMPORTED', sourceUri: `imported_from_${req.file.originalname}`, manifest: JSON.stringify(packageJson.botpanel || {}), isEnabled: pInfo.isEnabled, settings: pInfo.settings }
357
- });
358
- }
359
- } else {
360
- for (const pInfo of pluginsInfo) {
361
- if (pInfo.sourceUri) {
362
- const newPlugin = await PluginManager.installFromGithub(newBotId, pInfo.sourceUri, tx);
363
- await tx.installedPlugin.update({ where: { id: newPlugin.id }, data: { isEnabled: pInfo.isEnabled, settings: pInfo.settings } });
364
- }
365
- }
366
- }
367
- });
368
-
369
- const finalBot = await prisma.bot.findUnique({ where: { id: newBotId }});
370
- res.status(201).json(finalBot);
371
-
200
+ const servers = await prisma.server.findMany();
201
+ res.json(servers);
372
202
  } catch (error) {
373
- console.error("[API Error] /import POST:", error);
374
- if (botStorageDir) {
375
- await fs.rm(botStorageDir, { recursive: true, force: true }).catch(() => {});
376
- }
377
- res.status(500).json({ error: `Не удалось импортировать бота: ${error.message}` });
203
+ console.error("[API /api/bots] Ошибка получения списка серверов:", error);
204
+ res.status(500).json({ error: 'Не удалось получить список серверов' });
378
205
  }
379
206
  });
380
207
 
381
-
382
208
  router.get('/:botId/plugins', authorize('plugin:list'), async (req, res) => {
383
209
  try {
384
210
  const botId = parseInt(req.params.botId);
@@ -388,29 +214,35 @@ router.get('/:botId/plugins', authorize('plugin:list'), async (req, res) => {
388
214
  });
389
215
 
390
216
  router.post('/:botId/plugins/install/github', authorize('plugin:install'), async (req, res) => {
217
+ const { botId } = req.params;
218
+ const { repoUrl } = req.body;
391
219
  try {
392
- const botId = parseInt(req.params.botId);
393
- const { repoUrl } = req.body;
394
- const newPlugin = await PluginManager.installFromGithub(botId, repoUrl);
220
+ const newPlugin = await pluginManager.installFromGithub(parseInt(botId), repoUrl);
395
221
  res.status(201).json(newPlugin);
396
- } catch (error) { res.status(500).json({ error: error.message }); }
222
+ } catch (error) {
223
+ res.status(500).json({ message: error.message });
224
+ }
397
225
  });
398
226
 
399
- router.post('/:botId/plugins/register/local', authorize('plugin:install'), async (req, res) => {
227
+ router.post('/:botId/plugins/install/local', authorize('plugin:install'), async (req, res) => {
228
+ const { botId } = req.params;
229
+ const { path } = req.body;
400
230
  try {
401
- const botId = parseInt(req.params.botId);
402
- const { path } = req.body;
403
- const newPlugin = await PluginManager.installFromLocalPath(botId, path);
231
+ const newPlugin = await pluginManager.installFromLocalPath(parseInt(botId), path);
404
232
  res.status(201).json(newPlugin);
405
- } catch (error) { res.status(500).json({ error: error.message }); }
233
+ } catch (error) {
234
+ res.status(500).json({ message: error.message });
235
+ }
406
236
  });
407
237
 
408
238
  router.delete('/:botId/plugins/:pluginId', authorize('plugin:delete'), async (req, res) => {
239
+ const { pluginId } = req.params;
409
240
  try {
410
- const pluginId = parseInt(req.params.pluginId);
411
- await PluginManager.deletePlugin(pluginId);
241
+ await pluginManager.deletePlugin(parseInt(pluginId));
412
242
  res.status(204).send();
413
- } catch (error) { res.status(500).json({ error: error.message }); }
243
+ } catch (error) {
244
+ res.status(500).json({ message: error.message });
245
+ }
414
246
  });
415
247
 
416
248
  router.get('/:botId/plugins/:pluginId/settings', authorize('plugin:settings:view'), async (req, res) => {
@@ -459,21 +291,50 @@ router.put('/:botId/plugins/:pluginId', authorize('plugin:settings:edit'), async
459
291
  } catch (error) { res.status(500).json({ error: 'Не удалось обновить плагин' }); }
460
292
  });
461
293
 
462
-
463
294
  router.get('/:botId/management-data', authorize('management:view'), async (req, res) => {
464
295
  try {
465
- const botId = parseInt(req.params.botId);
466
-
467
- const commandTemplates = commandManager.getCommandTemplates();
468
- const templatesMap = new Map(commandTemplates.map(t => [t.name, t]));
296
+ const botId = parseInt(req.params.botId, 10);
297
+ if (isNaN(botId)) return res.status(400).json({ error: 'Неверный ID бота' });
469
298
 
470
- let dbCommands = await prisma.command.findMany({ where: { botId }, orderBy: [{ owner: 'asc' }, { name: 'asc' }] });
299
+ const page = parseInt(req.query.page) || 1;
300
+ const pageSize = parseInt(req.query.pageSize) || 100;
301
+ const searchQuery = req.query.search || '';
471
302
 
472
- const dbCommandNames = new Set(dbCommands.map(cmd => cmd.name));
473
- const commandsToCreate = [];
303
+ const userSkip = (page - 1) * pageSize;
474
304
 
475
- for (const template of commandTemplates) {
476
- if (!dbCommandNames.has(template.name)) {
305
+ const whereClause = {
306
+ botId,
307
+ };
308
+
309
+ if (searchQuery) {
310
+ whereClause.username = {
311
+ contains: searchQuery,
312
+ };
313
+ }
314
+
315
+ const [dbCommands, groups, allPermissions] = await Promise.all([
316
+ prisma.command.findMany({ where: { botId }, orderBy: [{ owner: 'asc' }, { name: 'asc' }] }),
317
+ prisma.group.findMany({ where: { botId }, include: { permissions: { include: { permission: true } } }, orderBy: { name: 'asc' } }),
318
+ prisma.permission.findMany({ where: { botId }, orderBy: { name: 'asc' } })
319
+ ]);
320
+
321
+ const [users, usersCount] = await Promise.all([
322
+ prisma.user.findMany({
323
+ where: whereClause,
324
+ include: { groups: { include: { group: true } } },
325
+ orderBy: { username: 'asc' },
326
+ take: pageSize,
327
+ skip: userSkip,
328
+ }),
329
+ prisma.user.count({ where: whereClause })
330
+ ]);
331
+
332
+ const templatesMap = new Map(commandManager.getCommandTemplates().map(t => [t.name, t]));
333
+ let dbCommandsFromDb = await prisma.command.findMany({ where: { botId } });
334
+
335
+ const commandsToCreate = [];
336
+ for (const template of templatesMap.values()) {
337
+ if (!dbCommandsFromDb.some(cmd => cmd.name === template.name)) {
477
338
  let permissionId = null;
478
339
  if (template.permissions) {
479
340
  const permission = await prisma.permission.upsert({
@@ -505,37 +366,40 @@ router.get('/:botId/management-data', authorize('management:view'), async (req,
505
366
 
506
367
  if (commandsToCreate.length > 0) {
507
368
  await prisma.command.createMany({ data: commandsToCreate });
508
- dbCommands = await prisma.command.findMany({ where: { botId }, orderBy: [{ owner: 'asc' }, { name: 'asc' }] });
369
+ dbCommandsFromDb = await prisma.command.findMany({ where: { botId }, orderBy: [{ owner: 'asc' }, { name: 'asc' }] });
509
370
  }
510
371
 
511
- const finalCommands = dbCommands.map(cmd => {
372
+ const finalCommands = dbCommandsFromDb.map(cmd => {
512
373
  const template = templatesMap.get(cmd.name);
374
+ let args = [];
375
+
376
+ if (cmd.isVisual) {
377
+ try {
378
+ args = JSON.parse(cmd.argumentsJson || '[]');
379
+ } catch (e) {
380
+ console.error(`Error parsing argumentsJson for visual command ${cmd.name} (ID: ${cmd.id}):`, e);
381
+ args = [];
382
+ }
383
+ } else {
384
+ if (template && template.args && template.args.length > 0) {
385
+ args = template.args;
386
+ } else {
387
+ try {
388
+ args = JSON.parse(cmd.argumentsJson || '[]');
389
+ } catch (e) {
390
+ args = [];
391
+ }
392
+ }
393
+ }
394
+
513
395
  return {
514
396
  ...cmd,
515
- args: template ? template.args : [],
397
+ args: args,
516
398
  aliases: JSON.parse(cmd.aliases || '[]'),
517
- allowedChatTypes: JSON.parse(cmd.allowedChatTypes || '[]')
399
+ allowedChatTypes: JSON.parse(cmd.allowedChatTypes || '[]'),
518
400
  };
519
401
  })
520
402
 
521
- const page = parseInt(req.query.page) || 1;
522
- const pageSize = parseInt(req.query.pageSize) || 100;
523
-
524
- const userSkip = (page - 1) * pageSize;
525
-
526
- const [groups, users, allPermissions, usersCount] = await Promise.all([
527
- prisma.group.findMany({ where: { botId }, include: { permissions: { include: { permission: true } } }, orderBy: { name: 'asc' } }),
528
- prisma.user.findMany({
529
- where: { botId },
530
- include: { groups: { include: { group: true } } },
531
- orderBy: { username: 'asc' },
532
- take: pageSize,
533
- skip: userSkip,
534
- }),
535
- prisma.permission.findMany({ where: { botId }, orderBy: { name: 'asc' } }),
536
- prisma.user.count({ where: { botId } })
537
- ]);
538
-
539
403
  res.json({
540
404
  groups,
541
405
  permissions: allPermissions,
@@ -558,35 +422,28 @@ router.get('/:botId/management-data', authorize('management:view'), async (req,
558
422
  router.put('/:botId/commands/:commandId', authorize('management:edit'), async (req, res) => {
559
423
  try {
560
424
  const commandId = parseInt(req.params.commandId, 10);
561
- const botId = parseInt(req.params.botId, 10);
562
- const { isEnabled, cooldown, aliases, permissionId, allowedChatTypes } = req.body;
425
+ const { name, description, cooldown, aliases, permissionId, allowedChatTypes, isEnabled, argumentsJson, graphJson } = req.body;
563
426
 
564
427
  const dataToUpdate = {};
565
- if (typeof isEnabled === 'boolean') dataToUpdate.isEnabled = isEnabled;
566
- if (typeof cooldown === 'number') dataToUpdate.cooldown = cooldown;
567
- if (Array.isArray(aliases)) dataToUpdate.aliases = JSON.stringify(aliases);
568
- if (permissionId !== undefined) {
569
- dataToUpdate.permissionId = permissionId === null ? null : parseInt(permissionId, 10);
570
- }
571
- if (Array.isArray(allowedChatTypes)) {
572
- dataToUpdate.allowedChatTypes = JSON.stringify(allowedChatTypes);
573
- }
574
-
575
- if (Object.keys(dataToUpdate).length === 0) {
576
- return res.status(400).json({ error: "Нет данных для обновления" });
577
- }
428
+ if (name !== undefined) dataToUpdate.name = name;
429
+ if (description !== undefined) dataToUpdate.description = description;
430
+ if (cooldown !== undefined) dataToUpdate.cooldown = parseInt(cooldown, 10);
431
+ if (aliases !== undefined) dataToUpdate.aliases = Array.isArray(aliases) ? JSON.stringify(aliases) : aliases;
432
+ if (permissionId !== undefined) dataToUpdate.permissionId = permissionId ? parseInt(permissionId, 10) : null;
433
+ if (allowedChatTypes !== undefined) dataToUpdate.allowedChatTypes = Array.isArray(allowedChatTypes) ? JSON.stringify(allowedChatTypes) : allowedChatTypes;
434
+ if (isEnabled !== undefined) dataToUpdate.isEnabled = isEnabled;
435
+ if (argumentsJson !== undefined) dataToUpdate.argumentsJson = Array.isArray(argumentsJson) ? JSON.stringify(argumentsJson) : argumentsJson;
436
+ if (graphJson !== undefined) dataToUpdate.graphJson = graphJson;
578
437
 
579
438
  const updatedCommand = await prisma.command.update({
580
- where: { id: commandId, botId: botId },
439
+ where: { id: commandId },
581
440
  data: dataToUpdate,
582
441
  });
583
442
 
584
- BotManager.reloadBotConfigInRealTime(botId);
585
-
586
443
  res.json(updatedCommand);
587
444
  } catch (error) {
588
445
  console.error(`[API Error] /commands/:commandId PUT:`, error);
589
- res.status(500).json({ error: 'Не удалось обновить команду' });
446
+ res.status(500).json({ error: 'Failed to update command' });
590
447
  }
591
448
  });
592
449
 
@@ -605,8 +462,7 @@ router.post('/:botId/groups', authorize('management:edit'), async (req, res) =>
605
462
  }
606
463
  });
607
464
 
608
- BotManager.reloadBotConfigInRealTime(botId);
609
-
465
+ botManager.reloadBotConfigInRealTime(botId);
610
466
 
611
467
  res.status(201).json(newGroup);
612
468
  } catch (error) {
@@ -638,10 +494,10 @@ router.put('/:botId/groups/:groupId', authorize('management:edit'), async (req,
638
494
  });
639
495
 
640
496
  for (const user of usersInGroup) {
641
- BotManager.invalidateUserCache(botId, user.username);
497
+ botManager.invalidateUserCache(botId, user.username);
642
498
  }
643
499
 
644
- BotManager.reloadBotConfigInRealTime(botId);
500
+ botManager.reloadBotConfigInRealTime(botId);
645
501
 
646
502
  res.status(200).send();
647
503
  } catch (error) {
@@ -659,8 +515,7 @@ router.delete('/:botId/groups/:groupId', authorize('management:edit'), async (re
659
515
  return res.status(403).json({ error: `Нельзя удалить группу с источником "${group.owner}".` });
660
516
  }
661
517
  await prisma.group.delete({ where: { id: groupId } });
662
- BotManager.reloadBotConfigInRealTime(botId);
663
-
518
+ botManager.reloadBotConfigInRealTime(botId);
664
519
 
665
520
  res.status(204).send();
666
521
  } catch (error) { res.status(500).json({ error: 'Не удалось удалить группу.' }); }
@@ -675,7 +530,7 @@ router.post('/:botId/permissions', authorize('management:edit'), async (req, res
675
530
  data: { name, description, botId, owner: 'admin' }
676
531
  });
677
532
 
678
- BotManager.reloadBotConfigInRealTime(botId);
533
+ botManager.reloadBotConfigInRealTime(botId);
679
534
 
680
535
  res.status(201).json(newPermission);
681
536
  } catch (error) {
@@ -708,7 +563,7 @@ router.put('/:botId/users/:userId', authorize('management:edit'), async (req, re
708
563
  include: { groups: true }
709
564
  });
710
565
 
711
- BotManager.invalidateUserCache(botId, updatedUser.username);
566
+ botManager.invalidateUserCache(botId, updatedUser.username);
712
567
 
713
568
  UserService.clearCache(updatedUser.username, botId);
714
569
 
@@ -720,15 +575,14 @@ router.put('/:botId/users/:userId', authorize('management:edit'), async (req, re
720
575
  }
721
576
  });
722
577
 
723
-
724
578
  router.post('/start-all', authorize('bot:start_stop'), async (req, res) => {
725
579
  try {
726
580
  console.log('[API] Получен запрос на запуск всех ботов.');
727
581
  const allBots = await prisma.bot.findMany({ include: { server: true } });
728
582
  let startedCount = 0;
729
583
  for (const botConfig of allBots) {
730
- if (!BotManager.bots.has(botConfig.id)) {
731
- await BotManager.startBot(botConfig);
584
+ if (!botManager.bots.has(botConfig.id)) {
585
+ await botManager.startBot(botConfig);
732
586
  startedCount++;
733
587
  }
734
588
  }
@@ -742,10 +596,10 @@ router.post('/start-all', authorize('bot:start_stop'), async (req, res) => {
742
596
  router.post('/stop-all', authorize('bot:start_stop'), (req, res) => {
743
597
  try {
744
598
  console.log('[API] Получен запрос на остановку всех ботов.');
745
- const botIds = Array.from(BotManager.bots.keys());
599
+ const botIds = Array.from(botManager.bots.keys());
746
600
  let stoppedCount = 0;
747
601
  for (const botId of botIds) {
748
- BotManager.stopBot(botId);
602
+ botManager.stopBot(botId);
749
603
  stoppedCount++;
750
604
  }
751
605
  res.json({ success: true, message: `Остановлено ${stoppedCount} ботов.` });
@@ -755,7 +609,6 @@ router.post('/stop-all', authorize('bot:start_stop'), (req, res) => {
755
609
  }
756
610
  });
757
611
 
758
-
759
612
  router.get('/:id/settings/all', authorize('bot:update'), async (req, res) => {
760
613
  try {
761
614
  const botId = parseInt(req.params.id, 10);
@@ -780,6 +633,7 @@ router.get('/:id/settings/all', authorize('bot:update'), async (req, res) => {
780
633
  username: bot.username,
781
634
  prefix: bot.prefix,
782
635
  note: bot.note,
636
+ owners: bot.owners,
783
637
  serverId: bot.serverId,
784
638
  proxyHost: bot.proxyHost,
785
639
  proxyPort: bot.proxyPort,
@@ -832,4 +686,394 @@ router.get('/:id/settings/all', authorize('bot:update'), async (req, res) => {
832
686
  }
833
687
  });
834
688
 
835
- module.exports = router;
689
+ const nodeRegistry = require('../../core/NodeRegistry');
690
+
691
+ router.get('/:botId/visual-editor/nodes', authorize('management:view'), (req, res) => {
692
+ try {
693
+ const { graphType } = req.query;
694
+ const nodesByCategory = nodeRegistry.getNodesByCategory(graphType);
695
+ res.json(nodesByCategory);
696
+ } catch (error) {
697
+ console.error('[API Error] /visual-editor/nodes GET:', error);
698
+ res.status(500).json({ error: 'Failed to get available nodes' });
699
+ }
700
+ });
701
+
702
+ router.get('/:botId/visual-editor/node-config', authorize('management:view'), (req, res) => {
703
+ try {
704
+ const { types } = req.query;
705
+ if (!types) {
706
+ return res.status(400).json({ error: 'Node types must be provided' });
707
+ }
708
+ const typeArray = Array.isArray(types) ? types : [types];
709
+ const config = nodeRegistry.getNodesByTypes(typeArray);
710
+ res.json(config);
711
+ } catch (error) {
712
+ console.error('[API Error] /visual-editor/node-config GET:', error);
713
+ res.status(500).json({ error: 'Failed to get node configuration' });
714
+ }
715
+ });
716
+
717
+ router.get('/:botId/visual-editor/permissions', authorize('management:view'), async (req, res) => {
718
+ try {
719
+ const botId = parseInt(req.params.botId, 10);
720
+ const permissions = await prisma.permission.findMany({
721
+ where: { botId },
722
+ orderBy: { name: 'asc' }
723
+ });
724
+ res.json(permissions);
725
+ } catch (error) {
726
+ console.error('[API Error] /visual-editor/permissions GET:', error);
727
+ res.status(500).json({ error: 'Failed to get permissions' });
728
+ }
729
+ });
730
+
731
+ router.post('/:botId/commands/visual', authorize('management:edit'), async (req, res) => {
732
+ try {
733
+ const botId = parseInt(req.params.botId, 10);
734
+ const {
735
+ name,
736
+ description,
737
+ aliases = [],
738
+ permissionId,
739
+ cooldown = 0,
740
+ allowedChatTypes = ['chat', 'private'],
741
+ argumentsJson = '[]',
742
+ graphJson = 'null'
743
+ } = req.body;
744
+
745
+ if (!name) {
746
+ return res.status(400).json({ error: 'Command name is required' });
747
+ }
748
+
749
+ const newCommand = await prisma.command.create({
750
+ data: {
751
+ botId,
752
+ name,
753
+ description,
754
+ aliases: JSON.stringify(aliases),
755
+ permissionId: permissionId || null,
756
+ cooldown,
757
+ allowedChatTypes: JSON.stringify(allowedChatTypes),
758
+ isVisual: true,
759
+ argumentsJson,
760
+ graphJson
761
+ }
762
+ });
763
+
764
+ botManager.reloadBotConfigInRealTime(botId);
765
+ res.status(201).json(newCommand);
766
+ } catch (error) {
767
+ if (error.code === 'P2002') {
768
+ return res.status(409).json({ error: 'Command with this name already exists' });
769
+ }
770
+ console.error('[API Error] /commands/visual POST:', error);
771
+ res.status(500).json({ error: 'Failed to create visual command' });
772
+ }
773
+ });
774
+
775
+ router.put('/:botId/commands/:commandId/visual', authorize('management:edit'), async (req, res) => {
776
+ try {
777
+ const botId = parseInt(req.params.botId, 10);
778
+ const commandId = parseInt(req.params.commandId, 10);
779
+ const {
780
+ name,
781
+ description,
782
+ aliases,
783
+ permissionId,
784
+ cooldown,
785
+ allowedChatTypes,
786
+ argumentsJson,
787
+ graphJson
788
+ } = req.body;
789
+
790
+ const dataToUpdate = { isVisual: true };
791
+
792
+ if (name) dataToUpdate.name = name;
793
+ if (description !== undefined) dataToUpdate.description = description;
794
+ if (Array.isArray(aliases)) dataToUpdate.aliases = JSON.stringify(aliases);
795
+ if (permissionId !== undefined) dataToUpdate.permissionId = permissionId || null;
796
+ if (typeof cooldown === 'number') dataToUpdate.cooldown = cooldown;
797
+ if (Array.isArray(allowedChatTypes)) dataToUpdate.allowedChatTypes = JSON.stringify(allowedChatTypes);
798
+ if (argumentsJson !== undefined) dataToUpdate.argumentsJson = argumentsJson;
799
+ if (graphJson !== undefined) dataToUpdate.graphJson = graphJson;
800
+
801
+ const updatedCommand = await prisma.command.update({
802
+ where: { id: commandId, botId },
803
+ data: dataToUpdate
804
+ });
805
+
806
+ botManager.reloadBotConfigInRealTime(botId);
807
+ res.json(updatedCommand);
808
+ } catch (error) {
809
+ if (error.code === 'P2002') {
810
+ return res.status(409).json({ error: 'Command with this name already exists' });
811
+ }
812
+ console.error('[API Error] /commands/:commandId/visual PUT:', error);
813
+ res.status(500).json({ error: 'Failed to update visual command' });
814
+ }
815
+ });
816
+
817
+ router.get('/:botId/commands/:commandId/export', authorize('management:view'), async (req, res) => {
818
+ try {
819
+ const botId = parseInt(req.params.botId, 10);
820
+ const commandId = parseInt(req.params.commandId, 10);
821
+
822
+ const command = await prisma.command.findUnique({
823
+ where: { id: commandId, botId: botId },
824
+ });
825
+
826
+ if (!command) {
827
+ return res.status(404).json({ error: 'Command not found' });
828
+ }
829
+
830
+ const exportData = {
831
+ version: '1.0',
832
+ type: 'command',
833
+ ...command
834
+ };
835
+
836
+ delete exportData.id;
837
+ delete exportData.botId;
838
+
839
+ res.json(exportData);
840
+ } catch (error) {
841
+ console.error('Failed to export command:', error);
842
+ res.status(500).json({ error: 'Failed to export command' });
843
+ }
844
+ });
845
+
846
+ router.post('/:botId/commands/import', authorize('management:edit'), async (req, res) => {
847
+ try {
848
+ const botId = parseInt(req.params.botId, 10);
849
+ const importData = req.body;
850
+
851
+ if (importData.type !== 'command') {
852
+ return res.status(400).json({ error: 'Invalid file type. Expected "command".' });
853
+ }
854
+
855
+ let commandName = importData.name;
856
+ let counter = 1;
857
+
858
+ while (await prisma.command.findFirst({ where: { botId, name: commandName } })) {
859
+ commandName = `${importData.name}_imported_${counter}`;
860
+ counter++;
861
+ }
862
+
863
+ let finalGraphJson = importData.graphJson;
864
+
865
+ if (finalGraphJson && finalGraphJson !== 'null') {
866
+ const graph = JSON.parse(finalGraphJson);
867
+ const nodeIdMap = new Map();
868
+
869
+ // Рандомизация ID узлов
870
+ if (graph.nodes) {
871
+ graph.nodes.forEach(node => {
872
+ const oldId = node.id;
873
+ const newId = `${node.type}-${randomUUID()}`;
874
+ nodeIdMap.set(oldId, newId);
875
+ node.id = newId;
876
+ });
877
+ }
878
+
879
+ // Обновление и рандомизация ID связей
880
+ if (graph.connections) {
881
+ graph.connections.forEach(conn => {
882
+ conn.id = `edge-${randomUUID()}`;
883
+ conn.sourceNodeId = nodeIdMap.get(conn.sourceNodeId) || conn.sourceNodeId;
884
+ conn.targetNodeId = nodeIdMap.get(conn.targetNodeId) || conn.targetNodeId;
885
+ });
886
+ }
887
+
888
+ finalGraphJson = JSON.stringify(graph);
889
+ }
890
+
891
+ const newCommand = await prisma.command.create({
892
+ data: {
893
+ botId: botId,
894
+ name: commandName,
895
+ description: importData.description,
896
+ aliases: importData.aliases,
897
+ permissionId: null,
898
+ cooldown: importData.cooldown,
899
+ allowedChatTypes: importData.allowedChatTypes,
900
+ isVisual: importData.isVisual,
901
+ isEnabled: importData.isEnabled,
902
+ argumentsJson: importData.argumentsJson,
903
+ graphJson: finalGraphJson,
904
+ owner: 'visual_editor',
905
+ }
906
+ });
907
+
908
+ botManager.reloadBotConfigInRealTime(botId);
909
+ res.status(201).json(newCommand);
910
+ } catch (error) {
911
+ console.error("Failed to import command:", error);
912
+ res.status(500).json({ error: 'Failed to import command' });
913
+ }
914
+ });
915
+
916
+ router.post('/:botId/commands', authorize('management:edit'), async (req, res) => {
917
+ try {
918
+ const botId = parseInt(req.params.botId, 10);
919
+ const {
920
+ name,
921
+ description,
922
+ aliases = [],
923
+ permissionId,
924
+ cooldown = 0,
925
+ allowedChatTypes = ['chat', 'private'],
926
+ isVisual = false,
927
+ argumentsJson = '[]',
928
+ graphJson = 'null'
929
+ } = req.body;
930
+
931
+ if (!name) {
932
+ return res.status(400).json({ error: 'Command name is required' });
933
+ }
934
+
935
+ const newCommand = await prisma.command.create({
936
+ data: {
937
+ botId,
938
+ name,
939
+ description,
940
+ aliases: JSON.stringify(aliases),
941
+ permissionId: permissionId || null,
942
+ cooldown,
943
+ allowedChatTypes: JSON.stringify(allowedChatTypes),
944
+ isVisual,
945
+ argumentsJson,
946
+ graphJson,
947
+ owner: isVisual ? 'visual_editor' : 'manual',
948
+ }
949
+ });
950
+
951
+ botManager.reloadBotConfigInRealTime(botId);
952
+ res.status(201).json(newCommand);
953
+ } catch (error) {
954
+ if (error.code === 'P2002') {
955
+ return res.status(409).json({ error: 'Command with this name already exists' });
956
+ }
957
+ console.error('[API Error] /commands POST:', error);
958
+ res.status(500).json({ error: 'Failed to create command' });
959
+ }
960
+ });
961
+
962
+ router.delete('/:botId/commands/:commandId', authorize('management:edit'), async (req, res) => {
963
+ try {
964
+ const botId = parseInt(req.params.botId, 10);
965
+ const commandId = parseInt(req.params.commandId, 10);
966
+
967
+ await prisma.command.delete({
968
+ where: { id: commandId, botId: botId },
969
+ });
970
+
971
+ botManager.reloadBotConfigInRealTime(botId);
972
+ res.status(204).send();
973
+ } catch (error) {
974
+ console.error(`[API Error] /commands/:commandId DELETE:`, error);
975
+ res.status(500).json({ error: 'Failed to delete command' });
976
+ }
977
+ });
978
+
979
+ router.get('/:botId/event-graphs/:graphId', authorize('management:view'), async (req, res) => {
980
+ try {
981
+ const botId = parseInt(req.params.botId, 10);
982
+ const graphId = parseInt(req.params.graphId, 10);
983
+
984
+ const eventGraph = await prisma.eventGraph.findUnique({
985
+ where: { id: graphId, botId },
986
+ include: { triggers: true },
987
+ });
988
+
989
+ if (!eventGraph) {
990
+ return res.status(404).json({ error: 'Граф события не найден' });
991
+ }
992
+
993
+ res.json(eventGraph);
994
+ } catch (error) {
995
+ console.error(`[API Error] /event-graphs/:graphId GET:`, error);
996
+ res.status(500).json({ error: 'Не удалось получить граф события' });
997
+ }
998
+ });
999
+
1000
+ router.post('/:botId/event-graphs', authorize('management:edit'), async (req, res) => {
1001
+ try {
1002
+ const botId = parseInt(req.params.botId, 10);
1003
+ const { name } = req.body;
1004
+
1005
+ if (!name) {
1006
+ return res.status(400).json({ error: 'Имя графа обязательно' });
1007
+ }
1008
+
1009
+ const newEventGraph = await prisma.eventGraph.create({
1010
+ data: {
1011
+ botId,
1012
+ name,
1013
+ isEnabled: true,
1014
+ graphJson: 'null',
1015
+ },
1016
+ });
1017
+
1018
+ res.status(201).json(newEventGraph);
1019
+ } catch (error) {
1020
+ if (error.code === 'P2002') {
1021
+ return res.status(409).json({ error: 'Граф событий с таким именем уже существует' });
1022
+ }
1023
+ console.error(`[API Error] /event-graphs POST:`, error);
1024
+ res.status(500).json({ error: 'Не удалось создать граф событий' });
1025
+ }
1026
+ });
1027
+
1028
+ router.delete('/:botId/event-graphs/:graphId', authorize('management:edit'), async (req, res) => {
1029
+ try {
1030
+ const botId = parseInt(req.params.botId, 10);
1031
+ const graphId = parseInt(req.params.graphId, 10);
1032
+
1033
+ await prisma.eventGraph.delete({
1034
+ where: { id: graphId, botId: botId },
1035
+ });
1036
+
1037
+ res.status(204).send();
1038
+ } catch (error) {
1039
+ console.error(`[API Error] /event-graphs/:graphId DELETE:`, error);
1040
+ res.status(500).json({ error: 'Не удалось удалить граф событий' });
1041
+ }
1042
+ });
1043
+
1044
+ router.put('/:botId/event-graphs/:graphId', authorize('management:edit'), async (req, res) => {
1045
+ const { botId, graphId } = req.params;
1046
+ const { name, isEnabled, graphJson, eventType } = req.body;
1047
+
1048
+ if (!name || !eventType || !graphJson) {
1049
+ return res.status(400).json({ error: 'Поля name, eventType и graphJson обязательны.' });
1050
+ }
1051
+
1052
+ if (typeof isEnabled !== 'boolean') {
1053
+ return res.status(400).json({ error: 'Поле isEnabled должно быть true или false.' });
1054
+ }
1055
+
1056
+ try {
1057
+ const updatedGraph = await prisma.eventGraph.update({
1058
+ where: { id: parseInt(graphId), botId: parseInt(botId) },
1059
+ data: {
1060
+ name,
1061
+ isEnabled,
1062
+ graphJson,
1063
+ eventType,
1064
+ }
1065
+ });
1066
+
1067
+ botManager.eventGraphManager.updateGraph(parseInt(botId), updatedGraph);
1068
+
1069
+ res.json(updatedGraph);
1070
+ } catch (error) {
1071
+ console.error(`[API Error] /event-graphs/:graphId PUT:`, error);
1072
+ res.status(500).json({ error: 'Ошибка при обновлении графа событий.' });
1073
+ }
1074
+ });
1075
+
1076
+ router.post('/:botId/visual-editor/save', authorize('management:edit'), async (req, res) => {
1077
+ });
1078
+
1079
+ module.exports = router;