blockmine 1.22.0 → 1.23.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.
Files changed (108) hide show
  1. package/.claude/agents/code-architect.md +34 -0
  2. package/.claude/agents/code-explorer.md +51 -0
  3. package/.claude/agents/code-reviewer.md +46 -0
  4. package/.claude/commands/feature-dev.md +125 -0
  5. package/.claude/settings.json +5 -1
  6. package/.claude/settings.local.json +12 -1
  7. package/.claude/skills/frontend-design/SKILL.md +42 -0
  8. package/CHANGELOG.md +32 -1
  9. package/README.md +302 -152
  10. package/backend/package-lock.json +681 -9
  11. package/backend/package.json +8 -0
  12. package/backend/prisma/migrations/20251116111851_add_execution_trace/migration.sql +22 -0
  13. package/backend/prisma/migrations/20251120154914_add_panel_api_keys/migration.sql +21 -0
  14. package/backend/prisma/migrations/20251121110241_add_proxy_table/migration.sql +45 -0
  15. package/backend/prisma/schema.prisma +70 -1
  16. package/backend/src/__tests__/services/BotLifecycleService.test.js +9 -4
  17. package/backend/src/ai/plugin-assistant-system-prompt.md +788 -0
  18. package/backend/src/api/middleware/auth.js +27 -0
  19. package/backend/src/api/middleware/botAccess.js +7 -3
  20. package/backend/src/api/middleware/panelApiAuth.js +135 -0
  21. package/backend/src/api/routes/aiAssistant.js +995 -0
  22. package/backend/src/api/routes/auth.js +90 -54
  23. package/backend/src/api/routes/botCommands.js +107 -0
  24. package/backend/src/api/routes/botGroups.js +165 -0
  25. package/backend/src/api/routes/botHistory.js +108 -0
  26. package/backend/src/api/routes/botPermissions.js +99 -0
  27. package/backend/src/api/routes/botStatus.js +36 -0
  28. package/backend/src/api/routes/botUsers.js +162 -0
  29. package/backend/src/api/routes/bots.js +108 -59
  30. package/backend/src/api/routes/eventGraphs.js +4 -1
  31. package/backend/src/api/routes/logs.js +13 -3
  32. package/backend/src/api/routes/panel.js +3 -3
  33. package/backend/src/api/routes/panelApiKeys.js +179 -0
  34. package/backend/src/api/routes/pluginIde.js +1715 -135
  35. package/backend/src/api/routes/plugins.js +170 -13
  36. package/backend/src/api/routes/proxies.js +130 -0
  37. package/backend/src/api/routes/search.js +4 -0
  38. package/backend/src/api/routes/servers.js +20 -3
  39. package/backend/src/api/routes/settings.js +5 -0
  40. package/backend/src/api/routes/system.js +3 -3
  41. package/backend/src/api/routes/traces.js +131 -0
  42. package/backend/src/config/debug.config.js +36 -0
  43. package/backend/src/core/BotHistoryStore.js +180 -0
  44. package/backend/src/core/BotManager.js +14 -4
  45. package/backend/src/core/BotProcess.js +1517 -1092
  46. package/backend/src/core/EventGraphManager.js +194 -280
  47. package/backend/src/core/GraphExecutionEngine.js +1004 -321
  48. package/backend/src/core/MessageQueue.js +12 -6
  49. package/backend/src/core/PluginLoader.js +99 -5
  50. package/backend/src/core/PluginManager.js +74 -13
  51. package/backend/src/core/TaskScheduler.js +1 -1
  52. package/backend/src/core/commands/whois.js +1 -1
  53. package/backend/src/core/node-registries/actions.js +72 -2
  54. package/backend/src/core/node-registries/arrays.js +18 -0
  55. package/backend/src/core/node-registries/data.js +1 -1
  56. package/backend/src/core/node-registries/events.js +14 -0
  57. package/backend/src/core/node-registries/logic.js +17 -0
  58. package/backend/src/core/node-registries/strings.js +34 -0
  59. package/backend/src/core/node-registries/type.js +25 -0
  60. package/backend/src/core/nodes/actions/bot_look_at.js +1 -1
  61. package/backend/src/core/nodes/actions/create_command.js +189 -0
  62. package/backend/src/core/nodes/actions/delete_command.js +92 -0
  63. package/backend/src/core/nodes/actions/http_request.js +23 -4
  64. package/backend/src/core/nodes/actions/send_message.js +2 -12
  65. package/backend/src/core/nodes/actions/update_command.js +133 -0
  66. package/backend/src/core/nodes/arrays/join.js +28 -0
  67. package/backend/src/core/nodes/data/cast.js +2 -1
  68. package/backend/src/core/nodes/data/string_literal.js +2 -13
  69. package/backend/src/core/nodes/logic/not.js +22 -0
  70. package/backend/src/core/nodes/strings/starts_with.js +1 -1
  71. package/backend/src/core/nodes/strings/to_lower.js +22 -0
  72. package/backend/src/core/nodes/strings/to_upper.js +22 -0
  73. package/backend/src/core/nodes/type/to_string.js +32 -0
  74. package/backend/src/core/services/BotLifecycleService.js +835 -596
  75. package/backend/src/core/services/CommandExecutionService.js +430 -351
  76. package/backend/src/core/services/DebugSessionManager.js +347 -0
  77. package/backend/src/core/services/GraphCollaborationManager.js +501 -0
  78. package/backend/src/core/services/MinecraftBotManager.js +259 -0
  79. package/backend/src/core/services/MinecraftViewerService.js +216 -0
  80. package/backend/src/core/services/TraceCollectorService.js +545 -0
  81. package/backend/src/core/system/RuntimeCommandRegistry.js +116 -0
  82. package/backend/src/core/system/Transport.js +0 -4
  83. package/backend/src/core/validation/nodeSchemas.js +6 -6
  84. package/backend/src/real-time/botApi/handlers/graphHandlers.js +2 -2
  85. package/backend/src/real-time/botApi/handlers/graphWebSocketHandlers.js +1 -1
  86. package/backend/src/real-time/botApi/utils.js +11 -0
  87. package/backend/src/real-time/panelNamespace.js +387 -0
  88. package/backend/src/real-time/presence.js +7 -2
  89. package/backend/src/real-time/socketHandler.js +395 -4
  90. package/backend/src/server.js +18 -0
  91. package/frontend/dist/assets/index-DqzDkFsP.js +11210 -0
  92. package/frontend/dist/assets/index-t6K1u4OV.css +32 -0
  93. package/frontend/dist/index.html +2 -2
  94. package/frontend/package-lock.json +9437 -0
  95. package/frontend/package.json +8 -0
  96. package/package.json +2 -2
  97. package/screen/console.png +0 -0
  98. package/screen/dashboard.png +0 -0
  99. package/screen/graph_collabe.png +0 -0
  100. package/screen/graph_live_debug.png +0 -0
  101. package/screen/management_command.png +0 -0
  102. package/screen/node_debug_trace.png +0 -0
  103. package/screen/plugin_/320/276/320/261/320/267/320/276/321/200.png +0 -0
  104. package/screen/websocket.png +0 -0
  105. 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
  106. 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
  107. package/frontend/dist/assets/index-CfTo92bP.css +0 -1
  108. package/frontend/dist/assets/index-CiFD5X9Z.js +0 -8344
@@ -0,0 +1,259 @@
1
+ const mineflayer = require('mineflayer');
2
+ const { pathfinder, Movements, goals } = require('mineflayer-pathfinder');
3
+ const Vec3 = require('vec3').Vec3;
4
+
5
+ class MinecraftBotManager {
6
+ constructor({ logger }) {
7
+ this.logger = logger;
8
+ this.bots = new Map();
9
+ this.sessionIdCounter = 0;
10
+ }
11
+
12
+ async createBot(sessionId, config) {
13
+ const { host, port, version, username, password } = config;
14
+
15
+ if (this.bots.has(sessionId)) {
16
+ await this.disconnectBot(sessionId);
17
+ }
18
+
19
+ this.logger.info(`[Minecraft] Creating bot ${username} for session ${sessionId}`);
20
+
21
+ const bot = mineflayer.createBot({
22
+ host,
23
+ port: port || 25565,
24
+ username,
25
+ password: password || undefined,
26
+ version: version || '1.19.2',
27
+ hideErrors: false
28
+ });
29
+
30
+ bot.loadPlugin(pathfinder);
31
+
32
+ const botState = {
33
+ bot,
34
+ config,
35
+ status: 'connecting',
36
+ health: 20,
37
+ food: 20,
38
+ position: null,
39
+ yaw: 0,
40
+ pitch: 0
41
+ };
42
+
43
+ this.bots.set(sessionId, botState);
44
+ this._attachEventHandlers(sessionId, bot);
45
+
46
+ return sessionId;
47
+ }
48
+
49
+ _attachEventHandlers(sessionId, bot) {
50
+ bot.on('spawn', () => {
51
+ this.logger.info(`[Minecraft] Bot spawned for session ${sessionId}`);
52
+ const state = this.bots.get(sessionId);
53
+ if (state) {
54
+ state.status = 'online';
55
+ state.position = bot.entity.position;
56
+ state.yaw = bot.entity.yaw;
57
+ state.pitch = bot.entity.pitch;
58
+ }
59
+ });
60
+
61
+ bot.on('health', () => {
62
+ const state = this.bots.get(sessionId);
63
+ if (state) {
64
+ state.health = bot.health;
65
+ state.food = bot.food;
66
+ }
67
+ });
68
+
69
+ bot.on('move', () => {
70
+ const state = this.bots.get(sessionId);
71
+ if (state && bot.entity) {
72
+ state.position = bot.entity.position;
73
+ state.yaw = bot.entity.yaw;
74
+ state.pitch = bot.entity.pitch;
75
+ }
76
+ });
77
+
78
+ bot.on('end', (reason) => {
79
+ this.logger.info(`[Minecraft] Bot disconnected for session ${sessionId}: ${reason}`);
80
+ const state = this.bots.get(sessionId);
81
+ if (state) {
82
+ state.status = 'offline';
83
+ }
84
+ });
85
+
86
+ bot.on('error', (err) => {
87
+ this.logger.error(`[Minecraft] Bot error for session ${sessionId}:`, err);
88
+ });
89
+
90
+ bot.on('kicked', (reason) => {
91
+ this.logger.warn(`[Minecraft] Bot kicked for session ${sessionId}: ${reason}`);
92
+ });
93
+ }
94
+
95
+ async disconnectBot(sessionId) {
96
+ const state = this.bots.get(sessionId);
97
+ if (!state) return;
98
+
99
+ this.logger.info(`[Minecraft] Disconnecting bot for session ${sessionId}`);
100
+
101
+ if (state.bot) {
102
+ state.bot.quit();
103
+ }
104
+
105
+ this.bots.delete(sessionId);
106
+ }
107
+
108
+ getBotState(sessionId) {
109
+ const state = this.bots.get(sessionId);
110
+ if (!state || !state.bot) return null;
111
+
112
+ const bot = state.bot;
113
+
114
+ return {
115
+ status: state.status,
116
+ health: state.health || 20,
117
+ food: state.food || 20,
118
+ position: state.position ? {
119
+ x: state.position.x,
120
+ y: state.position.y,
121
+ z: state.position.z
122
+ } : null,
123
+ yaw: state.yaw || 0,
124
+ pitch: state.pitch || 0,
125
+ gameMode: bot.game?.gameMode,
126
+ dimension: bot.game?.dimension,
127
+ inventory: this._getInventory(bot),
128
+ nearbyPlayers: this._getNearbyPlayers(bot),
129
+ nearbyMobs: this._getNearbyMobs(bot)
130
+ };
131
+ }
132
+
133
+ _getInventory(bot) {
134
+ if (!bot.inventory) return [];
135
+
136
+ return bot.inventory.items().map(item => ({
137
+ name: item.name,
138
+ displayName: item.displayName,
139
+ count: item.count,
140
+ slot: item.slot
141
+ }));
142
+ }
143
+
144
+ _getNearbyPlayers(bot) {
145
+ if (!bot.entities) return [];
146
+
147
+ return Object.values(bot.entities)
148
+ .filter(e => e.type === 'player' && e.username !== bot.username)
149
+ .map(e => ({
150
+ username: e.username,
151
+ position: {
152
+ x: e.position.x,
153
+ y: e.position.y,
154
+ z: e.position.z
155
+ },
156
+ distance: bot.entity ? bot.entity.position.distanceTo(e.position) : 0
157
+ }));
158
+ }
159
+
160
+ _getNearbyMobs(bot) {
161
+ if (!bot.entities) return [];
162
+
163
+ return Object.values(bot.entities)
164
+ .filter(e => e.type === 'mob')
165
+ .map(e => ({
166
+ name: e.name || e.displayName,
167
+ mobType: e.mobType,
168
+ position: {
169
+ x: e.position.x,
170
+ y: e.position.y,
171
+ z: e.position.z
172
+ },
173
+ distance: bot.entity ? bot.entity.position.distanceTo(e.position) : 0
174
+ }));
175
+ }
176
+
177
+ async handleControlCommand(sessionId, command) {
178
+ const state = this.bots.get(sessionId);
179
+ if (!state || !state.bot) return;
180
+
181
+ const bot = state.bot;
182
+
183
+ try {
184
+ switch (command.type) {
185
+ case 'move':
186
+ bot.setControlState(command.direction, command.active);
187
+ break;
188
+
189
+ case 'look':
190
+ const yaw = command.yaw !== undefined ? command.yaw : bot.entity.yaw;
191
+ const pitch = command.pitch !== undefined ? command.pitch : bot.entity.pitch;
192
+ await bot.look(yaw, pitch, true);
193
+ break;
194
+
195
+ case 'chat':
196
+ bot.chat(command.message);
197
+ break;
198
+
199
+ case 'dig':
200
+ await this._digBlock(bot, command.position);
201
+ break;
202
+
203
+ case 'place':
204
+ await this._placeBlock(bot, command.position, command.blockType);
205
+ break;
206
+
207
+ default:
208
+ this.logger.warn(`[Minecraft] Unknown command type: ${command.type}`);
209
+ }
210
+ } catch (error) {
211
+ this.logger.error(`[Minecraft] Error handling command:`, error);
212
+ }
213
+ }
214
+
215
+ async _digBlock(bot, position) {
216
+ if (!position) return;
217
+
218
+ const block = bot.blockAt(new Vec3(position.x, position.y, position.z));
219
+ if (!block) return;
220
+
221
+ try {
222
+ await bot.dig(block);
223
+ } catch (error) {
224
+ this.logger.error('[Minecraft] Error digging block:', error);
225
+ }
226
+ }
227
+
228
+ async _placeBlock(bot, position, blockType) {
229
+ if (!position || !blockType) return;
230
+
231
+ const referenceBlock = bot.blockAt(new Vec3(position.x, position.y, position.z));
232
+ if (!referenceBlock) return;
233
+
234
+ const itemToPlace = bot.inventory.items().find(item => item.name === blockType);
235
+ if (!itemToPlace) return;
236
+
237
+ try {
238
+ await bot.equip(itemToPlace, 'hand');
239
+ await bot.placeBlock(referenceBlock, new Vec3(0, 1, 0));
240
+ } catch (error) {
241
+ this.logger.error('[Minecraft] Error placing block:', error);
242
+ }
243
+ }
244
+
245
+ getBot(sessionId) {
246
+ const state = this.bots.get(sessionId);
247
+ return state?.bot || null;
248
+ }
249
+
250
+ getAllSessions() {
251
+ return Array.from(this.bots.keys());
252
+ }
253
+
254
+ generateSessionId() {
255
+ return `mc_${++this.sessionIdCounter}_${Date.now()}`;
256
+ }
257
+ }
258
+
259
+ module.exports = MinecraftBotManager;
@@ -0,0 +1,216 @@
1
+ const { v4: uuidv4 } = require('uuid');
2
+
3
+ class MinecraftViewerService {
4
+ constructor({ io, botProcessManager, logger }) {
5
+ this.processManager = botProcessManager;
6
+ this.logger = logger;
7
+ this.viewerNamespace = io.of('/minecraft-viewer');
8
+ this.activeViewers = new Map(); // botId -> Set<socketId>
9
+ this.pendingRequests = new Map();
10
+
11
+ this._setupNamespace();
12
+ this._setupIPCHandlers();
13
+ }
14
+
15
+ _setupNamespace() {
16
+ this.viewerNamespace.on('connection', (socket) => {
17
+ this.logger.info(`[MinecraftViewer] Client connected: ${socket.id}`);
18
+
19
+ socket.on('viewer:connect', ({ botId }) => {
20
+ const child = this.processManager.getProcess(botId);
21
+ if (!child) {
22
+ socket.emit('viewer:error', { message: 'Bot not running' });
23
+ return;
24
+ }
25
+
26
+ socket.botId = botId;
27
+ socket.join(`bot:${botId}`);
28
+
29
+ if (!this.activeViewers.has(botId)) {
30
+ this.activeViewers.set(botId, new Set());
31
+ }
32
+ this.activeViewers.get(botId).add(socket.id);
33
+
34
+ this.logger.info(`[MinecraftViewer] Socket ${socket.id} connected to bot ${botId}`);
35
+
36
+ const requestId = uuidv4();
37
+ this.processManager.sendMessage(botId, {
38
+ type: 'viewer:get_state',
39
+ requestId,
40
+ includeBlocks: true
41
+ });
42
+
43
+ this.pendingRequests.set(requestId, {
44
+ resolve: (state) => {
45
+ socket.emit('viewer:connected', { botId });
46
+ socket.emit('viewer:state', state);
47
+ },
48
+ reject: (error) => {
49
+ socket.emit('viewer:error', { message: error.message });
50
+ }
51
+ });
52
+
53
+ this._startStateStream(botId);
54
+ });
55
+
56
+ socket.on('viewer:control', ({ command }) => {
57
+ const botId = socket.botId;
58
+ if (!botId) return;
59
+
60
+ this.processManager.sendMessage(botId, {
61
+ type: 'viewer:control',
62
+ command
63
+ });
64
+ });
65
+
66
+ socket.on('viewer:disconnect', () => {
67
+ const botId = socket.botId;
68
+ if (botId) {
69
+ this._removeViewer(botId, socket.id);
70
+ }
71
+ });
72
+
73
+ socket.on('disconnect', () => {
74
+ this.logger.info(`[MinecraftViewer] Client disconnected: ${socket.id}`);
75
+ const botId = socket.botId;
76
+ if (botId) {
77
+ this._removeViewer(botId, socket.id);
78
+ }
79
+ });
80
+ });
81
+ }
82
+
83
+ _setupIPCHandlers() {
84
+ this.processManager.getAllProcesses().forEach((child, botId) => {
85
+ this._attachChildHandlers(child, botId);
86
+ });
87
+
88
+ const originalSpawn = this.processManager.spawn.bind(this.processManager);
89
+ this.processManager.spawn = async (...args) => {
90
+ const child = await originalSpawn(...args);
91
+ const botId = child.botConfig.id;
92
+ this._attachChildHandlers(child, botId);
93
+ return child;
94
+ };
95
+ }
96
+
97
+ _attachChildHandlers(child, botId) {
98
+ child.on('message', (message) => {
99
+ if (message.type === 'viewer:state_response') {
100
+ const pending = this.pendingRequests.get(message.requestId);
101
+ if (pending) {
102
+ pending.resolve(message.payload);
103
+ this.pendingRequests.delete(message.requestId);
104
+ }
105
+ } else if (message.type === 'viewer:spawn') {
106
+ this.viewerNamespace.to(`bot:${botId}`).emit('viewer:spawn', message.payload);
107
+ } else if (message.type === 'viewer:move') {
108
+ this.viewerNamespace.to(`bot:${botId}`).emit('viewer:move', message.payload);
109
+ } else if (message.type === 'viewer:health') {
110
+ this.viewerNamespace.to(`bot:${botId}`).emit('viewer:health', message.payload);
111
+ } else if (message.type === 'viewer:chat') {
112
+ this.viewerNamespace.to(`bot:${botId}`).emit('viewer:chat', message.payload);
113
+ }
114
+ });
115
+ }
116
+
117
+ _startStateStream(botId) {
118
+ if (this.activeViewers.get(botId)?.size === 1) {
119
+ let lastPosition = null;
120
+ let lastSentPosition = null;
121
+ let lastSentPlayers = new Map(); // username -> {x, y, z, yaw, pitch}
122
+ let tickCounter = 0;
123
+
124
+ const interval = setInterval(() => {
125
+ if (this.activeViewers.get(botId)?.size > 0) {
126
+ tickCounter++;
127
+ const shouldSendBlocks = tickCounter % 60 === 0;
128
+
129
+ const requestId = uuidv4();
130
+ this.processManager.sendMessage(botId, {
131
+ type: 'viewer:get_state',
132
+ requestId,
133
+ includeBlocks: shouldSendBlocks
134
+ });
135
+
136
+ this.pendingRequests.set(requestId, {
137
+ resolve: (state) => {
138
+ const currentPos = state.position;
139
+
140
+ // Проверяем изменения в nearbyPlayers
141
+ let playersChanged = false;
142
+ if (state.nearbyPlayers) {
143
+ // Проверяем количество
144
+ if (state.nearbyPlayers.length !== lastSentPlayers.size) {
145
+ playersChanged = true;
146
+ } else {
147
+ // Проверяем каждого игрока
148
+ for (const player of state.nearbyPlayers) {
149
+ const last = lastSentPlayers.get(player.username);
150
+ if (!last ||
151
+ Math.abs(last.x - player.position.x) > 0.1 ||
152
+ Math.abs(last.y - player.position.y) > 0.1 ||
153
+ Math.abs(last.z - player.position.z) > 0.1 ||
154
+ Math.abs(last.yaw - player.yaw) > 0.05 ||
155
+ Math.abs(last.pitch - player.pitch) > 0.05) {
156
+ playersChanged = true;
157
+ break;
158
+ }
159
+ }
160
+ }
161
+ }
162
+
163
+ // Отправляем если позиция бота изменилась, игроки изменились, или пора отправлять блоки
164
+ const shouldSend = shouldSendBlocks || playersChanged || !lastSentPosition ||
165
+ Math.abs(currentPos?.x - lastSentPosition.x) > 0.1 ||
166
+ Math.abs(currentPos?.y - lastSentPosition.y) > 0.1 ||
167
+ Math.abs(currentPos?.z - lastSentPosition.z) > 0.1;
168
+
169
+ if (shouldSend) {
170
+ this.viewerNamespace.to(`bot:${botId}`).emit('viewer:update', state);
171
+ lastSentPosition = currentPos ? { ...currentPos } : null;
172
+
173
+ // Обновляем кэш игроков
174
+ lastSentPlayers.clear();
175
+ if (state.nearbyPlayers) {
176
+ state.nearbyPlayers.forEach(player => {
177
+ lastSentPlayers.set(player.username, {
178
+ x: player.position.x,
179
+ y: player.position.y,
180
+ z: player.position.z,
181
+ yaw: player.yaw,
182
+ pitch: player.pitch
183
+ });
184
+ });
185
+ }
186
+ }
187
+
188
+ // Обновляем lastPosition для блоков
189
+ if (shouldSendBlocks || !lastPosition ||
190
+ Math.abs(currentPos?.x - lastPosition.x) > 8 ||
191
+ Math.abs(currentPos?.y - lastPosition.y) > 8 ||
192
+ Math.abs(currentPos?.z - lastPosition.z) > 8) {
193
+ lastPosition = currentPos ? { ...currentPos } : null;
194
+ }
195
+ },
196
+ reject: () => {}
197
+ });
198
+ } else {
199
+ clearInterval(interval);
200
+ }
201
+ }, 100);
202
+ }
203
+ }
204
+
205
+ _removeViewer(botId, socketId) {
206
+ const viewers = this.activeViewers.get(botId);
207
+ if (viewers) {
208
+ viewers.delete(socketId);
209
+ if (viewers.size === 0) {
210
+ this.activeViewers.delete(botId);
211
+ }
212
+ }
213
+ }
214
+ }
215
+
216
+ module.exports = MinecraftViewerService;