cdp-tunnel 2.5.11 → 2.5.13

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.
@@ -230,56 +230,7 @@ importScripts('features/automation-badge.js');
230
230
  }
231
231
  });
232
232
 
233
- // 将标签页添加到CDP组(添加延迟等待)
234
- setTimeout(function() {
235
- var openerClientId = openerTabId ? State.getClientIdByTabId(openerTabId) : null;
236
- var groupClientId = openerClientId;
237
- if (!groupClientId) {
238
- var cdpClients = State.getCDPClients() || [];
239
- if (cdpClients.length > 0 && cdpClients[0] && cdpClients[0].id) {
240
- groupClientId = cdpClients[0].id;
241
- }
242
- }
243
-
244
- if (!groupClientId) return;
245
- var baseName = CDPUtils.getGroupBaseName(groupClientId);
246
- Logger.info('[TabGroup] background onCreated, baseName:', baseName);
247
-
248
- chrome.tabGroups.query({}, function(allGroups) {
249
- var existing = CDPUtils.findGroupByName(allGroups, baseName);
250
- if (existing) {
251
- chrome.tabs.group({ tabIds: tabId, groupId: existing.id }, function(groupId) {
252
- if (chrome.runtime.lastError) {
253
- Logger.error('[TabGroup] Failed to add tab to group:', chrome.runtime.lastError.message);
254
- } else {
255
- State.setGroupIdForClient(groupClientId, existing.id);
256
- Logger.info('[TabGroup] Tab added to group:', groupId);
257
- }
258
- });
259
- } else {
260
- chrome.tabs.group({ tabIds: tabId }, function(groupId) {
261
- if (chrome.runtime.lastError) {
262
- Logger.error('[TabGroup] Failed to create group:', chrome.runtime.lastError.message);
263
- return;
264
- }
265
- if (groupId) {
266
- chrome.tabGroups.update(groupId, {
267
- title: baseName,
268
- color: CDPUtils.getGroupColorForClient(groupClientId),
269
- collapsed: true
270
- }, function(group) {
271
- if (chrome.runtime.lastError) {
272
- Logger.error('[TabGroup] Failed to update group:', chrome.runtime.lastError.message);
273
- } else {
274
- State.setGroupIdForClient(groupClientId, groupId);
275
- Logger.info('[TabGroup] Group created:', group);
276
- }
277
- });
278
- }
279
- });
280
- }
281
- });
282
- }, 2000);
233
+ SpecialHandler.addTabToAutomationGroup(tabId, openerClientId);
283
234
 
284
235
  Logger.info('[Tabs] Sending Target.attachedToTarget event');
285
236
 
@@ -135,17 +135,96 @@ var LocalHandler = (function() {
135
135
  return {};
136
136
  }
137
137
 
138
- function tabGetGroupInfo(context) {
138
+ function tabUngroup(context) {
139
139
  var clientId = context.clientId;
140
140
  var groupId = null;
141
- var baseName = null;
142
141
  try {
143
142
  groupId = State.getGroupIdForClient(clientId);
143
+ } catch (e) {
144
+ Logger.error('[TabUngroup] Error getting groupId: ' + (e.message || e));
145
+ return Promise.resolve({ success: false, ungroupedCount: 0, error: e.message || String(e) });
146
+ }
147
+ if (groupId == null) {
148
+ return Promise.resolve({ success: true, ungroupedCount: 0 });
149
+ }
150
+ return new Promise(function(resolve) {
151
+ chrome.tabs.query({ groupId: groupId }, function(tabs) {
152
+ if (chrome.runtime.lastError) {
153
+ Logger.error('[TabUngroup] chrome.runtime.lastError: ' + chrome.runtime.lastError.message);
154
+ resolve({ success: false, ungroupedCount: 0, error: chrome.runtime.lastError.message });
155
+ return;
156
+ }
157
+ if (!tabs || tabs.length === 0) {
158
+ resolve({ success: true, ungroupedCount: 0 });
159
+ return;
160
+ }
161
+ var tabIds = tabs.map(function(tab) { return tab.id; });
162
+ chrome.tabs.ungroup(tabIds, function() {
163
+ if (chrome.runtime.lastError) {
164
+ Logger.error('[TabUngroup] ungroup lastError: ' + chrome.runtime.lastError.message);
165
+ resolve({ success: false, ungroupedCount: 0, error: chrome.runtime.lastError.message });
166
+ return;
167
+ }
168
+ State.removeGroupForClient(clientId);
169
+ resolve({ success: true, ungroupedCount: tabIds.length });
170
+ });
171
+ });
172
+ });
173
+ }
174
+
175
+ function tabGetGroupInfo(context) {
176
+ var clientId = context.clientId;
177
+ var cachedGroupId = null;
178
+ var baseName = null;
179
+ try {
180
+ cachedGroupId = State.getGroupIdForClient(clientId);
144
181
  baseName = CDPUtils.getGroupBaseName(clientId);
145
182
  } catch (e) {
146
183
  Logger.error('[TabGetGroupInfo] Error: ' + (e.message || e));
147
184
  }
148
- return Promise.resolve({ groupId: groupId, baseName: baseName, clientId: clientId });
185
+
186
+ var attachedTabIds = State.getAttachedTabIds();
187
+ var matchedTabId = null;
188
+ for (var i = 0; i < attachedTabIds.length; i++) {
189
+ if (State.getClientIdByTabId(attachedTabIds[i]) === clientId) {
190
+ matchedTabId = attachedTabIds[i];
191
+ break;
192
+ }
193
+ }
194
+
195
+ if (matchedTabId == null) {
196
+ return Promise.resolve({
197
+ groupId: -1,
198
+ cachedGroupId: cachedGroupId,
199
+ baseName: baseName,
200
+ clientId: clientId,
201
+ tabId: null
202
+ });
203
+ }
204
+
205
+ var tabId = matchedTabId;
206
+ return new Promise(function(resolve) {
207
+ chrome.tabs.get(tabId, function(tab) {
208
+ if (chrome.runtime.lastError) {
209
+ Logger.error('[TabGetGroupInfo] chrome.tabs.get error: ' + chrome.runtime.lastError.message);
210
+ resolve({
211
+ groupId: -1,
212
+ cachedGroupId: cachedGroupId,
213
+ baseName: baseName,
214
+ clientId: clientId,
215
+ tabId: tabId
216
+ });
217
+ return;
218
+ }
219
+ resolve({
220
+ groupId: tab.groupId != null ? tab.groupId : -1,
221
+ cachedGroupId: cachedGroupId,
222
+ baseName: baseName,
223
+ clientId: clientId,
224
+ tabId: tabId
225
+ });
226
+ });
227
+ });
149
228
  }
150
229
 
151
230
  function tabGetMuteStatus(params) {
@@ -322,6 +401,7 @@ var LocalHandler = (function() {
322
401
  getTargetInfoById: getTargetInfoById,
323
402
  mapToTargetInfo: mapToTargetInfo,
324
403
  tabGetMuteStatus: tabGetMuteStatus,
325
- tabGetGroupInfo: tabGetGroupInfo
404
+ tabGetGroupInfo: tabGetGroupInfo,
405
+ tabUngroup: tabUngroup
326
406
  };
327
407
  })();
@@ -149,28 +149,8 @@ var SpecialHandler = (function() {
149
149
  }
150
150
  var baseName = CDPUtils.getGroupBaseName(groupClientId);
151
151
 
152
- var retries = 0;
153
- var maxRetries = 20;
154
- function tryGroup() {
155
- chrome.tabs.get(tabId, function(tab) {
156
- if (chrome.runtime.lastError || !tab) {
157
- Logger.error('[TabGroup] Tab not found:', tabId, 'retries:', retries);
158
- return;
159
- }
160
- if (tab.status === 'complete') {
161
- Logger.info('[TabGroup] Tab ready, executing group operation for:', baseName);
162
- doGroup(tabId, groupClientId, baseName);
163
- } else if (retries < maxRetries) {
164
- retries++;
165
- Logger.info('[TabGroup] Tab not ready (', tab.status, '), retry', retries, '/', maxRetries);
166
- setTimeout(tryGroup, 200);
167
- } else {
168
- Logger.warn('[TabGroup] Tab never reached complete status, grouping anyway. tabId:', tabId);
169
- doGroup(tabId, groupClientId, baseName);
170
- }
171
- });
172
- }
173
- tryGroup();
152
+ Logger.info('[TabGroup] Grouping tab immediately for:', baseName);
153
+ doGroup(tabId, groupClientId, baseName);
174
154
  }
175
155
 
176
156
  function doGroup(tabId, clientId, baseName, retries) {
@@ -608,6 +588,7 @@ function checkTabVisibility(tabId) {
608
588
  pageAddScriptToEvaluateOnNewDocument: pageAddScriptToEvaluateOnNewDocument,
609
589
  runtimeRunIfWaitingForDebugger: runtimeRunIfWaitingForDebugger,
610
590
  domSetFileInputFiles: domSetFileInputFiles,
611
- updateTabGroupName: updateTabGroupName
591
+ updateTabGroupName: updateTabGroupName,
592
+ addTabToAutomationGroup: addTabToAutomationGroup
612
593
  };
613
594
  })();
@@ -24,6 +24,7 @@ var CDP_HANDLERS = {
24
24
 
25
25
  'Tab.getMuteStatus': { type: 'LOCAL', handler: LocalHandler.tabGetMuteStatus },
26
26
  'Tab.getGroupInfo': { type: 'LOCAL', handler: LocalHandler.tabGetGroupInfo },
27
+ 'Tab.ungroup': { type: 'LOCAL', handler: LocalHandler.tabUngroup },
27
28
 
28
29
  'SystemInfo.getInfo': { type: 'LOCAL', handler: LocalHandler.systemInfoGetInfo },
29
30
  'SystemInfo.getProcessInfo': { type: 'LOCAL', handler: LocalHandler.systemInfoGetProcessInfo },
@@ -409,8 +409,8 @@ var WebSocketManager = (function() {
409
409
  return;
410
410
  }
411
411
  var groupId = State.getGroupIdForClient(clientId);
412
- Logger.info('[Monitor] Tab', tabId, 'groupId=' + (tab.groupId || 'none'), 'expectedGroup=' + (groupId || 'none'), 'clientId=' + (clientId || 'none'));
413
- if (tab.groupId) return;
412
+ Logger.info('[Monitor] Tab', tabId, 'groupId=' + (tab.groupId > -1 ? tab.groupId : 'none'), 'expectedGroup=' + (groupId > -1 ? groupId : 'none'), 'clientId=' + (clientId || 'none'));
413
+ if (tab.groupId > -1) return;
414
414
  Logger.info('[Monitor] Tab', tabId, 'escaped! Forcing regroup for client:', clientId);
415
415
  if (groupId) {
416
416
  chrome.tabs.group({ tabIds: tabId, groupId: groupId }, function() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cdp-tunnel",
3
- "version": "2.5.11",
3
+ "version": "2.5.13",
4
4
  "description": "Bridge Chrome's debugger API to WebSocket — control your existing browser with Playwright/Puppeteer via CDP",
5
5
  "main": "server/proxy-server.js",
6
6
  "bin": "./cli/index.js",
@@ -322,6 +322,132 @@ wss.on('connection', (ws, req) => {
322
322
  }
323
323
  });
324
324
 
325
+ function cleanupClient(ws, id, reason) {
326
+ const sessionsToClean = [];
327
+ for (const [sessionId, clientId] of sessionToClientId.entries()) {
328
+ if (clientId === id) {
329
+ sessionsToClean.push(sessionId);
330
+ sessionToClientId.delete(sessionId);
331
+ }
332
+ }
333
+
334
+ clientConnections.delete(ws);
335
+ clientById.delete(id);
336
+
337
+ logConnectionEvent('CLIENT_DISCONNECTED', {
338
+ id,
339
+ reason,
340
+ sessionsCleaned: sessionsToClean.length,
341
+ totalPlugins: pluginConnections.size,
342
+ totalClients: clientConnections.size
343
+ });
344
+
345
+ logDisconnect('CLIENT_CLEANUP', {
346
+ clientId: id,
347
+ reason,
348
+ sessionsLost: sessionsToClean.length,
349
+ cdpMethodsUsed: ws.cdpTrace ? [...new Set(ws.cdpTrace)] : [],
350
+ uptime: ws.connectedAt ? `${((Date.now() - ws.connectedAt) / 1000).toFixed(0)}s` : 'unknown',
351
+ remainingClients: clientConnections.size,
352
+ pluginAlive: pluginConnections.size > 0,
353
+ pairedPluginId: ws.pairedPlugin?.id || null
354
+ });
355
+
356
+ if (ws.cdpTrace && ws.cdpTrace.length && shouldLog('debug')) {
357
+ const unique = [...new Set(ws.cdpTrace)];
358
+ console.log(`[CDP TRACE] ${id} methods (${ws.cdpTrace.length}): ${unique.join(', ')}`);
359
+ }
360
+
361
+ if (ws.pairedPlugin) {
362
+ safeSend(ws.pairedPlugin, JSON.stringify({
363
+ type: 'client-disconnected',
364
+ clientId: id,
365
+ sessions: sessionsToClean
366
+ }), 'plugin');
367
+ }
368
+
369
+ broadcastClientList();
370
+
371
+ for (const [tId, cId] of targetIdToClientId.entries()) {
372
+ if (cId === id) targetIdToClientId.delete(tId);
373
+ }
374
+ for (const [bcId, cId] of browserContextToClientId.entries()) {
375
+ if (cId === id) browserContextToClientId.delete(bcId);
376
+ }
377
+ if (clientIdToBrowserContext.has(id)) {
378
+ clientIdToBrowserContext.delete(id);
379
+ }
380
+ for (const [gId, mapping] of globalRequestIdMap.entries()) {
381
+ if (mapping.clientId === id) globalRequestIdMap.delete(gId);
382
+ }
383
+
384
+ if (ws.pairedPlugin) {
385
+ ws.pairedPlugin.pairedClientId = null;
386
+ }
387
+ connectionPairs.delete(id);
388
+ }
389
+
390
+ function sendPendingRequestErrors(pluginWs) {
391
+ const toDelete = [];
392
+ for (const [gId, mapping] of globalRequestIdMap.entries()) {
393
+ const clientWs = clientById.get(mapping.clientId);
394
+ if (clientWs && clientWs.pairedPlugin === pluginWs) {
395
+ const errorResponse = {
396
+ id: mapping.originalId,
397
+ error: { code: -32000, message: 'Plugin disconnected: request cancelled' }
398
+ };
399
+ if (mapping.sessionId) {
400
+ errorResponse.sessionId = mapping.sessionId;
401
+ }
402
+ safeSend(clientWs, JSON.stringify(errorResponse), 'client');
403
+ toDelete.push(gId);
404
+ }
405
+ }
406
+ toDelete.forEach(gId => globalRequestIdMap.delete(gId));
407
+ }
408
+
409
+ function cleanupPlugin(ws, id, reason) {
410
+ pluginConnections.delete(ws);
411
+
412
+ if (pluginConnections.size === 0) {
413
+ updateExtensionState(false);
414
+ }
415
+
416
+ sendPendingRequestErrors(ws);
417
+
418
+ const affectedClients = [];
419
+ clientConnections.forEach(clientWs => {
420
+ if (clientWs.pairedPlugin === ws) {
421
+ if (clientWs.pluginMessageHandler) {
422
+ ws.off('message', clientWs.pluginMessageHandler);
423
+ clientWs.pluginMessageHandler = null;
424
+ }
425
+ clientWs.pairedPlugin = null;
426
+ affectedClients.push(clientWs.id);
427
+ if (clientWs.readyState === WebSocket.OPEN) {
428
+ clientWs.send(JSON.stringify({
429
+ type: 'plugin-disconnected',
430
+ message: 'Plugin connection lost'
431
+ }));
432
+ }
433
+ }
434
+ });
435
+
436
+ logDisconnect('PLUGIN_CLEANUP', {
437
+ pluginId: id,
438
+ reason,
439
+ remainingPlugins: pluginConnections.size,
440
+ affectedClients,
441
+ uptime: ws.connectedAt ? `${((Date.now() - ws.connectedAt) / 1000).toFixed(0)}s` : 'unknown',
442
+ activeSessions: sessionToClientId.size,
443
+ pendingRequests: pendingAttachRequests.size
444
+ });
445
+
446
+ if (ws.pairedClientId) {
447
+ connectionPairs.delete(ws.pairedClientId);
448
+ }
449
+ }
450
+
325
451
  /**
326
452
  * 处理 Chrome 扩展连接
327
453
  */
@@ -687,15 +813,12 @@ function handlePluginConnection(ws, clientInfo) {
687
813
  }
688
814
  });
689
815
 
690
- // 连接关闭
691
816
  ws.on('close', (code, reason) => {
692
- pluginConnections.delete(ws);
693
817
  if (shouldLog('info')) {
694
818
  console.log(`\n[PLUGIN DISCONNECTED] ${id}`);
695
819
  console.log(` - Code: ${code}, Reason: ${reason || 'none'}`);
696
820
  console.log(` - Total plugin connections: ${pluginConnections.size}`);
697
821
  }
698
-
699
822
  logConnectionEvent('PLUGIN_DISCONNECTED', {
700
823
  id,
701
824
  code,
@@ -703,51 +826,7 @@ function handlePluginConnection(ws, clientInfo) {
703
826
  totalPlugins: pluginConnections.size,
704
827
  totalClients: clientConnections.size
705
828
  });
706
-
707
- if (pluginConnections.size === 0) {
708
- updateExtensionState(false);
709
- }
710
-
711
- // 清理配对关系并通知所有受影响的 Client
712
- const affectedClients = [];
713
- clientConnections.forEach(clientWs => {
714
- if (clientWs.pairedPlugin === ws) {
715
- // 清理 page 连接的事件监听器
716
- if (clientWs.pluginMessageHandler) {
717
- ws.off('message', clientWs.pluginMessageHandler);
718
- clientWs.pluginMessageHandler = null;
719
- }
720
- clientWs.pairedPlugin = null;
721
- affectedClients.push(clientWs.id);
722
- if (clientWs.readyState === WebSocket.OPEN) {
723
- clientWs.send(JSON.stringify({
724
- type: 'plugin-disconnected',
725
- message: 'Plugin connection lost'
726
- }));
727
- }
728
- if (shouldLog('debug')) {
729
- console.log(` - Cleared pairedPlugin for client: ${clientWs.id}`);
730
- }
731
- }
732
- });
733
-
734
- logDisconnect('PLUGIN_DISCONNECTED', {
735
- pluginId: id,
736
- code, reason: reason?.toString() || 'none',
737
- remainingPlugins: pluginConnections.size,
738
- affectedClients,
739
- uptime: ws.connectedAt ? `${((Date.now() - ws.connectedAt) / 1000).toFixed(0)}s` : 'unknown',
740
- activeSessions: sessionToClientId.size,
741
- pendingRequests: pendingAttachRequests.size
742
- });
743
-
744
- if (affectedClients.length > 0) {
745
- logConnectionEvent('PLUGIN_DISCONNECT_AFFECTED_CLIENTS', { pluginId: id, affectedClients });
746
- }
747
-
748
- if (ws.pairedClientId) {
749
- connectionPairs.delete(ws.pairedClientId);
750
- }
829
+ cleanupPlugin(ws, id, `close:${code}`);
751
830
  });
752
831
 
753
832
  // 错误处理
@@ -998,111 +1077,24 @@ function handleClientConnection(ws, clientInfo, customClientId = null) {
998
1077
  }
999
1078
  });
1000
1079
 
1001
- // 连接关闭
1002
1080
  ws.on('close', async (code, reason) => {
1003
- // 记录断开事件到日志文件
1004
1081
  logCDP('EVENT', `CLIENT DISCONNECTED id=${id} code=${code} reason=${reason.toString() || 'none'}`);
1005
-
1006
- // 收集该 client 的所有 session
1007
- const sessionsToClean = [];
1008
- for (const [sessionId, clientId] of sessionToClientId.entries()) {
1009
- if (clientId === id) {
1010
- sessionsToClean.push(sessionId);
1011
- sessionToClientId.delete(sessionId);
1012
- }
1013
- }
1014
-
1015
- clientConnections.delete(ws);
1016
- clientById.delete(id);
1017
1082
  if (shouldLog('info')) {
1018
1083
  console.log(`\n[CLIENT DISCONNECTED] ${id}`);
1019
1084
  console.log(` - Code: ${code}, Reason: ${reason || 'none'}`);
1020
- console.log(` - Sessions to clean: ${sessionsToClean.length}`);
1021
- console.log(` - Total client connections: ${clientConnections.size}`);
1022
- }
1023
-
1024
- logConnectionEvent('CLIENT_DISCONNECTED', {
1025
- id,
1026
- code,
1027
- reason: reason?.toString() || 'none',
1028
- sessionsCleaned: sessionsToClean.length,
1029
- totalPlugins: pluginConnections.size,
1030
- totalClients: clientConnections.size
1031
- });
1032
-
1033
- const isUnexpected = code !== 1000 && code !== 1001;
1034
- if (isUnexpected) {
1035
- logDisconnect('CLIENT_DISCONNECTED_UNEXPECTED', {
1036
- clientId: id,
1037
- code, reason: reason?.toString() || 'none',
1038
- sessionsLost: sessionsToClean.length,
1039
- cdpMethodsUsed: ws.cdpTrace ? [...new Set(ws.cdpTrace)] : [],
1040
- uptime: ws.connectedAt ? `${((Date.now() - ws.connectedAt) / 1000).toFixed(0)}s` : 'unknown',
1041
- remainingClients: clientConnections.size,
1042
- pluginAlive: pluginConnections.size > 0,
1043
- pairedPluginId: ws.pairedPlugin?.id || null
1044
- });
1045
- }
1046
-
1047
- if (ws.cdpTrace && ws.cdpTrace.length && shouldLog('debug')) {
1048
- const unique = [...new Set(ws.cdpTrace)];
1049
- console.log(`[CDP TRACE] ${id} methods (${ws.cdpTrace.length}): ${unique.join(', ')}`);
1050
- }
1051
-
1052
- // 向 plugin 发送清理命令
1053
- if (ws.pairedPlugin) {
1054
- safeSend(ws.pairedPlugin, JSON.stringify({
1055
- type: 'client-disconnected',
1056
- clientId: id,
1057
- sessions: sessionsToClean
1058
- }), 'plugin');
1059
- if (shouldLog('debug')) {
1060
- console.log(` - Notified plugin of client disconnect`);
1061
- }
1062
- }
1063
-
1064
- // 广播更新后的客户端列表
1065
- broadcastClientList();
1066
-
1067
- // 清理该 client 的所有映射
1068
- for (const [tId, cId] of targetIdToClientId.entries()) {
1069
- if (cId === id) targetIdToClientId.delete(tId);
1070
- }
1071
- for (const [bcId, cId] of browserContextToClientId.entries()) {
1072
- if (cId === id) browserContextToClientId.delete(bcId);
1073
- }
1074
- if (clientIdToBrowserContext.has(id)) {
1075
- clientIdToBrowserContext.delete(id);
1076
- }
1077
- for (const [gId, mapping] of globalRequestIdMap.entries()) {
1078
- if (mapping.clientId === id) globalRequestIdMap.delete(gId);
1079
1085
  }
1080
-
1081
- // 清理配对关系
1082
- if (ws.pairedPlugin) {
1083
- ws.pairedPlugin.pairedClientId = null;
1084
- }
1085
- connectionPairs.delete(id);
1086
+ cleanupClient(ws, id, `close:${code}`);
1086
1087
  });
1087
1088
 
1088
- // 错误处理
1089
1089
  ws.on('error', (error) => {
1090
1090
  console.error(`[CLIENT ERROR] ${id}:`, error.message);
1091
-
1092
1091
  logConnectionEvent('CLIENT_ERROR', {
1093
1092
  id,
1094
1093
  error: error.message,
1095
1094
  totalPlugins: pluginConnections.size,
1096
1095
  totalClients: clientConnections.size
1097
1096
  });
1098
-
1099
- clientConnections.delete(ws);
1100
- clientById.delete(id);
1101
-
1102
- if (ws.pairedPlugin) {
1103
- ws.pairedPlugin.pairedClientId = null;
1104
- }
1105
- connectionPairs.delete(id);
1097
+ cleanupClient(ws, id, `error:${error.message}`);
1106
1098
  });
1107
1099
  }
1108
1100
 
@@ -1430,29 +1422,13 @@ const heartbeatInterval = setInterval(() => {
1430
1422
  const now = new Date().toISOString();
1431
1423
  const nowMs = Date.now();
1432
1424
 
1433
- // 检查 plugin 连接
1434
1425
  pluginConnections.forEach((ws) => {
1435
1426
  if (!ws.isAlive) {
1436
1427
  if (shouldLog('warn')) {
1437
1428
  console.log(`[${now}] Plugin ${ws.id} not responding, terminating...`);
1438
1429
  }
1439
1430
  logConnectionEvent('HEARTBEAT_TIMEOUT', { type: 'plugin', id: ws.id });
1440
- logDisconnect('HEARTBEAT_TIMEOUT_PLUGIN', {
1441
- pluginId: ws.id,
1442
- pairedClientId: ws.pairedClientId || null,
1443
- uptime: ws.connectedAt ? `${((Date.now() - ws.connectedAt) / 1000).toFixed(0)}s` : 'unknown',
1444
- remainingPlugins: pluginConnections.size,
1445
- activeClients: clientConnections.size,
1446
- activeSessions: sessionToClientId.size
1447
- });
1448
- pluginConnections.delete(ws);
1449
- if (ws.pairedClientId) {
1450
- connectionPairs.delete(ws.pairedClientId);
1451
- const clientWs = clientById.get(ws.pairedClientId);
1452
- if (clientWs) {
1453
- clientWs.pairedPlugin = null;
1454
- }
1455
- }
1431
+ cleanupPlugin(ws, ws.id, 'heartbeat_timeout');
1456
1432
  return ws.terminate();
1457
1433
  }
1458
1434
  ws.isAlive = false;
@@ -1460,29 +1436,16 @@ const heartbeatInterval = setInterval(() => {
1460
1436
  logConnectionEvent('HEARTBEAT_PING', { type: 'plugin', id: ws.id, bufferedAmount: ws.bufferedAmount });
1461
1437
  });
1462
1438
 
1463
- // 检查 client 连接
1464
1439
  clientConnections.forEach((ws) => {
1465
1440
  if (!ws.isAlive) {
1466
1441
  if (shouldLog('warn')) {
1467
1442
  console.log(`[${now}] Client ${ws.id} not responding, terminating...`);
1468
1443
  }
1469
1444
  logConnectionEvent('HEARTBEAT_TIMEOUT', { type: 'client', id: ws.id });
1470
- logDisconnect('HEARTBEAT_TIMEOUT_CLIENT', {
1471
- clientId: ws.id,
1472
- uptime: ws.connectedAt ? `${((Date.now() - ws.connectedAt) / 1000).toFixed(0)}s` : 'unknown',
1473
- remainingClients: clientConnections.size,
1474
- pluginAlive: pluginConnections.size > 0
1475
- });
1476
- clientConnections.delete(ws);
1477
- clientById.delete(ws.id);
1478
- if (ws.pairedPlugin) {
1479
- ws.pairedPlugin.pairedClientId = null;
1480
- }
1481
- connectionPairs.delete(ws.id);
1445
+ cleanupClient(ws, ws.id, 'heartbeat_timeout');
1482
1446
  return ws.terminate();
1483
1447
  }
1484
1448
 
1485
- // 检查空闲超时
1486
1449
  if (ws.lastActivityTime && (nowMs - ws.lastActivityTime > CONFIG.CLIENT_IDLE_TIMEOUT)) {
1487
1450
  const idleSeconds = Math.round((nowMs - ws.lastActivityTime) / 1000);
1488
1451
  if (shouldLog('info')) {
@@ -1512,10 +1475,7 @@ setInterval(() => {
1512
1475
  }
1513
1476
  });
1514
1477
  toRemove.forEach(ws => {
1515
- pluginConnections.delete(ws);
1516
- if (shouldLog('debug')) {
1517
- console.log(`[CLEANUP] Removed zombie plugin: ${ws.id}, state: ${ws.readyState}`);
1518
- }
1478
+ cleanupPlugin(ws, ws.id, 'zombie_cleanup');
1519
1479
  });
1520
1480
 
1521
1481
  toRemove.length = 0;
@@ -1525,11 +1485,7 @@ setInterval(() => {
1525
1485
  }
1526
1486
  });
1527
1487
  toRemove.forEach(ws => {
1528
- clientConnections.delete(ws);
1529
- clientById.delete(ws.id);
1530
- if (shouldLog('debug')) {
1531
- console.log(`[CLEANUP] Removed zombie client: ${ws.id}, state: ${ws.readyState}`);
1532
- }
1488
+ cleanupClient(ws, ws.id, 'zombie_cleanup');
1533
1489
  });
1534
1490
  }, 60000);
1535
1491