blockmine 1.22.0 → 1.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/.claude/settings.json +5 -1
  2. package/.claude/settings.local.json +10 -1
  3. package/CHANGELOG.md +27 -3
  4. package/CLAUDE.md +284 -0
  5. package/README.md +302 -152
  6. package/backend/package-lock.json +681 -9
  7. package/backend/package.json +8 -0
  8. package/backend/prisma/migrations/20251116111851_add_execution_trace/migration.sql +22 -0
  9. package/backend/prisma/migrations/20251120154914_add_panel_api_keys/migration.sql +21 -0
  10. package/backend/prisma/migrations/20251121110241_add_proxy_table/migration.sql +45 -0
  11. package/backend/prisma/migrations/migration_lock.toml +2 -2
  12. package/backend/prisma/schema.prisma +70 -1
  13. package/backend/src/__tests__/services/BotLifecycleService.test.js +9 -4
  14. package/backend/src/ai/plugin-assistant-system-prompt.md +788 -0
  15. package/backend/src/api/middleware/auth.js +27 -0
  16. package/backend/src/api/middleware/botAccess.js +7 -3
  17. package/backend/src/api/middleware/panelApiAuth.js +135 -0
  18. package/backend/src/api/routes/aiAssistant.js +995 -0
  19. package/backend/src/api/routes/auth.js +669 -633
  20. package/backend/src/api/routes/botCommands.js +107 -0
  21. package/backend/src/api/routes/botGroups.js +165 -0
  22. package/backend/src/api/routes/botHistory.js +108 -0
  23. package/backend/src/api/routes/botPermissions.js +99 -0
  24. package/backend/src/api/routes/botStatus.js +36 -0
  25. package/backend/src/api/routes/botUsers.js +162 -0
  26. package/backend/src/api/routes/bots.js +2451 -2402
  27. package/backend/src/api/routes/eventGraphs.js +4 -1
  28. package/backend/src/api/routes/logs.js +13 -3
  29. package/backend/src/api/routes/panel.js +66 -66
  30. package/backend/src/api/routes/panelApiKeys.js +179 -0
  31. package/backend/src/api/routes/pluginIde.js +1715 -135
  32. package/backend/src/api/routes/plugins.js +376 -219
  33. package/backend/src/api/routes/proxies.js +130 -0
  34. package/backend/src/api/routes/search.js +4 -0
  35. package/backend/src/api/routes/servers.js +20 -3
  36. package/backend/src/api/routes/settings.js +5 -0
  37. package/backend/src/api/routes/system.js +174 -174
  38. package/backend/src/api/routes/traces.js +131 -0
  39. package/backend/src/config/debug.config.js +36 -0
  40. package/backend/src/core/BotHistoryStore.js +180 -0
  41. package/backend/src/core/BotManager.js +14 -4
  42. package/backend/src/core/BotProcess.js +1517 -1092
  43. package/backend/src/core/EventGraphManager.js +37 -123
  44. package/backend/src/core/GraphExecutionEngine.js +977 -321
  45. package/backend/src/core/MessageQueue.js +12 -6
  46. package/backend/src/core/PluginLoader.js +99 -5
  47. package/backend/src/core/PluginManager.js +74 -13
  48. package/backend/src/core/TaskScheduler.js +1 -1
  49. package/backend/src/core/commands/whois.js +1 -1
  50. package/backend/src/core/node-registries/actions.js +70 -0
  51. package/backend/src/core/node-registries/arrays.js +18 -0
  52. package/backend/src/core/node-registries/data.js +1 -1
  53. package/backend/src/core/node-registries/events.js +14 -0
  54. package/backend/src/core/node-registries/logic.js +17 -0
  55. package/backend/src/core/node-registries/strings.js +34 -0
  56. package/backend/src/core/node-registries/type.js +25 -0
  57. package/backend/src/core/nodes/actions/bot_look_at.js +1 -1
  58. package/backend/src/core/nodes/actions/create_command.js +189 -0
  59. package/backend/src/core/nodes/actions/delete_command.js +92 -0
  60. package/backend/src/core/nodes/actions/update_command.js +133 -0
  61. package/backend/src/core/nodes/arrays/join.js +28 -0
  62. package/backend/src/core/nodes/data/cast.js +2 -1
  63. package/backend/src/core/nodes/logic/not.js +22 -0
  64. package/backend/src/core/nodes/strings/starts_with.js +1 -1
  65. package/backend/src/core/nodes/strings/to_lower.js +22 -0
  66. package/backend/src/core/nodes/strings/to_upper.js +22 -0
  67. package/backend/src/core/nodes/type/to_string.js +32 -0
  68. package/backend/src/core/services/BotLifecycleService.js +255 -16
  69. package/backend/src/core/services/CommandExecutionService.js +430 -351
  70. package/backend/src/core/services/DebugSessionManager.js +347 -0
  71. package/backend/src/core/services/GraphCollaborationManager.js +501 -0
  72. package/backend/src/core/services/MinecraftBotManager.js +259 -0
  73. package/backend/src/core/services/MinecraftViewerService.js +216 -0
  74. package/backend/src/core/services/TraceCollectorService.js +545 -0
  75. package/backend/src/core/system/RuntimeCommandRegistry.js +116 -0
  76. package/backend/src/core/system/Transport.js +0 -4
  77. package/backend/src/core/validation/nodeSchemas.js +6 -6
  78. package/backend/src/real-time/botApi/handlers/graphHandlers.js +2 -2
  79. package/backend/src/real-time/botApi/handlers/graphWebSocketHandlers.js +1 -1
  80. package/backend/src/real-time/botApi/utils.js +11 -0
  81. package/backend/src/real-time/panelNamespace.js +387 -0
  82. package/backend/src/real-time/presence.js +7 -2
  83. package/backend/src/real-time/socketHandler.js +395 -4
  84. package/backend/src/server.js +18 -0
  85. package/frontend/dist/assets/index-B1serztM.js +11210 -0
  86. package/frontend/dist/assets/index-t6K1u4OV.css +32 -0
  87. package/frontend/dist/index.html +2 -2
  88. package/frontend/package-lock.json +9437 -0
  89. package/frontend/package.json +8 -0
  90. package/package.json +2 -2
  91. package/screen/console.png +0 -0
  92. package/screen/dashboard.png +0 -0
  93. package/screen/graph_collabe.png +0 -0
  94. package/screen/graph_live_debug.png +0 -0
  95. package/screen/management_command.png +0 -0
  96. package/screen/node_debug_trace.png +0 -0
  97. package/screen/plugin_/320/276/320/261/320/267/320/276/321/200.png +0 -0
  98. package/screen/websocket.png +0 -0
  99. package/screen//320/275/320/260/321/201/321/202/321/200/320/276/320/271/320/272/320/270_/320/276/321/202/320/264/320/265/320/273/321/214/320/275/321/213/321/205_/320/272/320/276/320/274/320/260/320/275/320/264_/320/272/320/260/320/266/320/264/321/203_/320/272/320/276/320/274/320/260/320/275/320/273/320/264/321/203_/320/274/320/276/320/266/320/275/320/276_/320/275/320/260/321/201/321/202/321/200/320/260/320/270/320/262/320/260/321/202/321/214.png +0 -0
  100. package/screen//320/277/320/273/320/260/320/275/320/270/321/200/320/276/320/262/321/211/320/270/320/272_/320/274/320/276/320/266/320/275/320/276_/320/267/320/260/320/264/320/260/320/262/320/260/321/202/321/214_/320/264/320/265/320/271/321/201/321/202/320/262/320/270/321/217_/320/277/320/276_/320/262/321/200/320/265/320/274/320/265/320/275/320/270.png +0 -0
  101. package/frontend/dist/assets/index-CfTo92bP.css +0 -1
  102. package/frontend/dist/assets/index-CiFD5X9Z.js +0 -8344
@@ -1,322 +1,978 @@
1
- const prismaService = require('./PrismaService');
2
- const { parseVariables } = require('./utils/variableParser');
3
- const validationService = require('./services/ValidationService');
4
- const { MAX_RECURSION_DEPTH } = require('./config/validation');
5
- const prisma = prismaService.getClient();
6
-
7
- const BreakLoopSignal = require('./BreakLoopSignal');
8
-
9
- class GraphExecutionEngine {
10
- constructor(nodeRegistry, botManagerOrApi = null) {
11
- if (!nodeRegistry || typeof nodeRegistry.getNodeConfig !== 'function') {
12
- throw new Error('GraphExecutionEngine requires a valid NodeRegistry instance.');
13
- }
14
- this.nodeRegistry = nodeRegistry;
15
- this.botManager = botManagerOrApi;
16
- this.activeGraph = null;
17
- this.context = null;
18
- this.memo = new Map();
19
- }
20
-
21
- async execute(graph, context, eventType) {
22
- if (!graph || graph === 'null') return context;
23
-
24
- const parsedGraph = validationService.parseGraph(graph, 'GraphExecutionEngine.execute');
25
- if (!parsedGraph) {
26
- return context;
27
- }
28
-
29
- const validation = validationService.validateGraphStructure(parsedGraph, 'GraphExecutionEngine');
30
- if (validation.shouldSkip) {
31
- return context;
32
- }
33
-
34
- try {
35
- this.activeGraph = parsedGraph;
36
- this.context = context;
37
-
38
- if (!this.context.variables) {
39
- this.context.variables = parseVariables(
40
- this.activeGraph.variables,
41
- 'GraphExecutionEngine'
42
- );
43
- }
44
-
45
- if (!this.context.persistenceIntent) this.context.persistenceIntent = new Map();
46
- this.memo.clear();
47
-
48
- const eventName = eventType || 'command';
49
- const startNode = this.activeGraph.nodes.find(n => n.type === `event:${eventName}`);
50
-
51
- if (startNode) {
52
- await this.traverse(startNode, 'exec');
53
- } else if (!eventType) {
54
- throw new Error(`Не найдена стартовая нода события (event:command) в графе.`);
55
- }
56
-
57
- } catch (error) {
58
- if (!(error instanceof BreakLoopSignal)) {
59
- console.error(`[GraphExecutionEngine] Критическая ошибка выполнения графа: ${error.stack}`);
60
- }
61
- }
62
-
63
- return this.context;
64
- }
65
-
66
- async traverse(node, fromPinId) {
67
- const connection = this.activeGraph.connections.find(c => c.sourceNodeId === node.id && c.sourcePinId === fromPinId);
68
- if (!connection) return;
69
-
70
- const nextNode = this.activeGraph.nodes.find(n => n.id === connection.targetNodeId);
71
- if (!nextNode) return;
72
-
73
- await this.executeNode(nextNode);
74
- }
75
-
76
- async executeNode(node) {
77
- const nodeConfig = this.nodeRegistry.getNodeConfig(node.type);
78
- if (nodeConfig && typeof nodeConfig.executor === 'function') {
79
- const helpers = {
80
- resolvePinValue: this.resolvePinValue.bind(this),
81
- traverse: this.traverse.bind(this),
82
- memo: this.memo,
83
- clearLoopBodyMemo: this.clearLoopBodyMemo.bind(this),
84
- };
85
- await nodeConfig.executor.call(this, node, this.context, helpers);
86
- return;
87
- }
88
-
89
- const execCacheKey = `${node.id}_executed`;
90
- if (this.memo.has(execCacheKey)) {
91
- return;
92
- }
93
- this.memo.set(execCacheKey, true);
94
-
95
- switch (node.type) {
96
-
97
-
98
-
99
-
100
-
101
-
102
-
103
- case 'string:contains':
104
- case 'string:matches':
105
- case 'string:equals': {
106
- await this.traverse(node, 'exec');
107
- break;
108
- }
109
- case 'array:get_random_element': {
110
- await this.traverse(node, 'element');
111
- break;
112
- }
113
- case 'array:add_element':
114
- case 'array:remove_by_index':
115
- case 'array:get_by_index':
116
- case 'array:find_index':
117
- case 'array:contains': {
118
- await this.traverse(node, 'result');
119
- break;
120
- }
121
- case 'data:array_literal':
122
- case 'data:make_object':
123
- case 'data:get_variable':
124
- case 'data:get_argument':
125
- case 'data:length':
126
- case 'data:get_entity_field':
127
- case 'data:cast':
128
- case 'data:string_literal':
129
- case 'data:get_user_field':
130
- case 'data:get_server_players':
131
- case 'data:get_bot_look':
132
- case 'math:operation':
133
- case 'math:random_number':
134
- case 'logic:operation':
135
- case 'string:concat':
136
- case 'object:create':
137
- case 'object:get':
138
- case 'object:set':
139
- case 'object:delete':
140
- case 'object:has_key': {
141
- await this.traverse(node, 'value');
142
- break;
143
- }
144
- }
145
- }
146
-
147
- clearLoopBodyMemo(loopNode) {
148
- const nodesToClear = new Set();
149
- const queue = [];
150
-
151
- const initialConnection = this.activeGraph.connections.find(
152
- c => c.sourceNodeId === loopNode.id && c.sourcePinId === 'loop_body'
153
- );
154
- if (initialConnection) {
155
- const firstNode = this.activeGraph.nodes.find(n => n.id === initialConnection.targetNodeId);
156
- if (firstNode) {
157
- queue.push(firstNode);
158
- }
159
- }
160
-
161
- const visited = new Set();
162
- while (queue.length > 0) {
163
- const currentNode = queue.shift();
164
- if (visited.has(currentNode.id)) continue;
165
- visited.add(currentNode.id);
166
-
167
- nodesToClear.add(currentNode.id);
168
-
169
- const connections = this.activeGraph.connections.filter(c => c.sourceNodeId === currentNode.id);
170
- for (const conn of connections) {
171
- const nextNode = this.activeGraph.nodes.find(n => n.id === conn.targetNodeId);
172
- if (nextNode) {
173
- queue.push(nextNode);
174
- }
175
- }
176
- }
177
-
178
- for (const nodeId of nodesToClear) {
179
- for (const key of this.memo.keys()) {
180
- if (key.startsWith(nodeId)) {
181
- this.memo.delete(key);
182
- }
183
- }
184
- }
185
- }
186
-
187
- async resolvePinValue(node, pinId, defaultValue = null) {
188
- const connection = this.activeGraph.connections.find(c => c.targetNodeId === node.id && c.targetPinId === pinId);
189
- if (connection) {
190
- const sourceNode = this.activeGraph.nodes.find(n => n.id === connection.sourceNodeId);
191
- return await this.evaluateOutputPin(sourceNode, connection.sourcePinId, defaultValue);
192
- }
193
- return node.data && node.data[pinId] !== undefined ? node.data[pinId] : defaultValue;
194
- }
195
-
196
- async evaluateOutputPin(node, pinId, defaultValue = null) {
197
- if (!node) return defaultValue;
198
-
199
- const cacheKey = `${node.id}:${pinId}`;
200
- if (this.memo.has(cacheKey)) {
201
- return this.memo.get(cacheKey);
202
- }
203
-
204
- const nodeConfig = this.nodeRegistry.getNodeConfig(node.type);
205
- if (nodeConfig && typeof nodeConfig.evaluator === 'function') {
206
- const helpers = {
207
- resolvePinValue: this.resolvePinValue.bind(this),
208
- memo: this.memo,
209
- };
210
- const result = await nodeConfig.evaluator.call(this, node, pinId, this.context, helpers);
211
-
212
- const isVolatile = this.isNodeVolatile(node);
213
- if (!isVolatile) {
214
- this.memo.set(cacheKey, result);
215
- }
216
- return result;
217
- }
218
-
219
- let result;
220
-
221
- switch (node.type) {
222
- case 'user:set_blacklist':
223
- result = this.memo.get(`${node.id}:updated_user`);
224
- break;
225
- case 'event:command':
226
- if (pinId === 'args') result = this.context.args || {};
227
- else if (pinId === 'user') result = this.context.user || {};
228
- else if (pinId === 'chat_type') result = this.context.chat_type || 'chat';
229
- else if (pinId === 'success') result = this.context.success !== undefined ? this.context.success : true;
230
- else result = this.context[pinId];
231
- break;
232
- case 'event:chat':
233
- if (pinId === 'username') result = this.context.username;
234
- else if (pinId === 'message') result = this.context.message;
235
- else if (pinId === 'chatType') result = this.context.chat_type;
236
- else result = this.context[pinId];
237
- break;
238
- case 'event:raw_message':
239
- if (pinId === 'rawText') result = this.context.rawText;
240
- else result = this.context[pinId];
241
- break;
242
- case 'event:playerJoined':
243
- case 'event:playerLeft':
244
- result = this.context[pinId];
245
- break;
246
- case 'event:entitySpawn':
247
- case 'event:entityMoved':
248
- case 'event:entityGone':
249
- result = this.context[pinId];
250
- break;
251
- case 'event:health':
252
- case 'event:botDied':
253
- result = this.context[pinId];
254
- break;
255
- case 'event:websocket_call':
256
- if (pinId === 'graphName') result = this.context.graphName;
257
- else if (pinId === 'data') result = this.context.data;
258
- else if (pinId === 'socketId') result = this.context.socketId;
259
- else if (pinId === 'keyPrefix') result = this.context.keyPrefix;
260
- else result = this.context[pinId];
261
- break;
262
-
263
- case 'flow:for_each': {
264
- if (pinId === 'element') {
265
- result = this.memo.get(`${node.id}:element`);
266
- } else if (pinId === 'index') {
267
- result = this.memo.get(`${node.id}:index`);
268
- }
269
- break;
270
- }
271
-
272
- default:
273
- result = defaultValue;
274
- break;
275
- }
276
-
277
- const isVolatile = this.isNodeVolatile(node);
278
-
279
- if (!isVolatile) {
280
- this.memo.set(cacheKey, result);
281
- }
282
-
283
- return result;
284
- }
285
-
286
- isNodeVolatile(node, visited = new Set(), depth = 0) {
287
- if (!node) return false;
288
-
289
- if (depth > MAX_RECURSION_DEPTH) {
290
- console.warn(`[GraphExecutionEngine] isNodeVolatile достиг максимальной глубины рекурсии (${MAX_RECURSION_DEPTH})`);
291
- return false;
292
- }
293
-
294
- if (visited.has(node.id)) {
295
- return false;
296
- }
297
- visited.add(node.id);
298
-
299
- if (node.type === 'data:get_variable') {
300
- return true;
301
- }
302
-
303
- const connections = this.activeGraph.connections.filter(c => c.targetNodeId === node.id);
304
- for (const conn of connections) {
305
- const sourceNode = this.activeGraph.nodes.find(n => n.id === conn.sourceNodeId);
306
- if (this.isNodeVolatile(sourceNode, visited, depth + 1)) {
307
- return true;
308
- }
309
- }
310
-
311
- return false;
312
- }
313
-
314
- hasConnection(node, pinId) {
315
- if (!this.activeGraph || !this.activeGraph.connections) return false;
316
- return this.activeGraph.connections.some(conn =>
317
- conn.targetNodeId === node.id && conn.targetPinId === pinId
318
- );
319
- }
320
- }
321
-
1
+ const prismaService = require('./PrismaService');
2
+ const { parseVariables } = require('./utils/variableParser');
3
+ const validationService = require('./services/ValidationService');
4
+ const { MAX_RECURSION_DEPTH } = require('./config/validation');
5
+ const debugConfig = require('../config/debug.config');
6
+ const prisma = prismaService.getClient();
7
+
8
+ const BreakLoopSignal = require('./BreakLoopSignal');
9
+ const { getTraceCollector } = require('./services/TraceCollectorService');
10
+ const { getGlobalDebugManager } = require('./services/DebugSessionManager');
11
+
12
+ class GraphExecutionEngine {
13
+ // Static флаг для предотвращения дублирования IPC handler
14
+ static _ipcHandlerAttached = false;
15
+ // Static Map для хранения всех pending requests из всех instances
16
+ static _allPendingRequests = new Map();
17
+
18
+ constructor(nodeRegistry, botManagerOrApi = null) {
19
+ if (!nodeRegistry || typeof nodeRegistry.getNodeConfig !== 'function') {
20
+ throw new Error('GraphExecutionEngine requires a valid NodeRegistry instance.');
21
+ }
22
+ this.nodeRegistry = nodeRegistry;
23
+ this.botManager = botManagerOrApi;
24
+ this.activeGraph = null;
25
+ this.context = null;
26
+ this.memo = new Map();
27
+ this.traceCollector = getTraceCollector();
28
+ this.currentTraceId = null;
29
+
30
+ // Используем статическую Map для всех instance
31
+ this.pendingDebugRequests = GraphExecutionEngine._allPendingRequests;
32
+
33
+ if (process.on && !GraphExecutionEngine._ipcHandlerAttached) {
34
+ process.on('message', (message) => {
35
+ if (message.type === 'debug:breakpoint_response' || message.type === 'debug:step_response') {
36
+ GraphExecutionEngine._handleGlobalDebugResponse(message);
37
+ }
38
+ });
39
+ GraphExecutionEngine._ipcHandlerAttached = true;
40
+ }
41
+ }
42
+
43
+ async execute(graph, context, eventType) {
44
+ if (!graph || graph === 'null') return context;
45
+
46
+ const parsedGraph = validationService.parseGraph(graph, 'GraphExecutionEngine.execute');
47
+ if (!parsedGraph) {
48
+ return context;
49
+ }
50
+
51
+ const validation = validationService.validateGraphStructure(parsedGraph, 'GraphExecutionEngine');
52
+ if (validation.shouldSkip) {
53
+ return context;
54
+ }
55
+
56
+ const graphId = context.graphId || null;
57
+ const botId = context.botId || null;
58
+
59
+ if (graphId && botId) {
60
+ this.currentTraceId = await this.traceCollector.startTrace(
61
+ botId,
62
+ graphId,
63
+ eventType || 'command',
64
+ context.eventArgs || {}
65
+ );
66
+ }
67
+
68
+ try {
69
+ this.activeGraph = parsedGraph;
70
+ this.context = context;
71
+
72
+ if (!this.context.variables) {
73
+ this.context.variables = parseVariables(
74
+ this.activeGraph.variables,
75
+ 'GraphExecutionEngine'
76
+ );
77
+ }
78
+
79
+ if (!this.context.persistenceIntent) this.context.persistenceIntent = new Map();
80
+ this.memo.clear();
81
+
82
+ const eventName = eventType || 'command';
83
+ const startNode = this.activeGraph.nodes.find(n => n.type === `event:${eventName}`);
84
+
85
+ if (startNode) {
86
+ if (this.currentTraceId) {
87
+ this.traceCollector.recordStep(this.currentTraceId, {
88
+ nodeId: startNode.id,
89
+ nodeType: startNode.type,
90
+ status: 'executed',
91
+ duration: 0,
92
+ inputs: {},
93
+ outputs: await this._captureNodeOutputs(startNode),
94
+ });
95
+ }
96
+
97
+ await this.traverse(startNode, 'exec');
98
+ } else if (!eventType) {
99
+ throw new Error(`Не найдена стартовая нода события (event:command) в графе.`);
100
+ }
101
+
102
+ if (this.currentTraceId) {
103
+ // Перед завершением trace, захватываем outputs для всех data-нод
104
+ await this._captureAllDataNodeOutputs();
105
+
106
+ const trace = await this.traceCollector.completeTrace(this.currentTraceId);
107
+
108
+ // Если работаем в дочернем процессе (BotProcess), отправляем трассировку в главный процесс
109
+ if (trace && process.send) {
110
+ process.send({
111
+ type: 'trace:completed',
112
+ trace: {
113
+ ...trace,
114
+ steps: trace.steps,
115
+ eventArgs: typeof trace.eventArgs === 'string' ? trace.eventArgs : JSON.stringify(trace.eventArgs)
116
+ }
117
+ });
118
+ }
119
+
120
+ // Отправляем debug событие только если DebugSessionManager инициализирован
121
+ // (он может быть не инициализирован в дочерних процессах BotProcess)
122
+ if (graphId) {
123
+ try {
124
+ const debugManager = getGlobalDebugManager();
125
+ const debugState = debugManager.get(graphId);
126
+ if (debugState && debugState.activeExecution) {
127
+ debugState.broadcast('debug:completed', {
128
+ trace: await this.traceCollector.getTrace(this.currentTraceId)
129
+ });
130
+ }
131
+ } catch (error) {
132
+ // DebugSessionManager не инициализирован - это нормально для дочерних процессов
133
+ // Просто игнорируем
134
+ }
135
+ }
136
+
137
+ this.currentTraceId = null;
138
+ }
139
+
140
+ } catch (error) {
141
+ if (this.currentTraceId) {
142
+ // Даже при ошибке захватываем outputs для data-нод
143
+ try {
144
+ await this._captureAllDataNodeOutputs();
145
+ } catch (captureError) {
146
+ console.error(`[GraphExecutor] Error capturing outputs on failure:`, captureError);
147
+ }
148
+
149
+ const trace = await this.traceCollector.failTrace(this.currentTraceId, error);
150
+
151
+ // Если работаем в дочернем процессе (BotProcess), отправляем трассировку с ошибкой в главный процесс
152
+ if (trace && process.send) {
153
+ process.send({
154
+ type: 'trace:completed',
155
+ trace: {
156
+ ...trace,
157
+ steps: trace.steps,
158
+ eventArgs: typeof trace.eventArgs === 'string' ? trace.eventArgs : JSON.stringify(trace.eventArgs)
159
+ }
160
+ });
161
+ }
162
+
163
+ this.currentTraceId = null;
164
+ }
165
+
166
+ if (!(error instanceof BreakLoopSignal)) {
167
+ console.error(`[GraphExecutionEngine] Критическая ошибка выполнения графа: ${error.stack}`);
168
+ }
169
+ }
170
+
171
+ return this.context;
172
+ }
173
+
174
+ async traverse(node, fromPinId) {
175
+ const connection = this.activeGraph.connections.find(c => c.sourceNodeId === node.id && c.sourcePinId === fromPinId);
176
+ if (!connection) return;
177
+
178
+ const nextNode = this.activeGraph.nodes.find(n => n.id === connection.targetNodeId);
179
+ if (!nextNode) return;
180
+
181
+ if (this.currentTraceId) {
182
+ this.traceCollector.recordTraversal(
183
+ this.currentTraceId,
184
+ node.id,
185
+ fromPinId,
186
+ nextNode.id
187
+ );
188
+ }
189
+
190
+ await this.executeNode(nextNode);
191
+ }
192
+
193
+ async executeNode(node) {
194
+ const startTime = Date.now();
195
+
196
+ // Записываем шаг ДО проверки брейкпоинта, чтобы в trace были данные о предыдущих нодах
197
+ if (this.currentTraceId) {
198
+ this.traceCollector.recordStep(this.currentTraceId, {
199
+ nodeId: node.id,
200
+ nodeType: node.type,
201
+ status: 'executed',
202
+ duration: null,
203
+ inputs: await this._captureNodeInputs(node),
204
+ outputs: {},
205
+ });
206
+ }
207
+
208
+ // Проверка брейкпоинта ПЕРЕД выполнением (но ПОСЛЕ записи в trace)
209
+ await this.checkBreakpoint(node);
210
+
211
+ // Проверка step mode ПЕРЕД выполнением (важно делать ДО executor, чтобы поймать ноду до traverse)
212
+ await this._checkStepMode(node);
213
+
214
+ const nodeConfig = this.nodeRegistry.getNodeConfig(node.type);
215
+ if (nodeConfig && typeof nodeConfig.executor === 'function') {
216
+ const helpers = {
217
+ resolvePinValue: this.resolvePinValue.bind(this),
218
+ traverse: this.traverse.bind(this),
219
+ memo: this.memo,
220
+ clearLoopBodyMemo: this.clearLoopBodyMemo.bind(this),
221
+ };
222
+
223
+ try {
224
+ await nodeConfig.executor.call(this, node, this.context, helpers);
225
+
226
+ const executionTime = Date.now() - startTime;
227
+
228
+ if (this.currentTraceId) {
229
+ this.traceCollector.updateStepOutputs(this.currentTraceId, node.id, await this._captureNodeOutputs(node));
230
+ this.traceCollector.updateStepDuration(this.currentTraceId, node.id, executionTime);
231
+ }
232
+ } catch (error) {
233
+ if (this.currentTraceId) {
234
+ this.traceCollector.updateStepError(
235
+ this.currentTraceId,
236
+ node.id,
237
+ error.message,
238
+ Date.now() - startTime
239
+ );
240
+ }
241
+ throw error;
242
+ }
243
+
244
+ return;
245
+ }
246
+
247
+ const execCacheKey = `${node.id}_executed`;
248
+ if (this.memo.has(execCacheKey)) {
249
+ return;
250
+ }
251
+ this.memo.set(execCacheKey, true);
252
+
253
+ try {
254
+ await this._executeLegacyNode(node);
255
+
256
+
257
+ const executionTime = Date.now() - startTime;
258
+
259
+ if (this.currentTraceId) {
260
+ this.traceCollector.updateStepOutputs(this.currentTraceId, node.id, await this._captureNodeOutputs(node));
261
+ this.traceCollector.updateStepDuration(this.currentTraceId, node.id, executionTime);
262
+ }
263
+ } catch (error) {
264
+ if (this.currentTraceId) {
265
+ this.traceCollector.updateStepError(
266
+ this.currentTraceId,
267
+ node.id,
268
+ error.message,
269
+ Date.now() - startTime
270
+ );
271
+ }
272
+ throw error;
273
+ }
274
+ }
275
+
276
+ async _checkStepMode(node) {
277
+ try {
278
+ // Если работаем в дочернем процессе, используем IPC
279
+ if (process.send) {
280
+ return await this._checkStepModeViaIpc(node);
281
+ }
282
+
283
+ // Иначе используем прямой доступ к DebugManager
284
+ const { getGlobalDebugManager } = require('./services/DebugSessionManager');
285
+ const debugManager = getGlobalDebugManager();
286
+ const graphId = this.activeGraph?.id;
287
+
288
+ if (graphId) {
289
+ const debugState = debugManager.get(graphId);
290
+ if (debugState && debugState.shouldStepPause(node.id)) {
291
+ console.log(`[Debug] Step mode: pausing after executing node ${node.id}`);
292
+
293
+ // Получаем inputs для отправки в debug session
294
+ const inputs = await this._captureNodeInputs(node);
295
+
296
+ // Получаем текущую трассировку для отображения выполненных шагов
297
+ const executedSteps = this.currentTraceId
298
+ ? await this.traceCollector.getTrace(this.currentTraceId)
299
+ : null;
300
+
301
+ // Паузим выполнение
302
+ await debugState.pause({
303
+ nodeId: node.id,
304
+ nodeType: node.type,
305
+ inputs,
306
+ executedSteps,
307
+ context: {
308
+ user: this.context.user,
309
+ variables: this.context.variables,
310
+ commandArguments: this.context.commandArguments,
311
+ }
312
+ });
313
+ }
314
+ }
315
+ } catch (error) {
316
+ if (error.message !== 'DebugSessionManager not initialized! Call initializeDebugManager(io) first.') {
317
+ throw error;
318
+ }
319
+ }
320
+ }
321
+
322
+ async _checkStepModeViaIpc(node) {
323
+ // Step mode работает аналогично breakpoint, просто отправляем тип 'step'
324
+ const { randomUUID } = require('crypto');
325
+ const requestId = randomUUID();
326
+
327
+ const inputs = await this._captureNodeInputs(node);
328
+ const executedSteps = this.currentTraceId
329
+ ? await this.traceCollector.getTrace(this.currentTraceId)
330
+ : null;
331
+
332
+ process.send({
333
+ type: 'debug:check_step_mode',
334
+ requestId,
335
+ payload: {
336
+ graphId: this.context.graphId,
337
+ nodeId: node.id,
338
+ nodeType: node.type,
339
+ inputs,
340
+ executedSteps,
341
+ context: {
342
+ user: this.context.user,
343
+ variables: this.context.variables,
344
+ commandArguments: this.context.commandArguments,
345
+ }
346
+ }
347
+ });
348
+
349
+ // Ждём ответа (если step mode не активен, ответ придёт сразу)
350
+ await new Promise((resolve) => {
351
+ this.pendingDebugRequests.set(requestId, resolve);
352
+
353
+ // Таймаут на случай, если ответ не придёт
354
+ const timeoutId = setTimeout(() => {
355
+ if (this.pendingDebugRequests.has(requestId)) {
356
+ this.pendingDebugRequests.delete(requestId);
357
+ resolve(null);
358
+ }
359
+ }, debugConfig.STEP_MODE_TIMEOUT);
360
+
361
+ // Сохраняем timeoutId для возможной отмены
362
+ this.pendingDebugRequests.set(`${requestId}_timeout`, timeoutId);
363
+ });
364
+ }
365
+
366
+ async _executeLegacyNode(node) {
367
+ switch (node.type) {
368
+
369
+
370
+
371
+
372
+
373
+
374
+
375
+ case 'string:contains':
376
+ case 'string:matches':
377
+ case 'string:equals': {
378
+ await this.traverse(node, 'exec');
379
+ break;
380
+ }
381
+ case 'array:get_random_element': {
382
+ await this.traverse(node, 'element');
383
+ break;
384
+ }
385
+ case 'array:add_element':
386
+ case 'array:remove_by_index':
387
+ case 'array:get_by_index':
388
+ case 'array:find_index':
389
+ case 'array:contains': {
390
+ await this.traverse(node, 'result');
391
+ break;
392
+ }
393
+ case 'data:array_literal':
394
+ case 'data:make_object':
395
+ case 'data:get_variable':
396
+ case 'data:get_argument':
397
+ case 'data:length':
398
+ case 'data:get_entity_field':
399
+ case 'data:cast':
400
+ case 'data:string_literal':
401
+ case 'data:get_user_field':
402
+ case 'data:get_server_players':
403
+ case 'data:get_bot_look':
404
+ case 'math:operation':
405
+ case 'math:random_number':
406
+ case 'logic:operation':
407
+ case 'string:concat':
408
+ case 'object:create':
409
+ case 'object:get':
410
+ case 'object:set':
411
+ case 'object:delete':
412
+ case 'object:has_key': {
413
+ await this.traverse(node, 'value');
414
+ break;
415
+ }
416
+ }
417
+ }
418
+
419
+ clearLoopBodyMemo(loopNode) {
420
+ const nodesToClear = new Set();
421
+ const queue = [];
422
+
423
+ const initialConnection = this.activeGraph.connections.find(
424
+ c => c.sourceNodeId === loopNode.id && c.sourcePinId === 'loop_body'
425
+ );
426
+ if (initialConnection) {
427
+ const firstNode = this.activeGraph.nodes.find(n => n.id === initialConnection.targetNodeId);
428
+ if (firstNode) {
429
+ queue.push(firstNode);
430
+ }
431
+ }
432
+
433
+ const visited = new Set();
434
+ while (queue.length > 0) {
435
+ const currentNode = queue.shift();
436
+ if (visited.has(currentNode.id)) continue;
437
+ visited.add(currentNode.id);
438
+
439
+ nodesToClear.add(currentNode.id);
440
+
441
+ const connections = this.activeGraph.connections.filter(c => c.sourceNodeId === currentNode.id);
442
+ for (const conn of connections) {
443
+ const nextNode = this.activeGraph.nodes.find(n => n.id === conn.targetNodeId);
444
+ if (nextNode) {
445
+ queue.push(nextNode);
446
+ }
447
+ }
448
+ }
449
+
450
+ for (const nodeId of nodesToClear) {
451
+ for (const key of this.memo.keys()) {
452
+ if (key.startsWith(nodeId)) {
453
+ this.memo.delete(key);
454
+ }
455
+ }
456
+ }
457
+ }
458
+
459
+ async resolvePinValue(node, pinId, defaultValue = null) {
460
+ const connection = this.activeGraph.connections.find(c => c.targetNodeId === node.id && c.targetPinId === pinId);
461
+ if (connection) {
462
+ const sourceNode = this.activeGraph.nodes.find(n => n.id === connection.sourceNodeId);
463
+ return await this.evaluateOutputPin(sourceNode, connection.sourcePinId, defaultValue);
464
+ }
465
+ return node.data && node.data[pinId] !== undefined ? node.data[pinId] : defaultValue;
466
+ }
467
+
468
+ async evaluateOutputPin(node, pinId, defaultValue = null) {
469
+ if (!node) return defaultValue;
470
+
471
+ const cacheKey = `${node.id}:${pinId}`;
472
+ if (this.memo.has(cacheKey)) {
473
+ return this.memo.get(cacheKey);
474
+ }
475
+
476
+ const nodeConfig = this.nodeRegistry.getNodeConfig(node.type);
477
+ if (nodeConfig && typeof nodeConfig.evaluator === 'function') {
478
+ const helpers = {
479
+ resolvePinValue: this.resolvePinValue.bind(this),
480
+ memo: this.memo,
481
+ };
482
+
483
+ // Записываем data ноду в трассировку перед вычислением (только один раз)
484
+ const traceKey = `trace_recorded:${node.id}`;
485
+ const isDataNode = !nodeConfig.pins.inputs.some(p => p.type === 'Exec');
486
+
487
+ if (this.currentTraceId && isDataNode && !this.memo.has(traceKey)) {
488
+ // Это data нода (нет exec пинов) и она ещё не записана - записываем её inputs
489
+ const inputs = await this._captureNodeInputs(node);
490
+
491
+ this.traceCollector.recordStep(this.currentTraceId, {
492
+ nodeId: node.id,
493
+ nodeType: node.type,
494
+ status: 'executed',
495
+ duration: 0,
496
+ inputs,
497
+ outputs: {},
498
+ });
499
+
500
+ // Помечаем, что нода уже записана в трассировку
501
+ this.memo.set(traceKey, true);
502
+ }
503
+
504
+ const result = await nodeConfig.evaluator.call(this, node, pinId, this.context, helpers);
505
+
506
+ const isVolatile = this.isNodeVolatile(node);
507
+ if (!isVolatile) {
508
+ this.memo.set(cacheKey, result);
509
+ }
510
+
511
+ // Обновляем outputs для data ноды после вычисления И после записи в memo
512
+ // Обновляем только один раз для всей ноды (не для каждого пина отдельно)
513
+ const traceOutputsKey = `trace_outputs_recorded:${node.id}`;
514
+ if (this.currentTraceId && isDataNode && this.memo.has(traceKey) && !this.memo.has(traceOutputsKey)) {
515
+ const outputs = {};
516
+ for (const outputPin of nodeConfig.pins.outputs) {
517
+ if (outputPin.type !== 'Exec') {
518
+ const outKey = `${node.id}:${outputPin.id}`;
519
+ if (this.memo.has(outKey)) {
520
+ outputs[outputPin.id] = this.memo.get(outKey);
521
+ }
522
+ }
523
+ }
524
+ this.traceCollector.updateStepOutputs(this.currentTraceId, node.id, outputs);
525
+ this.memo.set(traceOutputsKey, true); // Помечаем, что outputs уже записаны
526
+ }
527
+
528
+ return result;
529
+ }
530
+
531
+ let result;
532
+
533
+ switch (node.type) {
534
+ case 'user:set_blacklist':
535
+ result = this.memo.get(`${node.id}:updated_user`);
536
+ break;
537
+ case 'event:command':
538
+ if (pinId === 'args') result = this.context.eventArgs?.args || {};
539
+ else if (pinId === 'user') result = this.context.eventArgs?.user || {};
540
+ else if (pinId === 'chat_type') result = this.context.eventArgs?.typeChat || 'chat';
541
+ else if (pinId === 'command_name') result = this.context.eventArgs?.commandName;
542
+ else if (pinId === 'success') result = this.context.success !== undefined ? this.context.success : true;
543
+ else result = this.context.eventArgs?.[pinId];
544
+ break;
545
+ case 'event:chat':
546
+ if (pinId === 'username') result = this.context.eventArgs?.username || this.context.username;
547
+ else if (pinId === 'message') result = this.context.eventArgs?.message || this.context.message;
548
+ else if (pinId === 'chatType') result = this.context.eventArgs?.chatType || this.context.chat_type;
549
+ else result = this.context.eventArgs?.[pinId] || this.context[pinId];
550
+ break;
551
+ case 'event:raw_message':
552
+ if (pinId === 'rawText') result = this.context.eventArgs?.rawText || this.context.rawText;
553
+ else result = this.context.eventArgs?.[pinId] || this.context[pinId];
554
+ break;
555
+ case 'event:playerJoined':
556
+ case 'event:playerLeft':
557
+ if (pinId === 'user') result = this.context.eventArgs?.user || this.context.user;
558
+ else result = this.context.eventArgs?.[pinId] || this.context[pinId];
559
+ break;
560
+ case 'event:entitySpawn':
561
+ case 'event:entityMoved':
562
+ case 'event:entityGone':
563
+ if (pinId === 'entity') result = this.context.eventArgs?.entity || this.context.entity;
564
+ else result = this.context.eventArgs?.[pinId] || this.context[pinId];
565
+ break;
566
+ case 'event:health':
567
+ case 'event:botDied':
568
+ case 'event:botStartup':
569
+ result = this.context.eventArgs?.[pinId] || this.context[pinId];
570
+ break;
571
+ case 'event:websocket_call':
572
+ if (pinId === 'graphName') result = this.context.eventArgs?.graphName || this.context.graphName;
573
+ else if (pinId === 'data') result = this.context.eventArgs?.data || this.context.data;
574
+ else if (pinId === 'socketId') result = this.context.eventArgs?.socketId || this.context.socketId;
575
+ else if (pinId === 'keyPrefix') result = this.context.eventArgs?.keyPrefix || this.context.keyPrefix;
576
+ else result = this.context.eventArgs?.[pinId] || this.context[pinId];
577
+ break;
578
+
579
+ case 'flow:for_each': {
580
+ if (pinId === 'element') {
581
+ result = this.memo.get(`${node.id}:element`);
582
+ } else if (pinId === 'index') {
583
+ result = this.memo.get(`${node.id}:index`);
584
+ }
585
+ break;
586
+ }
587
+
588
+ default:
589
+ result = defaultValue;
590
+ break;
591
+ }
592
+
593
+ const isVolatile = this.isNodeVolatile(node);
594
+
595
+ if (!isVolatile) {
596
+ this.memo.set(cacheKey, result);
597
+ }
598
+
599
+ return result;
600
+ }
601
+
602
+ isNodeVolatile(node, visited = new Set(), depth = 0) {
603
+ if (!node) return false;
604
+
605
+ if (depth > MAX_RECURSION_DEPTH) {
606
+ console.warn(`[GraphExecutionEngine] isNodeVolatile достиг максимальной глубины рекурсии (${MAX_RECURSION_DEPTH})`);
607
+ return false;
608
+ }
609
+
610
+ if (visited.has(node.id)) {
611
+ return false;
612
+ }
613
+ visited.add(node.id);
614
+
615
+ if (node.type === 'data:get_variable') {
616
+ return true;
617
+ }
618
+
619
+ const connections = this.activeGraph.connections.filter(c => c.targetNodeId === node.id);
620
+ for (const conn of connections) {
621
+ const sourceNode = this.activeGraph.nodes.find(n => n.id === conn.sourceNodeId);
622
+ if (this.isNodeVolatile(sourceNode, visited, depth + 1)) {
623
+ return true;
624
+ }
625
+ }
626
+
627
+ return false;
628
+ }
629
+
630
+ hasConnection(node, pinId) {
631
+ if (!this.activeGraph || !this.activeGraph.connections) return false;
632
+ return this.activeGraph.connections.some(conn =>
633
+ conn.targetNodeId === node.id && conn.targetPinId === pinId
634
+ );
635
+ }
636
+
637
+ /**
638
+ * Захватить outputs для всех data-нод в trace
639
+ * Вызывается перед завершением trace, чтобы гарантировать,
640
+ * что для всех data-нод записаны outputs (даже если их выходы не подключены)
641
+ */
642
+ async _captureAllDataNodeOutputs() {
643
+ if (!this.currentTraceId) return;
644
+
645
+ const trace = await this.traceCollector.getTrace(this.currentTraceId);
646
+ if (!trace || !trace.steps) return;
647
+
648
+ // Проходим по всем шагам и находим data-ноды
649
+ for (const step of trace.steps) {
650
+ if (step.type === 'traversal') continue;
651
+
652
+ // Проверяем, есть ли уже outputs
653
+ if (step.outputs && Object.keys(step.outputs).length > 0) continue;
654
+
655
+ // Находим ноду в графе
656
+ const node = this.activeGraph.nodes.find(n => n.id === step.nodeId);
657
+ if (!node) continue;
658
+
659
+ // Получаем конфигурацию ноды
660
+ const nodeConfig = this.nodeRegistry.getNodeConfig(node.type);
661
+ if (!nodeConfig) continue;
662
+
663
+ // Проверяем, является ли это data-нодой (нет exec входов)
664
+ const isDataNode = !nodeConfig.pins.inputs.some(p => p.type === 'Exec');
665
+ if (!isDataNode) continue;
666
+
667
+ // Захватываем outputs
668
+ try {
669
+ const outputs = await this._captureNodeOutputs(node);
670
+ if (outputs && Object.keys(outputs).length > 0) {
671
+ this.traceCollector.updateStepOutputs(this.currentTraceId, node.id, outputs);
672
+ }
673
+ } catch (error) {
674
+ console.error(`[GraphExecutor] Error capturing outputs for data node ${node.id}:`, error);
675
+ }
676
+ }
677
+ }
678
+
679
+ /**
680
+ * Захватить значения входов ноды для трассировки
681
+ */
682
+ async _captureNodeInputs(node) {
683
+ const inputs = {};
684
+
685
+ // Получаем конфигурацию ноды
686
+ const nodeConfig = this.nodeRegistry.getNodeConfig(node.type);
687
+ if (!nodeConfig || !nodeConfig.pins || !nodeConfig.pins.inputs) {
688
+ return inputs;
689
+ }
690
+
691
+ // Захватываем значения всех входов
692
+ for (const inputPin of nodeConfig.pins.inputs) {
693
+ if (inputPin.type === 'Exec') continue; // Пропускаем exec пины
694
+
695
+ try {
696
+ // Используем resolvePinValue для получения актуального значения
697
+ const value = await this.resolvePinValue(node, inputPin.id);
698
+ // Записываем все значения, включая undefined и null
699
+ inputs[inputPin.id] = value;
700
+ } catch (error) {
701
+ inputs[inputPin.id] = '<error capturing>';
702
+ }
703
+ }
704
+ return inputs;
705
+ }
706
+
707
+ /**
708
+ * Захватить значения выходов ноды для трассировки
709
+ */
710
+ async _captureNodeOutputs(node) {
711
+ const outputs = {};
712
+
713
+ // Получаем конфигурацию ноды
714
+ const nodeConfig = this.nodeRegistry.getNodeConfig(node.type);
715
+ if (!nodeConfig || !nodeConfig.pins || !nodeConfig.pins.outputs) {
716
+ return outputs;
717
+ }
718
+
719
+ // Захватываем значения всех выходов
720
+ for (const outputPin of nodeConfig.pins.outputs) {
721
+ if (outputPin.type === 'Exec') continue; // Пропускаем exec пины
722
+
723
+ try {
724
+ // Активно вызываем evaluateOutputPin вместо чтения из memo
725
+ // Это необходимо для event нод, которые используют switch-case и не пишут в memo
726
+ const value = await this.evaluateOutputPin(node, outputPin.id);
727
+ // Записываем все значения, включая undefined и null
728
+ outputs[outputPin.id] = value;
729
+ } catch (error) {
730
+ outputs[outputPin.id] = '<error capturing>';
731
+ }
732
+ }
733
+ return outputs;
734
+ }
735
+
736
+ /**
737
+ * Проверить брейкпоинт перед выполнением ноды
738
+ */
739
+ async checkBreakpoint(node) {
740
+ try {
741
+ // Если работаем в дочернем процессе, используем IPC
742
+ if (process.send) {
743
+ return await this._checkBreakpointViaIpc(node);
744
+ }
745
+
746
+ // Иначе используем прямой доступ к DebugManager
747
+ const debugManager = getGlobalDebugManager();
748
+ const graphId = this.context.graphId;
749
+
750
+ if (!graphId) return;
751
+
752
+ const debugState = debugManager.get(graphId);
753
+ if (!debugState) return;
754
+
755
+ const breakpoint = debugState.breakpoints.get(node.id);
756
+ if (!breakpoint || !breakpoint.enabled) return;
757
+
758
+ const shouldPause = await this.evaluateBreakpointCondition(breakpoint);
759
+
760
+ if (shouldPause) {
761
+ console.log(`[Debug] Hit breakpoint at node ${node.id}, pausing execution`);
762
+
763
+ breakpoint.hitCount++;
764
+
765
+ const inputs = await this._captureNodeInputs(node);
766
+
767
+ const executedSteps = this.currentTraceId
768
+ ? await this.traceCollector.getTrace(this.currentTraceId)
769
+ : null;
770
+
771
+ const overrides = await debugState.pause({
772
+ nodeId: node.id,
773
+ nodeType: node.type,
774
+ inputs,
775
+ executedSteps, // Добавляем выполненные шаги
776
+ context: {
777
+ user: this.context.user,
778
+ variables: this.context.variables,
779
+ commandArguments: this.context.commandArguments,
780
+ },
781
+ breakpoint: {
782
+ condition: breakpoint.condition,
783
+ hitCount: breakpoint.hitCount
784
+ }
785
+ });
786
+
787
+ if (overrides && overrides.__stopped) {
788
+ throw new Error('Execution stopped by debugger');
789
+ }
790
+
791
+ if (overrides) {
792
+ this.applyWhatIfOverrides(node, overrides);
793
+ }
794
+ }
795
+ } catch (error) {
796
+ console.error(`[checkBreakpoint] ERROR:`, error.message, error.stack);
797
+ if (error.message === 'DebugSessionManager not initialized! Call initializeDebugManager(io) first.') {
798
+ return;
799
+ }
800
+ // НЕ пробрасываем ошибку дальше, чтобы не сломать выполнение
801
+ console.error(`[checkBreakpoint] Ignoring error to continue execution`);
802
+ }
803
+ }
804
+
805
+ /**
806
+ * Оценить условие брейкпоинта
807
+ */
808
+ async evaluateBreakpointCondition(breakpoint) {
809
+ // Если нет условия, всегда срабатывает
810
+ if (!breakpoint.condition || breakpoint.condition.trim() === '') {
811
+ return true;
812
+ }
813
+
814
+ try {
815
+ // Создаем sandbox для безопасного выполнения условия
816
+ const sandbox = {
817
+ user: this.context.user || {},
818
+ args: this.context.commandArguments || {},
819
+ variables: this.context.variables || {},
820
+ context: this.context
821
+ };
822
+
823
+ // Используем Function constructor для безопасного выполнения
824
+ const fn = new Function(
825
+ ...Object.keys(sandbox),
826
+ `return (${breakpoint.condition})`
827
+ );
828
+
829
+ const result = fn(...Object.values(sandbox));
830
+
831
+ return Boolean(result);
832
+ } catch (error) {
833
+ console.error(`[Debug] Error evaluating breakpoint condition: ${error.message}`);
834
+ console.error(`[Debug] Condition was: ${breakpoint.condition}`);
835
+ return false;
836
+ }
837
+ }
838
+
839
+ /**
840
+ * Применить what-if overrides к ноде
841
+ */
842
+ /**
843
+ * Проверить брейкпоинт через IPC (для дочерних процессов)
844
+ */
845
+ async _checkBreakpointViaIpc(node) {
846
+ try {
847
+ const { randomUUID } = require('crypto');
848
+ const requestId = randomUUID();
849
+
850
+ const inputs = await this._captureNodeInputs(node);
851
+ const executedSteps = this.currentTraceId
852
+ ? await this.traceCollector.getTrace(this.currentTraceId)
853
+ : null;
854
+
855
+ // Отправляем запрос в главный процесс
856
+ process.send({
857
+ type: 'debug:check_breakpoint',
858
+ requestId,
859
+ payload: {
860
+ graphId: this.context.graphId,
861
+ nodeId: node.id,
862
+ nodeType: node.type,
863
+ inputs,
864
+ executedSteps,
865
+ context: {
866
+ user: this.context.user,
867
+ variables: this.context.variables,
868
+ commandArguments: this.context.commandArguments,
869
+ }
870
+ }
871
+ });
872
+
873
+ // Ждём ответа
874
+ const overrides = await new Promise((resolve) => {
875
+ this.pendingDebugRequests.set(requestId, resolve);
876
+
877
+ // Таймаут на случай, если ответ не придёт
878
+ const timeoutId = setTimeout(() => {
879
+ if (this.pendingDebugRequests.has(requestId)) {
880
+ this.pendingDebugRequests.delete(requestId);
881
+ resolve(null);
882
+ }
883
+ }, debugConfig.BREAKPOINT_TIMEOUT);
884
+
885
+ // Сохраняем timeoutId для возможной отмены
886
+ this.pendingDebugRequests.set(`${requestId}_timeout`, timeoutId);
887
+ });
888
+
889
+ if (overrides && overrides.__stopped) {
890
+ throw new Error('Execution stopped by debugger');
891
+ }
892
+
893
+ if (overrides) {
894
+ this.applyWhatIfOverrides(node, overrides);
895
+ }
896
+ } catch (error) {
897
+ if (error.message === 'Execution stopped by debugger') {
898
+ throw error;
899
+ }
900
+ }
901
+ }
902
+
903
+ /**
904
+ * Статический обработчик IPC ответов (глобальный для всех instances)
905
+ */
906
+ static _handleGlobalDebugResponse(message) {
907
+ const { requestId, overrides } = message;
908
+ const resolve = GraphExecutionEngine._allPendingRequests.get(requestId);
909
+
910
+ if (resolve) {
911
+ // Отменяем таймаут
912
+ const timeoutId = GraphExecutionEngine._allPendingRequests.get(`${requestId}_timeout`);
913
+ if (timeoutId) {
914
+ clearTimeout(timeoutId);
915
+ GraphExecutionEngine._allPendingRequests.delete(`${requestId}_timeout`);
916
+ }
917
+
918
+ GraphExecutionEngine._allPendingRequests.delete(requestId);
919
+ resolve(overrides);
920
+ }
921
+ }
922
+
923
+ /**
924
+ * Обработать IPC ответ от главного процесса (legacy wrapper)
925
+ */
926
+ _handleDebugResponse(message) {
927
+ GraphExecutionEngine._handleGlobalDebugResponse(message);
928
+ }
929
+
930
+ applyWhatIfOverrides(node, overrides) {
931
+ if (!overrides || typeof overrides !== 'object') return;
932
+
933
+ console.log(`[Debug] Applying what-if overrides to node ${node.id}:`, overrides);
934
+
935
+ for (const [key, value] of Object.entries(overrides)) {
936
+ if (key === '__stopped') continue;
937
+
938
+ // Парсим ключ для определения типа изменения
939
+ // Форматы:
940
+ // - "var.varName" - переменная графа
941
+ // - "nodeId.out.pinName" - выходной пин ноды
942
+ // - "nodeId.in.pinName" - входной пин ноды
943
+ // - "pinName" - входной пин текущей ноды
944
+
945
+ if (key.startsWith('var.')) {
946
+ // Изменение переменной графа
947
+ const varName = key.substring(4);
948
+ if (!this.context.variables) {
949
+ this.context.variables = {};
950
+ }
951
+ this.context.variables[varName] = value;
952
+ console.log(`[Debug] Variable override: ${varName} = ${JSON.stringify(value)}`);
953
+ }
954
+ else if (key.includes('.out.')) {
955
+ // Изменение выходного пина ноды
956
+ const [nodeId, , pinName] = key.split('.');
957
+ const memoKey = `${nodeId}:${pinName}`;
958
+ this.memo.set(memoKey, value);
959
+ console.log(`[Debug] Output override: ${memoKey} = ${JSON.stringify(value)}`);
960
+ }
961
+ else if (key.includes('.in.')) {
962
+ // Изменение входного пина ноды (пока не применяется, но можно расширить)
963
+ const [nodeId, , pinName] = key.split('.');
964
+ console.log(`[Debug] Input override (informational): ${nodeId}.${pinName} = ${JSON.stringify(value)}`);
965
+ // Входы можно изменить через изменение outputs предыдущих нод или переменных
966
+ }
967
+ else {
968
+ // Изменение входного пина текущей ноды
969
+ if (!node.data) {
970
+ node.data = {};
971
+ }
972
+ node.data[key] = value;
973
+ }
974
+ }
975
+ }
976
+ }
977
+
322
978
  module.exports = GraphExecutionEngine;