blockmine 1.25.0 → 1.27.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 (164) hide show
  1. package/CHANGELOG.md +46 -3
  2. package/backend/cli.js +1 -1
  3. package/backend/package.json +2 -2
  4. package/backend/prisma/migrations/20260328173000_add_plugin_source_ref/migration.sql +2 -0
  5. package/backend/prisma/migrations/migration_lock.toml +2 -2
  6. package/backend/prisma/schema.prisma +2 -0
  7. package/backend/src/api/routes/apiKeys.js +8 -0
  8. package/backend/src/api/routes/bots.js +258 -9
  9. package/backend/src/api/routes/eventGraphs.js +151 -1
  10. package/backend/src/api/routes/health.js +38 -0
  11. package/backend/src/api/routes/nodeRegistry.js +63 -0
  12. package/backend/src/api/routes/plugins.js +254 -29
  13. package/backend/src/container.js +11 -8
  14. package/backend/src/core/BotCommandLoader.js +161 -0
  15. package/backend/src/core/BotConnection.js +125 -0
  16. package/backend/src/core/BotEventHandlers.js +234 -0
  17. package/backend/src/core/BotIPCHandler.js +445 -0
  18. package/backend/src/core/BotManager.js +15 -7
  19. package/backend/src/core/BotProcess.js +75 -142
  20. package/backend/src/core/EventGraphManager.js +7 -3
  21. package/backend/src/core/GraphDebugHandler.js +229 -0
  22. package/backend/src/core/GraphDebugIPC.js +117 -0
  23. package/backend/src/core/GraphExecutionEngine.js +545 -978
  24. package/backend/src/core/GraphTraversal.js +80 -0
  25. package/backend/src/core/GraphValidation.js +73 -0
  26. package/backend/src/core/NodeDefinition.js +138 -0
  27. package/backend/src/core/NodeRegistry.js +153 -141
  28. package/backend/src/core/PluginManager.js +272 -31
  29. package/backend/src/core/RewindSignal.js +9 -0
  30. package/backend/src/core/config/ConfigValidator.js +72 -0
  31. package/backend/src/core/config/FeatureFlags.js +52 -0
  32. package/backend/src/core/config/__tests__/ConfigValidator.test.js +232 -0
  33. package/backend/src/core/domain/entities/Bot.js +39 -0
  34. package/backend/src/core/domain/entities/Command.js +41 -0
  35. package/backend/src/core/domain/entities/EventGraph.js +39 -0
  36. package/backend/src/core/domain/entities/Plugin.js +45 -0
  37. package/backend/src/core/domain/entities/User.js +40 -0
  38. package/backend/src/core/domain/services/DependencyResolver.js +168 -0
  39. package/backend/src/core/domain/services/GraphValidator.js +117 -0
  40. package/backend/src/core/domain/services/PermissionChecker.js +34 -0
  41. package/backend/src/core/domain/services/__tests__/DependencyResolver.test.js +126 -0
  42. package/backend/src/core/domain/valueObjects/BotConfig.js +27 -0
  43. package/backend/src/core/domain/valueObjects/DependencyGraph.js +86 -0
  44. package/backend/src/core/domain/valueObjects/PluginManifest.js +36 -0
  45. package/backend/src/core/errors/BaseError.js +29 -0
  46. package/backend/src/core/errors/ErrorHandler.js +81 -0
  47. package/backend/src/core/errors/__tests__/ErrorHandler.test.js +188 -0
  48. package/backend/src/core/errors/index.js +68 -0
  49. package/backend/src/core/infrastructure/BatchingUtility.js +66 -0
  50. package/backend/src/core/infrastructure/CircuitBreaker.js +103 -0
  51. package/backend/src/core/infrastructure/ConnectionPool.js +81 -0
  52. package/backend/src/core/infrastructure/RateLimiter.js +64 -0
  53. package/backend/src/core/infrastructure/__tests__/BatchingUtility.test.js +86 -0
  54. package/backend/src/core/infrastructure/__tests__/CircuitBreaker.test.js +156 -0
  55. package/backend/src/core/infrastructure/__tests__/ConnectionPool.test.js +146 -0
  56. package/backend/src/core/infrastructure/__tests__/RateLimiter.test.js +171 -0
  57. package/backend/src/core/ipc/botApiFactory.js +72 -0
  58. package/backend/src/core/ipc/ipcMessageTypes.js +115 -0
  59. package/backend/src/core/logging/AuditLogger.js +61 -0
  60. package/backend/src/core/logging/StructuredLogger.js +80 -0
  61. package/backend/src/core/logging/__tests__/StructuredLogger.test.js +213 -0
  62. package/backend/src/core/logging/index.js +7 -0
  63. package/backend/src/core/metrics/MetricsCollector.js +104 -0
  64. package/backend/src/core/metrics/__tests__/MetricsCollector.test.js +131 -0
  65. package/backend/src/core/node-registries/actionsNodes.js +191 -0
  66. package/backend/src/core/node-registries/arraysNodes.js +152 -0
  67. package/backend/src/core/node-registries/botNodes.js +48 -0
  68. package/backend/src/core/node-registries/containerNodes.js +141 -0
  69. package/backend/src/core/node-registries/dataNodes.js +284 -0
  70. package/backend/src/core/node-registries/debugNodes.js +23 -0
  71. package/backend/src/core/node-registries/eventsNodes.js +223 -0
  72. package/backend/src/core/node-registries/flowNodes.js +151 -0
  73. package/backend/src/core/node-registries/furnaceNodes.js +123 -0
  74. package/backend/src/core/node-registries/index.js +108 -0
  75. package/backend/src/core/node-registries/inventory.js +102 -106
  76. package/backend/src/core/node-registries/logicNodes.js +54 -0
  77. package/backend/src/core/node-registries/mathNodes.js +38 -0
  78. package/backend/src/core/node-registries/navigationNodes.js +109 -0
  79. package/backend/src/core/node-registries/objectsNodes.js +90 -0
  80. package/backend/src/core/node-registries/stringsNodes.js +165 -0
  81. package/backend/src/core/node-registries/timeNodes.js +105 -0
  82. package/backend/src/core/node-registries/typeNodes.js +22 -0
  83. package/backend/src/core/node-registries/usersNodes.js +126 -0
  84. package/backend/src/core/nodes/arrays/shuffle.js +14 -0
  85. package/backend/src/core/nodes/bot/get_name.js +8 -0
  86. package/backend/src/core/nodes/bot/stop_bot.js +5 -0
  87. package/backend/src/core/nodes/container/open.js +101 -111
  88. package/backend/src/core/nodes/data/store_read.js +26 -0
  89. package/backend/src/core/nodes/data/store_write.js +23 -0
  90. package/backend/src/core/nodes/event/call_event.js +31 -0
  91. package/backend/src/core/nodes/event/custom_event.js +8 -0
  92. package/backend/src/core/nodes/flow/timer.js +35 -0
  93. package/backend/src/core/nodes/inventory/drop.js +73 -65
  94. package/backend/src/core/nodes/inventory/equip.js +54 -45
  95. package/backend/src/core/nodes/inventory/select_slot.js +48 -46
  96. package/backend/src/core/nodes/navigation/follow.js +54 -51
  97. package/backend/src/core/nodes/navigation/go_to.js +41 -53
  98. package/backend/src/core/nodes/navigation/go_to_entity.js +65 -69
  99. package/backend/src/core/nodes/navigation/go_to_player.js +65 -70
  100. package/backend/src/core/nodes/navigation/stop.js +17 -26
  101. package/backend/src/core/nodes/users/add_to_group.js +24 -0
  102. package/backend/src/core/nodes/users/check_permission.js +26 -0
  103. package/backend/src/core/nodes/users/remove_from_group.js +24 -0
  104. package/backend/src/core/services/BotIPCMessageRouter.js +337 -0
  105. package/backend/src/core/services/BotLifecycleService.js +41 -632
  106. package/backend/src/core/services/CacheManager.js +83 -23
  107. package/backend/src/core/services/CrashRestartManager.js +42 -0
  108. package/backend/src/core/services/DebugSessionManager.js +114 -12
  109. package/backend/src/core/services/EventGraphService.js +69 -0
  110. package/backend/src/core/services/MinecraftBotManager.js +9 -1
  111. package/backend/src/core/services/PluginManagementService.js +84 -0
  112. package/backend/src/core/services/TestModeContext.js +65 -0
  113. package/backend/src/core/services/__tests__/CacheManager.test.js +168 -0
  114. package/backend/src/core/services.js +1 -11
  115. package/backend/src/core/validation/InputValidator.js +167 -0
  116. package/backend/src/core/validation/__tests__/InputValidator.test.js +296 -0
  117. package/backend/src/real-time/botApi/index.js +1 -1
  118. package/backend/src/real-time/socketHandler.js +26 -0
  119. package/backend/src/server.js +10 -5
  120. package/frontend/dist/assets/{browser-ponyfill-DN7pwmHT.js → browser-ponyfill-D8y0Ty7C.js} +1 -1
  121. package/frontend/dist/assets/index-CFJLS0dk.css +32 -0
  122. package/frontend/dist/assets/{index-LSy71uwm.js → index-D91UGNMG.js} +1880 -1881
  123. package/frontend/dist/index.html +2 -2
  124. package/frontend/dist/locales/en/bots.json +4 -1
  125. package/frontend/dist/locales/en/common.json +7 -1
  126. package/frontend/dist/locales/en/login.json +2 -0
  127. package/frontend/dist/locales/en/management.json +79 -1
  128. package/frontend/dist/locales/en/nodes.json +59 -4
  129. package/frontend/dist/locales/en/plugin-detail.json +24 -4
  130. package/frontend/dist/locales/en/plugins.json +226 -7
  131. package/frontend/dist/locales/en/setup.json +2 -0
  132. package/frontend/dist/locales/en/sidebar.json +171 -3
  133. package/frontend/dist/locales/en/visual-editor.json +230 -31
  134. package/frontend/dist/locales/ru/bots.json +4 -1
  135. package/frontend/dist/locales/ru/login.json +2 -0
  136. package/frontend/dist/locales/ru/management.json +79 -1
  137. package/frontend/dist/locales/ru/minecraft-viewer.json +3 -0
  138. package/frontend/dist/locales/ru/nodes.json +105 -51
  139. package/frontend/dist/locales/ru/plugins.json +103 -4
  140. package/frontend/dist/locales/ru/setup.json +2 -0
  141. package/frontend/dist/locales/ru/sidebar.json +171 -3
  142. package/frontend/dist/locales/ru/visual-editor.json +232 -33
  143. package/frontend/package.json +2 -0
  144. package/nul +12 -0
  145. package/package.json +3 -3
  146. package/backend/package-lock.json +0 -6801
  147. package/backend/src/core/node-registries/actions.js +0 -202
  148. package/backend/src/core/node-registries/arrays.js +0 -155
  149. package/backend/src/core/node-registries/bot.js +0 -23
  150. package/backend/src/core/node-registries/container.js +0 -162
  151. package/backend/src/core/node-registries/data.js +0 -290
  152. package/backend/src/core/node-registries/debug.js +0 -26
  153. package/backend/src/core/node-registries/events.js +0 -201
  154. package/backend/src/core/node-registries/flow.js +0 -139
  155. package/backend/src/core/node-registries/furnace.js +0 -143
  156. package/backend/src/core/node-registries/logic.js +0 -62
  157. package/backend/src/core/node-registries/math.js +0 -42
  158. package/backend/src/core/node-registries/navigation.js +0 -111
  159. package/backend/src/core/node-registries/objects.js +0 -98
  160. package/backend/src/core/node-registries/strings.js +0 -187
  161. package/backend/src/core/node-registries/time.js +0 -113
  162. package/backend/src/core/node-registries/type.js +0 -25
  163. package/backend/src/core/node-registries/users.js +0 -79
  164. package/frontend/dist/assets/index-SfhKxI4-.css +0 -32
@@ -1,29 +1,42 @@
1
1
  const { LRUCache } = require('lru-cache');
2
2
 
3
+ const DEFAULT_MEMORY_THRESHOLD = 500 * 1024 * 1024;
4
+
3
5
  class CacheManager {
4
6
  constructor({ commandRepository, permissionRepository, logger } = {}) {
5
- // Кеш с TTL и максимальным размером
6
7
  this.tokenCache = new LRUCache({
7
8
  max: 500,
8
- ttl: 1000 * 60 * 5, // 5 минут
9
+ ttl: 1000 * 60 * 5,
9
10
  });
10
11
 
11
12
  this.playerListCache = new LRUCache({
12
13
  max: 100,
13
- ttl: 1000 * 2, // 2 секунды
14
+ ttl: 1000 * 2,
14
15
  });
15
16
 
16
17
  this.botConfigsCache = new Map();
17
-
18
- // Зависимости для автозагрузки конфигурации
18
+
19
19
  this.commandRepository = commandRepository;
20
20
  this.permissionRepository = permissionRepository;
21
21
  this.logger = logger;
22
+
23
+ this.memoryThreshold = DEFAULT_MEMORY_THRESHOLD;
24
+
25
+ this._metrics = {
26
+ tokenCache: { hits: 0, misses: 0 },
27
+ playerListCache: { hits: 0, misses: 0 },
28
+ botConfigsCache: { hits: 0, misses: 0 },
29
+ };
22
30
  }
23
31
 
24
- // Token cache
25
32
  getToken(token) {
26
- return this.tokenCache.get(token);
33
+ const value = this.tokenCache.get(token);
34
+ if (value !== undefined) {
35
+ this._metrics.tokenCache.hits++;
36
+ } else {
37
+ this._metrics.tokenCache.misses++;
38
+ }
39
+ return value;
27
40
  }
28
41
 
29
42
  setToken(token, data) {
@@ -34,18 +47,28 @@ class CacheManager {
34
47
  this.tokenCache.delete(token);
35
48
  }
36
49
 
37
- // Player list cache
38
50
  getPlayerList(botId) {
39
- return this.playerListCache.get(botId);
51
+ const value = this.playerListCache.get(botId);
52
+ if (value !== undefined) {
53
+ this._metrics.playerListCache.hits++;
54
+ } else {
55
+ this._metrics.playerListCache.misses++;
56
+ }
57
+ return value;
40
58
  }
41
59
 
42
60
  setPlayerList(botId, players) {
43
61
  this.playerListCache.set(botId, players);
44
62
  }
45
63
 
46
- // Bot config cache
47
64
  getBotConfig(botId) {
48
- return this.botConfigsCache.get(botId);
65
+ const value = this.botConfigsCache.get(botId);
66
+ if (value !== undefined) {
67
+ this._metrics.botConfigsCache.hits++;
68
+ } else {
69
+ this._metrics.botConfigsCache.misses++;
70
+ }
71
+ return value;
49
72
  }
50
73
 
51
74
  setBotConfig(botId, config) {
@@ -56,27 +79,64 @@ class CacheManager {
56
79
  this.botConfigsCache.delete(botId);
57
80
  }
58
81
 
59
- // Очистка всего кеша для бота
60
82
  clearBotCache(botId) {
61
83
  this.deleteBotConfig(botId);
62
84
  this.playerListCache.delete(botId);
63
85
  }
64
86
 
65
- /**
66
- * Получает конфигурацию бота из кеша или загружает из БД
67
- * @param {number} botId - ID бота
68
- * @returns {Promise<object>} - Конфигурация бота
69
- */
87
+ getMetrics() {
88
+ const calc = ({ hits, misses }) => {
89
+ const total = hits + misses;
90
+ return {
91
+ hits,
92
+ misses,
93
+ hitRate: total === 0 ? 0 : hits / total,
94
+ };
95
+ };
96
+
97
+ return {
98
+ tokenCache: calc(this._metrics.tokenCache),
99
+ playerListCache: calc(this._metrics.playerListCache),
100
+ botConfigsCache: calc(this._metrics.botConfigsCache),
101
+ };
102
+ }
103
+
104
+ resetMetrics() {
105
+ this._metrics = {
106
+ tokenCache: { hits: 0, misses: 0 },
107
+ playerListCache: { hits: 0, misses: 0 },
108
+ botConfigsCache: { hits: 0, misses: 0 },
109
+ };
110
+ }
111
+
112
+ checkMemoryUsage() {
113
+ const { heapUsed } = process.memoryUsage();
114
+ const threshold = this.memoryThreshold;
115
+ const exceeded = heapUsed > threshold;
116
+
117
+ if (exceeded) {
118
+ if (this.logger) {
119
+ this.logger.warn(
120
+ { heapUsed, threshold },
121
+ 'Memory threshold exceeded, clearing playerListCache'
122
+ );
123
+ }
124
+ this.playerListCache.clear();
125
+ }
126
+
127
+ return { heapUsed, threshold, exceeded };
128
+ }
129
+
70
130
  async getOrLoadBotConfig(botId) {
71
131
  let config = this.botConfigsCache.get(botId);
72
-
132
+
73
133
  if (!config) {
74
134
  if (this.logger) {
75
- this.logger.debug({ botId }, 'Кеш конфигурации отсутствует, загрузка из БД');
135
+ this.logger.debug({ botId }, 'Cache miss, loading from DB');
76
136
  }
77
-
137
+
78
138
  if (!this.commandRepository || !this.permissionRepository) {
79
- throw new Error('CacheManager не имеет доступа к репозиториям для загрузки конфигурации');
139
+ throw new Error('CacheManager does not have access to repositories for loading configuration');
80
140
  }
81
141
 
82
142
  const [commands, permissions] = await Promise.all([
@@ -98,9 +158,9 @@ class CacheManager {
98
158
  }
99
159
 
100
160
  this.botConfigsCache.set(botId, config);
101
-
161
+
102
162
  if (this.logger) {
103
- this.logger.debug({ botId }, 'Конфигурация загружена и закеширована');
163
+ this.logger.debug({ botId }, 'Configuration loaded and cached');
104
164
  }
105
165
  }
106
166
 
@@ -0,0 +1,42 @@
1
+ class CrashRestartManager {
2
+ constructor(maxRestarts = 3, restartWindowMs = 60000) {
3
+ this.maxRestarts = maxRestarts;
4
+ this.restartWindowMs = restartWindowMs;
5
+ this.crashHistory = new Map();
6
+ }
7
+
8
+ handleCrash(botId, botConfig, appendLog) {
9
+ const history = this.crashHistory.get(botId) || { count: 0, firstCrash: Date.now(), crashes: [] };
10
+
11
+ history.count++;
12
+ history.crashes.push(Date.now());
13
+
14
+ history.crashes = history.crashes.filter(t => Date.now() - t < this.restartWindowMs);
15
+ history.count = history.crashes.length;
16
+ history.firstCrash = history.crashes[0] || Date.now();
17
+
18
+ this.crashHistory.set(botId, history);
19
+
20
+ const withinWindow = Date.now() - history.firstCrash < this.restartWindowMs;
21
+
22
+ if (history.count > this.maxRestarts && withinWindow) {
23
+ appendLog(`[CRASH] Бот превысил лимит рестартов (${this.maxRestarts}). Авто-рестарт отключен.`);
24
+ return { shouldRestart: false, reason: 'max_restarts_exceeded' };
25
+ }
26
+
27
+ const delay = Math.min(1000 * Math.pow(2, history.count - 1), 30000);
28
+ appendLog(`[CRASH] Перезапуск через ${delay / 1000} сек... (попытка ${history.count}/${this.maxRestarts})`);
29
+
30
+ return { shouldRestart: true, delay };
31
+ }
32
+
33
+ resetCounter(botId) {
34
+ this.crashHistory.delete(botId);
35
+ }
36
+
37
+ getCrashCount(botId) {
38
+ return this.crashHistory.get(botId)?.count || 0;
39
+ }
40
+ }
41
+
42
+ module.exports = CrashRestartManager;
@@ -25,6 +25,12 @@ class GraphDebugState {
25
25
  // Режим пошагового выполнения (step over)
26
26
  this.stepMode = false; // Если true, остановится на следующей ноде
27
27
  this.stepFromNodeId = null; // ID ноды, с которой начали step over (чтобы её пропустить)
28
+
29
+ this.testMode = false;
30
+ this.history = [];
31
+ this.historyLimit = 200;
32
+ this.rewindTargetIndex = null;
33
+ this.replayState = null;
28
34
  }
29
35
 
30
36
  /**
@@ -53,7 +59,6 @@ class GraphDebugState {
53
59
  * Возвращает Promise, который разрешится когда пользователь нажмет Continue
54
60
  */
55
61
  async pause(state) {
56
- // Генерируем sessionId для этой паузы
57
62
  const sessionId = randomUUID();
58
63
 
59
64
  this.activeExecution = {
@@ -62,10 +67,14 @@ class GraphDebugState {
62
67
  pausedAt: Date.now()
63
68
  };
64
69
 
70
+ this.pushSnapshot(state);
65
71
 
66
72
  this.broadcast('debug:paused', {
67
73
  sessionId,
68
- ...state
74
+ ...state,
75
+ historyLength: this.history.length,
76
+ canStepBack: this.history.length > 1,
77
+ testMode: this.testMode
69
78
  });
70
79
 
71
80
 
@@ -86,18 +95,18 @@ class GraphDebugState {
86
95
  * @param {boolean} stepMode - Если true, остановится на следующей ноде
87
96
  */
88
97
  resume(overrides = null, stepMode = false) {
98
+ const effectiveStepMode = this.testMode ? true : stepMode;
99
+
89
100
  if (this.resumeCallback) {
90
- // Объединяем pendingOverrides с переданными overrides
91
101
  const finalOverrides = {
92
102
  ...this.pendingOverrides,
93
103
  ...(overrides || {})
94
104
  };
95
105
 
96
- console.log('[GraphDebugState] Resuming with overrides:', finalOverrides, 'stepMode:', stepMode);
106
+ console.log('[GraphDebugState] Resuming with overrides:', finalOverrides, 'stepMode:', effectiveStepMode, 'testMode:', this.testMode);
97
107
 
98
- // Устанавливаем флаг stepMode и запоминаем текущую ноду
99
- this.stepMode = stepMode;
100
- if (stepMode && this.activeExecution) {
108
+ this.stepMode = effectiveStepMode;
109
+ if (effectiveStepMode && this.activeExecution) {
101
110
  this.stepFromNodeId = this.activeExecution.nodeId;
102
111
  console.log('[GraphDebugState] Step mode: will skip current node', this.stepFromNodeId);
103
112
  }
@@ -107,15 +116,14 @@ class GraphDebugState {
107
116
  this.pausePromise = null;
108
117
  }
109
118
 
110
- // Если не step mode, очищаем состояние полностью
111
- if (!stepMode) {
119
+ if (!effectiveStepMode) {
112
120
  this.activeExecution = null;
113
121
  this.stepFromNodeId = null;
114
122
  }
115
123
 
116
- this.pendingOverrides = {}; // Очищаем после применения
124
+ this.pendingOverrides = {};
117
125
 
118
- this.broadcast('debug:resumed', { stepMode });
126
+ this.broadcast('debug:resumed', { stepMode: effectiveStepMode, testMode: this.testMode });
119
127
  }
120
128
 
121
129
  /**
@@ -133,6 +141,14 @@ class GraphDebugState {
133
141
  this.stepMode = false; // Сбрасываем step mode
134
142
  this.stepFromNodeId = null; // Очищаем запомненную ноду
135
143
 
144
+ if (this.testMode) {
145
+ this.testMode = false;
146
+ this.history = [];
147
+ this.rewindTargetIndex = null;
148
+ this.replayState = null;
149
+ this.broadcast('debug:test-mode-stopped', {});
150
+ }
151
+
136
152
  this.broadcast('debug:stopped', {});
137
153
  }
138
154
 
@@ -217,6 +233,89 @@ class GraphDebugState {
217
233
  console.log(`[GraphDebugState] Set override: ${key} =`, value);
218
234
  }
219
235
 
236
+ pushSnapshot(state) {
237
+ if (!state) return;
238
+ const snapshot = {
239
+ index: this.history.length,
240
+ nodeId: state.nodeId,
241
+ nodeType: state.nodeType,
242
+ inputs: this._cloneSafely(state.inputs),
243
+ variables: this._cloneSafely(state.context?.variables),
244
+ commandArguments: this._cloneSafely(state.context?.commandArguments),
245
+ executedSteps: this._cloneSafely(state.executedSteps),
246
+ capturedAt: Date.now()
247
+ };
248
+ this.history.push(snapshot);
249
+ if (this.history.length > this.historyLimit) {
250
+ this.history.shift();
251
+ this.history.forEach((s, i) => { s.index = i; });
252
+ }
253
+ }
254
+
255
+ _cloneSafely(value) {
256
+ if (value === undefined || value === null) return value;
257
+ try {
258
+ return JSON.parse(JSON.stringify(value));
259
+ } catch (e) {
260
+ return null;
261
+ }
262
+ }
263
+
264
+ enableTestMode(initialPayload = {}) {
265
+ this.testMode = true;
266
+ this.stepMode = true;
267
+ this.stepFromNodeId = null;
268
+ this.history = [];
269
+ this.rewindTargetIndex = null;
270
+ this.replayState = null;
271
+ this.broadcast('debug:test-mode-started', {
272
+ ...initialPayload,
273
+ testMode: true
274
+ });
275
+ }
276
+
277
+ disableTestMode() {
278
+ this.testMode = false;
279
+ this.history = [];
280
+ this.rewindTargetIndex = null;
281
+ this.replayState = null;
282
+ this.broadcast('debug:test-mode-stopped', {});
283
+ }
284
+
285
+ consumeRewindTarget(nodeId) {
286
+ if (this.rewindTargetIndex === null) return false;
287
+ const target = this.history[this.rewindTargetIndex];
288
+ if (target && target.nodeId === nodeId) {
289
+ this.rewindTargetIndex = null;
290
+ return true;
291
+ }
292
+ return false;
293
+ }
294
+
295
+ stepBack() {
296
+ if (this.history.length < 2) {
297
+ this.broadcast('debug:rewind-failed', { reason: 'no-history' });
298
+ return null;
299
+ }
300
+ const target = this.history[this.history.length - 2];
301
+ this.history = this.history.slice(0, this.history.length - 1);
302
+ this.rewindTargetIndex = this.history.length - 1;
303
+ this.replayState = {
304
+ variables: target.variables,
305
+ commandArguments: target.commandArguments,
306
+ targetNodeId: target.nodeId
307
+ };
308
+ if (this.resumeCallback) {
309
+ this.resumeCallback({ __rewind: true, target });
310
+ this.resumeCallback = null;
311
+ this.pausePromise = null;
312
+ }
313
+ this.activeExecution = null;
314
+ this.pendingOverrides = {};
315
+ this.broadcast('debug:rewound', { target });
316
+ return target;
317
+ }
318
+
220
319
  /**
221
320
  * Получить текущее состояние (для отправки новым пользователям)
222
321
  */
@@ -224,7 +323,10 @@ class GraphDebugState {
224
323
  return {
225
324
  breakpoints: Array.from(this.breakpoints.values()),
226
325
  activeExecution: this.activeExecution,
227
- connectedUsers: Array.from(this.connectedUsers.values())
326
+ connectedUsers: Array.from(this.connectedUsers.values()),
327
+ testMode: this.testMode,
328
+ historyLength: this.history.length,
329
+ canStepBack: this.history.length > 1
228
330
  };
229
331
  }
230
332
 
@@ -0,0 +1,69 @@
1
+ const GraphValidator = require('../domain/services/GraphValidator');
2
+ const { GraphExecutionError, ValidationError } = require('../errors');
3
+
4
+ const graphValidator = new GraphValidator();
5
+
6
+ class EventGraphService {
7
+ constructor({ eventGraphManager, eventGraphRepository, cacheManager, logger } = {}) {
8
+ this.eventGraphManager = eventGraphManager;
9
+ this.eventGraphRepository = eventGraphRepository;
10
+ this.cache = cacheManager;
11
+ this.logger = logger;
12
+ this._graphCache = new Map();
13
+ }
14
+
15
+ async loadGraphsForBot(botId) {
16
+ this.logger.info({ botId }, 'eventGraph.load.start');
17
+ try {
18
+ await this.eventGraphManager.loadGraphsForBot(botId);
19
+ this.logger.info({ botId }, 'eventGraph.load.success');
20
+ } catch (error) {
21
+ this.logger.error({ botId, error }, 'eventGraph.load.error');
22
+ throw new GraphExecutionError('eventGraph.errors.loadFailed', { cause: error, context: { botId } });
23
+ }
24
+ }
25
+
26
+ unloadGraphsForBot(botId) {
27
+ this.eventGraphManager.unloadGraphsForBot(botId);
28
+ this._graphCache.delete(botId);
29
+ this.logger.info({ botId }, 'eventGraph.unload.success');
30
+ }
31
+
32
+ async handleEvent(botId, eventType, args) {
33
+ try {
34
+ await this.eventGraphManager.handleEvent(botId, eventType, args);
35
+ } catch (error) {
36
+ this.logger.error({ botId, eventType, error }, 'eventGraph.event.error');
37
+ }
38
+ }
39
+
40
+ async validateGraph(graphJson) {
41
+ let graph;
42
+ try {
43
+ graph = typeof graphJson === 'string' ? JSON.parse(graphJson) : graphJson;
44
+ } catch {
45
+ throw new ValidationError('eventGraph.errors.invalidJson');
46
+ }
47
+
48
+ const result = graphValidator.validate(graph);
49
+ if (!result.valid) {
50
+ throw new ValidationError('eventGraph.errors.invalidStructure', { context: { errors: result.errors } });
51
+ }
52
+ return true;
53
+ }
54
+
55
+ async getGraphsForBot(botId) {
56
+ if (this._graphCache.has(botId)) {
57
+ return this._graphCache.get(botId);
58
+ }
59
+ const graphs = await this.eventGraphRepository.findByBotId(botId);
60
+ this._graphCache.set(botId, graphs);
61
+ return graphs;
62
+ }
63
+
64
+ invalidateCache(botId) {
65
+ this._graphCache.delete(botId);
66
+ }
67
+ }
68
+
69
+ module.exports = EventGraphService;
@@ -88,7 +88,15 @@ class MinecraftBotManager {
88
88
  });
89
89
 
90
90
  bot.on('kicked', (reason) => {
91
- this.logger.warn(`[Minecraft] Bot kicked for session ${sessionId}: ${reason}`);
91
+ let reasonText;
92
+ if (typeof reason === 'string') {
93
+ try { reasonText = JSON.parse(reason).text || reason; } catch (e) { reasonText = reason; }
94
+ } else if (reason && typeof reason === 'object') {
95
+ reasonText = reason.text || reason.message || reason.reason || JSON.stringify(reason);
96
+ } else {
97
+ reasonText = String(reason);
98
+ }
99
+ this.logger.warn(`[Minecraft] Bot kicked for session ${sessionId}: ${reasonText}`);
92
100
  });
93
101
  }
94
102
 
@@ -0,0 +1,84 @@
1
+ const DependencyResolver = require('../domain/services/DependencyResolver');
2
+ const { validatePluginManifest } = require('../validation/InputValidator');
3
+ const { PluginError, ValidationError } = require('../errors');
4
+
5
+ const dependencyResolver = new DependencyResolver();
6
+
7
+ class PluginManagementService {
8
+ constructor({ pluginManager, pluginRepository, logger } = {}) {
9
+ this.pluginManager = pluginManager;
10
+ this.pluginRepository = pluginRepository;
11
+ this.logger = logger;
12
+ }
13
+
14
+ async installFromGithub(botId, repoUrl, tag = null) {
15
+ this.logger.info({ botId, repoUrl, tag }, 'plugin.install.github.start');
16
+ try {
17
+ const result = await this.pluginManager.installFromGithub(botId, repoUrl, undefined, false, tag);
18
+ this.logger.info({ botId, pluginName: result.name }, 'plugin.install.github.success');
19
+ return result;
20
+ } catch (error) {
21
+ this.logger.error({ botId, repoUrl, error }, 'plugin.install.github.error');
22
+ throw new PluginError('plugin.errors.installFailed', { cause: error, context: { repoUrl } });
23
+ }
24
+ }
25
+
26
+ async installFromLocalPath(botId, directoryPath) {
27
+ this.logger.info({ botId, directoryPath }, 'plugin.install.local.start');
28
+ try {
29
+ const result = await this.pluginManager.installFromLocalPath(botId, directoryPath);
30
+ this.logger.info({ botId, pluginName: result.name }, 'plugin.install.local.success');
31
+ return result;
32
+ } catch (error) {
33
+ this.logger.error({ botId, directoryPath, error }, 'plugin.install.local.error');
34
+ throw new PluginError('plugin.errors.installFailed', { cause: error, context: { directoryPath } });
35
+ }
36
+ }
37
+
38
+ async updatePlugin(pluginId, targetTag = null, targetRepoUrl = null) {
39
+ this.logger.info({ pluginId, targetTag }, 'plugin.update.start');
40
+ try {
41
+ const result = await this.pluginManager.updatePlugin(pluginId, targetTag, targetRepoUrl);
42
+ this.logger.info({ pluginId, newVersion: result.version }, 'plugin.update.success');
43
+ return result;
44
+ } catch (error) {
45
+ this.logger.error({ pluginId, error }, 'plugin.update.error');
46
+ throw new PluginError('plugin.errors.updateFailed', { cause: error, context: { pluginId } });
47
+ }
48
+ }
49
+
50
+ async deletePlugin(pluginId) {
51
+ this.logger.info({ pluginId }, 'plugin.delete.start');
52
+ try {
53
+ await this.pluginManager.deletePlugin(pluginId);
54
+ this.logger.info({ pluginId }, 'plugin.delete.success');
55
+ } catch (error) {
56
+ this.logger.error({ pluginId, error }, 'plugin.delete.error');
57
+ throw new PluginError('plugin.errors.deleteFailed', { cause: error, context: { pluginId } });
58
+ }
59
+ }
60
+
61
+ async validateManifest(manifest) {
62
+ const result = validatePluginManifest(manifest);
63
+ if (!result.valid) {
64
+ throw new ValidationError('plugin.errors.invalidManifest', { context: { errors: result.errors } });
65
+ }
66
+ return true;
67
+ }
68
+
69
+ async checkDependencies(botId, manifest) {
70
+ const installedPlugins = await this.pluginRepository.findByBotId(botId);
71
+ const fakePlugin = { manifest: JSON.stringify(manifest) };
72
+ return dependencyResolver.checkCompatibility(fakePlugin, installedPlugins);
73
+ }
74
+
75
+ async checkForUpdates(botId, catalog) {
76
+ return this.pluginManager.checkForUpdates(botId, catalog);
77
+ }
78
+
79
+ async getInstalledPlugins(botId) {
80
+ return this.pluginRepository.findByBotId(botId);
81
+ }
82
+ }
83
+
84
+ module.exports = PluginManagementService;
@@ -0,0 +1,65 @@
1
+ function createMockHandler(label) {
2
+ const handler = {
3
+ get(target, prop) {
4
+ if (prop === 'then') return undefined;
5
+ if (prop === Symbol.toPrimitive) return () => `[mock:${label}]`;
6
+ if (prop === 'toString') return () => `[mock:${label}]`;
7
+ if (prop in target) return target[prop];
8
+ const child = function (...args) {
9
+ console.log(`[TestMode] mock call: ${label}.${String(prop)}(${args.length} args)`);
10
+ return undefined;
11
+ };
12
+ child.__mock = true;
13
+ const proxied = new Proxy(child, createMockHandler(`${label}.${String(prop)}`));
14
+ target[prop] = proxied;
15
+ return proxied;
16
+ },
17
+ apply(target, thisArg, args) {
18
+ console.log(`[TestMode] mock invoke: ${label}(${args.length} args)`);
19
+ return undefined;
20
+ }
21
+ };
22
+ return handler;
23
+ }
24
+
25
+ function createTestBotMock() {
26
+ const base = function () {};
27
+ base.username = 'TestBot';
28
+ base.entity = { position: { x: 0, y: 64, z: 0 }, yaw: 0, pitch: 0 };
29
+ base.players = {};
30
+ base.entities = {};
31
+ base.health = 20;
32
+ base.food = 20;
33
+ base.sendMessage = (...args) => {
34
+ console.log('[TestMode] bot.sendMessage:', ...args);
35
+ };
36
+ base.lookAt = () => {};
37
+ base.chat = (msg) => console.log('[TestMode] bot.chat:', msg);
38
+ return new Proxy(base, createMockHandler('bot'));
39
+ }
40
+
41
+ function createTestApiMock() {
42
+ const base = function () {};
43
+ return new Proxy(base, createMockHandler('api'));
44
+ }
45
+
46
+ function buildTestContext({ botId, graphId, eventArgs }) {
47
+ return {
48
+ botId,
49
+ graphId,
50
+ eventArgs,
51
+ user: eventArgs?.user || (eventArgs?.username ? { username: eventArgs.username } : { username: 'TestUser' }),
52
+ commandArguments: eventArgs?.commandArguments || {},
53
+ bot: createTestBotMock(),
54
+ api: createTestApiMock(),
55
+ services: new Proxy({}, createMockHandler('services')),
56
+ typeChat: 'chat',
57
+ __testMode: true
58
+ };
59
+ }
60
+
61
+ module.exports = {
62
+ createTestBotMock,
63
+ createTestApiMock,
64
+ buildTestContext
65
+ };