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.
- package/.claude/settings.json +5 -1
- package/.claude/settings.local.json +10 -1
- package/CHANGELOG.md +27 -3
- package/CLAUDE.md +284 -0
- package/README.md +302 -152
- package/backend/package-lock.json +681 -9
- package/backend/package.json +8 -0
- package/backend/prisma/migrations/20251116111851_add_execution_trace/migration.sql +22 -0
- package/backend/prisma/migrations/20251120154914_add_panel_api_keys/migration.sql +21 -0
- package/backend/prisma/migrations/20251121110241_add_proxy_table/migration.sql +45 -0
- package/backend/prisma/migrations/migration_lock.toml +2 -2
- package/backend/prisma/schema.prisma +70 -1
- package/backend/src/__tests__/services/BotLifecycleService.test.js +9 -4
- package/backend/src/ai/plugin-assistant-system-prompt.md +788 -0
- package/backend/src/api/middleware/auth.js +27 -0
- package/backend/src/api/middleware/botAccess.js +7 -3
- package/backend/src/api/middleware/panelApiAuth.js +135 -0
- package/backend/src/api/routes/aiAssistant.js +995 -0
- package/backend/src/api/routes/auth.js +669 -633
- package/backend/src/api/routes/botCommands.js +107 -0
- package/backend/src/api/routes/botGroups.js +165 -0
- package/backend/src/api/routes/botHistory.js +108 -0
- package/backend/src/api/routes/botPermissions.js +99 -0
- package/backend/src/api/routes/botStatus.js +36 -0
- package/backend/src/api/routes/botUsers.js +162 -0
- package/backend/src/api/routes/bots.js +2451 -2402
- package/backend/src/api/routes/eventGraphs.js +4 -1
- package/backend/src/api/routes/logs.js +13 -3
- package/backend/src/api/routes/panel.js +66 -66
- package/backend/src/api/routes/panelApiKeys.js +179 -0
- package/backend/src/api/routes/pluginIde.js +1715 -135
- package/backend/src/api/routes/plugins.js +376 -219
- package/backend/src/api/routes/proxies.js +130 -0
- package/backend/src/api/routes/search.js +4 -0
- package/backend/src/api/routes/servers.js +20 -3
- package/backend/src/api/routes/settings.js +5 -0
- package/backend/src/api/routes/system.js +174 -174
- package/backend/src/api/routes/traces.js +131 -0
- package/backend/src/config/debug.config.js +36 -0
- package/backend/src/core/BotHistoryStore.js +180 -0
- package/backend/src/core/BotManager.js +14 -4
- package/backend/src/core/BotProcess.js +1517 -1092
- package/backend/src/core/EventGraphManager.js +37 -123
- package/backend/src/core/GraphExecutionEngine.js +977 -321
- package/backend/src/core/MessageQueue.js +12 -6
- package/backend/src/core/PluginLoader.js +99 -5
- package/backend/src/core/PluginManager.js +74 -13
- package/backend/src/core/TaskScheduler.js +1 -1
- package/backend/src/core/commands/whois.js +1 -1
- package/backend/src/core/node-registries/actions.js +70 -0
- package/backend/src/core/node-registries/arrays.js +18 -0
- package/backend/src/core/node-registries/data.js +1 -1
- package/backend/src/core/node-registries/events.js +14 -0
- package/backend/src/core/node-registries/logic.js +17 -0
- package/backend/src/core/node-registries/strings.js +34 -0
- package/backend/src/core/node-registries/type.js +25 -0
- package/backend/src/core/nodes/actions/bot_look_at.js +1 -1
- package/backend/src/core/nodes/actions/create_command.js +189 -0
- package/backend/src/core/nodes/actions/delete_command.js +92 -0
- package/backend/src/core/nodes/actions/update_command.js +133 -0
- package/backend/src/core/nodes/arrays/join.js +28 -0
- package/backend/src/core/nodes/data/cast.js +2 -1
- package/backend/src/core/nodes/logic/not.js +22 -0
- package/backend/src/core/nodes/strings/starts_with.js +1 -1
- package/backend/src/core/nodes/strings/to_lower.js +22 -0
- package/backend/src/core/nodes/strings/to_upper.js +22 -0
- package/backend/src/core/nodes/type/to_string.js +32 -0
- package/backend/src/core/services/BotLifecycleService.js +255 -16
- package/backend/src/core/services/CommandExecutionService.js +430 -351
- package/backend/src/core/services/DebugSessionManager.js +347 -0
- package/backend/src/core/services/GraphCollaborationManager.js +501 -0
- package/backend/src/core/services/MinecraftBotManager.js +259 -0
- package/backend/src/core/services/MinecraftViewerService.js +216 -0
- package/backend/src/core/services/TraceCollectorService.js +545 -0
- package/backend/src/core/system/RuntimeCommandRegistry.js +116 -0
- package/backend/src/core/system/Transport.js +0 -4
- package/backend/src/core/validation/nodeSchemas.js +6 -6
- package/backend/src/real-time/botApi/handlers/graphHandlers.js +2 -2
- package/backend/src/real-time/botApi/handlers/graphWebSocketHandlers.js +1 -1
- package/backend/src/real-time/botApi/utils.js +11 -0
- package/backend/src/real-time/panelNamespace.js +387 -0
- package/backend/src/real-time/presence.js +7 -2
- package/backend/src/real-time/socketHandler.js +395 -4
- package/backend/src/server.js +18 -0
- package/frontend/dist/assets/index-B1serztM.js +11210 -0
- package/frontend/dist/assets/index-t6K1u4OV.css +32 -0
- package/frontend/dist/index.html +2 -2
- package/frontend/package-lock.json +9437 -0
- package/frontend/package.json +8 -0
- package/package.json +2 -2
- package/screen/console.png +0 -0
- package/screen/dashboard.png +0 -0
- package/screen/graph_collabe.png +0 -0
- package/screen/graph_live_debug.png +0 -0
- package/screen/management_command.png +0 -0
- package/screen/node_debug_trace.png +0 -0
- package/screen/plugin_/320/276/320/261/320/267/320/276/321/200.png +0 -0
- package/screen/websocket.png +0 -0
- 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
- 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
- package/frontend/dist/assets/index-CfTo92bP.css +0 -1
- package/frontend/dist/assets/index-CiFD5X9Z.js +0 -8344
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GraphCollaborationManager
|
|
3
|
+
* Управляет совместным редактированием графов в реальном времени
|
|
4
|
+
*/
|
|
5
|
+
class GraphCollaborationManager {
|
|
6
|
+
constructor() {
|
|
7
|
+
// Структура: Map<graphId, GraphRoom>
|
|
8
|
+
// GraphRoom = { users: Map<socketId, UserInfo>, io: Socket.IO namespace }
|
|
9
|
+
this.rooms = new Map();
|
|
10
|
+
this.io = null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Инициализация с Socket.IO instance
|
|
15
|
+
*/
|
|
16
|
+
initialize(io) {
|
|
17
|
+
this.io = io;
|
|
18
|
+
console.log('[GraphCollaboration] Manager initialized');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Пользователь присоединяется к графу
|
|
23
|
+
*/
|
|
24
|
+
joinGraph(socket, { botId, graphId, username, userId, debugMode = 'normal' }) {
|
|
25
|
+
const roomKey = this.getRoomKey(botId, graphId);
|
|
26
|
+
|
|
27
|
+
// Создаем комнату если не существует
|
|
28
|
+
if (!this.rooms.has(roomKey)) {
|
|
29
|
+
this.rooms.set(roomKey, {
|
|
30
|
+
botId,
|
|
31
|
+
graphId,
|
|
32
|
+
users: new Map(),
|
|
33
|
+
// Храним текущее состояние графа для синхронизации при переподключении
|
|
34
|
+
graphState: {
|
|
35
|
+
nodes: [],
|
|
36
|
+
edges: [],
|
|
37
|
+
lastUpdate: null,
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
console.log(`[GraphCollaboration] Created room for bot ${botId}, graph ${graphId}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const room = this.rooms.get(roomKey);
|
|
44
|
+
|
|
45
|
+
// Добавляем пользователя в комнату
|
|
46
|
+
const userInfo = {
|
|
47
|
+
socketId: socket.id,
|
|
48
|
+
username: username || 'Anonymous',
|
|
49
|
+
userId: userId || null,
|
|
50
|
+
cursor: null, // { x, y }
|
|
51
|
+
selectedNodes: [], // [nodeId, ...]
|
|
52
|
+
color: this.generateUserColor(socket.id), // Уникальный цвет пользователя
|
|
53
|
+
debugMode: debugMode || 'normal', // 'normal' | 'trace' | 'live'
|
|
54
|
+
joinedAt: Date.now(),
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
room.users.set(socket.id, userInfo);
|
|
58
|
+
|
|
59
|
+
// Присоединяем socket к комнате
|
|
60
|
+
socket.join(roomKey);
|
|
61
|
+
|
|
62
|
+
console.log(`[GraphCollaboration] User ${username} (${socket.id}) joined bot ${botId}, graph ${graphId} in ${debugMode} mode. Total users: ${room.users.size}`);
|
|
63
|
+
|
|
64
|
+
// Отправляем текущее состояние новому пользователю
|
|
65
|
+
socket.emit('collab:state', {
|
|
66
|
+
users: Array.from(room.users.values()).map(u => ({
|
|
67
|
+
socketId: u.socketId,
|
|
68
|
+
username: u.username,
|
|
69
|
+
userId: u.userId,
|
|
70
|
+
color: u.color,
|
|
71
|
+
cursor: u.cursor,
|
|
72
|
+
selectedNodes: u.selectedNodes,
|
|
73
|
+
debugMode: u.debugMode,
|
|
74
|
+
})),
|
|
75
|
+
// Отправляем текущее состояние графа (если есть другие пользователи)
|
|
76
|
+
graphState: room.users.size > 1 ? room.graphState : null,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Уведомляем остальных о новом пользователе
|
|
80
|
+
socket.to(roomKey).emit('collab:user-joined', {
|
|
81
|
+
user: {
|
|
82
|
+
socketId: userInfo.socketId,
|
|
83
|
+
username: userInfo.username,
|
|
84
|
+
userId: userInfo.userId,
|
|
85
|
+
color: userInfo.color,
|
|
86
|
+
debugMode: userInfo.debugMode,
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
return room;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Пользователь покидает граф
|
|
95
|
+
*/
|
|
96
|
+
leaveGraph(socket, { botId, graphId }) {
|
|
97
|
+
const roomKey = this.getRoomKey(botId, graphId);
|
|
98
|
+
const room = this.rooms.get(roomKey);
|
|
99
|
+
|
|
100
|
+
if (!room) return;
|
|
101
|
+
|
|
102
|
+
const userInfo = room.users.get(socket.id);
|
|
103
|
+
if (!userInfo) return;
|
|
104
|
+
|
|
105
|
+
room.users.delete(socket.id);
|
|
106
|
+
socket.leave(roomKey);
|
|
107
|
+
|
|
108
|
+
console.log(`[GraphCollaboration] User ${userInfo.username} (${socket.id}) left bot ${botId}, graph ${graphId}. Remaining users: ${room.users.size}`);
|
|
109
|
+
|
|
110
|
+
// Уведомляем остальных
|
|
111
|
+
socket.to(roomKey).emit('collab:user-left', {
|
|
112
|
+
socketId: socket.id,
|
|
113
|
+
username: userInfo.username,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Удаляем комнату если пустая
|
|
117
|
+
if (room.users.size === 0) {
|
|
118
|
+
this.rooms.delete(roomKey);
|
|
119
|
+
console.log(`[GraphCollaboration] Removed empty room for bot ${botId}, graph ${graphId}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Обновление позиции курсора
|
|
125
|
+
*/
|
|
126
|
+
updateCursor(socket, { botId, graphId, x, y }) {
|
|
127
|
+
const roomKey = this.getRoomKey(botId, graphId);
|
|
128
|
+
const room = this.rooms.get(roomKey);
|
|
129
|
+
|
|
130
|
+
if (!room) return;
|
|
131
|
+
|
|
132
|
+
const userInfo = room.users.get(socket.id);
|
|
133
|
+
if (!userInfo) return;
|
|
134
|
+
|
|
135
|
+
userInfo.cursor = { x, y };
|
|
136
|
+
|
|
137
|
+
// Broadcast другим пользователям
|
|
138
|
+
socket.to(roomKey).emit('collab:cursor-move', {
|
|
139
|
+
socketId: socket.id,
|
|
140
|
+
username: userInfo.username,
|
|
141
|
+
color: userInfo.color,
|
|
142
|
+
x,
|
|
143
|
+
y,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Обновление режима отладки пользователя
|
|
149
|
+
*/
|
|
150
|
+
updateDebugMode(socket, { botId, graphId, debugMode }) {
|
|
151
|
+
const roomKey = this.getRoomKey(botId, graphId);
|
|
152
|
+
const room = this.rooms.get(roomKey);
|
|
153
|
+
|
|
154
|
+
if (!room) return;
|
|
155
|
+
|
|
156
|
+
const userInfo = room.users.get(socket.id);
|
|
157
|
+
if (!userInfo) return;
|
|
158
|
+
|
|
159
|
+
userInfo.debugMode = debugMode;
|
|
160
|
+
|
|
161
|
+
console.log(`[GraphCollaboration] User ${userInfo.username} switched to ${debugMode} mode`);
|
|
162
|
+
|
|
163
|
+
// Broadcast другим пользователям
|
|
164
|
+
socket.to(roomKey).emit('collab:mode-changed', {
|
|
165
|
+
socketId: socket.id,
|
|
166
|
+
username: userInfo.username,
|
|
167
|
+
debugMode,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Обновление выбранных нод
|
|
173
|
+
*/
|
|
174
|
+
updateSelectedNodes(socket, { botId, graphId, nodeIds }) {
|
|
175
|
+
const roomKey = this.getRoomKey(botId, graphId);
|
|
176
|
+
const room = this.rooms.get(roomKey);
|
|
177
|
+
|
|
178
|
+
if (!room) return;
|
|
179
|
+
|
|
180
|
+
const userInfo = room.users.get(socket.id);
|
|
181
|
+
if (!userInfo) return;
|
|
182
|
+
|
|
183
|
+
userInfo.selectedNodes = nodeIds || [];
|
|
184
|
+
|
|
185
|
+
// Broadcast другим пользователям
|
|
186
|
+
socket.to(roomKey).emit('collab:selection-changed', {
|
|
187
|
+
socketId: socket.id,
|
|
188
|
+
username: userInfo.username,
|
|
189
|
+
color: userInfo.color,
|
|
190
|
+
nodeIds: userInfo.selectedNodes,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Начало создания соединения
|
|
196
|
+
*/
|
|
197
|
+
startConnection(socket, { botId, graphId, fromX, fromY, fromNodeId, fromHandleId }) {
|
|
198
|
+
const roomKey = this.getRoomKey(botId, graphId);
|
|
199
|
+
const userInfo = this.getUserInfo(socket, botId, graphId);
|
|
200
|
+
|
|
201
|
+
if (!userInfo) return;
|
|
202
|
+
|
|
203
|
+
// Broadcast начало соединения
|
|
204
|
+
socket.to(roomKey).emit('collab:connection-start', {
|
|
205
|
+
socketId: socket.id,
|
|
206
|
+
username: userInfo.username,
|
|
207
|
+
color: userInfo.color,
|
|
208
|
+
fromX,
|
|
209
|
+
fromY,
|
|
210
|
+
fromNodeId,
|
|
211
|
+
fromHandleId,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Обновление позиции соединения во время тяжения
|
|
217
|
+
*/
|
|
218
|
+
updateConnection(socket, { botId, graphId, toX, toY }) {
|
|
219
|
+
const roomKey = this.getRoomKey(botId, graphId);
|
|
220
|
+
const userInfo = this.getUserInfo(socket, botId, graphId);
|
|
221
|
+
|
|
222
|
+
if (!userInfo) return;
|
|
223
|
+
|
|
224
|
+
// Broadcast обновление позиции
|
|
225
|
+
socket.to(roomKey).emit('collab:connection-update', {
|
|
226
|
+
socketId: socket.id,
|
|
227
|
+
toX,
|
|
228
|
+
toY,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Завершение создания соединения
|
|
234
|
+
*/
|
|
235
|
+
endConnection(socket, { botId, graphId }) {
|
|
236
|
+
const roomKey = this.getRoomKey(botId, graphId);
|
|
237
|
+
|
|
238
|
+
// Broadcast завершение соединения
|
|
239
|
+
socket.to(roomKey).emit('collab:connection-end', {
|
|
240
|
+
socketId: socket.id,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Инициализировать состояние графа (вызывается первым пользователем после загрузки из БД)
|
|
246
|
+
*/
|
|
247
|
+
initializeGraphState(socket, { botId, graphId, nodes, edges }) {
|
|
248
|
+
const roomKey = this.getRoomKey(botId, graphId);
|
|
249
|
+
const room = this.rooms.get(roomKey);
|
|
250
|
+
|
|
251
|
+
if (!room) return;
|
|
252
|
+
|
|
253
|
+
room.graphState = {
|
|
254
|
+
nodes: nodes || [],
|
|
255
|
+
edges: edges || [],
|
|
256
|
+
lastUpdate: Date.now(),
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
console.log(`[GraphCollaboration] Graph state initialized for bot ${botId}, graph ${graphId}: ${nodes?.length} nodes, ${edges?.length} edges`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Broadcast изменения нод (движение, создание, удаление)
|
|
264
|
+
*/
|
|
265
|
+
broadcastNodeChange(socket, { botId, graphId, type, data }) {
|
|
266
|
+
const roomKey = this.getRoomKey(botId, graphId);
|
|
267
|
+
const room = this.rooms.get(roomKey);
|
|
268
|
+
|
|
269
|
+
if (room) {
|
|
270
|
+
// Обновляем состояние графа в комнате
|
|
271
|
+
switch (type) {
|
|
272
|
+
case 'create':
|
|
273
|
+
// data = { node }
|
|
274
|
+
if (data.node) {
|
|
275
|
+
room.graphState.nodes.push(data.node);
|
|
276
|
+
}
|
|
277
|
+
break;
|
|
278
|
+
case 'delete':
|
|
279
|
+
// data = { nodeIds: [] }
|
|
280
|
+
if (data.nodeIds && Array.isArray(data.nodeIds)) {
|
|
281
|
+
room.graphState.nodes = room.graphState.nodes.filter(n => !data.nodeIds.includes(n.id));
|
|
282
|
+
// Удаляем связанные edges
|
|
283
|
+
room.graphState.edges = room.graphState.edges.filter(
|
|
284
|
+
c => !data.nodeIds.includes(c.source) && !data.nodeIds.includes(c.target)
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
break;
|
|
288
|
+
case 'update':
|
|
289
|
+
// data = { nodeId, nodeData }
|
|
290
|
+
if (data.nodeId && data.nodeData) {
|
|
291
|
+
const nodeIndex = room.graphState.nodes.findIndex(n => n.id === data.nodeId);
|
|
292
|
+
if (nodeIndex !== -1) {
|
|
293
|
+
room.graphState.nodes[nodeIndex].data = {
|
|
294
|
+
...room.graphState.nodes[nodeIndex].data,
|
|
295
|
+
...data.nodeData
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
break;
|
|
300
|
+
case 'move':
|
|
301
|
+
// data = [{ id, position }]
|
|
302
|
+
if (Array.isArray(data)) {
|
|
303
|
+
data.forEach(change => {
|
|
304
|
+
const nodeIndex = room.graphState.nodes.findIndex(n => n.id === change.id);
|
|
305
|
+
if (nodeIndex !== -1) {
|
|
306
|
+
room.graphState.nodes[nodeIndex].position = change.position;
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
312
|
+
room.graphState.lastUpdate = Date.now();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Отправляем всем кроме отправителя
|
|
316
|
+
socket.to(roomKey).emit('collab:node-changed', {
|
|
317
|
+
type, // 'move' | 'create' | 'delete' | 'update'
|
|
318
|
+
data,
|
|
319
|
+
username: this.getUserInfo(socket, botId, graphId)?.username,
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Broadcast изменения связей (создание, удаление)
|
|
325
|
+
*/
|
|
326
|
+
broadcastEdgeChange(socket, { botId, graphId, type, data }) {
|
|
327
|
+
const roomKey = this.getRoomKey(botId, graphId);
|
|
328
|
+
const room = this.rooms.get(roomKey);
|
|
329
|
+
|
|
330
|
+
if (room) {
|
|
331
|
+
// Обновляем состояние графа в комнате
|
|
332
|
+
switch (type) {
|
|
333
|
+
case 'create':
|
|
334
|
+
// data = { edge }
|
|
335
|
+
if (data.edge) {
|
|
336
|
+
room.graphState.edges.push(data.edge);
|
|
337
|
+
}
|
|
338
|
+
break;
|
|
339
|
+
case 'delete':
|
|
340
|
+
// data = { edgeIds: [] }
|
|
341
|
+
if (data.edgeIds && Array.isArray(data.edgeIds)) {
|
|
342
|
+
room.graphState.edges = room.graphState.edges.filter(
|
|
343
|
+
c => !data.edgeIds.includes(c.id)
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
room.graphState.lastUpdate = Date.now();
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
socket.to(roomKey).emit('collab:edge-changed', {
|
|
352
|
+
type, // 'create' | 'delete'
|
|
353
|
+
data,
|
|
354
|
+
username: this.getUserInfo(socket, botId, graphId)?.username,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Broadcast hot reload графа
|
|
360
|
+
*/
|
|
361
|
+
broadcastGraphReloaded(botId, graphId) {
|
|
362
|
+
const roomKey = this.getRoomKey(botId, graphId);
|
|
363
|
+
|
|
364
|
+
if (this.io) {
|
|
365
|
+
this.io.to(roomKey).emit('collab:graph-reloaded', {
|
|
366
|
+
botId,
|
|
367
|
+
graphId,
|
|
368
|
+
timestamp: Date.now(),
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Получить информацию о пользователе
|
|
375
|
+
*/
|
|
376
|
+
getUserInfo(socket, botId, graphId) {
|
|
377
|
+
const roomKey = this.getRoomKey(botId, graphId);
|
|
378
|
+
const room = this.rooms.get(roomKey);
|
|
379
|
+
return room?.users.get(socket.id);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Получить всех пользователей в комнате
|
|
384
|
+
*/
|
|
385
|
+
getRoomUsers(botId, graphId) {
|
|
386
|
+
const roomKey = this.getRoomKey(botId, graphId);
|
|
387
|
+
const room = this.rooms.get(roomKey);
|
|
388
|
+
|
|
389
|
+
if (!room) return [];
|
|
390
|
+
|
|
391
|
+
return Array.from(room.users.values()).map(u => ({
|
|
392
|
+
socketId: u.socketId,
|
|
393
|
+
username: u.username,
|
|
394
|
+
userId: u.userId,
|
|
395
|
+
color: u.color,
|
|
396
|
+
cursor: u.cursor,
|
|
397
|
+
selectedNodes: u.selectedNodes,
|
|
398
|
+
debugMode: u.debugMode,
|
|
399
|
+
}));
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Удалить пользователя из всех комнат (при disconnect)
|
|
404
|
+
*/
|
|
405
|
+
removeUserFromAllRooms(socket) {
|
|
406
|
+
for (const [roomKey, room] of this.rooms.entries()) {
|
|
407
|
+
const userInfo = room.users.get(socket.id);
|
|
408
|
+
if (userInfo) {
|
|
409
|
+
room.users.delete(socket.id);
|
|
410
|
+
|
|
411
|
+
socket.to(roomKey).emit('collab:user-left', {
|
|
412
|
+
socketId: socket.id,
|
|
413
|
+
username: userInfo.username,
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
console.log(`[GraphCollaboration] User ${userInfo.username} (${socket.id}) disconnected from room ${roomKey}`);
|
|
417
|
+
|
|
418
|
+
if (room.users.size === 0) {
|
|
419
|
+
this.rooms.delete(roomKey);
|
|
420
|
+
console.log(`[GraphCollaboration] Removed empty room ${roomKey}`);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Генерация ключа комнаты
|
|
428
|
+
*/
|
|
429
|
+
getRoomKey(botId, graphId) {
|
|
430
|
+
return `graph:${botId}:${graphId}`;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Генерация уникального цвета для пользователя
|
|
435
|
+
*/
|
|
436
|
+
generateUserColor(socketId) {
|
|
437
|
+
const colors = [
|
|
438
|
+
'#FF6B6B', // Red
|
|
439
|
+
'#4ECDC4', // Cyan
|
|
440
|
+
'#45B7D1', // Blue
|
|
441
|
+
'#FFA07A', // Light Salmon
|
|
442
|
+
'#98D8C8', // Mint
|
|
443
|
+
'#F7DC6F', // Yellow
|
|
444
|
+
'#BB8FCE', // Purple
|
|
445
|
+
'#85C1E2', // Sky Blue
|
|
446
|
+
'#F8B739', // Orange
|
|
447
|
+
'#52C785', // Green
|
|
448
|
+
];
|
|
449
|
+
|
|
450
|
+
// Простой хеш для консистентности цвета
|
|
451
|
+
const hash = socketId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0);
|
|
452
|
+
return colors[hash % colors.length];
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Получить статистику
|
|
457
|
+
*/
|
|
458
|
+
getStats() {
|
|
459
|
+
const stats = {
|
|
460
|
+
totalRooms: this.rooms.size,
|
|
461
|
+
totalUsers: 0,
|
|
462
|
+
rooms: [],
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
for (const [roomKey, room] of this.rooms.entries()) {
|
|
466
|
+
stats.totalUsers += room.users.size;
|
|
467
|
+
stats.rooms.push({
|
|
468
|
+
roomKey,
|
|
469
|
+
botId: room.botId,
|
|
470
|
+
graphId: room.graphId,
|
|
471
|
+
userCount: room.users.size,
|
|
472
|
+
users: Array.from(room.users.values()).map(u => u.username),
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return stats;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
let globalCollaborationManager = null;
|
|
481
|
+
|
|
482
|
+
function initializeCollaborationManager(io) {
|
|
483
|
+
if (!globalCollaborationManager) {
|
|
484
|
+
globalCollaborationManager = new GraphCollaborationManager();
|
|
485
|
+
globalCollaborationManager.initialize(io);
|
|
486
|
+
}
|
|
487
|
+
return globalCollaborationManager;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function getGlobalCollaborationManager() {
|
|
491
|
+
if (!globalCollaborationManager) {
|
|
492
|
+
throw new Error('GraphCollaborationManager not initialized! Call initializeCollaborationManager(io) first.');
|
|
493
|
+
}
|
|
494
|
+
return globalCollaborationManager;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
module.exports = {
|
|
498
|
+
GraphCollaborationManager,
|
|
499
|
+
initializeCollaborationManager,
|
|
500
|
+
getGlobalCollaborationManager,
|
|
501
|
+
};
|