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 +2 -3
- package/backend/src/api/routes/auth.js +10 -3
- package/backend/src/api/routes/bots.js +20 -11
- package/backend/src/api/routes/pluginIde.js +463 -0
- package/backend/src/api/routes/plugins.js +4 -6
- package/backend/src/core/BotManager.js +707 -714
- package/backend/src/core/BotProcess.js +5 -0
- package/backend/src/core/EventGraphManager.js +3 -0
- package/backend/src/core/GraphExecutionEngine.js +18 -9
- package/backend/src/core/NodeRegistry.js +14 -0
- package/backend/src/core/PluginManager.js +7 -3
- package/backend/src/core/TaskScheduler.js +8 -3
- package/backend/src/lib/prisma.js +5 -0
- package/backend/src/real-time/socketHandler.js +9 -11
- package/backend/src/server.js +63 -19
- package/frontend/dist/assets/index-Cb7r5FoV.js +8203 -0
- package/frontend/dist/assets/index-OIucIMTn.css +1 -0
- package/frontend/dist/index.html +2 -2
- package/frontend/package.json +2 -0
- package/package.json +4 -1
- package/frontend/dist/assets/index-BSQ59n1K.js +0 -8203
- package/frontend/dist/assets/index-DC4RjP6E.css +0 -1
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 =
|
|
449
|
+
module.exports = {
|
|
450
|
+
router,
|
|
451
|
+
ALL_PERMISSIONS,
|
|
452
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const express = require('express');
|
|
2
|
-
const
|
|
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)
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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',
|
|
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',
|
|
56
|
+
router.get('/catalog/:name', async (req, res) => {
|
|
59
57
|
try {
|
|
60
58
|
const pluginName = req.params.name;
|
|
61
59
|
|