blockmine 1.5.5 → 1.5.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -97,8 +97,7 @@
97
97
  Это сердце No-Code автоматизации в BlockMine. Редактор позволяет вам создавать логику, перетаскивая и соединяя функциональные блоки (ноды).
98
98
 
99
99
  * **Создание команд**: Спроектируйте полноценную команду с аргументами, проверками прав и сложной логикой, не прикасаясь к коду.
100
- * **Обработка событий**: Создавайте графы, которые реагируют на игровые события (например, вход игрока, сообщение в чате, появление моба) и выполняют заданные действия.
101
- * **Наглядность**: Весь поток выполнения виден на одном экране, что упрощает понимание и отладку логики.
100
+ * **Обработка событий**: Создавайте ноды, которые реагируют на игровые события (например, вход игрока, сообщение в чате, появление моба) и выполняют заданные действия.
102
101
 
103
102
  ### 🔌 Плагины
104
103
  Плагины — это способ программного расширения функциональности. Они могут добавлять новые команды, новые ноды для визуального редактора или работать в фоновом режиме.
@@ -143,4 +142,4 @@ npm run dev
143
142
  ```
144
143
 
145
144
  * **Бэкенд** будет доступен на `http://localhost:3001`.
146
- * **Фронтенд** с горячей перезагрузкой будет доступен на `http://localhost:5173`. Открывайте этот адрес для разработки.
145
+ * **Фронтенд** с горячей перезагрузкой будет доступен на `http://localhost:5173`. Открывайте этот адрес для разработки.
@@ -1,4 +1,3 @@
1
-
2
1
  const express = require('express');
3
2
  const bcrypt = require('bcryptjs');
4
3
  const jwt = require('jsonwebtoken');
@@ -49,12 +48,16 @@ router.post('/setup', async (req, res) => {
49
48
  let newUser;
50
49
 
51
50
  await prisma.$transaction(async (tx) => {
51
+ const adminPermissions = ALL_PERMISSIONS
52
+ .map(p => p.id)
53
+ .filter(id => id !== '*' && id !== 'plugin:develop');
54
+
52
55
  const adminRole = await tx.panelRole.upsert({
53
56
  where: { name: 'Admin' },
54
57
  update: {},
55
58
  create: {
56
59
  name: 'Admin',
57
- permissions: JSON.stringify(['*'])
60
+ permissions: JSON.stringify(adminPermissions)
58
61
  },
59
62
  });
60
63
 
@@ -265,6 +268,7 @@ const ALL_PERMISSIONS = [
265
268
  { id: 'plugin:settings:view', label: 'Просмотр настроек плагинов' },
266
269
  { id: 'plugin:settings:edit', label: 'Редактирование настроек плагинов' },
267
270
  { id: 'plugin:browse', label: 'Просмотр каталога плагинов' },
271
+ { id: 'plugin:develop', label: 'Разработка и редактирование плагинов (IDE)' },
268
272
  { id: 'server:list', label: 'Просмотр серверов' },
269
273
  { id: 'server:create', label: 'Создание серверов' },
270
274
  { id: 'server:delete', label: 'Удаление серверов' },
@@ -442,4 +446,7 @@ router.delete('/roles/:id', authenticate, authorize('panel:role:delete'), async
442
446
  }
443
447
  });
444
448
 
445
- module.exports = router;
449
+ module.exports = {
450
+ router,
451
+ ALL_PERMISSIONS,
452
+ };
@@ -1,5 +1,5 @@
1
1
  const express = require('express');
2
- const { PrismaClient } = require('@prisma/client');
2
+ const prisma = require('../../lib/prisma');
3
3
  const path = require('path');
4
4
  const fs = require('fs/promises');
5
5
  const { botManager, pluginManager } = require('../../core/services');
@@ -10,18 +10,19 @@ const { authenticate, authorize } = require('../middleware/auth');
10
10
  const { encrypt } = require('../../core/utils/crypto');
11
11
  const { randomUUID } = require('crypto');
12
12
  const eventGraphsRouter = require('./eventGraphs');
13
+ const pluginIdeRouter = require('./pluginIde');
13
14
 
14
15
  const multer = require('multer');
15
16
  const archiver = require('archiver');
16
17
  const AdmZip = require('adm-zip');
17
18
 
18
- const prisma = new PrismaClient();
19
19
  const upload = multer({ storage: multer.memoryStorage() });
20
20
 
21
21
  const router = express.Router();
22
22
 
23
23
  router.use(authenticate);
24
24
  router.use('/:botId/event-graphs', eventGraphsRouter);
25
+ router.use('/:botId/plugins/ide', pluginIdeRouter);
25
26
 
26
27
  async function setupDefaultPermissionsForBot(botId, prismaClient = prisma) {
27
28
  const initialData = {
@@ -171,16 +172,26 @@ router.post('/:id/start', authorize('bot:start_stop'), async (req, res) => {
171
172
  try {
172
173
  const botId = parseInt(req.params.id, 10);
173
174
  const botConfig = await prisma.bot.findUnique({ where: { id: botId }, include: { server: true } });
174
- if (!botConfig) return res.status(404).json({ error: 'Бот не найден' });
175
- const result = await botManager.startBot(botConfig);
176
- res.json(result);
177
- } catch (error) { res.status(500).json({ error: 'Ошибка при запуске бота: ' + error.message }); }
175
+ if (!botConfig) {
176
+ return res.status(404).json({ success: false, message: 'Бот не найден' });
177
+ }
178
+ botManager.startBot(botConfig);
179
+ res.status(202).json({ success: true, message: 'Команда на запуск отправлена.' });
180
+ } catch (error) {
181
+ console.error(`[API] Ошибка запуска бота ${req.params.id}:`, error);
182
+ res.status(500).json({ success: false, message: 'Ошибка при запуске бота: ' + error.message });
183
+ }
178
184
  });
179
185
 
180
186
  router.post('/:id/stop', authorize('bot:start_stop'), (req, res) => {
181
- const botId = parseInt(req.params.id, 10);
182
- const result = botManager.stopBot(botId);
183
- res.json(result);
187
+ try {
188
+ const botId = parseInt(req.params.id, 10);
189
+ botManager.stopBot(botId);
190
+ res.status(202).json({ success: true, message: 'Команда на остановку отправлена.' });
191
+ } catch (error) {
192
+ console.error(`[API] Ошибка остановки бота ${req.params.id}:`, error);
193
+ res.status(500).json({ success: false, message: 'Ошибка при остановке бота: ' + error.message });
194
+ }
184
195
  });
185
196
 
186
197
  router.post('/:id/chat', authorize('bot:interact'), (req, res) => {
@@ -866,7 +877,6 @@ router.post('/:botId/commands/import', authorize('management:edit'), async (req,
866
877
  const graph = JSON.parse(finalGraphJson);
867
878
  const nodeIdMap = new Map();
868
879
 
869
- // Рандомизация ID узлов
870
880
  if (graph.nodes) {
871
881
  graph.nodes.forEach(node => {
872
882
  const oldId = node.id;
@@ -876,7 +886,6 @@ router.post('/:botId/commands/import', authorize('management:edit'), async (req,
876
886
  });
877
887
  }
878
888
 
879
- // Обновление и рандомизация ID связей
880
889
  if (graph.connections) {
881
890
  graph.connections.forEach(conn => {
882
891
  conn.id = `edge-${randomUUID()}`;
@@ -0,0 +1,463 @@
1
+ const express = require('express');
2
+ const { authenticate, authorize } = require('../middleware/auth');
3
+ const fse = require('fs-extra');
4
+ const path = require('path');
5
+ const os = require('os');
6
+ const { PrismaClient } = require('@prisma/client');
7
+ const slugify = require('slugify');
8
+
9
+ const prisma = new PrismaClient();
10
+ const router = express.Router({ mergeParams: true });
11
+
12
+ const DATA_DIR = path.join(os.homedir(), '.blockmine');
13
+ const PLUGINS_BASE_DIR = path.join(DATA_DIR, 'storage', 'plugins');
14
+
15
+ // All routes in this file require plugin development permission
16
+ router.use(authenticate, authorize('plugin:develop'));
17
+
18
+ // Middleware to resolve plugin path and ensure it's safe
19
+ const resolvePluginPath = async (req, res, next) => {
20
+ try {
21
+ const { botId, pluginName } = req.params;
22
+ if (!pluginName) {
23
+ return res.status(400).json({ error: 'Имя плагина обязательно в пути.' });
24
+ }
25
+
26
+ const botPluginsDir = path.join(PLUGINS_BASE_DIR, `bot_${botId}`);
27
+ const pluginPath = path.resolve(botPluginsDir, pluginName);
28
+
29
+ // Security check: ensure the resolved path is still within the bot's plugins directory
30
+ if (!pluginPath.startsWith(botPluginsDir)) {
31
+ return res.status(403).json({ error: 'Доступ запрещен: попытка доступа за пределы директории плагина.' });
32
+ }
33
+
34
+ if (!await fse.pathExists(pluginPath)) {
35
+ return res.status(404).json({ error: 'Директория плагина не найдена.' });
36
+ }
37
+
38
+ // Attach the safe path to the request object
39
+ req.pluginPath = pluginPath;
40
+ next();
41
+ } catch (error) {
42
+ console.error('[Plugin IDE Middleware Error]', error);
43
+ res.status(500).json({ error: 'Не удалось определить путь к плагину.' });
44
+ }
45
+ };
46
+
47
+ router.post('/create', async (req, res) => {
48
+ try {
49
+ const { botId } = req.params;
50
+ const {
51
+ name,
52
+ version = '1.0.0',
53
+ description = '',
54
+ author = '',
55
+ template = 'empty' // 'empty' or 'command'
56
+ } = req.body;
57
+
58
+ if (!name) {
59
+ return res.status(400).json({ error: 'Имя плагина обязательно.' });
60
+ }
61
+
62
+ const pluginNameSlug = slugify(name, { lower: true, strict: true });
63
+ const botPluginsDir = path.join(PLUGINS_BASE_DIR, `bot_${botId}`);
64
+ const pluginPath = path.join(botPluginsDir, pluginNameSlug);
65
+
66
+ if (await fse.pathExists(pluginPath)) {
67
+ return res.status(409).json({ error: `Плагин с именем "${pluginNameSlug}" уже существует физически.` });
68
+ }
69
+
70
+ const existingPlugin = await prisma.installedPlugin.findFirst({
71
+ where: { botId: parseInt(botId), name: pluginNameSlug }
72
+ });
73
+
74
+ if (existingPlugin) {
75
+ return res.status(409).json({ error: `Плагин с именем "${pluginNameSlug}" уже зарегистрирован для этого бота.` });
76
+ }
77
+
78
+ await fse.mkdirp(pluginPath);
79
+
80
+ const packageJson = {
81
+ name: pluginNameSlug,
82
+ version,
83
+ description,
84
+ author,
85
+ botpanel: {
86
+ main: 'index.js',
87
+ settings: {
88
+ "helloMessage": {
89
+ "type": "string",
90
+ "label": "Сообщение приветствия",
91
+ "description": "Используйте {targetUser} и {user.username} для подстановок.",
92
+ "default": "Привет, {targetUser}, от {user.username}!"
93
+ }
94
+ }
95
+ }
96
+ };
97
+ await fse.writeFile(path.join(pluginPath, 'package.json'), JSON.stringify(packageJson, null, 2));
98
+
99
+ if (template === 'command') {
100
+ await fse.mkdirp(path.join(pluginPath, 'commands'));
101
+
102
+ const commandContent = `module.exports = (bot) => {
103
+ const Command = bot.api.Command;
104
+
105
+ class HelloCommand extends Command {
106
+ constructor(settings) {
107
+ super({
108
+ name: 'hello',
109
+ description: 'Тестовая команда с расширенными параметрами',
110
+ aliases: ['hi', 'привет'],
111
+ permissions: '${pluginNameSlug}.use',
112
+ owner: 'plugin:${pluginNameSlug}',
113
+ cooldown: 10,
114
+ allowedChatTypes: ['chat'],
115
+ args: [
116
+ {
117
+ name: 'targetUser',
118
+ type: 'string',
119
+ description: 'Имя игрока, которого нужно поприветствовать.',
120
+ required: true
121
+ },
122
+ {
123
+ name: 'repeat',
124
+ type: 'number',
125
+ description: 'Количество повторений приветствия.',
126
+ required: false,
127
+ default: 1
128
+ }
129
+ ]
130
+ });
131
+
132
+ this.settings = settings;
133
+ }
134
+
135
+ async handler(bot, typeChat, user, args) {
136
+ const { targetUser, repeat } = args;
137
+
138
+ // Используем сообщение из настроек, или значение по-умолчанию, если оно не найдено
139
+ const messageTemplate = this.settings?.helloMessage || 'Привет, {targetUser}, от {user.username}!';
140
+
141
+ const message = messageTemplate
142
+ .replace('{targetUser}', targetUser)
143
+ .replace('{user.username}', user.username);
144
+
145
+ for (let i = 0; i < repeat; i++) {
146
+ await bot.api.sendMessage(typeChat, message, user.username);
147
+ }
148
+ }
149
+ }
150
+
151
+ return HelloCommand;
152
+ };`;
153
+ await fse.writeFile(path.join(pluginPath, 'commands', 'hello.js'), commandContent);
154
+
155
+ const indexJsContent = `const createHelloCommand = require('./commands/hello.js');
156
+
157
+ const PERMISSION_NAME = '${pluginNameSlug}';
158
+ const PLUGIN_OWNER_ID = 'plugin:${pluginNameSlug}';
159
+
160
+ async function onLoad(bot, options) {
161
+ const log = bot.sendLog;
162
+ const settings = options.settings;
163
+
164
+ try {
165
+ const HelloCommand = createHelloCommand(bot);
166
+
167
+ // Регистрация прав.
168
+ // Если право, указанное в команде, не будет найдено,
169
+ // система создаст его автоматически.
170
+ // Эта регистрация нужна для того, чтобы задать правам описание.
171
+ await bot.api.registerPermissions([
172
+ {
173
+ name: PERMISSION_NAME + '.use',
174
+ description: 'Доступ к команде hello из плагина ${pluginNameSlug}',
175
+ owner: PLUGIN_OWNER_ID
176
+ }
177
+ ]);
178
+
179
+ await bot.api.registerCommand(new HelloCommand(settings));
180
+
181
+ log(\`[\${PLUGIN_OWNER_ID}] Команда 'hello' успешно загружена.\`);
182
+ } catch (error) {
183
+ log(\`[\${PLUGIN_OWNER_ID}] Ошибка при загрузке: \${error.message}\`);
184
+ }
185
+ }
186
+
187
+ async function onUnload({ botId, prisma }) {
188
+ try {
189
+ await prisma.command.deleteMany({ where: { botId, owner: PLUGIN_OWNER_ID } });
190
+ await prisma.permission.deleteMany({ where: { botId, owner: PLUGIN_OWNER_ID } });
191
+ console.log(\`[\${PLUGIN_OWNER_ID}] Ресурсы для бота ID \${botId} успешно удалены.\`);
192
+ } catch (error) {
193
+ console.error(\`[\${PLUGIN_OWNER_ID}] Ошибка при очистке ресурсов:\`, error);
194
+ }
195
+ }
196
+
197
+ module.exports = {
198
+ onLoad,
199
+ onUnload,
200
+ };`;
201
+ await fse.writeFile(path.join(pluginPath, 'index.js'), indexJsContent);
202
+ } else {
203
+ const indexJsContent = `module.exports = {
204
+ onLoad: () => {
205
+ console.log(\`Плагин "${pluginNameSlug}" загружен.\`);
206
+ },
207
+ onUnload: () => {
208
+ console.log(\`Плагин "${pluginNameSlug}" выгружен.\`);
209
+ }
210
+ };`;
211
+ await fse.writeFile(path.join(pluginPath, 'index.js'), indexJsContent);
212
+ }
213
+
214
+ const newPlugin = await prisma.installedPlugin.create({
215
+ data: {
216
+ botId: parseInt(botId),
217
+ name: pluginNameSlug,
218
+ version,
219
+ description,
220
+ path: pluginPath,
221
+ sourceType: 'LOCAL_IDE',
222
+ sourceUri: pluginPath,
223
+ manifest: JSON.stringify(packageJson.botpanel),
224
+ isEnabled: true
225
+ }
226
+ });
227
+
228
+ res.status(201).json(newPlugin);
229
+
230
+ } catch (error) {
231
+ console.error('[Plugin IDE Error] /create:', error);
232
+ res.status(500).json({ error: 'Не удалось создать плагин.' });
233
+ }
234
+ });
235
+
236
+ const readDirectoryRecursive = async (basePath, currentPath = '') => {
237
+ const absolutePath = path.join(basePath, currentPath);
238
+ const dirents = await fse.readdir(absolutePath, { withFileTypes: true });
239
+ const files = await Promise.all(dirents.map(async (dirent) => {
240
+ const direntPath = path.join(currentPath, dirent.name);
241
+ if (dirent.isDirectory()) {
242
+ return {
243
+ name: dirent.name,
244
+ type: 'folder',
245
+ path: direntPath.replace(/\\\\/g, '/'),
246
+ children: await readDirectoryRecursive(basePath, direntPath)
247
+ };
248
+ } else {
249
+ return {
250
+ name: dirent.name,
251
+ type: 'file',
252
+ path: direntPath.replace(/\\\\/g, '/'),
253
+ };
254
+ }
255
+ }));
256
+ return files.sort((a, b) => {
257
+ if (a.type === b.type) return a.name.localeCompare(b.name);
258
+ return a.type === 'folder' ? -1 : 1;
259
+ });
260
+ };
261
+
262
+
263
+ router.get('/:pluginName/structure', resolvePluginPath, async (req, res) => {
264
+ try {
265
+ const structure = await readDirectoryRecursive(req.pluginPath);
266
+ res.json(structure);
267
+ } catch (error) {
268
+ console.error(`[Plugin IDE Error] /structure for ${req.params.pluginName}:`, error);
269
+ res.status(500).json({ error: 'Failed to read plugin structure.' });
270
+ }
271
+ });
272
+
273
+ router.get('/:pluginName/file', resolvePluginPath, async (req, res) => {
274
+ try {
275
+ const filePath = path.join(req.pluginPath, req.query.path);
276
+ if (!filePath.startsWith(req.pluginPath)) {
277
+ return res.status(403).json({ error: 'Доступ запрещен.' });
278
+ }
279
+ const content = await fse.readFile(filePath, 'utf-8');
280
+ res.send(content);
281
+ } catch (error) {
282
+ res.status(500).json({ error: 'Не удалось прочитать файл.' });
283
+ }
284
+ });
285
+
286
+ router.post('/:pluginName/file', resolvePluginPath, async (req, res) => {
287
+ try {
288
+ const { path: relativePath, content } = req.body;
289
+ if (!relativePath || content === undefined) {
290
+ return res.status(400).json({ error: 'File path and content are required.' });
291
+ }
292
+
293
+ const safePath = path.resolve(req.pluginPath, relativePath);
294
+
295
+ if (!safePath.startsWith(req.pluginPath)) {
296
+ return res.status(403).json({ error: 'Доступ запрещен.' });
297
+ }
298
+
299
+ await fse.writeFile(safePath, content, 'utf-8');
300
+ res.status(200).json({ message: 'Файл успешно сохранен.' });
301
+
302
+ } catch (error) {
303
+ console.error(`[Plugin IDE Error] /file POST for ${req.params.pluginName}:`, error);
304
+ res.status(500).json({ error: 'Не удалось сохранить файл.' });
305
+ }
306
+ });
307
+
308
+ router.post('/:pluginName/fs', resolvePluginPath, async (req, res) => {
309
+ try {
310
+ const { operation, path: relativePath, newPath, content } = req.body;
311
+
312
+ if (!operation || !relativePath) {
313
+ return res.status(400).json({ error: 'Operation and path are required.' });
314
+ }
315
+
316
+ const safePath = path.resolve(req.pluginPath, relativePath);
317
+ if (!safePath.startsWith(req.pluginPath)) {
318
+ return res.status(403).json({ error: 'Access denied: Path is outside of plugin directory.' });
319
+ }
320
+
321
+ switch (operation) {
322
+ case 'createFile':
323
+ if (await fse.pathExists(safePath)) return res.status(409).json({ error: 'File already exists.' });
324
+ await fse.writeFile(safePath, content || '', 'utf-8');
325
+ res.status(201).json({ message: 'File created successfully.' });
326
+ break;
327
+
328
+ case 'createFolder':
329
+ if (await fse.pathExists(safePath)) return res.status(409).json({ error: 'Folder already exists.' });
330
+ await fse.mkdirp(safePath);
331
+ res.status(201).json({ message: 'Folder created successfully.' });
332
+ break;
333
+
334
+ case 'delete':
335
+ if (!await fse.pathExists(safePath)) return res.status(404).json({ error: 'File or folder not found.' });
336
+ await fse.remove(safePath);
337
+ res.status(200).json({ message: 'File or folder deleted successfully.' });
338
+ break;
339
+
340
+ case 'rename':
341
+ if (!newPath) return res.status(400).json({ error: 'New path is required for rename operation.' });
342
+ const safeNewPath = path.resolve(req.pluginPath, newPath);
343
+ if (!safeNewPath.startsWith(req.pluginPath)) return res.status(403).json({ error: 'Access denied: New path is outside of plugin directory.' });
344
+ if (!await fse.pathExists(safePath)) return res.status(404).json({ error: 'Source file or folder not found.' });
345
+ if (await fse.pathExists(safeNewPath)) return res.status(409).json({ error: 'Destination path already exists.' });
346
+
347
+ await fse.move(safePath, safeNewPath);
348
+ res.status(200).json({ message: 'Renamed successfully.' });
349
+ break;
350
+
351
+ default:
352
+ res.status(400).json({ error: 'Invalid operation specified.' });
353
+ }
354
+ } catch (error) {
355
+ console.error(`[Plugin IDE Error] /fs POST for ${req.params.pluginName}:`, error);
356
+ res.status(500).json({ error: 'File system operation failed.' });
357
+ }
358
+ });
359
+
360
+ router.get('/:pluginName/manifest', resolvePluginPath, async (req, res) => {
361
+ try {
362
+ const packageJsonPath = path.join(req.pluginPath, 'package.json');
363
+ const packageJson = await fse.readJson(packageJsonPath);
364
+ res.json(packageJson);
365
+ } catch (error) {
366
+ res.status(500).json({ error: 'Не удалось прочитать package.json.' });
367
+ }
368
+ });
369
+
370
+ router.post('/:pluginName/manifest', resolvePluginPath, async (req, res) => {
371
+ try {
372
+ const manifestPath = path.join(req.pluginPath, 'package.json');
373
+ if (!await fse.pathExists(manifestPath)) {
374
+ return res.status(404).json({ error: 'package.json not found.' });
375
+ }
376
+
377
+ const currentManifest = await fse.readJson(manifestPath);
378
+ const { name, version, description, author } = req.body;
379
+
380
+ const newManifest = {
381
+ ...currentManifest,
382
+ name: name || currentManifest.name,
383
+ version: version || currentManifest.version,
384
+ description: description,
385
+ author: author,
386
+ };
387
+
388
+ await fse.writeJson(manifestPath, newManifest, { spaces: 2 });
389
+
390
+ // Also update the DB record
391
+ await prisma.installedPlugin.updateMany({
392
+ where: {
393
+ botId: parseInt(req.params.botId),
394
+ path: req.pluginPath,
395
+ },
396
+ data: {
397
+ name: newManifest.name,
398
+ version: newManifest.version,
399
+ description: newManifest.description,
400
+ }
401
+ });
402
+
403
+ res.status(200).json({ message: 'package.json успешно обновлен.' });
404
+ } catch (error) {
405
+ console.error(`[Plugin IDE Error] /manifest POST for ${req.params.pluginName}:`, error);
406
+ res.status(500).json({ error: 'Не удалось обновить package.json.' });
407
+ }
408
+ });
409
+
410
+ router.post('/:pluginName/fork', resolvePluginPath, async (req, res) => {
411
+ try {
412
+ const { botId, pluginName } = req.params;
413
+ const currentPlugin = await prisma.installedPlugin.findFirst({
414
+ where: { botId: parseInt(botId), name: pluginName }
415
+ });
416
+
417
+ if (!currentPlugin || currentPlugin.sourceType !== 'GITHUB') {
418
+ return res.status(400).json({ error: 'Копировать можно только плагины, установленные из GitHub.' });
419
+ }
420
+
421
+ const originalPath = req.pluginPath;
422
+ let newName = `${pluginName}-copy`;
423
+ let newPath = path.join(path.dirname(originalPath), newName);
424
+ let counter = 1;
425
+
426
+ while (await fse.pathExists(newPath) || await prisma.installedPlugin.findFirst({ where: { botId: parseInt(botId), name: newName } })) {
427
+ newName = `${pluginName}-copy-${counter}`;
428
+ newPath = path.join(path.dirname(originalPath), newName);
429
+ counter++;
430
+ }
431
+
432
+ await fse.copy(originalPath, newPath);
433
+
434
+ const packageJsonPath = path.join(newPath, 'package.json');
435
+ const packageJson = await fse.readJson(packageJsonPath);
436
+
437
+ packageJson.name = newName;
438
+ packageJson.description = `(Forked from ${pluginName}) ${packageJson.description || ''}`;
439
+ await fse.writeJson(packageJsonPath, packageJson, { spaces: 2 });
440
+
441
+ const forkedPlugin = await prisma.installedPlugin.create({
442
+ data: {
443
+ botId: parseInt(botId),
444
+ name: newName,
445
+ version: packageJson.version,
446
+ description: packageJson.description,
447
+ path: newPath,
448
+ sourceType: 'LOCAL_IDE',
449
+ sourceUri: newPath,
450
+ manifest: JSON.stringify(packageJson.botpanel || {}),
451
+ isEnabled: false // Forked plugins are disabled by default
452
+ }
453
+ });
454
+
455
+ res.status(201).json(forkedPlugin);
456
+
457
+ } catch (error) {
458
+ console.error(`[Plugin IDE Error] /fork POST for ${req.params.pluginName}:`, error);
459
+ res.status(500).json({ error: 'Не удалось скопировать плагин.' });
460
+ }
461
+ });
462
+
463
+ module.exports = router;
@@ -7,12 +7,10 @@ const { pluginManager } = require('../../core/services');
7
7
  const prisma = new PrismaClient();
8
8
  const OFFICIAL_CATALOG_URL = "https://raw.githubusercontent.com/blockmineJS/official-plugins-list/main/index.json";
9
9
 
10
- router.use(authenticate);
11
-
12
10
  const getCacheBustedUrl = (url) => `${url}?t=${new Date().getTime()}`;
13
11
 
14
12
 
15
- router.get('/catalog', authorize('plugin:browse'), async (req, res) => {
13
+ router.get('/catalog', async (req, res) => {
16
14
  try {
17
15
  const response = await fetch(getCacheBustedUrl(OFFICIAL_CATALOG_URL));
18
16
 
@@ -29,7 +27,7 @@ router.get('/catalog', authorize('plugin:browse'), async (req, res) => {
29
27
  }
30
28
  });
31
29
 
32
- router.post('/check-updates/:botId', authorize('plugin:update'), async (req, res) => {
30
+ router.post('/check-updates/:botId', authenticate, authorize('plugin:update'), async (req, res) => {
33
31
  try {
34
32
  const botId = parseInt(req.params.botId);
35
33
 
@@ -45,7 +43,7 @@ router.post('/check-updates/:botId', authorize('plugin:update'), async (req, res
45
43
  }
46
44
  });
47
45
 
48
- router.post('/update/:pluginId', authorize('plugin:update'), async (req, res) => {
46
+ router.post('/update/:pluginId', authenticate, authorize('plugin:update'), async (req, res) => {
49
47
  try {
50
48
  const pluginId = parseInt(req.params.pluginId);
51
49
  const updatedPlugin = await pluginManager.updatePlugin(pluginId);
@@ -55,7 +53,7 @@ router.post('/update/:pluginId', authorize('plugin:update'), async (req, res) =>
55
53
  }
56
54
  });
57
55
 
58
- router.get('/catalog/:name', authorize('plugin:browse'), async (req, res) => {
56
+ router.get('/catalog/:name', async (req, res) => {
59
57
  try {
60
58
  const pluginName = req.params.name;
61
59