blockmine 1.18.4 → 1.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.kiro/steering/product.md +27 -0
- package/.kiro/steering/structure.md +89 -0
- package/.kiro/steering/tech.md +94 -0
- package/CHANGELOG.md +147 -112
- package/backend/cli.js +59 -57
- package/backend/prisma/migrations/20250701190321_add/migration.sql +2 -2
- package/backend/prisma/migrations/20250709150611_add_run_on_startup_to_tasks/migration.sql +21 -21
- package/backend/prisma/migrations/20250709151124_make_cron_pattern_optional/migration.sql +21 -21
- package/backend/prisma/migrations/20250718181335_add_plugin_data_store/migration.sql +14 -14
- package/backend/prisma/migrations/20250719115906_add_plugin_owner_to_graphs/migration.sql +45 -45
- package/backend/prisma/migrations/20250723160648_add_bot_sort_order/migration.sql +2 -2
- package/backend/prisma/migrations/20250816083216_add_panel_user_bot_access/migration.sql +30 -0
- package/backend/prisma/migrations/migration_lock.toml +2 -2
- package/backend/prisma/schema.prisma +244 -229
- package/backend/src/api/middleware/botAccess.js +35 -0
- package/backend/src/api/routes/auth.js +633 -595
- package/backend/src/api/routes/bots.js +294 -179
- package/backend/src/api/routes/eventGraphs.js +459 -459
- package/backend/src/api/routes/pluginIde.js +6 -2
- package/backend/src/api/routes/servers.js +27 -0
- package/backend/src/core/BotManager.js +0 -1
- package/backend/src/core/BotProcess.js +1 -2
- package/backend/src/core/GraphExecutionEngine.js +917 -917
- package/backend/src/core/PluginLoader.js +208 -86
- package/backend/src/core/PluginManager.js +465 -427
- package/backend/src/core/commands/dev.js +6 -1
- package/backend/src/real-time/presence.js +74 -0
- package/backend/src/real-time/socketHandler.js +2 -0
- package/backend/src/server.js +193 -186
- package/frontend/dist/assets/index-BFd7YoAj.css +1 -0
- package/frontend/dist/assets/index-DxdxTe6I.js +8352 -0
- package/frontend/dist/index.html +2 -2
- package/package.json +1 -1
- package/frontend/dist/assets/index-CA3XrPPP.css +0 -1
- package/frontend/dist/assets/index-CM9ljR30.js +0 -8352
|
@@ -1,918 +1,918 @@
|
|
|
1
|
-
const User = require('./UserService');
|
|
2
|
-
const { PrismaClient } = require('@prisma/client');
|
|
3
|
-
const prisma = new PrismaClient();
|
|
4
|
-
|
|
5
|
-
class BreakLoopSignal extends Error {
|
|
6
|
-
constructor() {
|
|
7
|
-
super("Loop break signal");
|
|
8
|
-
this.name = "BreakLoopSignal";
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
class GraphExecutionEngine {
|
|
13
|
-
constructor(nodeRegistry, botManagerOrApi = null) {
|
|
14
|
-
if (!nodeRegistry || typeof nodeRegistry.getNodeConfig !== 'function') {
|
|
15
|
-
throw new Error('GraphExecutionEngine requires a valid NodeRegistry instance.');
|
|
16
|
-
}
|
|
17
|
-
this.nodeRegistry = nodeRegistry;
|
|
18
|
-
this.botManager = botManagerOrApi;
|
|
19
|
-
this.activeGraph = null;
|
|
20
|
-
this.context = null;
|
|
21
|
-
this.memo = new Map();
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
async execute(graph, context, eventType) {
|
|
25
|
-
if (!graph || graph === 'null') return context;
|
|
26
|
-
|
|
27
|
-
let parsedGraph;
|
|
28
|
-
if (typeof graph === 'string') {
|
|
29
|
-
try {
|
|
30
|
-
parsedGraph = JSON.parse(graph);
|
|
31
|
-
} catch (e) {
|
|
32
|
-
console.error('[GraphExecutionEngine] Ошибка парсинга JSON графа:', e);
|
|
33
|
-
return context;
|
|
34
|
-
}
|
|
35
|
-
} else {
|
|
36
|
-
parsedGraph = graph;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
if (!parsedGraph.nodes || !parsedGraph.connections) {
|
|
40
|
-
console.error('[GraphExecutionEngine] Неверный формат графа. Отсутствуют nodes или connections.');
|
|
41
|
-
return context;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
try {
|
|
45
|
-
this.activeGraph = parsedGraph;
|
|
46
|
-
this.context = context;
|
|
47
|
-
|
|
48
|
-
if (!this.context.variables) {
|
|
49
|
-
this.context.variables = {};
|
|
50
|
-
if (Array.isArray(this.activeGraph.variables)) {
|
|
51
|
-
for (const variable of this.activeGraph.variables) {
|
|
52
|
-
let value = variable.value;
|
|
53
|
-
try {
|
|
54
|
-
switch(variable.type) {
|
|
55
|
-
case 'number':
|
|
56
|
-
value = Number(value);
|
|
57
|
-
break;
|
|
58
|
-
case 'boolean':
|
|
59
|
-
value = value === 'true';
|
|
60
|
-
break;
|
|
61
|
-
case 'array':
|
|
62
|
-
value = JSON.parse(value);
|
|
63
|
-
break;
|
|
64
|
-
}
|
|
65
|
-
} catch (e) {
|
|
66
|
-
console.error(`Error parsing variable default value for ${variable.name}:`, e);
|
|
67
|
-
}
|
|
68
|
-
this.context.variables[variable.name] = value;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
if (!this.context.persistenceIntent) this.context.persistenceIntent = new Map();
|
|
74
|
-
this.memo.clear();
|
|
75
|
-
|
|
76
|
-
const eventName = eventType || 'command';
|
|
77
|
-
const startNode = this.activeGraph.nodes.find(n => n.type === `event:${eventName}`);
|
|
78
|
-
|
|
79
|
-
if (startNode) {
|
|
80
|
-
await this.traverse(startNode, 'exec');
|
|
81
|
-
} else if (!eventType) {
|
|
82
|
-
throw new Error(`Не найдена стартовая нода события (event:command) в графе.`);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
} catch (error) {
|
|
86
|
-
if (!(error instanceof BreakLoopSignal)) {
|
|
87
|
-
console.error(`[GraphExecutionEngine] Критическая ошибка выполнения графа: ${error.stack}`);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
return this.context;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
async traverse(node, fromPinId) {
|
|
95
|
-
const connection = this.activeGraph.connections.find(c => c.sourceNodeId === node.id && c.sourcePinId === fromPinId);
|
|
96
|
-
if (!connection) return;
|
|
97
|
-
|
|
98
|
-
const nextNode = this.activeGraph.nodes.find(n => n.id === connection.targetNodeId);
|
|
99
|
-
if (!nextNode) return;
|
|
100
|
-
|
|
101
|
-
await this.executeNode(nextNode);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
async executeNode(node) {
|
|
105
|
-
const execCacheKey = `${node.id}_executed`;
|
|
106
|
-
if (this.memo.has(execCacheKey)) {
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
this.memo.set(execCacheKey, true);
|
|
110
|
-
|
|
111
|
-
switch (node.type) {
|
|
112
|
-
case 'user:set_blacklist': {
|
|
113
|
-
const userObject = await this.resolvePinValue(node, 'user', null);
|
|
114
|
-
const blacklistStatus = await this.resolvePinValue(node, 'blacklist_status', false);
|
|
115
|
-
let updatedUser = null;
|
|
116
|
-
|
|
117
|
-
if (userObject && userObject.username) {
|
|
118
|
-
const user = await User.getUser(userObject.username, this.context.botId);
|
|
119
|
-
if (user) {
|
|
120
|
-
updatedUser = await prisma.user.update({
|
|
121
|
-
where: { id: user.id },
|
|
122
|
-
data: { isBlacklisted: blacklistStatus }
|
|
123
|
-
});
|
|
124
|
-
User.clearCache(user.username, this.context.botId);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
this.memo.set(`${node.id}:updated_user`, updatedUser);
|
|
128
|
-
await this.traverse(node, 'exec');
|
|
129
|
-
break;
|
|
130
|
-
}
|
|
131
|
-
case 'action:send_message': {
|
|
132
|
-
const message = String(await this.resolvePinValue(node, 'message', ''));
|
|
133
|
-
const chatType = await this.resolvePinValue(node, 'chat_type', this.context.typeChat);
|
|
134
|
-
const recipient = await this.resolvePinValue(node, 'recipient', this.context.user?.username);
|
|
135
|
-
this.context.bot.sendMessage(chatType, message, recipient);
|
|
136
|
-
await this.traverse(node, 'exec');
|
|
137
|
-
break;
|
|
138
|
-
}
|
|
139
|
-
case 'action:server_command': {
|
|
140
|
-
const command = await this.resolvePinValue(node, 'command', '');
|
|
141
|
-
if (command) this.context.bot.executeCommand(command);
|
|
142
|
-
await this.traverse(node, 'exec');
|
|
143
|
-
break;
|
|
144
|
-
}
|
|
145
|
-
case 'action:send_log': {
|
|
146
|
-
const message = await this.resolvePinValue(node, 'message', '');
|
|
147
|
-
if (this.botManager?.appendLog && this.context?.botId) {
|
|
148
|
-
this.botManager.appendLog(this.context.botId, `[Graph] ${message}`);
|
|
149
|
-
} else {
|
|
150
|
-
console.log(`[Graph Log] ${message}`);
|
|
151
|
-
}
|
|
152
|
-
await this.traverse(node, 'exec');
|
|
153
|
-
break;
|
|
154
|
-
}
|
|
155
|
-
case 'action:bot_look_at': {
|
|
156
|
-
const target = await this.resolvePinValue(node, 'target');
|
|
157
|
-
const yOffset = await this.resolvePinValue(node, 'add_y', 0);
|
|
158
|
-
|
|
159
|
-
if (target && this.context.bot?.lookAt) {
|
|
160
|
-
let finalPosition;
|
|
161
|
-
if (target.position) {
|
|
162
|
-
finalPosition = { ...target.position };
|
|
163
|
-
} else if (target.x !== undefined && target.y !== undefined && target.z !== undefined) {
|
|
164
|
-
finalPosition = { ...target };
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
if (finalPosition) {
|
|
168
|
-
finalPosition.y += Number(yOffset || 0);
|
|
169
|
-
this.context.bot.lookAt(finalPosition);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
await this.traverse(node, 'exec');
|
|
173
|
-
break;
|
|
174
|
-
}
|
|
175
|
-
case 'action:bot_set_variable': {
|
|
176
|
-
const varName = await this.resolvePinValue(node, 'name', '');
|
|
177
|
-
const varValue = await this.resolvePinValue(node, 'value');
|
|
178
|
-
let shouldPersist = await this.resolvePinValue(node, 'persist', false);
|
|
179
|
-
|
|
180
|
-
if (this.context.eventType === 'command') {
|
|
181
|
-
shouldPersist = false;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
if (varName) {
|
|
185
|
-
this.context.variables[varName] = varValue;
|
|
186
|
-
if (this.context.persistenceIntent) {
|
|
187
|
-
this.context.persistenceIntent.set(varName, shouldPersist);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
await this.traverse(node, 'exec');
|
|
191
|
-
break;
|
|
192
|
-
}
|
|
193
|
-
case 'flow:branch': {
|
|
194
|
-
const condition = await this.resolvePinValue(node, 'condition', false);
|
|
195
|
-
await this.traverse(node, condition ? 'exec_true' : 'exec_false');
|
|
196
|
-
break;
|
|
197
|
-
}
|
|
198
|
-
case 'flow:break': {
|
|
199
|
-
throw new BreakLoopSignal();
|
|
200
|
-
}
|
|
201
|
-
case 'flow:for_each': {
|
|
202
|
-
const array = await this.resolvePinValue(node, 'array', []);
|
|
203
|
-
if (Array.isArray(array)) {
|
|
204
|
-
try {
|
|
205
|
-
for (let i = 0; i < array.length; i++) {
|
|
206
|
-
const element = array[i];
|
|
207
|
-
this.memo.set(`${node.id}:element`, element);
|
|
208
|
-
this.memo.set(`${node.id}:index`, i);
|
|
209
|
-
this.clearLoopBodyMemo(node);
|
|
210
|
-
await this.traverse(node, 'loop_body');
|
|
211
|
-
}
|
|
212
|
-
} catch (e) {
|
|
213
|
-
if (e instanceof BreakLoopSignal) {
|
|
214
|
-
} else {
|
|
215
|
-
throw e;
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
await this.traverse(node, 'completed');
|
|
220
|
-
break;
|
|
221
|
-
}
|
|
222
|
-
case 'flow:while': {
|
|
223
|
-
let iteration = 0;
|
|
224
|
-
const maxIterations = 1000;
|
|
225
|
-
|
|
226
|
-
try {
|
|
227
|
-
while (iteration < maxIterations) {
|
|
228
|
-
const condition = await this.resolvePinValue(node, 'condition', false);
|
|
229
|
-
if (!condition) break;
|
|
230
|
-
|
|
231
|
-
this.memo.set(`${node.id}:iteration`, iteration);
|
|
232
|
-
this.clearLoopBodyMemo(node);
|
|
233
|
-
await this.traverse(node, 'loop_body');
|
|
234
|
-
iteration++;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
if (iteration >= maxIterations) {
|
|
238
|
-
console.warn(`[GraphExecutionEngine] Цикл while достиг максимального количества итераций (${maxIterations})`);
|
|
239
|
-
}
|
|
240
|
-
} catch (e) {
|
|
241
|
-
if (e instanceof BreakLoopSignal) {
|
|
242
|
-
} else {
|
|
243
|
-
throw e;
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
await this.traverse(node, 'completed');
|
|
248
|
-
break;
|
|
249
|
-
}
|
|
250
|
-
case 'flow:sequence': {
|
|
251
|
-
const pinCount = node.data?.pinCount || 2;
|
|
252
|
-
for (let i = 0; i < pinCount; i++) {
|
|
253
|
-
await this.traverse(node, `exec_${i}`);
|
|
254
|
-
}
|
|
255
|
-
break;
|
|
256
|
-
}
|
|
257
|
-
case 'flow:switch': {
|
|
258
|
-
const value = await this.resolvePinValue(node, 'value');
|
|
259
|
-
const caseCount = node.data?.caseCount || 0;
|
|
260
|
-
let matched = false;
|
|
261
|
-
|
|
262
|
-
for (let i = 0; i < caseCount; i++) {
|
|
263
|
-
const caseValue = node.data?.[`case_${i}`];
|
|
264
|
-
if (caseValue !== undefined) {
|
|
265
|
-
let isMatch = false;
|
|
266
|
-
|
|
267
|
-
if (Array.isArray(value) && Array.isArray(caseValue)) {
|
|
268
|
-
isMatch = JSON.stringify(value) === JSON.stringify(caseValue);
|
|
269
|
-
} else if (typeof value === 'object' && typeof caseValue === 'object' && value !== null && caseValue !== null) {
|
|
270
|
-
isMatch = JSON.stringify(value) === JSON.stringify(caseValue);
|
|
271
|
-
} else if (typeof value === 'number' && typeof caseValue === 'number') {
|
|
272
|
-
isMatch = value === caseValue;
|
|
273
|
-
} else if (typeof value === 'boolean' && typeof caseValue === 'boolean') {
|
|
274
|
-
isMatch = value === caseValue;
|
|
275
|
-
} else {
|
|
276
|
-
isMatch = String(value) === String(caseValue);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
if (isMatch) {
|
|
280
|
-
await this.traverse(node, `case_${i}`);
|
|
281
|
-
matched = true;
|
|
282
|
-
break;
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
if (!matched) {
|
|
288
|
-
await this.traverse(node, 'default');
|
|
289
|
-
}
|
|
290
|
-
break;
|
|
291
|
-
}
|
|
292
|
-
case 'debug:log': {
|
|
293
|
-
const value = await this.resolvePinValue(node, 'value');
|
|
294
|
-
console.log('[Graph Debug]', value);
|
|
295
|
-
await this.traverse(node, 'exec');
|
|
296
|
-
break;
|
|
297
|
-
}
|
|
298
|
-
case 'string:contains':
|
|
299
|
-
case 'string:matches':
|
|
300
|
-
case 'string:equals': {
|
|
301
|
-
await this.traverse(node, 'exec');
|
|
302
|
-
break;
|
|
303
|
-
}
|
|
304
|
-
case 'array:get_random_element': {
|
|
305
|
-
await this.traverse(node, 'element');
|
|
306
|
-
break;
|
|
307
|
-
}
|
|
308
|
-
case 'array:add_element':
|
|
309
|
-
case 'array:remove_by_index':
|
|
310
|
-
case 'array:get_by_index':
|
|
311
|
-
case 'array:find_index':
|
|
312
|
-
case 'array:contains': {
|
|
313
|
-
await this.traverse(node, 'result');
|
|
314
|
-
break;
|
|
315
|
-
}
|
|
316
|
-
case 'data:array_literal':
|
|
317
|
-
case 'data:make_object':
|
|
318
|
-
case 'data:get_variable':
|
|
319
|
-
case 'data:get_argument':
|
|
320
|
-
case 'data:length':
|
|
321
|
-
case 'data:get_entity_field':
|
|
322
|
-
case 'data:cast':
|
|
323
|
-
case 'data:string_literal':
|
|
324
|
-
case 'data:get_user_field':
|
|
325
|
-
case 'data:get_server_players':
|
|
326
|
-
case 'data:get_bot_look':
|
|
327
|
-
case 'math:operation':
|
|
328
|
-
case 'math:random_number':
|
|
329
|
-
case 'logic:operation':
|
|
330
|
-
case 'string:concat':
|
|
331
|
-
case 'object:create':
|
|
332
|
-
case 'object:get':
|
|
333
|
-
case 'object:set':
|
|
334
|
-
case 'object:delete':
|
|
335
|
-
case 'object:has_key': {
|
|
336
|
-
await this.traverse(node, 'value');
|
|
337
|
-
break;
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
clearLoopBodyMemo(loopNode) {
|
|
343
|
-
const nodesToClear = new Set();
|
|
344
|
-
const queue = [];
|
|
345
|
-
|
|
346
|
-
const initialConnection = this.activeGraph.connections.find(
|
|
347
|
-
c => c.sourceNodeId === loopNode.id && c.sourcePinId === 'loop_body'
|
|
348
|
-
);
|
|
349
|
-
if (initialConnection) {
|
|
350
|
-
const firstNode = this.activeGraph.nodes.find(n => n.id === initialConnection.targetNodeId);
|
|
351
|
-
if (firstNode) {
|
|
352
|
-
queue.push(firstNode);
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
const visited = new Set();
|
|
357
|
-
while (queue.length > 0) {
|
|
358
|
-
const currentNode = queue.shift();
|
|
359
|
-
if (visited.has(currentNode.id)) continue;
|
|
360
|
-
visited.add(currentNode.id);
|
|
361
|
-
|
|
362
|
-
nodesToClear.add(currentNode.id);
|
|
363
|
-
|
|
364
|
-
const connections = this.activeGraph.connections.filter(c => c.sourceNodeId === currentNode.id);
|
|
365
|
-
for (const conn of connections) {
|
|
366
|
-
const nextNode = this.activeGraph.nodes.find(n => n.id === conn.targetNodeId);
|
|
367
|
-
if (nextNode) {
|
|
368
|
-
queue.push(nextNode);
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
for (const nodeId of nodesToClear) {
|
|
374
|
-
for (const key of this.memo.keys()) {
|
|
375
|
-
if (key.startsWith(nodeId)) {
|
|
376
|
-
this.memo.delete(key);
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
async resolvePinValue(node, pinId, defaultValue = null) {
|
|
383
|
-
const connection = this.activeGraph.connections.find(c => c.targetNodeId === node.id && c.targetPinId === pinId);
|
|
384
|
-
if (connection) {
|
|
385
|
-
const sourceNode = this.activeGraph.nodes.find(n => n.id === connection.sourceNodeId);
|
|
386
|
-
return await this.evaluateOutputPin(sourceNode, connection.sourcePinId, defaultValue);
|
|
387
|
-
}
|
|
388
|
-
return node.data && node.data[pinId] !== undefined ? node.data[pinId] : defaultValue;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
async evaluateOutputPin(node, pinId, defaultValue = null) {
|
|
392
|
-
if (!node) return defaultValue;
|
|
393
|
-
|
|
394
|
-
const cacheKey = `${node.id}:${pinId}`;
|
|
395
|
-
if (this.memo.has(cacheKey)) {
|
|
396
|
-
return this.memo.get(cacheKey);
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
let result;
|
|
400
|
-
|
|
401
|
-
switch (node.type) {
|
|
402
|
-
case 'user:check_blacklist': {
|
|
403
|
-
const userIdentifier = await this.resolvePinValue(node, 'user', null);
|
|
404
|
-
let isBlacklisted = false;
|
|
405
|
-
let usernameToFind = null;
|
|
406
|
-
|
|
407
|
-
if (typeof userIdentifier === 'string') {
|
|
408
|
-
usernameToFind = userIdentifier;
|
|
409
|
-
} else if (userIdentifier && typeof userIdentifier === 'object' && userIdentifier.username) {
|
|
410
|
-
usernameToFind = userIdentifier.username;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
if (usernameToFind) {
|
|
414
|
-
const user = await User.getUser(usernameToFind, this.context.botId);
|
|
415
|
-
if (user) {
|
|
416
|
-
isBlacklisted = user.isBlacklisted;
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
result = isBlacklisted;
|
|
420
|
-
break;
|
|
421
|
-
}
|
|
422
|
-
case 'user:get_groups': {
|
|
423
|
-
const userIdentifier = await this.resolvePinValue(node, 'user', null);
|
|
424
|
-
let groups = [];
|
|
425
|
-
let usernameToFind = null;
|
|
426
|
-
|
|
427
|
-
if (typeof userIdentifier === 'string') {
|
|
428
|
-
usernameToFind = userIdentifier;
|
|
429
|
-
} else if (userIdentifier && typeof userIdentifier === 'object' && userIdentifier.username) {
|
|
430
|
-
usernameToFind = userIdentifier.username;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
if (usernameToFind) {
|
|
434
|
-
const user = await User.getUser(usernameToFind, this.context.botId);
|
|
435
|
-
if (user && user.groups) {
|
|
436
|
-
groups = user.groups.map(g => g.group.name);
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
result = groups;
|
|
440
|
-
break;
|
|
441
|
-
}
|
|
442
|
-
case 'user:get_permissions': {
|
|
443
|
-
const userIdentifier = await this.resolvePinValue(node, 'user', null);
|
|
444
|
-
let permissions = [];
|
|
445
|
-
let usernameToFind = null;
|
|
446
|
-
|
|
447
|
-
if (typeof userIdentifier === 'string') {
|
|
448
|
-
usernameToFind = userIdentifier;
|
|
449
|
-
} else if (userIdentifier && typeof userIdentifier === 'object' && userIdentifier.username) {
|
|
450
|
-
usernameToFind = userIdentifier.username;
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
if (usernameToFind) {
|
|
454
|
-
const user = await User.getUser(usernameToFind, this.context.botId);
|
|
455
|
-
if (user && user.permissionsSet) {
|
|
456
|
-
permissions = Array.from(user.permissionsSet);
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
result = permissions;
|
|
460
|
-
break;
|
|
461
|
-
}
|
|
462
|
-
case 'bot:get_position': {
|
|
463
|
-
if (this.context.bot?.entity?.position) {
|
|
464
|
-
result = this.context.bot.entity.position;
|
|
465
|
-
} else {
|
|
466
|
-
result = null;
|
|
467
|
-
}
|
|
468
|
-
break;
|
|
469
|
-
}
|
|
470
|
-
case 'user:set_blacklist':
|
|
471
|
-
result = this.memo.get(`${node.id}:updated_user`);
|
|
472
|
-
break;
|
|
473
|
-
case 'event:command':
|
|
474
|
-
if (pinId === 'args') result = this.context.args || {};
|
|
475
|
-
else if (pinId === 'user') result = this.context.user || {};
|
|
476
|
-
else if (pinId === 'chat_type') result = this.context.typeChat || 'chat';
|
|
477
|
-
else result = this.context[pinId];
|
|
478
|
-
break;
|
|
479
|
-
case 'event:chat':
|
|
480
|
-
if (pinId === 'username') result = this.context.username;
|
|
481
|
-
else if (pinId === 'message') result = this.context.message;
|
|
482
|
-
else if (pinId === 'chatType') result = this.context.chat_type;
|
|
483
|
-
else result = this.context[pinId];
|
|
484
|
-
break;
|
|
485
|
-
case 'event:raw_message':
|
|
486
|
-
if (pinId === 'rawText') result = this.context.rawText;
|
|
487
|
-
else result = this.context[pinId];
|
|
488
|
-
break;
|
|
489
|
-
case 'event:playerJoined':
|
|
490
|
-
case 'event:playerLeft':
|
|
491
|
-
result = this.context[pinId];
|
|
492
|
-
break;
|
|
493
|
-
case 'event:entitySpawn':
|
|
494
|
-
case 'event:entityMoved':
|
|
495
|
-
case 'event:entityGone':
|
|
496
|
-
result = this.context[pinId];
|
|
497
|
-
break;
|
|
498
|
-
|
|
499
|
-
case 'data:get_variable':
|
|
500
|
-
const varName = node.data?.variableName || node.data?.selectedVariable || '';
|
|
501
|
-
if (!varName) {
|
|
502
|
-
console.warn('[GraphExecutionEngine] data:get_variable: не указано имя переменной', node.data);
|
|
503
|
-
result = null;
|
|
504
|
-
} else {
|
|
505
|
-
result = this.context.variables.hasOwnProperty(varName) ? this.context.variables[varName] : null;
|
|
506
|
-
}
|
|
507
|
-
break;
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
case 'data:get_argument': {
|
|
512
|
-
const args = this.context.args || {};
|
|
513
|
-
const argName = node.data?.argumentName || '';
|
|
514
|
-
if (pinId === 'value') {
|
|
515
|
-
result = args && argName && args[argName] !== undefined ? args[argName] : defaultValue;
|
|
516
|
-
} else if (pinId === 'exists') {
|
|
517
|
-
result = args && argName && args[argName] !== undefined;
|
|
518
|
-
}
|
|
519
|
-
break;
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
case 'data:length': {
|
|
523
|
-
const data = await this.resolvePinValue(node, 'data');
|
|
524
|
-
if (Array.isArray(data) || typeof data === 'string') {
|
|
525
|
-
result = data.length;
|
|
526
|
-
} else {
|
|
527
|
-
result = 0;
|
|
528
|
-
}
|
|
529
|
-
break;
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
case 'math:operation': {
|
|
533
|
-
const op = node.data?.operation || '+';
|
|
534
|
-
const a = Number(await this.resolvePinValue(node, 'a', 0));
|
|
535
|
-
const b = Number(await this.resolvePinValue(node, 'b', 0));
|
|
536
|
-
switch (op) {
|
|
537
|
-
case '+': result = a + b; break;
|
|
538
|
-
case '-': result = a - b; break;
|
|
539
|
-
case '*': result = a * b; break;
|
|
540
|
-
case '/': result = b !== 0 ? a / b : 0; break;
|
|
541
|
-
default: result = 0;
|
|
542
|
-
}
|
|
543
|
-
break;
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
case 'math:random_number': {
|
|
547
|
-
const minRaw = await this.resolvePinValue(node, 'min', node.data.min ?? '0');
|
|
548
|
-
const maxRaw = await this.resolvePinValue(node, 'max', node.data.max ?? '1');
|
|
549
|
-
|
|
550
|
-
const minStr = String(minRaw);
|
|
551
|
-
const maxStr = String(maxRaw);
|
|
552
|
-
|
|
553
|
-
const min = parseFloat(minStr.replace(',', '.'));
|
|
554
|
-
const max = parseFloat(maxStr.replace(',', '.'));
|
|
555
|
-
|
|
556
|
-
if (isNaN(min) || isNaN(max)) {
|
|
557
|
-
result = NaN;
|
|
558
|
-
break;
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
const produceFloat = minStr.includes('.') || minStr.includes(',') || !Number.isInteger(min) ||
|
|
562
|
-
maxStr.includes('.') || maxStr.includes(',') || !Number.isInteger(max);
|
|
563
|
-
|
|
564
|
-
if (produceFloat) {
|
|
565
|
-
result = Math.random() * (max - min) + min;
|
|
566
|
-
} else {
|
|
567
|
-
const minInt = Math.ceil(min);
|
|
568
|
-
const maxInt = Math.floor(max);
|
|
569
|
-
result = Math.floor(Math.random() * (maxInt - minInt + 1)) + minInt;
|
|
570
|
-
}
|
|
571
|
-
break;
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
case 'logic:operation': {
|
|
575
|
-
const op = node.data?.operation || 'AND';
|
|
576
|
-
const inputs = [];
|
|
577
|
-
const pinCount = node.data?.pinCount || 2;
|
|
578
|
-
|
|
579
|
-
for (let i = 0; i < pinCount; i++) {
|
|
580
|
-
const value = await this.resolvePinValue(node, `pin_${i}`, false);
|
|
581
|
-
inputs.push(value);
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
switch (op) {
|
|
585
|
-
case 'AND': result = inputs.every(Boolean); break;
|
|
586
|
-
case 'OR': result = inputs.some(Boolean); break;
|
|
587
|
-
case 'NOT': result = !inputs[0]; break;
|
|
588
|
-
default: result = false;
|
|
589
|
-
}
|
|
590
|
-
break;
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
case 'string:concat': {
|
|
594
|
-
const pinCount = node.data?.pinCount || 2;
|
|
595
|
-
let finalString = '';
|
|
596
|
-
for (let i = 0; i < pinCount; i++) {
|
|
597
|
-
const part = await this.resolvePinValue(node, `pin_${i}`, '');
|
|
598
|
-
finalString += String(part ?? '');
|
|
599
|
-
}
|
|
600
|
-
result = finalString;
|
|
601
|
-
break;
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
case 'data:array_literal': {
|
|
605
|
-
const numPins = node.data?.pinCount || 0;
|
|
606
|
-
const items = [];
|
|
607
|
-
for (let i = 0; i < numPins; i++) {
|
|
608
|
-
const value = await this.resolvePinValue(node, `pin_${i}`) ||
|
|
609
|
-
node.data?.[`item_${i}`] ||
|
|
610
|
-
node.data?.[`value_${i}`];
|
|
611
|
-
items.push(value);
|
|
612
|
-
}
|
|
613
|
-
result = items;
|
|
614
|
-
break;
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
case 'data:make_object': {
|
|
618
|
-
const numPins = node.data?.pinCount || 0;
|
|
619
|
-
const obj = {};
|
|
620
|
-
for (let i = 0; i < numPins; i++) {
|
|
621
|
-
const key = node.data[`key_${i}`];
|
|
622
|
-
if (key) {
|
|
623
|
-
obj[key] = await this.resolvePinValue(node, `value_${i}`);
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
result = obj;
|
|
627
|
-
break;
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
case 'data:get_entity_field': {
|
|
631
|
-
const entity = await this.resolvePinValue(node, 'entity');
|
|
632
|
-
result = entity ? entity[pinId] : defaultValue;
|
|
633
|
-
break;
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
case 'data:cast': {
|
|
637
|
-
const value = await this.resolvePinValue(node, 'value');
|
|
638
|
-
const targetType = node.data?.targetType || 'String';
|
|
639
|
-
switch (targetType) {
|
|
640
|
-
case 'String': result = String(value ?? ''); break;
|
|
641
|
-
case 'Number': result = Number(value); if (isNaN(result)) result = 0; break;
|
|
642
|
-
case 'Boolean': result = ['true', '1', 'yes'].includes(String(value).toLowerCase()); break;
|
|
643
|
-
default: result = value;
|
|
644
|
-
}
|
|
645
|
-
break;
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
case 'string:contains': {
|
|
649
|
-
if (pinId === 'result') {
|
|
650
|
-
const haystack = String(await this.resolvePinValue(node, 'haystack', ''));
|
|
651
|
-
const needle = String(await this.resolvePinValue(node, 'needle', ''));
|
|
652
|
-
const caseSensitive = await this.resolvePinValue(node, 'case_sensitive', false);
|
|
653
|
-
|
|
654
|
-
if (caseSensitive) {
|
|
655
|
-
return haystack.includes(needle);
|
|
656
|
-
} else {
|
|
657
|
-
return haystack.toLowerCase().includes(needle.toLowerCase());
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
break;
|
|
661
|
-
}
|
|
662
|
-
case 'string:matches': {
|
|
663
|
-
if (pinId === 'result') {
|
|
664
|
-
const str = String(await this.resolvePinValue(node, 'string', ''));
|
|
665
|
-
const regexStr = String(await this.resolvePinValue(node, 'regex', ''));
|
|
666
|
-
try {
|
|
667
|
-
result = new RegExp(regexStr).test(str);
|
|
668
|
-
} catch (e) { result = false; }
|
|
669
|
-
}
|
|
670
|
-
break;
|
|
671
|
-
}
|
|
672
|
-
case 'data:string_literal':
|
|
673
|
-
result = await this.resolvePinValue(node, 'value', '');
|
|
674
|
-
break;
|
|
675
|
-
|
|
676
|
-
case 'data:get_user_field': {
|
|
677
|
-
const userIdentifier = await this.resolvePinValue(node, 'user');
|
|
678
|
-
let userObject = null;
|
|
679
|
-
|
|
680
|
-
if (userIdentifier && typeof userIdentifier === 'object' && userIdentifier.username) {
|
|
681
|
-
userObject = userIdentifier;
|
|
682
|
-
} else if (typeof userIdentifier === 'string' && userIdentifier.length > 0) {
|
|
683
|
-
userObject = await User.getUser(userIdentifier, this.context.botId);
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
if (userObject) {
|
|
687
|
-
if (pinId === 'username') {
|
|
688
|
-
result = userObject.username;
|
|
689
|
-
} else if (pinId === 'groups') {
|
|
690
|
-
result = userObject.groups ? userObject.groups.map(g => g.group?.name).filter(Boolean) : [];
|
|
691
|
-
} else if (pinId === 'permissions') {
|
|
692
|
-
result = userObject.permissionsSet ? Array.from(userObject.permissionsSet) : [];
|
|
693
|
-
} else if (pinId === 'isBlacklisted') {
|
|
694
|
-
result = !!userObject.isBlacklisted;
|
|
695
|
-
} else {
|
|
696
|
-
result = defaultValue;
|
|
697
|
-
}
|
|
698
|
-
} else {
|
|
699
|
-
result = defaultValue;
|
|
700
|
-
}
|
|
701
|
-
break;
|
|
702
|
-
}
|
|
703
|
-
case 'data:get_server_players':
|
|
704
|
-
result = this.context.players || [];
|
|
705
|
-
break;
|
|
706
|
-
case 'data:get_bot_look':
|
|
707
|
-
result = this.context.botState ? { yaw: this.context.botState.yaw, pitch: this.context.botState.pitch } : null;
|
|
708
|
-
break;
|
|
709
|
-
|
|
710
|
-
case 'array:get_random_element': {
|
|
711
|
-
const arr = await this.resolvePinValue(node, 'array', []);
|
|
712
|
-
if (!Array.isArray(arr) || arr.length === 0) {
|
|
713
|
-
result = null;
|
|
714
|
-
} else {
|
|
715
|
-
const randomIndex = Math.floor(Math.random() * arr.length);
|
|
716
|
-
this.memo.set(`${node.id}:index`, randomIndex);
|
|
717
|
-
if (pinId === 'element') {
|
|
718
|
-
result = arr[randomIndex];
|
|
719
|
-
} else if (pinId === 'index') {
|
|
720
|
-
result = randomIndex;
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
break;
|
|
724
|
-
}
|
|
725
|
-
case 'array:add_element': {
|
|
726
|
-
const arr = await this.resolvePinValue(node, 'array', []);
|
|
727
|
-
const element = await this.resolvePinValue(node, 'element', null);
|
|
728
|
-
result = Array.isArray(arr) ? [...arr, element] : [element];
|
|
729
|
-
break;
|
|
730
|
-
}
|
|
731
|
-
case 'array:remove_by_index': {
|
|
732
|
-
const arr = await this.resolvePinValue(node, 'array', []);
|
|
733
|
-
const index = await this.resolvePinValue(node, 'index', -1);
|
|
734
|
-
if (!Array.isArray(arr) || index < 0 || index >= arr.length) {
|
|
735
|
-
result = arr || [];
|
|
736
|
-
} else {
|
|
737
|
-
const newArr = [...arr];
|
|
738
|
-
newArr.splice(index, 1);
|
|
739
|
-
result = newArr;
|
|
740
|
-
}
|
|
741
|
-
break;
|
|
742
|
-
}
|
|
743
|
-
case 'array:get_by_index': {
|
|
744
|
-
const arr = await this.resolvePinValue(node, 'array', []);
|
|
745
|
-
const index = await this.resolvePinValue(node, 'index', -1);
|
|
746
|
-
result = (!Array.isArray(arr) || index < 0 || index >= arr.length) ? null : arr[index];
|
|
747
|
-
break;
|
|
748
|
-
}
|
|
749
|
-
case 'array:find_index': {
|
|
750
|
-
const arr = await this.resolvePinValue(node, 'array', []);
|
|
751
|
-
const element = await this.resolvePinValue(node, 'element', null);
|
|
752
|
-
result = Array.isArray(arr) ? arr.indexOf(element) : -1;
|
|
753
|
-
break;
|
|
754
|
-
}
|
|
755
|
-
case 'array:contains': {
|
|
756
|
-
const arr = await this.resolvePinValue(node, 'array', []);
|
|
757
|
-
const element = await this.resolvePinValue(node, 'element', null);
|
|
758
|
-
if (Array.isArray(arr)) {
|
|
759
|
-
const index = arr.indexOf(element);
|
|
760
|
-
this.memo.set(`${node.id}:index`, index);
|
|
761
|
-
if (pinId === 'result') {
|
|
762
|
-
result = index !== -1;
|
|
763
|
-
} else if (pinId === 'index') {
|
|
764
|
-
result = index;
|
|
765
|
-
}
|
|
766
|
-
} else {
|
|
767
|
-
this.memo.set(`${node.id}:index`, -1);
|
|
768
|
-
if (pinId === 'result') {
|
|
769
|
-
result = false;
|
|
770
|
-
} else if (pinId === 'index') {
|
|
771
|
-
result = -1;
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
break;
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
case 'object:create': {
|
|
779
|
-
if (node.data?.advanced) {
|
|
780
|
-
try {
|
|
781
|
-
result = JSON.parse(node.data.jsonValue || '{}');
|
|
782
|
-
} catch (e) {
|
|
783
|
-
console.error('Ошибка парсинга JSON в object:create:', e);
|
|
784
|
-
result = {};
|
|
785
|
-
}
|
|
786
|
-
} else {
|
|
787
|
-
const numPins = node.data?.pinCount || 0;
|
|
788
|
-
const obj = {};
|
|
789
|
-
for (let i = 0; i < numPins; i++) {
|
|
790
|
-
const key = node.data[`key_${i}`];
|
|
791
|
-
if (key) {
|
|
792
|
-
obj[key] = await this.resolvePinValue(node, `value_${i}`);
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
result = obj;
|
|
796
|
-
}
|
|
797
|
-
break;
|
|
798
|
-
}
|
|
799
|
-
case 'object:get': {
|
|
800
|
-
const obj = await this.resolvePinValue(node, 'object', {});
|
|
801
|
-
const key = await this.resolvePinValue(node, 'key', '');
|
|
802
|
-
result = obj[key] ?? defaultValue;
|
|
803
|
-
break;
|
|
804
|
-
}
|
|
805
|
-
case 'object:set': {
|
|
806
|
-
const obj = await this.resolvePinValue(node, 'object', {});
|
|
807
|
-
const key = await this.resolvePinValue(node, 'key', '');
|
|
808
|
-
const val = await this.resolvePinValue(node, 'value');
|
|
809
|
-
const newObj = { ...obj };
|
|
810
|
-
newObj[key] = val;
|
|
811
|
-
result = newObj;
|
|
812
|
-
break;
|
|
813
|
-
}
|
|
814
|
-
case 'object:delete': {
|
|
815
|
-
const obj = await this.resolvePinValue(node, 'object', {});
|
|
816
|
-
const key = await this.resolvePinValue(node, 'key', '');
|
|
817
|
-
const newObj = { ...obj };
|
|
818
|
-
delete newObj[key];
|
|
819
|
-
result = newObj;
|
|
820
|
-
break;
|
|
821
|
-
}
|
|
822
|
-
case 'object:has_key': {
|
|
823
|
-
const obj = await this.resolvePinValue(node, 'object', {});
|
|
824
|
-
const key = await this.resolvePinValue(node, 'key', '');
|
|
825
|
-
const exists = obj.hasOwnProperty(key);
|
|
826
|
-
this.memo.set(`${node.id}:value`, exists ? obj[key] : null);
|
|
827
|
-
if (pinId === 'result') {
|
|
828
|
-
result = exists;
|
|
829
|
-
} else if (pinId === 'value') {
|
|
830
|
-
result = this.memo.get(`${node.id}:value`);
|
|
831
|
-
}
|
|
832
|
-
break;
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
case 'flow:for_each': {
|
|
836
|
-
if (pinId === 'element') {
|
|
837
|
-
result = this.memo.get(`${node.id}:element`);
|
|
838
|
-
} else if (pinId === 'index') {
|
|
839
|
-
result = this.memo.get(`${node.id}:index`);
|
|
840
|
-
}
|
|
841
|
-
break;
|
|
842
|
-
}
|
|
843
|
-
case 'flow:while': {
|
|
844
|
-
if (pinId === 'iteration') {
|
|
845
|
-
result = this.memo.get(`${node.id}:iteration`);
|
|
846
|
-
}
|
|
847
|
-
break;
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
case 'string:equals': {
|
|
851
|
-
const strA = String(await this.resolvePinValue(node, 'a', ''));
|
|
852
|
-
const strB = String(await this.resolvePinValue(node, 'b', ''));
|
|
853
|
-
const caseSensitive = node.data?.case_sensitive ?? true;
|
|
854
|
-
if (pinId === 'result') {
|
|
855
|
-
result = caseSensitive ? strA === strB : strA.toLowerCase() === strB.toLowerCase();
|
|
856
|
-
}
|
|
857
|
-
break;
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
case 'logic:compare': {
|
|
861
|
-
const op = node.data?.operation || '==';
|
|
862
|
-
const valA = await this.resolvePinValue(node, 'a');
|
|
863
|
-
const valB = await this.resolvePinValue(node, 'b');
|
|
864
|
-
if (pinId === 'result') {
|
|
865
|
-
switch (op) {
|
|
866
|
-
case '==': result = valA == valB; break;
|
|
867
|
-
case '!=': result = valA != valB; break;
|
|
868
|
-
case '>': result = valA > valB; break;
|
|
869
|
-
case '<': result = valA < valB; break;
|
|
870
|
-
case '>=': result = valA >= valB; break;
|
|
871
|
-
case '<=': result = valA <= valB; break;
|
|
872
|
-
default: result = false;
|
|
873
|
-
}
|
|
874
|
-
}
|
|
875
|
-
break;
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
default:
|
|
879
|
-
result = defaultValue;
|
|
880
|
-
break;
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
const isVolatile = this.isNodeVolatile(node);
|
|
884
|
-
|
|
885
|
-
if (!isVolatile) {
|
|
886
|
-
this.memo.set(cacheKey, result);
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
return result;
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
isNodeVolatile(node) {
|
|
893
|
-
if (!node) return false;
|
|
894
|
-
|
|
895
|
-
if (node.type === 'data:get_variable') {
|
|
896
|
-
return true;
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
const connections = this.activeGraph.connections.filter(c => c.targetNodeId === node.id);
|
|
900
|
-
for (const conn of connections) {
|
|
901
|
-
const sourceNode = this.activeGraph.nodes.find(n => n.id === conn.sourceNodeId);
|
|
902
|
-
if (this.isNodeVolatile(sourceNode)) {
|
|
903
|
-
return true;
|
|
904
|
-
}
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
return false;
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
hasConnection(node, pinId) {
|
|
911
|
-
if (!this.activeGraph || !this.activeGraph.connections) return false;
|
|
912
|
-
return this.activeGraph.connections.some(conn =>
|
|
913
|
-
conn.targetNodeId === node.id && conn.targetPinId === pinId
|
|
914
|
-
);
|
|
915
|
-
}
|
|
916
|
-
}
|
|
917
|
-
|
|
1
|
+
const User = require('./UserService');
|
|
2
|
+
const { PrismaClient } = require('@prisma/client');
|
|
3
|
+
const prisma = new PrismaClient();
|
|
4
|
+
|
|
5
|
+
class BreakLoopSignal extends Error {
|
|
6
|
+
constructor() {
|
|
7
|
+
super("Loop break signal");
|
|
8
|
+
this.name = "BreakLoopSignal";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
class GraphExecutionEngine {
|
|
13
|
+
constructor(nodeRegistry, botManagerOrApi = null) {
|
|
14
|
+
if (!nodeRegistry || typeof nodeRegistry.getNodeConfig !== 'function') {
|
|
15
|
+
throw new Error('GraphExecutionEngine requires a valid NodeRegistry instance.');
|
|
16
|
+
}
|
|
17
|
+
this.nodeRegistry = nodeRegistry;
|
|
18
|
+
this.botManager = botManagerOrApi;
|
|
19
|
+
this.activeGraph = null;
|
|
20
|
+
this.context = null;
|
|
21
|
+
this.memo = new Map();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async execute(graph, context, eventType) {
|
|
25
|
+
if (!graph || graph === 'null') return context;
|
|
26
|
+
|
|
27
|
+
let parsedGraph;
|
|
28
|
+
if (typeof graph === 'string') {
|
|
29
|
+
try {
|
|
30
|
+
parsedGraph = JSON.parse(graph);
|
|
31
|
+
} catch (e) {
|
|
32
|
+
console.error('[GraphExecutionEngine] Ошибка парсинга JSON графа:', e);
|
|
33
|
+
return context;
|
|
34
|
+
}
|
|
35
|
+
} else {
|
|
36
|
+
parsedGraph = graph;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!parsedGraph.nodes || !parsedGraph.connections) {
|
|
40
|
+
console.error('[GraphExecutionEngine] Неверный формат графа. Отсутствуют nodes или connections.');
|
|
41
|
+
return context;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
this.activeGraph = parsedGraph;
|
|
46
|
+
this.context = context;
|
|
47
|
+
|
|
48
|
+
if (!this.context.variables) {
|
|
49
|
+
this.context.variables = {};
|
|
50
|
+
if (Array.isArray(this.activeGraph.variables)) {
|
|
51
|
+
for (const variable of this.activeGraph.variables) {
|
|
52
|
+
let value = variable.value;
|
|
53
|
+
try {
|
|
54
|
+
switch(variable.type) {
|
|
55
|
+
case 'number':
|
|
56
|
+
value = Number(value);
|
|
57
|
+
break;
|
|
58
|
+
case 'boolean':
|
|
59
|
+
value = value === 'true';
|
|
60
|
+
break;
|
|
61
|
+
case 'array':
|
|
62
|
+
value = JSON.parse(value);
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
} catch (e) {
|
|
66
|
+
console.error(`Error parsing variable default value for ${variable.name}:`, e);
|
|
67
|
+
}
|
|
68
|
+
this.context.variables[variable.name] = value;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!this.context.persistenceIntent) this.context.persistenceIntent = new Map();
|
|
74
|
+
this.memo.clear();
|
|
75
|
+
|
|
76
|
+
const eventName = eventType || 'command';
|
|
77
|
+
const startNode = this.activeGraph.nodes.find(n => n.type === `event:${eventName}`);
|
|
78
|
+
|
|
79
|
+
if (startNode) {
|
|
80
|
+
await this.traverse(startNode, 'exec');
|
|
81
|
+
} else if (!eventType) {
|
|
82
|
+
throw new Error(`Не найдена стартовая нода события (event:command) в графе.`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
} catch (error) {
|
|
86
|
+
if (!(error instanceof BreakLoopSignal)) {
|
|
87
|
+
console.error(`[GraphExecutionEngine] Критическая ошибка выполнения графа: ${error.stack}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return this.context;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async traverse(node, fromPinId) {
|
|
95
|
+
const connection = this.activeGraph.connections.find(c => c.sourceNodeId === node.id && c.sourcePinId === fromPinId);
|
|
96
|
+
if (!connection) return;
|
|
97
|
+
|
|
98
|
+
const nextNode = this.activeGraph.nodes.find(n => n.id === connection.targetNodeId);
|
|
99
|
+
if (!nextNode) return;
|
|
100
|
+
|
|
101
|
+
await this.executeNode(nextNode);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async executeNode(node) {
|
|
105
|
+
const execCacheKey = `${node.id}_executed`;
|
|
106
|
+
if (this.memo.has(execCacheKey)) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
this.memo.set(execCacheKey, true);
|
|
110
|
+
|
|
111
|
+
switch (node.type) {
|
|
112
|
+
case 'user:set_blacklist': {
|
|
113
|
+
const userObject = await this.resolvePinValue(node, 'user', null);
|
|
114
|
+
const blacklistStatus = await this.resolvePinValue(node, 'blacklist_status', false);
|
|
115
|
+
let updatedUser = null;
|
|
116
|
+
|
|
117
|
+
if (userObject && userObject.username) {
|
|
118
|
+
const user = await User.getUser(userObject.username, this.context.botId);
|
|
119
|
+
if (user) {
|
|
120
|
+
updatedUser = await prisma.user.update({
|
|
121
|
+
where: { id: user.id },
|
|
122
|
+
data: { isBlacklisted: blacklistStatus }
|
|
123
|
+
});
|
|
124
|
+
User.clearCache(user.username, this.context.botId);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
this.memo.set(`${node.id}:updated_user`, updatedUser);
|
|
128
|
+
await this.traverse(node, 'exec');
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
case 'action:send_message': {
|
|
132
|
+
const message = String(await this.resolvePinValue(node, 'message', ''));
|
|
133
|
+
const chatType = await this.resolvePinValue(node, 'chat_type', this.context.typeChat);
|
|
134
|
+
const recipient = await this.resolvePinValue(node, 'recipient', this.context.user?.username);
|
|
135
|
+
this.context.bot.sendMessage(chatType, message, recipient);
|
|
136
|
+
await this.traverse(node, 'exec');
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
case 'action:server_command': {
|
|
140
|
+
const command = await this.resolvePinValue(node, 'command', '');
|
|
141
|
+
if (command) this.context.bot.executeCommand(command);
|
|
142
|
+
await this.traverse(node, 'exec');
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
case 'action:send_log': {
|
|
146
|
+
const message = await this.resolvePinValue(node, 'message', '');
|
|
147
|
+
if (this.botManager?.appendLog && this.context?.botId) {
|
|
148
|
+
this.botManager.appendLog(this.context.botId, `[Graph] ${message}`);
|
|
149
|
+
} else {
|
|
150
|
+
console.log(`[Graph Log] ${message}`);
|
|
151
|
+
}
|
|
152
|
+
await this.traverse(node, 'exec');
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
case 'action:bot_look_at': {
|
|
156
|
+
const target = await this.resolvePinValue(node, 'target');
|
|
157
|
+
const yOffset = await this.resolvePinValue(node, 'add_y', 0);
|
|
158
|
+
|
|
159
|
+
if (target && this.context.bot?.lookAt) {
|
|
160
|
+
let finalPosition;
|
|
161
|
+
if (target.position) {
|
|
162
|
+
finalPosition = { ...target.position };
|
|
163
|
+
} else if (target.x !== undefined && target.y !== undefined && target.z !== undefined) {
|
|
164
|
+
finalPosition = { ...target };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (finalPosition) {
|
|
168
|
+
finalPosition.y += Number(yOffset || 0);
|
|
169
|
+
this.context.bot.lookAt(finalPosition);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
await this.traverse(node, 'exec');
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
case 'action:bot_set_variable': {
|
|
176
|
+
const varName = await this.resolvePinValue(node, 'name', '');
|
|
177
|
+
const varValue = await this.resolvePinValue(node, 'value');
|
|
178
|
+
let shouldPersist = await this.resolvePinValue(node, 'persist', false);
|
|
179
|
+
|
|
180
|
+
if (this.context.eventType === 'command') {
|
|
181
|
+
shouldPersist = false;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (varName) {
|
|
185
|
+
this.context.variables[varName] = varValue;
|
|
186
|
+
if (this.context.persistenceIntent) {
|
|
187
|
+
this.context.persistenceIntent.set(varName, shouldPersist);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
await this.traverse(node, 'exec');
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
case 'flow:branch': {
|
|
194
|
+
const condition = await this.resolvePinValue(node, 'condition', false);
|
|
195
|
+
await this.traverse(node, condition ? 'exec_true' : 'exec_false');
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
case 'flow:break': {
|
|
199
|
+
throw new BreakLoopSignal();
|
|
200
|
+
}
|
|
201
|
+
case 'flow:for_each': {
|
|
202
|
+
const array = await this.resolvePinValue(node, 'array', []);
|
|
203
|
+
if (Array.isArray(array)) {
|
|
204
|
+
try {
|
|
205
|
+
for (let i = 0; i < array.length; i++) {
|
|
206
|
+
const element = array[i];
|
|
207
|
+
this.memo.set(`${node.id}:element`, element);
|
|
208
|
+
this.memo.set(`${node.id}:index`, i);
|
|
209
|
+
this.clearLoopBodyMemo(node);
|
|
210
|
+
await this.traverse(node, 'loop_body');
|
|
211
|
+
}
|
|
212
|
+
} catch (e) {
|
|
213
|
+
if (e instanceof BreakLoopSignal) {
|
|
214
|
+
} else {
|
|
215
|
+
throw e;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
await this.traverse(node, 'completed');
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
case 'flow:while': {
|
|
223
|
+
let iteration = 0;
|
|
224
|
+
const maxIterations = 1000;
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
while (iteration < maxIterations) {
|
|
228
|
+
const condition = await this.resolvePinValue(node, 'condition', false);
|
|
229
|
+
if (!condition) break;
|
|
230
|
+
|
|
231
|
+
this.memo.set(`${node.id}:iteration`, iteration);
|
|
232
|
+
this.clearLoopBodyMemo(node);
|
|
233
|
+
await this.traverse(node, 'loop_body');
|
|
234
|
+
iteration++;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (iteration >= maxIterations) {
|
|
238
|
+
console.warn(`[GraphExecutionEngine] Цикл while достиг максимального количества итераций (${maxIterations})`);
|
|
239
|
+
}
|
|
240
|
+
} catch (e) {
|
|
241
|
+
if (e instanceof BreakLoopSignal) {
|
|
242
|
+
} else {
|
|
243
|
+
throw e;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
await this.traverse(node, 'completed');
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
case 'flow:sequence': {
|
|
251
|
+
const pinCount = node.data?.pinCount || 2;
|
|
252
|
+
for (let i = 0; i < pinCount; i++) {
|
|
253
|
+
await this.traverse(node, `exec_${i}`);
|
|
254
|
+
}
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
case 'flow:switch': {
|
|
258
|
+
const value = await this.resolvePinValue(node, 'value');
|
|
259
|
+
const caseCount = node.data?.caseCount || 0;
|
|
260
|
+
let matched = false;
|
|
261
|
+
|
|
262
|
+
for (let i = 0; i < caseCount; i++) {
|
|
263
|
+
const caseValue = node.data?.[`case_${i}`];
|
|
264
|
+
if (caseValue !== undefined) {
|
|
265
|
+
let isMatch = false;
|
|
266
|
+
|
|
267
|
+
if (Array.isArray(value) && Array.isArray(caseValue)) {
|
|
268
|
+
isMatch = JSON.stringify(value) === JSON.stringify(caseValue);
|
|
269
|
+
} else if (typeof value === 'object' && typeof caseValue === 'object' && value !== null && caseValue !== null) {
|
|
270
|
+
isMatch = JSON.stringify(value) === JSON.stringify(caseValue);
|
|
271
|
+
} else if (typeof value === 'number' && typeof caseValue === 'number') {
|
|
272
|
+
isMatch = value === caseValue;
|
|
273
|
+
} else if (typeof value === 'boolean' && typeof caseValue === 'boolean') {
|
|
274
|
+
isMatch = value === caseValue;
|
|
275
|
+
} else {
|
|
276
|
+
isMatch = String(value) === String(caseValue);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (isMatch) {
|
|
280
|
+
await this.traverse(node, `case_${i}`);
|
|
281
|
+
matched = true;
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (!matched) {
|
|
288
|
+
await this.traverse(node, 'default');
|
|
289
|
+
}
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
case 'debug:log': {
|
|
293
|
+
const value = await this.resolvePinValue(node, 'value');
|
|
294
|
+
console.log('[Graph Debug]', value);
|
|
295
|
+
await this.traverse(node, 'exec');
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
case 'string:contains':
|
|
299
|
+
case 'string:matches':
|
|
300
|
+
case 'string:equals': {
|
|
301
|
+
await this.traverse(node, 'exec');
|
|
302
|
+
break;
|
|
303
|
+
}
|
|
304
|
+
case 'array:get_random_element': {
|
|
305
|
+
await this.traverse(node, 'element');
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
case 'array:add_element':
|
|
309
|
+
case 'array:remove_by_index':
|
|
310
|
+
case 'array:get_by_index':
|
|
311
|
+
case 'array:find_index':
|
|
312
|
+
case 'array:contains': {
|
|
313
|
+
await this.traverse(node, 'result');
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
case 'data:array_literal':
|
|
317
|
+
case 'data:make_object':
|
|
318
|
+
case 'data:get_variable':
|
|
319
|
+
case 'data:get_argument':
|
|
320
|
+
case 'data:length':
|
|
321
|
+
case 'data:get_entity_field':
|
|
322
|
+
case 'data:cast':
|
|
323
|
+
case 'data:string_literal':
|
|
324
|
+
case 'data:get_user_field':
|
|
325
|
+
case 'data:get_server_players':
|
|
326
|
+
case 'data:get_bot_look':
|
|
327
|
+
case 'math:operation':
|
|
328
|
+
case 'math:random_number':
|
|
329
|
+
case 'logic:operation':
|
|
330
|
+
case 'string:concat':
|
|
331
|
+
case 'object:create':
|
|
332
|
+
case 'object:get':
|
|
333
|
+
case 'object:set':
|
|
334
|
+
case 'object:delete':
|
|
335
|
+
case 'object:has_key': {
|
|
336
|
+
await this.traverse(node, 'value');
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
clearLoopBodyMemo(loopNode) {
|
|
343
|
+
const nodesToClear = new Set();
|
|
344
|
+
const queue = [];
|
|
345
|
+
|
|
346
|
+
const initialConnection = this.activeGraph.connections.find(
|
|
347
|
+
c => c.sourceNodeId === loopNode.id && c.sourcePinId === 'loop_body'
|
|
348
|
+
);
|
|
349
|
+
if (initialConnection) {
|
|
350
|
+
const firstNode = this.activeGraph.nodes.find(n => n.id === initialConnection.targetNodeId);
|
|
351
|
+
if (firstNode) {
|
|
352
|
+
queue.push(firstNode);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const visited = new Set();
|
|
357
|
+
while (queue.length > 0) {
|
|
358
|
+
const currentNode = queue.shift();
|
|
359
|
+
if (visited.has(currentNode.id)) continue;
|
|
360
|
+
visited.add(currentNode.id);
|
|
361
|
+
|
|
362
|
+
nodesToClear.add(currentNode.id);
|
|
363
|
+
|
|
364
|
+
const connections = this.activeGraph.connections.filter(c => c.sourceNodeId === currentNode.id);
|
|
365
|
+
for (const conn of connections) {
|
|
366
|
+
const nextNode = this.activeGraph.nodes.find(n => n.id === conn.targetNodeId);
|
|
367
|
+
if (nextNode) {
|
|
368
|
+
queue.push(nextNode);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
for (const nodeId of nodesToClear) {
|
|
374
|
+
for (const key of this.memo.keys()) {
|
|
375
|
+
if (key.startsWith(nodeId)) {
|
|
376
|
+
this.memo.delete(key);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
async resolvePinValue(node, pinId, defaultValue = null) {
|
|
383
|
+
const connection = this.activeGraph.connections.find(c => c.targetNodeId === node.id && c.targetPinId === pinId);
|
|
384
|
+
if (connection) {
|
|
385
|
+
const sourceNode = this.activeGraph.nodes.find(n => n.id === connection.sourceNodeId);
|
|
386
|
+
return await this.evaluateOutputPin(sourceNode, connection.sourcePinId, defaultValue);
|
|
387
|
+
}
|
|
388
|
+
return node.data && node.data[pinId] !== undefined ? node.data[pinId] : defaultValue;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async evaluateOutputPin(node, pinId, defaultValue = null) {
|
|
392
|
+
if (!node) return defaultValue;
|
|
393
|
+
|
|
394
|
+
const cacheKey = `${node.id}:${pinId}`;
|
|
395
|
+
if (this.memo.has(cacheKey)) {
|
|
396
|
+
return this.memo.get(cacheKey);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
let result;
|
|
400
|
+
|
|
401
|
+
switch (node.type) {
|
|
402
|
+
case 'user:check_blacklist': {
|
|
403
|
+
const userIdentifier = await this.resolvePinValue(node, 'user', null);
|
|
404
|
+
let isBlacklisted = false;
|
|
405
|
+
let usernameToFind = null;
|
|
406
|
+
|
|
407
|
+
if (typeof userIdentifier === 'string') {
|
|
408
|
+
usernameToFind = userIdentifier;
|
|
409
|
+
} else if (userIdentifier && typeof userIdentifier === 'object' && userIdentifier.username) {
|
|
410
|
+
usernameToFind = userIdentifier.username;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (usernameToFind) {
|
|
414
|
+
const user = await User.getUser(usernameToFind, this.context.botId);
|
|
415
|
+
if (user) {
|
|
416
|
+
isBlacklisted = user.isBlacklisted;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
result = isBlacklisted;
|
|
420
|
+
break;
|
|
421
|
+
}
|
|
422
|
+
case 'user:get_groups': {
|
|
423
|
+
const userIdentifier = await this.resolvePinValue(node, 'user', null);
|
|
424
|
+
let groups = [];
|
|
425
|
+
let usernameToFind = null;
|
|
426
|
+
|
|
427
|
+
if (typeof userIdentifier === 'string') {
|
|
428
|
+
usernameToFind = userIdentifier;
|
|
429
|
+
} else if (userIdentifier && typeof userIdentifier === 'object' && userIdentifier.username) {
|
|
430
|
+
usernameToFind = userIdentifier.username;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (usernameToFind) {
|
|
434
|
+
const user = await User.getUser(usernameToFind, this.context.botId);
|
|
435
|
+
if (user && user.groups) {
|
|
436
|
+
groups = user.groups.map(g => g.group.name);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
result = groups;
|
|
440
|
+
break;
|
|
441
|
+
}
|
|
442
|
+
case 'user:get_permissions': {
|
|
443
|
+
const userIdentifier = await this.resolvePinValue(node, 'user', null);
|
|
444
|
+
let permissions = [];
|
|
445
|
+
let usernameToFind = null;
|
|
446
|
+
|
|
447
|
+
if (typeof userIdentifier === 'string') {
|
|
448
|
+
usernameToFind = userIdentifier;
|
|
449
|
+
} else if (userIdentifier && typeof userIdentifier === 'object' && userIdentifier.username) {
|
|
450
|
+
usernameToFind = userIdentifier.username;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (usernameToFind) {
|
|
454
|
+
const user = await User.getUser(usernameToFind, this.context.botId);
|
|
455
|
+
if (user && user.permissionsSet) {
|
|
456
|
+
permissions = Array.from(user.permissionsSet);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
result = permissions;
|
|
460
|
+
break;
|
|
461
|
+
}
|
|
462
|
+
case 'bot:get_position': {
|
|
463
|
+
if (this.context.bot?.entity?.position) {
|
|
464
|
+
result = this.context.bot.entity.position;
|
|
465
|
+
} else {
|
|
466
|
+
result = null;
|
|
467
|
+
}
|
|
468
|
+
break;
|
|
469
|
+
}
|
|
470
|
+
case 'user:set_blacklist':
|
|
471
|
+
result = this.memo.get(`${node.id}:updated_user`);
|
|
472
|
+
break;
|
|
473
|
+
case 'event:command':
|
|
474
|
+
if (pinId === 'args') result = this.context.args || {};
|
|
475
|
+
else if (pinId === 'user') result = this.context.user || {};
|
|
476
|
+
else if (pinId === 'chat_type') result = this.context.typeChat || 'chat';
|
|
477
|
+
else result = this.context[pinId];
|
|
478
|
+
break;
|
|
479
|
+
case 'event:chat':
|
|
480
|
+
if (pinId === 'username') result = this.context.username;
|
|
481
|
+
else if (pinId === 'message') result = this.context.message;
|
|
482
|
+
else if (pinId === 'chatType') result = this.context.chat_type;
|
|
483
|
+
else result = this.context[pinId];
|
|
484
|
+
break;
|
|
485
|
+
case 'event:raw_message':
|
|
486
|
+
if (pinId === 'rawText') result = this.context.rawText;
|
|
487
|
+
else result = this.context[pinId];
|
|
488
|
+
break;
|
|
489
|
+
case 'event:playerJoined':
|
|
490
|
+
case 'event:playerLeft':
|
|
491
|
+
result = this.context[pinId];
|
|
492
|
+
break;
|
|
493
|
+
case 'event:entitySpawn':
|
|
494
|
+
case 'event:entityMoved':
|
|
495
|
+
case 'event:entityGone':
|
|
496
|
+
result = this.context[pinId];
|
|
497
|
+
break;
|
|
498
|
+
|
|
499
|
+
case 'data:get_variable':
|
|
500
|
+
const varName = node.data?.variableName || node.data?.selectedVariable || '';
|
|
501
|
+
if (!varName) {
|
|
502
|
+
console.warn('[GraphExecutionEngine] data:get_variable: не указано имя переменной', node.data);
|
|
503
|
+
result = null;
|
|
504
|
+
} else {
|
|
505
|
+
result = this.context.variables.hasOwnProperty(varName) ? this.context.variables[varName] : null;
|
|
506
|
+
}
|
|
507
|
+
break;
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
case 'data:get_argument': {
|
|
512
|
+
const args = this.context.args || {};
|
|
513
|
+
const argName = node.data?.argumentName || '';
|
|
514
|
+
if (pinId === 'value') {
|
|
515
|
+
result = args && argName && args[argName] !== undefined ? args[argName] : defaultValue;
|
|
516
|
+
} else if (pinId === 'exists') {
|
|
517
|
+
result = args && argName && args[argName] !== undefined;
|
|
518
|
+
}
|
|
519
|
+
break;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
case 'data:length': {
|
|
523
|
+
const data = await this.resolvePinValue(node, 'data');
|
|
524
|
+
if (Array.isArray(data) || typeof data === 'string') {
|
|
525
|
+
result = data.length;
|
|
526
|
+
} else {
|
|
527
|
+
result = 0;
|
|
528
|
+
}
|
|
529
|
+
break;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
case 'math:operation': {
|
|
533
|
+
const op = node.data?.operation || '+';
|
|
534
|
+
const a = Number(await this.resolvePinValue(node, 'a', 0));
|
|
535
|
+
const b = Number(await this.resolvePinValue(node, 'b', 0));
|
|
536
|
+
switch (op) {
|
|
537
|
+
case '+': result = a + b; break;
|
|
538
|
+
case '-': result = a - b; break;
|
|
539
|
+
case '*': result = a * b; break;
|
|
540
|
+
case '/': result = b !== 0 ? a / b : 0; break;
|
|
541
|
+
default: result = 0;
|
|
542
|
+
}
|
|
543
|
+
break;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
case 'math:random_number': {
|
|
547
|
+
const minRaw = await this.resolvePinValue(node, 'min', node.data.min ?? '0');
|
|
548
|
+
const maxRaw = await this.resolvePinValue(node, 'max', node.data.max ?? '1');
|
|
549
|
+
|
|
550
|
+
const minStr = String(minRaw);
|
|
551
|
+
const maxStr = String(maxRaw);
|
|
552
|
+
|
|
553
|
+
const min = parseFloat(minStr.replace(',', '.'));
|
|
554
|
+
const max = parseFloat(maxStr.replace(',', '.'));
|
|
555
|
+
|
|
556
|
+
if (isNaN(min) || isNaN(max)) {
|
|
557
|
+
result = NaN;
|
|
558
|
+
break;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const produceFloat = minStr.includes('.') || minStr.includes(',') || !Number.isInteger(min) ||
|
|
562
|
+
maxStr.includes('.') || maxStr.includes(',') || !Number.isInteger(max);
|
|
563
|
+
|
|
564
|
+
if (produceFloat) {
|
|
565
|
+
result = Math.random() * (max - min) + min;
|
|
566
|
+
} else {
|
|
567
|
+
const minInt = Math.ceil(min);
|
|
568
|
+
const maxInt = Math.floor(max);
|
|
569
|
+
result = Math.floor(Math.random() * (maxInt - minInt + 1)) + minInt;
|
|
570
|
+
}
|
|
571
|
+
break;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
case 'logic:operation': {
|
|
575
|
+
const op = node.data?.operation || 'AND';
|
|
576
|
+
const inputs = [];
|
|
577
|
+
const pinCount = node.data?.pinCount || 2;
|
|
578
|
+
|
|
579
|
+
for (let i = 0; i < pinCount; i++) {
|
|
580
|
+
const value = await this.resolvePinValue(node, `pin_${i}`, false);
|
|
581
|
+
inputs.push(value);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
switch (op) {
|
|
585
|
+
case 'AND': result = inputs.every(Boolean); break;
|
|
586
|
+
case 'OR': result = inputs.some(Boolean); break;
|
|
587
|
+
case 'NOT': result = !inputs[0]; break;
|
|
588
|
+
default: result = false;
|
|
589
|
+
}
|
|
590
|
+
break;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
case 'string:concat': {
|
|
594
|
+
const pinCount = node.data?.pinCount || 2;
|
|
595
|
+
let finalString = '';
|
|
596
|
+
for (let i = 0; i < pinCount; i++) {
|
|
597
|
+
const part = await this.resolvePinValue(node, `pin_${i}`, '');
|
|
598
|
+
finalString += String(part ?? '');
|
|
599
|
+
}
|
|
600
|
+
result = finalString;
|
|
601
|
+
break;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
case 'data:array_literal': {
|
|
605
|
+
const numPins = node.data?.pinCount || 0;
|
|
606
|
+
const items = [];
|
|
607
|
+
for (let i = 0; i < numPins; i++) {
|
|
608
|
+
const value = await this.resolvePinValue(node, `pin_${i}`) ||
|
|
609
|
+
node.data?.[`item_${i}`] ||
|
|
610
|
+
node.data?.[`value_${i}`];
|
|
611
|
+
items.push(value);
|
|
612
|
+
}
|
|
613
|
+
result = items;
|
|
614
|
+
break;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
case 'data:make_object': {
|
|
618
|
+
const numPins = node.data?.pinCount || 0;
|
|
619
|
+
const obj = {};
|
|
620
|
+
for (let i = 0; i < numPins; i++) {
|
|
621
|
+
const key = node.data[`key_${i}`];
|
|
622
|
+
if (key) {
|
|
623
|
+
obj[key] = await this.resolvePinValue(node, `value_${i}`);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
result = obj;
|
|
627
|
+
break;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
case 'data:get_entity_field': {
|
|
631
|
+
const entity = await this.resolvePinValue(node, 'entity');
|
|
632
|
+
result = entity ? entity[pinId] : defaultValue;
|
|
633
|
+
break;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
case 'data:cast': {
|
|
637
|
+
const value = await this.resolvePinValue(node, 'value');
|
|
638
|
+
const targetType = node.data?.targetType || 'String';
|
|
639
|
+
switch (targetType) {
|
|
640
|
+
case 'String': result = String(value ?? ''); break;
|
|
641
|
+
case 'Number': result = Number(value); if (isNaN(result)) result = 0; break;
|
|
642
|
+
case 'Boolean': result = ['true', '1', 'yes'].includes(String(value).toLowerCase()); break;
|
|
643
|
+
default: result = value;
|
|
644
|
+
}
|
|
645
|
+
break;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
case 'string:contains': {
|
|
649
|
+
if (pinId === 'result') {
|
|
650
|
+
const haystack = String(await this.resolvePinValue(node, 'haystack', ''));
|
|
651
|
+
const needle = String(await this.resolvePinValue(node, 'needle', ''));
|
|
652
|
+
const caseSensitive = await this.resolvePinValue(node, 'case_sensitive', false);
|
|
653
|
+
|
|
654
|
+
if (caseSensitive) {
|
|
655
|
+
return haystack.includes(needle);
|
|
656
|
+
} else {
|
|
657
|
+
return haystack.toLowerCase().includes(needle.toLowerCase());
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
break;
|
|
661
|
+
}
|
|
662
|
+
case 'string:matches': {
|
|
663
|
+
if (pinId === 'result') {
|
|
664
|
+
const str = String(await this.resolvePinValue(node, 'string', ''));
|
|
665
|
+
const regexStr = String(await this.resolvePinValue(node, 'regex', ''));
|
|
666
|
+
try {
|
|
667
|
+
result = new RegExp(regexStr).test(str);
|
|
668
|
+
} catch (e) { result = false; }
|
|
669
|
+
}
|
|
670
|
+
break;
|
|
671
|
+
}
|
|
672
|
+
case 'data:string_literal':
|
|
673
|
+
result = await this.resolvePinValue(node, 'value', '');
|
|
674
|
+
break;
|
|
675
|
+
|
|
676
|
+
case 'data:get_user_field': {
|
|
677
|
+
const userIdentifier = await this.resolvePinValue(node, 'user');
|
|
678
|
+
let userObject = null;
|
|
679
|
+
|
|
680
|
+
if (userIdentifier && typeof userIdentifier === 'object' && userIdentifier.username) {
|
|
681
|
+
userObject = userIdentifier;
|
|
682
|
+
} else if (typeof userIdentifier === 'string' && userIdentifier.length > 0) {
|
|
683
|
+
userObject = await User.getUser(userIdentifier, this.context.botId);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
if (userObject) {
|
|
687
|
+
if (pinId === 'username') {
|
|
688
|
+
result = userObject.username;
|
|
689
|
+
} else if (pinId === 'groups') {
|
|
690
|
+
result = userObject.groups ? userObject.groups.map(g => g.group?.name).filter(Boolean) : [];
|
|
691
|
+
} else if (pinId === 'permissions') {
|
|
692
|
+
result = userObject.permissionsSet ? Array.from(userObject.permissionsSet) : [];
|
|
693
|
+
} else if (pinId === 'isBlacklisted') {
|
|
694
|
+
result = !!userObject.isBlacklisted;
|
|
695
|
+
} else {
|
|
696
|
+
result = defaultValue;
|
|
697
|
+
}
|
|
698
|
+
} else {
|
|
699
|
+
result = defaultValue;
|
|
700
|
+
}
|
|
701
|
+
break;
|
|
702
|
+
}
|
|
703
|
+
case 'data:get_server_players':
|
|
704
|
+
result = this.context.players || [];
|
|
705
|
+
break;
|
|
706
|
+
case 'data:get_bot_look':
|
|
707
|
+
result = this.context.botState ? { yaw: this.context.botState.yaw, pitch: this.context.botState.pitch } : null;
|
|
708
|
+
break;
|
|
709
|
+
|
|
710
|
+
case 'array:get_random_element': {
|
|
711
|
+
const arr = await this.resolvePinValue(node, 'array', []);
|
|
712
|
+
if (!Array.isArray(arr) || arr.length === 0) {
|
|
713
|
+
result = null;
|
|
714
|
+
} else {
|
|
715
|
+
const randomIndex = Math.floor(Math.random() * arr.length);
|
|
716
|
+
this.memo.set(`${node.id}:index`, randomIndex);
|
|
717
|
+
if (pinId === 'element') {
|
|
718
|
+
result = arr[randomIndex];
|
|
719
|
+
} else if (pinId === 'index') {
|
|
720
|
+
result = randomIndex;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
break;
|
|
724
|
+
}
|
|
725
|
+
case 'array:add_element': {
|
|
726
|
+
const arr = await this.resolvePinValue(node, 'array', []);
|
|
727
|
+
const element = await this.resolvePinValue(node, 'element', null);
|
|
728
|
+
result = Array.isArray(arr) ? [...arr, element] : [element];
|
|
729
|
+
break;
|
|
730
|
+
}
|
|
731
|
+
case 'array:remove_by_index': {
|
|
732
|
+
const arr = await this.resolvePinValue(node, 'array', []);
|
|
733
|
+
const index = await this.resolvePinValue(node, 'index', -1);
|
|
734
|
+
if (!Array.isArray(arr) || index < 0 || index >= arr.length) {
|
|
735
|
+
result = arr || [];
|
|
736
|
+
} else {
|
|
737
|
+
const newArr = [...arr];
|
|
738
|
+
newArr.splice(index, 1);
|
|
739
|
+
result = newArr;
|
|
740
|
+
}
|
|
741
|
+
break;
|
|
742
|
+
}
|
|
743
|
+
case 'array:get_by_index': {
|
|
744
|
+
const arr = await this.resolvePinValue(node, 'array', []);
|
|
745
|
+
const index = await this.resolvePinValue(node, 'index', -1);
|
|
746
|
+
result = (!Array.isArray(arr) || index < 0 || index >= arr.length) ? null : arr[index];
|
|
747
|
+
break;
|
|
748
|
+
}
|
|
749
|
+
case 'array:find_index': {
|
|
750
|
+
const arr = await this.resolvePinValue(node, 'array', []);
|
|
751
|
+
const element = await this.resolvePinValue(node, 'element', null);
|
|
752
|
+
result = Array.isArray(arr) ? arr.indexOf(element) : -1;
|
|
753
|
+
break;
|
|
754
|
+
}
|
|
755
|
+
case 'array:contains': {
|
|
756
|
+
const arr = await this.resolvePinValue(node, 'array', []);
|
|
757
|
+
const element = await this.resolvePinValue(node, 'element', null);
|
|
758
|
+
if (Array.isArray(arr)) {
|
|
759
|
+
const index = arr.indexOf(element);
|
|
760
|
+
this.memo.set(`${node.id}:index`, index);
|
|
761
|
+
if (pinId === 'result') {
|
|
762
|
+
result = index !== -1;
|
|
763
|
+
} else if (pinId === 'index') {
|
|
764
|
+
result = index;
|
|
765
|
+
}
|
|
766
|
+
} else {
|
|
767
|
+
this.memo.set(`${node.id}:index`, -1);
|
|
768
|
+
if (pinId === 'result') {
|
|
769
|
+
result = false;
|
|
770
|
+
} else if (pinId === 'index') {
|
|
771
|
+
result = -1;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
break;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
|
|
778
|
+
case 'object:create': {
|
|
779
|
+
if (node.data?.advanced) {
|
|
780
|
+
try {
|
|
781
|
+
result = JSON.parse(node.data.jsonValue || '{}');
|
|
782
|
+
} catch (e) {
|
|
783
|
+
console.error('Ошибка парсинга JSON в object:create:', e);
|
|
784
|
+
result = {};
|
|
785
|
+
}
|
|
786
|
+
} else {
|
|
787
|
+
const numPins = node.data?.pinCount || 0;
|
|
788
|
+
const obj = {};
|
|
789
|
+
for (let i = 0; i < numPins; i++) {
|
|
790
|
+
const key = node.data[`key_${i}`];
|
|
791
|
+
if (key) {
|
|
792
|
+
obj[key] = await this.resolvePinValue(node, `value_${i}`);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
result = obj;
|
|
796
|
+
}
|
|
797
|
+
break;
|
|
798
|
+
}
|
|
799
|
+
case 'object:get': {
|
|
800
|
+
const obj = await this.resolvePinValue(node, 'object', {});
|
|
801
|
+
const key = await this.resolvePinValue(node, 'key', '');
|
|
802
|
+
result = obj[key] ?? defaultValue;
|
|
803
|
+
break;
|
|
804
|
+
}
|
|
805
|
+
case 'object:set': {
|
|
806
|
+
const obj = await this.resolvePinValue(node, 'object', {});
|
|
807
|
+
const key = await this.resolvePinValue(node, 'key', '');
|
|
808
|
+
const val = await this.resolvePinValue(node, 'value');
|
|
809
|
+
const newObj = { ...obj };
|
|
810
|
+
newObj[key] = val;
|
|
811
|
+
result = newObj;
|
|
812
|
+
break;
|
|
813
|
+
}
|
|
814
|
+
case 'object:delete': {
|
|
815
|
+
const obj = await this.resolvePinValue(node, 'object', {});
|
|
816
|
+
const key = await this.resolvePinValue(node, 'key', '');
|
|
817
|
+
const newObj = { ...obj };
|
|
818
|
+
delete newObj[key];
|
|
819
|
+
result = newObj;
|
|
820
|
+
break;
|
|
821
|
+
}
|
|
822
|
+
case 'object:has_key': {
|
|
823
|
+
const obj = await this.resolvePinValue(node, 'object', {});
|
|
824
|
+
const key = await this.resolvePinValue(node, 'key', '');
|
|
825
|
+
const exists = obj.hasOwnProperty(key);
|
|
826
|
+
this.memo.set(`${node.id}:value`, exists ? obj[key] : null);
|
|
827
|
+
if (pinId === 'result') {
|
|
828
|
+
result = exists;
|
|
829
|
+
} else if (pinId === 'value') {
|
|
830
|
+
result = this.memo.get(`${node.id}:value`);
|
|
831
|
+
}
|
|
832
|
+
break;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
case 'flow:for_each': {
|
|
836
|
+
if (pinId === 'element') {
|
|
837
|
+
result = this.memo.get(`${node.id}:element`);
|
|
838
|
+
} else if (pinId === 'index') {
|
|
839
|
+
result = this.memo.get(`${node.id}:index`);
|
|
840
|
+
}
|
|
841
|
+
break;
|
|
842
|
+
}
|
|
843
|
+
case 'flow:while': {
|
|
844
|
+
if (pinId === 'iteration') {
|
|
845
|
+
result = this.memo.get(`${node.id}:iteration`);
|
|
846
|
+
}
|
|
847
|
+
break;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
case 'string:equals': {
|
|
851
|
+
const strA = String(await this.resolvePinValue(node, 'a', ''));
|
|
852
|
+
const strB = String(await this.resolvePinValue(node, 'b', ''));
|
|
853
|
+
const caseSensitive = node.data?.case_sensitive ?? true;
|
|
854
|
+
if (pinId === 'result') {
|
|
855
|
+
result = caseSensitive ? strA === strB : strA.toLowerCase() === strB.toLowerCase();
|
|
856
|
+
}
|
|
857
|
+
break;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
case 'logic:compare': {
|
|
861
|
+
const op = node.data?.operation || '==';
|
|
862
|
+
const valA = await this.resolvePinValue(node, 'a');
|
|
863
|
+
const valB = await this.resolvePinValue(node, 'b');
|
|
864
|
+
if (pinId === 'result') {
|
|
865
|
+
switch (op) {
|
|
866
|
+
case '==': result = valA == valB; break;
|
|
867
|
+
case '!=': result = valA != valB; break;
|
|
868
|
+
case '>': result = valA > valB; break;
|
|
869
|
+
case '<': result = valA < valB; break;
|
|
870
|
+
case '>=': result = valA >= valB; break;
|
|
871
|
+
case '<=': result = valA <= valB; break;
|
|
872
|
+
default: result = false;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
break;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
default:
|
|
879
|
+
result = defaultValue;
|
|
880
|
+
break;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
const isVolatile = this.isNodeVolatile(node);
|
|
884
|
+
|
|
885
|
+
if (!isVolatile) {
|
|
886
|
+
this.memo.set(cacheKey, result);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
return result;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
isNodeVolatile(node) {
|
|
893
|
+
if (!node) return false;
|
|
894
|
+
|
|
895
|
+
if (node.type === 'data:get_variable') {
|
|
896
|
+
return true;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
const connections = this.activeGraph.connections.filter(c => c.targetNodeId === node.id);
|
|
900
|
+
for (const conn of connections) {
|
|
901
|
+
const sourceNode = this.activeGraph.nodes.find(n => n.id === conn.sourceNodeId);
|
|
902
|
+
if (this.isNodeVolatile(sourceNode)) {
|
|
903
|
+
return true;
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
return false;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
hasConnection(node, pinId) {
|
|
911
|
+
if (!this.activeGraph || !this.activeGraph.connections) return false;
|
|
912
|
+
return this.activeGraph.connections.some(conn =>
|
|
913
|
+
conn.targetNodeId === node.id && conn.targetPinId === pinId
|
|
914
|
+
);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
918
918
|
module.exports = GraphExecutionEngine;
|