cdp-tunnel 1.0.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/.github/workflows/publish.yml +92 -0
- package/.github/workflows/release-assets.yml +50 -0
- package/LICENSE +81 -0
- package/PUBLISH.md +65 -0
- package/README.md +228 -0
- package/cli/guide.html +753 -0
- package/cli/icon.svg +13 -0
- package/cli/icon128.png +0 -0
- package/cli/index.js +357 -0
- package/docs/README_CN.md +204 -0
- package/docs/config-page-screenshot.png +0 -0
- package/extension-new/background.js +294 -0
- package/extension-new/cdp/handler/forward.js +44 -0
- package/extension-new/cdp/handler/local.js +233 -0
- package/extension-new/cdp/handler/special.js +442 -0
- package/extension-new/cdp/index.js +104 -0
- package/extension-new/cdp/response.js +49 -0
- package/extension-new/config-page-preview.html +769 -0
- package/extension-new/config-page.js +318 -0
- package/extension-new/core/debugger.js +310 -0
- package/extension-new/core/state.js +384 -0
- package/extension-new/core/websocket.js +326 -0
- package/extension-new/features/automation-badge.js +113 -0
- package/extension-new/features/screencast.js +221 -0
- package/extension-new/icons/icon128.png +0 -0
- package/extension-new/icons/icon16.png +0 -0
- package/extension-new/icons/icon48.png +0 -0
- package/extension-new/manifest.json +39 -0
- package/extension-new/popup.html +72 -0
- package/extension-new/popup.js +34 -0
- package/extension-new/utils/config.js +20 -0
- package/extension-new/utils/diagnostics.js +560 -0
- package/extension-new/utils/helpers.js +25 -0
- package/extension-new/utils/logger.js +64 -0
- package/package.json +42 -0
- package/server/modules/config.js +28 -0
- package/server/modules/logger.js +197 -0
- package/server/proxy-server.js +1431 -0
- package/tests/playwright-demo.js +45 -0
- package/tests/playwright-interactive.js +261 -0
- package/tests/playwright-multi-demo.js +60 -0
- package/tests/playwright-multi.js +85 -0
- package/tests/playwright-single.js +41 -0
- package/tests/screenshot-config.js +35 -0
- package/tests/test-client.js +89 -0
- package/tests/test-multi-client.js +129 -0
|
@@ -0,0 +1,1431 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket 代理服务器
|
|
3
|
+
* 用于连接 Chrome 扩展 (CDP) 和 Playwright/Puppeteer 客户端
|
|
4
|
+
*
|
|
5
|
+
* 功能:
|
|
6
|
+
* - 单端口 9221,通过路径区分连接类型
|
|
7
|
+
* - /plugin 路径: 接收 Chrome 扩展的 CDP 连接
|
|
8
|
+
* - /client 路径: 接收 Playwright/Puppeteer 客户端连接
|
|
9
|
+
* - 双向透传消息
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const http = require('http');
|
|
13
|
+
const WebSocket = require('ws');
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const os = require('os');
|
|
17
|
+
const { CONFIG, BROWSER_ID, shouldLog } = require('./modules/config');
|
|
18
|
+
const { logCDP, logEvent, clearLog, logStatus, logConnectionEvent, flushAllLogs } = require('./modules/logger');
|
|
19
|
+
|
|
20
|
+
const PORT = CONFIG.PORT;
|
|
21
|
+
const CONFIG_DIR = path.join(os.homedir(), '.cdp-tunnel');
|
|
22
|
+
const EXTENSION_STATE_FILE = path.join(CONFIG_DIR, 'extension-state.json');
|
|
23
|
+
|
|
24
|
+
function updateExtensionState(connected) {
|
|
25
|
+
try {
|
|
26
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
27
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
28
|
+
}
|
|
29
|
+
fs.writeFileSync(EXTENSION_STATE_FILE, JSON.stringify({
|
|
30
|
+
connected: connected,
|
|
31
|
+
lastSeen: Date.now()
|
|
32
|
+
}));
|
|
33
|
+
} catch (e) {}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
clearLog();
|
|
37
|
+
|
|
38
|
+
const wss = new WebSocket.Server({ noServer: true });
|
|
39
|
+
const server = http.createServer((req, res) => handleHttpRequest(req, res));
|
|
40
|
+
|
|
41
|
+
const pluginConnections = new Set();
|
|
42
|
+
const clientConnections = new Set();
|
|
43
|
+
|
|
44
|
+
const connectionPairs = new Map();
|
|
45
|
+
const clientById = new Map();
|
|
46
|
+
const sessionToClientId = new Map();
|
|
47
|
+
const pendingAttachRequests = new Map();
|
|
48
|
+
const clientIdToPlugin = new Map();
|
|
49
|
+
const globalRequestIdMap = new Map();
|
|
50
|
+
const targetIdToClientId = new Map();
|
|
51
|
+
const pendingAttachedEvents = new Map();
|
|
52
|
+
const browserContextToClientId = new Map();
|
|
53
|
+
let globalRequestIdCounter = 0;
|
|
54
|
+
|
|
55
|
+
let cachedTargets = [];
|
|
56
|
+
let lastTargetsUpdate = 0;
|
|
57
|
+
|
|
58
|
+
console.log('='.repeat(60));
|
|
59
|
+
console.log(' WebSocket CDP Proxy Server');
|
|
60
|
+
console.log('='.repeat(60));
|
|
61
|
+
console.log(` Server started on port ${PORT}`);
|
|
62
|
+
console.log(` - Plugin path: ws://localhost:${PORT}/plugin`);
|
|
63
|
+
console.log(` - Client path: ws://localhost:${PORT}/client`);
|
|
64
|
+
console.log(` - CDP endpoint: http://localhost:${PORT}`);
|
|
65
|
+
console.log('='.repeat(60));
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 获取请求的 Host
|
|
69
|
+
*/
|
|
70
|
+
function getHost(req) {
|
|
71
|
+
return req.headers.host || `localhost:${PORT}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 生成 WebSocket 调试地址
|
|
76
|
+
*/
|
|
77
|
+
function buildWebSocketDebuggerUrl(req) {
|
|
78
|
+
return `ws://${getHost(req)}/devtools/browser/${BROWSER_ID}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function buildTargetWebSocketUrl(req, targetId) {
|
|
82
|
+
return `ws://${getHost(req)}/devtools/page/${targetId}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function requestTargetsFromPlugin() {
|
|
86
|
+
const now = Date.now();
|
|
87
|
+
if (now - lastTargetsUpdate < CONFIG.TARGETS_CACHE_TTL && cachedTargets.length > 0) {
|
|
88
|
+
return cachedTargets;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const plugin = pluginConnections.values().next().value;
|
|
92
|
+
if (!plugin || plugin.readyState !== WebSocket.OPEN) {
|
|
93
|
+
return cachedTargets;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return new Promise((resolve) => {
|
|
97
|
+
const requestId = `targets_${Date.now()}`;
|
|
98
|
+
const timeout = setTimeout(() => {
|
|
99
|
+
resolve(cachedTargets);
|
|
100
|
+
}, CONFIG.TARGETS_REQUEST_TIMEOUT);
|
|
101
|
+
|
|
102
|
+
const handler = (data) => {
|
|
103
|
+
try {
|
|
104
|
+
const msg = JSON.parse(data.toString());
|
|
105
|
+
if (msg.id === requestId && msg.result?.targetInfos) {
|
|
106
|
+
clearTimeout(timeout);
|
|
107
|
+
plugin.off('message', handler);
|
|
108
|
+
cachedTargets = msg.result.targetInfos;
|
|
109
|
+
lastTargetsUpdate = now;
|
|
110
|
+
resolve(cachedTargets);
|
|
111
|
+
}
|
|
112
|
+
} catch (e) {}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
plugin.on('message', handler);
|
|
116
|
+
plugin.send(JSON.stringify({ id: requestId, method: 'Target.getTargets' }));
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 处理 HTTP 请求
|
|
122
|
+
*/
|
|
123
|
+
async function handleHttpRequest(req, res) {
|
|
124
|
+
const url = new URL(req.url, `http://localhost:${PORT}`);
|
|
125
|
+
|
|
126
|
+
if (url.pathname === '/json/version' || url.pathname === '/json/version/') {
|
|
127
|
+
const payload = {
|
|
128
|
+
Browser: 'CDP Bridge',
|
|
129
|
+
'Protocol-Version': '1.3',
|
|
130
|
+
'User-Agent': 'Chrome',
|
|
131
|
+
'V8-Version': '',
|
|
132
|
+
webSocketDebuggerUrl: buildWebSocketDebuggerUrl(req)
|
|
133
|
+
};
|
|
134
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
135
|
+
res.end(JSON.stringify(payload));
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (url.pathname === '/json' || url.pathname === '/json/' ||
|
|
140
|
+
url.pathname === '/json/list' || url.pathname === '/json/list/') {
|
|
141
|
+
const targets = await requestTargetsFromPlugin();
|
|
142
|
+
const targetList = targets
|
|
143
|
+
.filter(t => {
|
|
144
|
+
if (t.type !== 'page') return false;
|
|
145
|
+
const url = t.url || '';
|
|
146
|
+
if (url.startsWith('chrome://') ||
|
|
147
|
+
url.startsWith('chrome-extension://') ||
|
|
148
|
+
url.startsWith('devtools://') ||
|
|
149
|
+
url.startsWith('about:blank') ||
|
|
150
|
+
url.startsWith('edge://')) {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
return true;
|
|
154
|
+
})
|
|
155
|
+
.map(t => ({
|
|
156
|
+
description: '',
|
|
157
|
+
devtoolsFrontendUrl: '',
|
|
158
|
+
id: t.targetId,
|
|
159
|
+
title: t.title || '',
|
|
160
|
+
type: t.type,
|
|
161
|
+
url: t.url || '',
|
|
162
|
+
webSocketDebuggerUrl: buildTargetWebSocketUrl(req, t.targetId)
|
|
163
|
+
}));
|
|
164
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
165
|
+
res.end(JSON.stringify(targetList));
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
170
|
+
res.end('Not Found');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* 根据 upgrade 请求的 URL 路径区分连接类型
|
|
175
|
+
*/
|
|
176
|
+
server.on('upgrade', (req, socket, head) => {
|
|
177
|
+
const url = new URL(req.url, `http://localhost:${PORT}`);
|
|
178
|
+
const path = url.pathname;
|
|
179
|
+
const isPlugin = path === '/plugin';
|
|
180
|
+
const isClient = path === '/client' ||
|
|
181
|
+
path.startsWith('/client-') ||
|
|
182
|
+
path.startsWith('/devtools/browser/') ||
|
|
183
|
+
path.startsWith('/devtools/page/');
|
|
184
|
+
|
|
185
|
+
if (!isPlugin && !isClient) {
|
|
186
|
+
socket.destroy();
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
191
|
+
wss.emit('connection', ws, req);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
wss.on('connection', (ws, req) => {
|
|
196
|
+
const url = new URL(req.url, `http://localhost:${PORT}`);
|
|
197
|
+
const path = url.pathname;
|
|
198
|
+
|
|
199
|
+
const clientInfo = {
|
|
200
|
+
ip: req.socket.remoteAddress,
|
|
201
|
+
port: req.socket.remotePort
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
if (path === '/plugin') {
|
|
205
|
+
handlePluginConnection(ws, clientInfo);
|
|
206
|
+
} else if (path === '/client' || path.startsWith('/client-') || path.startsWith('/devtools/browser/')) {
|
|
207
|
+
const customClientId = path.startsWith('/client-') ? path.replace('/client-', '') : null;
|
|
208
|
+
handleClientConnection(ws, clientInfo, customClientId);
|
|
209
|
+
} else if (path.startsWith('/devtools/page/')) {
|
|
210
|
+
const targetId = path.replace('/devtools/page/', '');
|
|
211
|
+
handlePageConnection(ws, clientInfo, targetId);
|
|
212
|
+
} else {
|
|
213
|
+
console.log(`[REJECTED] Unknown path: ${path} from ${clientInfo.ip}:${clientInfo.port}`);
|
|
214
|
+
ws.close(1008, 'Invalid path. Use /plugin or /client');
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* 处理 Chrome 扩展连接
|
|
220
|
+
*/
|
|
221
|
+
function handlePluginConnection(ws, clientInfo) {
|
|
222
|
+
const id = generateId('plugin');
|
|
223
|
+
|
|
224
|
+
if (pluginConnections.size > 0) {
|
|
225
|
+
const toRemove = [];
|
|
226
|
+
pluginConnections.forEach(oldWs => {
|
|
227
|
+
if (oldWs !== ws) {
|
|
228
|
+
if (oldWs.readyState === WebSocket.OPEN) {
|
|
229
|
+
oldWs.send(JSON.stringify({ type: 'server-restart' }));
|
|
230
|
+
oldWs.close(1001, 'Server restarted');
|
|
231
|
+
}
|
|
232
|
+
toRemove.push(oldWs);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
toRemove.forEach(oldWs => {
|
|
236
|
+
pluginConnections.delete(oldWs);
|
|
237
|
+
if (shouldLog('info')) {
|
|
238
|
+
console.log(`[PLUGIN] Removed old connection: ${oldWs.id}`);
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
sessionToClientId.clear();
|
|
244
|
+
pendingAttachRequests.clear();
|
|
245
|
+
connectionPairs.clear();
|
|
246
|
+
clientConnections.forEach(clientWs => {
|
|
247
|
+
clientWs.pairedPlugin = null;
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
pluginConnections.add(ws);
|
|
251
|
+
|
|
252
|
+
const pluginType = 'plugin';
|
|
253
|
+
|
|
254
|
+
if (shouldLog('info')) {
|
|
255
|
+
console.log(`\n[PLUGIN CONNECTED] ID: ${id}`);
|
|
256
|
+
console.log(` - Remote: ${clientInfo.ip}:${clientInfo.port}`);
|
|
257
|
+
console.log(` - Total plugin connections: ${pluginConnections.size}`);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
logConnectionEvent('PLUGIN_CONNECTED', {
|
|
261
|
+
id,
|
|
262
|
+
ip: clientInfo.ip,
|
|
263
|
+
port: clientInfo.port,
|
|
264
|
+
totalPlugins: pluginConnections.size,
|
|
265
|
+
totalClients: clientConnections.size
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
updateExtensionState(true);
|
|
269
|
+
|
|
270
|
+
// 如果有待配对的客户端,自动配对
|
|
271
|
+
if (clientConnections.size > 0) {
|
|
272
|
+
for (const clientWs of clientConnections) {
|
|
273
|
+
if (!connectionPairs.has(clientWs.id)) {
|
|
274
|
+
connectionPairs.set(clientWs.id, ws);
|
|
275
|
+
ws.pairedClientId = clientWs.id;
|
|
276
|
+
clientWs.pairedPlugin = ws;
|
|
277
|
+
if (shouldLog('info')) {
|
|
278
|
+
console.log(` - Paired with client: ${clientWs.id}`);
|
|
279
|
+
}
|
|
280
|
+
logConnectionEvent('PLUGIN_PAIRED', { pluginId: id, clientId: clientWs.id });
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
ws.id = id;
|
|
287
|
+
ws.isAlive = true;
|
|
288
|
+
ws.pluginType = pluginType;
|
|
289
|
+
|
|
290
|
+
// 发送当前客户端列表给新连接的插件
|
|
291
|
+
const clients = [];
|
|
292
|
+
clientConnections.forEach((client) => {
|
|
293
|
+
clients.push({
|
|
294
|
+
id: client.id,
|
|
295
|
+
connectedAt: client.connectedAt,
|
|
296
|
+
lastActivity: client.lastActivityTime
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
ws.send(JSON.stringify({
|
|
300
|
+
type: 'client-list',
|
|
301
|
+
clients: clients
|
|
302
|
+
}));
|
|
303
|
+
|
|
304
|
+
// 心跳检测
|
|
305
|
+
ws.on('pong', () => {
|
|
306
|
+
ws.isAlive = true;
|
|
307
|
+
logConnectionEvent('HEARTBEAT_PONG', { type: 'plugin', id: ws.id });
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// 消息转发: Plugin -> Client
|
|
311
|
+
ws.on('message', (data) => {
|
|
312
|
+
const messageSize = data.length;
|
|
313
|
+
let messagePreview;
|
|
314
|
+
let parsed;
|
|
315
|
+
try {
|
|
316
|
+
parsed = JSON.parse(data);
|
|
317
|
+
messagePreview = parsed.method || parsed.id || 'response';
|
|
318
|
+
} catch {
|
|
319
|
+
parsed = null;
|
|
320
|
+
messagePreview = `binary (${messageSize} bytes)`;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (shouldLog('debug')) {
|
|
324
|
+
console.log(`[PLUGIN -> CLIENT] ${id}: ${messagePreview}`);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// 处理 keepalive 消息
|
|
328
|
+
if (parsed && parsed.type === 'keepalive') {
|
|
329
|
+
ws.isAlive = true;
|
|
330
|
+
logConnectionEvent('KEEPALIVE_RECEIVED', { type: 'plugin', id: ws.id });
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// 记录所有 PLUGIN -> CLIENT 消息到日志文件
|
|
335
|
+
logCDP('PLUGIN -> CLIENT', data.toString().substring(0, CONFIG.LOG_MESSAGE_PREVIEW_LENGTH), parsed?.sessionId, ws.pluginType);
|
|
336
|
+
|
|
337
|
+
// 调试:打印所有收到的消息
|
|
338
|
+
console.log(`[PLUGIN MSG] id=${parsed?.id} method=${parsed?.method || 'none'} type=${parsed?.type || 'none'} sessionId=${parsed?.sessionId?.substring(0,8) || 'none'}`);
|
|
339
|
+
|
|
340
|
+
// 处理 type: 'event' 消息(来自 background.js 的 screencast 等事件)
|
|
341
|
+
if (parsed && parsed.type === 'event' && parsed.method) {
|
|
342
|
+
logCDP('DEBUG', `Converting type:event message: ${parsed.method}`, parsed?.sessionId);
|
|
343
|
+
|
|
344
|
+
// 处理 Target.attachedToTarget 事件,建立 sessionId -> clientId 映射
|
|
345
|
+
if (parsed.method === 'Target.attachedToTarget') {
|
|
346
|
+
const targetId = parsed.params?.targetInfo?.targetId;
|
|
347
|
+
const sessionId = parsed.params?.sessionId;
|
|
348
|
+
|
|
349
|
+
console.log(`[ATTACHED EVENT (type:event)] targetId=${targetId} sessionId=${sessionId?.substring(0,8)}`);
|
|
350
|
+
|
|
351
|
+
// 查找 targetId 对应的 clientId
|
|
352
|
+
const clientId = targetIdToClientId.get(targetId);
|
|
353
|
+
if (clientId && sessionId) {
|
|
354
|
+
sessionToClientId.set(sessionId, clientId);
|
|
355
|
+
console.log(`[SESSION MAPPED from event] sessionId=${sessionId.substring(0,8)} -> clientId=${clientId} (targetId=${targetId})`);
|
|
356
|
+
targetIdToClientId.delete(targetId);
|
|
357
|
+
|
|
358
|
+
// 转换为 CDP 格式并发送给对应的客户端
|
|
359
|
+
const cdpMsg = {
|
|
360
|
+
method: parsed.method,
|
|
361
|
+
params: parsed.params
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
const clientWs = clientById.get(clientId);
|
|
365
|
+
if (clientWs && clientWs.readyState === WebSocket.OPEN) {
|
|
366
|
+
clientWs.send(JSON.stringify(cdpMsg));
|
|
367
|
+
console.log(`[ATTACHED EVENT] Sent to client: ${clientId}`);
|
|
368
|
+
}
|
|
369
|
+
return;
|
|
370
|
+
} else if (targetId && sessionId) {
|
|
371
|
+
// targetId 还没有映射,缓存事件等待 Target.createTarget 响应
|
|
372
|
+
pendingAttachedEvents.set(targetId, { sessionId, parsed, data });
|
|
373
|
+
console.log(`[ATTACHED EVENT] Cached for targetId=${targetId}, waiting for createTarget response`);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const cdpMsg = {
|
|
379
|
+
method: parsed.method,
|
|
380
|
+
params: parsed.params
|
|
381
|
+
};
|
|
382
|
+
if (parsed.sessionId) {
|
|
383
|
+
cdpMsg.sessionId = parsed.sessionId;
|
|
384
|
+
}
|
|
385
|
+
const cdpData = JSON.stringify(cdpMsg);
|
|
386
|
+
|
|
387
|
+
// 发送给配对的 client
|
|
388
|
+
if (ws.pairedClientId) {
|
|
389
|
+
const clientWs = clientById.get(ws.pairedClientId);
|
|
390
|
+
if (safeSend(clientWs, cdpData, 'client')) {
|
|
391
|
+
logCDP('DEBUG', `Sent converted event to client: ${parsed.method}`, parsed?.sessionId);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
// 广播给所有 client
|
|
396
|
+
broadcastToClients(cdpData, ws);
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (parsed && parsed.id === undefined && !parsed.method) {
|
|
401
|
+
logCDP('DEBUG', `BLOCKED message (no id, no method): ${JSON.stringify(parsed).substring(0, 100)}`, null);
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// 路由消息到正确的客户端
|
|
406
|
+
// 优先级:请求ID路由 > Target.attachedToTarget事件 > sessionId路由 > 广播到所有客户端
|
|
407
|
+
|
|
408
|
+
// 1. 请求 ID 路由:响应对应特定客户端的请求(优先级最高)
|
|
409
|
+
// 响应消息有 id,可能也有 sessionId,但应该用 id 路由
|
|
410
|
+
if (parsed && parsed.id !== undefined) {
|
|
411
|
+
const globalId = parsed.id;
|
|
412
|
+
const mapping = globalRequestIdMap.get(globalId);
|
|
413
|
+
console.log(`[RESPONSE DEBUG] globalId=${globalId} hasMapping=${!!mapping} sessionId=${parsed.sessionId?.substring(0,8) || 'none'} method=${parsed.method || 'response'}`);
|
|
414
|
+
if (mapping) {
|
|
415
|
+
const clientWs = clientById.get(mapping.clientId);
|
|
416
|
+
if (clientWs && clientWs.readyState === WebSocket.OPEN) {
|
|
417
|
+
// 如果是 Target.createBrowserContext 响应,记录 browserContextId -> clientId 映射
|
|
418
|
+
if (mapping.isCreateBrowserContext && parsed.result?.browserContextId) {
|
|
419
|
+
const browserContextId = parsed.result.browserContextId;
|
|
420
|
+
browserContextToClientId.set(browserContextId, mapping.clientId);
|
|
421
|
+
console.log(`[BROWSER CONTEXT MAPPED] browserContextId=${browserContextId} -> clientId=${mapping.clientId}`);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// 如果是 Target.createTarget 响应,先发送缓存的 Target.attachedToTarget 事件
|
|
425
|
+
// 然后再发送响应
|
|
426
|
+
if (mapping.isCreateTarget && parsed.result?.targetId) {
|
|
427
|
+
const targetId = parsed.result.targetId;
|
|
428
|
+
targetIdToClientId.set(targetId, mapping.clientId);
|
|
429
|
+
console.log(`[TARGET MAPPED] targetId=${targetId} -> clientId=${mapping.clientId}`);
|
|
430
|
+
|
|
431
|
+
// 检查是否有缓存的 Target.attachedToTarget 事件
|
|
432
|
+
const cachedEvent = pendingAttachedEvents.get(targetId);
|
|
433
|
+
if (cachedEvent) {
|
|
434
|
+
sessionToClientId.set(cachedEvent.sessionId, mapping.clientId);
|
|
435
|
+
console.log(`[SESSION MAPPED from cached] sessionId=${cachedEvent.sessionId.substring(0,8)} -> clientId=${mapping.clientId} (targetId=${targetId})`);
|
|
436
|
+
pendingAttachedEvents.delete(targetId);
|
|
437
|
+
|
|
438
|
+
// 先发送缓存的事件给客户端
|
|
439
|
+
// 注意:Target.attachedToTarget 事件必须发送给 root session(没有顶层 sessionId)
|
|
440
|
+
// sessionId 在 params 里面,不在消息顶层
|
|
441
|
+
const cdpMsg = {
|
|
442
|
+
method: cachedEvent.parsed.method,
|
|
443
|
+
params: cachedEvent.parsed.params
|
|
444
|
+
};
|
|
445
|
+
const msgStr = JSON.stringify(cdpMsg);
|
|
446
|
+
console.log(`[ATTACHED EVENT] Full message: ${msgStr}`);
|
|
447
|
+
clientWs.send(msgStr);
|
|
448
|
+
console.log(`[ATTACHED EVENT] Sent cached event to client: ${mapping.clientId}`);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// 然后发送响应给客户端
|
|
453
|
+
const originalId = mapping.originalId;
|
|
454
|
+
parsed.id = originalId;
|
|
455
|
+
// 如果请求有 sessionId,但响应没有,添加 sessionId
|
|
456
|
+
if (mapping.sessionId && !parsed.sessionId) {
|
|
457
|
+
parsed.sessionId = mapping.sessionId;
|
|
458
|
+
}
|
|
459
|
+
const responseStr = JSON.stringify(parsed);
|
|
460
|
+
console.log(`[SEND TO CLIENT] ${responseStr.substring(0, 300)}`);
|
|
461
|
+
clientWs.send(responseStr);
|
|
462
|
+
console.log(`[ROUTE] Response global=${globalId} -> original=${originalId} -> client=${mapping.clientId} sessionId=${parsed.sessionId?.substring(0,8) || 'none'}`);
|
|
463
|
+
}
|
|
464
|
+
globalRequestIdMap.delete(globalId);
|
|
465
|
+
} else {
|
|
466
|
+
console.log(`[WARN] No mapping for global requestId: ${globalId}`);
|
|
467
|
+
}
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// 2. sessionId 路由:消息属于特定 session(事件,没有 id)
|
|
472
|
+
if (parsed && parsed.sessionId) {
|
|
473
|
+
const targetClientId = sessionToClientId.get(parsed.sessionId);
|
|
474
|
+
console.log(`[SESSION ROUTE] sessionId=${parsed.sessionId?.substring(0,8)} -> clientId=${targetClientId || 'not found'}`);
|
|
475
|
+
if (targetClientId) {
|
|
476
|
+
const clientWs = clientById.get(targetClientId);
|
|
477
|
+
if (clientWs && clientWs.readyState === WebSocket.OPEN) {
|
|
478
|
+
clientWs.send(data);
|
|
479
|
+
logCDP('DEBUG', `FORWARDED to client: ${targetClientId} (sessionId route)`, parsed?.sessionId);
|
|
480
|
+
}
|
|
481
|
+
} else {
|
|
482
|
+
console.log(`[WARN] No clientId for sessionId: ${parsed.sessionId?.substring(0, 8)}`);
|
|
483
|
+
}
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// 3. 事件广播:无 id 和 sessionId 的消息(如 Target.targetCreated)
|
|
488
|
+
// 只广播特定类型的事件,避免干扰其他客户端
|
|
489
|
+
if (parsed && parsed.method) {
|
|
490
|
+
// 处理 Target.attachedToTarget 事件,建立 sessionId -> clientId 映射
|
|
491
|
+
if (parsed.method === 'Target.attachedToTarget') {
|
|
492
|
+
const targetId = parsed.params?.targetInfo?.targetId;
|
|
493
|
+
const sessionId = parsed.params?.sessionId;
|
|
494
|
+
const openerId = parsed.params?.targetInfo?.openerId;
|
|
495
|
+
|
|
496
|
+
// 查找 targetId 对应的 clientId
|
|
497
|
+
let clientId = targetIdToClientId.get(targetId);
|
|
498
|
+
|
|
499
|
+
// 如果没有直接映射,检查 openerId(window.open 打开的新 tab)
|
|
500
|
+
if (!clientId && openerId) {
|
|
501
|
+
// 查找 openerId 对应的 clientId
|
|
502
|
+
// openerId 可能是某个已知的 targetId
|
|
503
|
+
clientId = targetIdToClientId.get(openerId);
|
|
504
|
+
if (clientId) {
|
|
505
|
+
console.log(`[OPENER TRACKING] targetId=${targetId?.substring(0,8)} openerId=${openerId?.substring(0,8)} -> clientId=${clientId}`);
|
|
506
|
+
// 记录新 targetId 的映射
|
|
507
|
+
targetIdToClientId.set(targetId, clientId);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (clientId && sessionId) {
|
|
512
|
+
sessionToClientId.set(sessionId, clientId);
|
|
513
|
+
console.log(`[SESSION MAPPED from event] sessionId=${sessionId.substring(0,8)} -> clientId=${clientId} (targetId=${targetId?.substring(0,8)})`);
|
|
514
|
+
targetIdToClientId.delete(targetId);
|
|
515
|
+
|
|
516
|
+
// 只发送给对应的客户端
|
|
517
|
+
const clientWs = clientById.get(clientId);
|
|
518
|
+
if (clientWs && clientWs.readyState === WebSocket.OPEN) {
|
|
519
|
+
clientWs.send(data);
|
|
520
|
+
}
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// 处理 Target.targetInfoChanged 事件
|
|
526
|
+
if (parsed.method === 'Target.targetInfoChanged') {
|
|
527
|
+
const targetId = parsed.params?.targetInfo?.targetId;
|
|
528
|
+
const openerId = parsed.params?.targetInfo?.openerId;
|
|
529
|
+
console.log(`[TARGET INFO CHANGED] targetId=${targetId?.substring(0,8)} openerId=${openerId?.substring(0, 8) || 'none'}`);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
if (parsed.method === 'Target.targetCreated') {
|
|
533
|
+
const targetId = parsed.params?.targetInfo?.targetId;
|
|
534
|
+
const openerId = parsed.params?.targetInfo?.openerId;
|
|
535
|
+
const browserContextId = parsed.params?.targetInfo?.browserContextId;
|
|
536
|
+
const targetType = parsed.params?.targetInfo?.type;
|
|
537
|
+
|
|
538
|
+
console.log(`[TARGET CREATED] targetId=${targetId?.substring(0,8)} type=${targetType} openerId=${openerId?.substring(0, 8) || 'none'} browserContextId=${browserContextId?.substring(0, 8) || 'none'}`);
|
|
539
|
+
|
|
540
|
+
// 如果有 openerId,尝试找到对应的 clientId
|
|
541
|
+
if (openerId && targetId) {
|
|
542
|
+
const openerClientId = targetIdToClientId.get(openerId);
|
|
543
|
+
if (openerClientId) {
|
|
544
|
+
targetIdToClientId.set(targetId, openerClientId);
|
|
545
|
+
console.log(`[TARGET CREATED with opener] targetId=${targetId?.substring(0,8)} openerId=${openerId?.substring(0,8)} -> clientId=${openerClientId}`);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// 如果有 browserContextId,尝试找到对应的 clientId
|
|
550
|
+
// browserContextId 是通过 Target.createBrowserContext 创建的
|
|
551
|
+
if (browserContextId && targetId) {
|
|
552
|
+
const contextClientId = browserContextToClientId.get(browserContextId);
|
|
553
|
+
if (contextClientId && !targetIdToClientId.has(targetId)) {
|
|
554
|
+
targetIdToClientId.set(targetId, contextClientId);
|
|
555
|
+
console.log(`[TARGET CREATED in context] targetId=${targetId?.substring(0,8)} browserContextId=${browserContextId?.substring(0,8)} -> clientId=${contextClientId}`);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Service Worker 处理:Service Worker 通常属于创建它的页面所在的客户端
|
|
560
|
+
// 通过 browserContextId 来判断归属
|
|
561
|
+
if (targetType === 'service_worker' && browserContextId && targetId) {
|
|
562
|
+
const contextClientId = browserContextToClientId.get(browserContextId);
|
|
563
|
+
if (contextClientId) {
|
|
564
|
+
targetIdToClientId.set(targetId, contextClientId);
|
|
565
|
+
console.log(`[SERVICE WORKER] targetId=${targetId?.substring(0,8)} -> clientId=${contextClientId}`);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// iframe (OOPIF) 处理:跨域 iframe 可能有独立的 target
|
|
570
|
+
// 通过 openerId 或 browserContextId 来判断归属
|
|
571
|
+
if (targetType === 'iframe' && targetId) {
|
|
572
|
+
// 优先使用 openerId
|
|
573
|
+
if (openerId) {
|
|
574
|
+
const openerClientId = targetIdToClientId.get(openerId);
|
|
575
|
+
if (openerClientId) {
|
|
576
|
+
targetIdToClientId.set(targetId, openerClientId);
|
|
577
|
+
console.log(`[IFRAME with opener] targetId=${targetId?.substring(0,8)} openerId=${openerId?.substring(0,8)} -> clientId=${openerClientId}`);
|
|
578
|
+
}
|
|
579
|
+
} else if (browserContextId) {
|
|
580
|
+
// 否则使用 browserContextId
|
|
581
|
+
const contextClientId = browserContextToClientId.get(browserContextId);
|
|
582
|
+
if (contextClientId) {
|
|
583
|
+
targetIdToClientId.set(targetId, contextClientId);
|
|
584
|
+
console.log(`[IFRAME in context] targetId=${targetId?.substring(0,8)} browserContextId=${browserContextId?.substring(0,8)} -> clientId=${contextClientId}`);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const broadcastMethods = [
|
|
591
|
+
'Target.targetCreated',
|
|
592
|
+
'Target.targetDestroyed',
|
|
593
|
+
'Target.targetInfoChanged'
|
|
594
|
+
];
|
|
595
|
+
if (broadcastMethods.includes(parsed.method)) {
|
|
596
|
+
for (const clientWs of clientConnections) {
|
|
597
|
+
if (clientWs.readyState === WebSocket.OPEN) {
|
|
598
|
+
clientWs.send(data);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
// 连接关闭
|
|
606
|
+
ws.on('close', (code, reason) => {
|
|
607
|
+
pluginConnections.delete(ws);
|
|
608
|
+
if (shouldLog('info')) {
|
|
609
|
+
console.log(`\n[PLUGIN DISCONNECTED] ${id}`);
|
|
610
|
+
console.log(` - Code: ${code}, Reason: ${reason || 'none'}`);
|
|
611
|
+
console.log(` - Total plugin connections: ${pluginConnections.size}`);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
logConnectionEvent('PLUGIN_DISCONNECTED', {
|
|
615
|
+
id,
|
|
616
|
+
code,
|
|
617
|
+
reason: reason?.toString() || 'none',
|
|
618
|
+
totalPlugins: pluginConnections.size,
|
|
619
|
+
totalClients: clientConnections.size
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
if (pluginConnections.size === 0) {
|
|
623
|
+
updateExtensionState(false);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// 清理配对关系并通知所有受影响的 Client
|
|
627
|
+
const affectedClients = [];
|
|
628
|
+
clientConnections.forEach(clientWs => {
|
|
629
|
+
if (clientWs.pairedPlugin === ws) {
|
|
630
|
+
// 清理 page 连接的事件监听器
|
|
631
|
+
if (clientWs.pluginMessageHandler) {
|
|
632
|
+
ws.off('message', clientWs.pluginMessageHandler);
|
|
633
|
+
clientWs.pluginMessageHandler = null;
|
|
634
|
+
}
|
|
635
|
+
clientWs.pairedPlugin = null;
|
|
636
|
+
affectedClients.push(clientWs.id);
|
|
637
|
+
if (clientWs.readyState === WebSocket.OPEN) {
|
|
638
|
+
clientWs.send(JSON.stringify({
|
|
639
|
+
type: 'plugin-disconnected',
|
|
640
|
+
message: 'Plugin connection lost'
|
|
641
|
+
}));
|
|
642
|
+
}
|
|
643
|
+
if (shouldLog('debug')) {
|
|
644
|
+
console.log(` - Cleared pairedPlugin for client: ${clientWs.id}`);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
if (affectedClients.length > 0) {
|
|
650
|
+
logConnectionEvent('PLUGIN_DISCONNECT_AFFECTED_CLIENTS', { pluginId: id, affectedClients });
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
if (ws.pairedClientId) {
|
|
654
|
+
connectionPairs.delete(ws.pairedClientId);
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
// 错误处理
|
|
659
|
+
ws.on('error', (error) => {
|
|
660
|
+
console.error(`[PLUGIN ERROR] ${id}:`, error.message);
|
|
661
|
+
|
|
662
|
+
logConnectionEvent('PLUGIN_ERROR', {
|
|
663
|
+
id,
|
|
664
|
+
error: error.message,
|
|
665
|
+
totalPlugins: pluginConnections.size,
|
|
666
|
+
totalClients: clientConnections.size
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
pluginConnections.delete(ws);
|
|
670
|
+
|
|
671
|
+
clientConnections.forEach(clientWs => {
|
|
672
|
+
if (clientWs.pairedPlugin === ws) {
|
|
673
|
+
// 清理 page 连接的事件监听器
|
|
674
|
+
if (clientWs.pluginMessageHandler) {
|
|
675
|
+
ws.off('message', clientWs.pluginMessageHandler);
|
|
676
|
+
clientWs.pluginMessageHandler = null;
|
|
677
|
+
}
|
|
678
|
+
clientWs.pairedPlugin = null;
|
|
679
|
+
console.log(` - Cleared pairedPlugin for client: ${clientWs.id} due to error`);
|
|
680
|
+
}
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
if (ws.pairedClientId) {
|
|
684
|
+
connectionPairs.delete(ws.pairedClientId);
|
|
685
|
+
}
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
ws.send(JSON.stringify({
|
|
689
|
+
type: 'connected',
|
|
690
|
+
role: 'plugin',
|
|
691
|
+
id: id,
|
|
692
|
+
fresh: true,
|
|
693
|
+
timestamp: Date.now()
|
|
694
|
+
}));
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* 处理 CDP 客户端连接 (Playwright/Puppeteer)
|
|
699
|
+
*/
|
|
700
|
+
function handleClientConnection(ws, clientInfo, customClientId = null) {
|
|
701
|
+
clientConnections.add(ws);
|
|
702
|
+
const id = customClientId || generateId('client');
|
|
703
|
+
if (shouldLog('info')) {
|
|
704
|
+
console.log(`\n[CLIENT CONNECTED] ID: ${id}${customClientId ? ' (custom)' : ''}`);
|
|
705
|
+
console.log(` - Remote: ${clientInfo.ip}:${clientInfo.port}`);
|
|
706
|
+
console.log(` - Total client connections: ${clientConnections.size}`);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
logConnectionEvent('CLIENT_CONNECTED', {
|
|
710
|
+
id,
|
|
711
|
+
ip: clientInfo.ip,
|
|
712
|
+
port: clientInfo.port,
|
|
713
|
+
totalPlugins: pluginConnections.size,
|
|
714
|
+
totalClients: clientConnections.size
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
// 检查是否有可用的 plugin 连接
|
|
718
|
+
if (pluginConnections.size === 0) {
|
|
719
|
+
if (shouldLog('warn')) {
|
|
720
|
+
console.log(` - WARNING: No plugin connections available!`);
|
|
721
|
+
console.log(` - Please ensure Chrome extension is connected.`);
|
|
722
|
+
}
|
|
723
|
+
logConnectionEvent('CLIENT_NO_PLUGIN', { clientId: id });
|
|
724
|
+
} else {
|
|
725
|
+
// 多客户端模式: 所有客户端共享同一个 plugin
|
|
726
|
+
// 每个 clientId 对应不同的 tab
|
|
727
|
+
const pluginWs = pluginConnections.values().next().value;
|
|
728
|
+
if (pluginWs) {
|
|
729
|
+
connectionPairs.set(id, pluginWs);
|
|
730
|
+
ws.pairedPlugin = pluginWs;
|
|
731
|
+
clientIdToPlugin.set(id, pluginWs);
|
|
732
|
+
|
|
733
|
+
if (shouldLog('info')) {
|
|
734
|
+
console.log(` - Paired with plugin: ${pluginWs.id} (shared mode)`);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
logConnectionEvent('CLIENT_PAIRED', { clientId: id, pluginId: pluginWs.id });
|
|
738
|
+
|
|
739
|
+
// 通知 Plugin 新客户端已连接
|
|
740
|
+
pluginWs.send(JSON.stringify({
|
|
741
|
+
type: 'client-connected',
|
|
742
|
+
clientId: id
|
|
743
|
+
}));
|
|
744
|
+
|
|
745
|
+
// 发送当前所有客户端列表
|
|
746
|
+
broadcastClientList();
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
ws.id = id;
|
|
751
|
+
ws.isAlive = true;
|
|
752
|
+
ws.cdpTrace = [];
|
|
753
|
+
ws.lastActivityTime = Date.now();
|
|
754
|
+
ws.connectedAt = Date.now();
|
|
755
|
+
clientById.set(id, ws);
|
|
756
|
+
|
|
757
|
+
// 心跳检测
|
|
758
|
+
ws.on('pong', () => {
|
|
759
|
+
ws.isAlive = true;
|
|
760
|
+
logConnectionEvent('HEARTBEAT_PONG', { type: 'client', id: ws.id });
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
// 消息转发: Client -> Plugin
|
|
764
|
+
ws.on('message', (data) => {
|
|
765
|
+
ws.lastActivityTime = Date.now();
|
|
766
|
+
|
|
767
|
+
const messageSize = data.length;
|
|
768
|
+
let messagePreview;
|
|
769
|
+
let parsed;
|
|
770
|
+
try {
|
|
771
|
+
parsed = JSON.parse(data);
|
|
772
|
+
messagePreview = parsed.method || parsed.id || 'response';
|
|
773
|
+
} catch {
|
|
774
|
+
parsed = null;
|
|
775
|
+
messagePreview = `binary (${messageSize} bytes)`;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
if (shouldLog('debug')) {
|
|
779
|
+
console.log(`[CLIENT -> PLUGIN] ${id}: ${messagePreview}`);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// 记录到日志文件
|
|
783
|
+
logCDP('CLIENT -> PLUGIN', data.toString().substring(0, CONFIG.LOG_MESSAGE_PREVIEW_LENGTH), parsed?.sessionId);
|
|
784
|
+
|
|
785
|
+
// 为每个请求分配全局唯一 ID,避免多客户端 ID 冲突
|
|
786
|
+
let modifiedData = data;
|
|
787
|
+
if (parsed && parsed.id !== undefined) {
|
|
788
|
+
const originalId = parsed.id;
|
|
789
|
+
globalRequestIdCounter++;
|
|
790
|
+
const globalId = globalRequestIdCounter;
|
|
791
|
+
|
|
792
|
+
// 保存映射:全局ID -> {clientId, originalId, sessionId}
|
|
793
|
+
// 如果请求有 sessionId,也保存它,用于响应路由
|
|
794
|
+
globalRequestIdMap.set(globalId, {
|
|
795
|
+
clientId: id,
|
|
796
|
+
originalId: originalId,
|
|
797
|
+
sessionId: parsed.sessionId // 保存请求的 sessionId
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
// 修改请求ID为全局ID
|
|
801
|
+
parsed.id = globalId;
|
|
802
|
+
modifiedData = JSON.stringify(parsed);
|
|
803
|
+
|
|
804
|
+
console.log(`[REQUEST ID MAPPED] client=${id} original=${originalId} -> global=${globalId} sessionId=${parsed.sessionId?.substring(0,8) || 'none'}`);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// 记录 Target.attachToTarget 请求,用于后续建立 session -> clientId 映射
|
|
808
|
+
if (parsed && parsed.method === 'Target.attachToTarget' && parsed.id !== undefined) {
|
|
809
|
+
pendingAttachRequests.set(parsed.id, id);
|
|
810
|
+
console.log(`[PENDING ATTACH] Request id=${parsed.id} from client=${id}, pending size=${pendingAttachRequests.size}`);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// 记录 Target.createTarget 请求,用于后续建立 targetId -> clientId 映射
|
|
814
|
+
// 注意:此时 parsed.id 已经是 globalId,originalId 已经保存在 mapping 中
|
|
815
|
+
if (parsed && parsed.method === 'Target.createTarget' && parsed.id !== undefined) {
|
|
816
|
+
// 获取当前请求的映射(刚刚创建的)
|
|
817
|
+
const currentMapping = globalRequestIdMap.get(parsed.id);
|
|
818
|
+
if (currentMapping) {
|
|
819
|
+
// 标记为 createTarget 请求
|
|
820
|
+
currentMapping.isCreateTarget = true;
|
|
821
|
+
}
|
|
822
|
+
console.log(`[PENDING CREATE TARGET] Request id=${parsed.id} from client=${id}`);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// 记录 Target.createBrowserContext 请求,用于后续建立 browserContextId -> clientId 映射
|
|
826
|
+
if (parsed && parsed.method === 'Target.createBrowserContext' && parsed.id !== undefined) {
|
|
827
|
+
const currentMapping = globalRequestIdMap.get(parsed.id);
|
|
828
|
+
if (currentMapping) {
|
|
829
|
+
currentMapping.isCreateBrowserContext = true;
|
|
830
|
+
}
|
|
831
|
+
console.log(`[PENDING CREATE CONTEXT] Request id=${parsed.id} from client=${id}`);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// 拦截 Browser.close - 清理会话状态
|
|
835
|
+
if (parsed && parsed.method === 'Browser.close') {
|
|
836
|
+
if (shouldLog('info')) {
|
|
837
|
+
console.log(`\n[BROWSER CLOSE] Client ${id} requested Browser.close`);
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// 清理该客户端的所有 session 映射
|
|
841
|
+
const sessionsToClean = [];
|
|
842
|
+
for (const [sessionId, clientId] of sessionToClientId.entries()) {
|
|
843
|
+
if (clientId === id) {
|
|
844
|
+
sessionsToClean.push(sessionId);
|
|
845
|
+
sessionToClientId.delete(sessionId);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
if (shouldLog('info')) {
|
|
849
|
+
console.log(` - Cleaned ${sessionsToClean.length} sessions`);
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// 通知扩展清理状态
|
|
853
|
+
if (ws.pairedPlugin && ws.pairedPlugin.readyState === WebSocket.OPEN) {
|
|
854
|
+
ws.pairedPlugin.send(JSON.stringify({
|
|
855
|
+
type: 'browser-close',
|
|
856
|
+
clientId: id,
|
|
857
|
+
sessions: sessionsToClean
|
|
858
|
+
}));
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// 返回 mock 响应(包含 sessionId)
|
|
862
|
+
const response = { id: parsed.id, result: {} };
|
|
863
|
+
if (parsed.sessionId) {
|
|
864
|
+
response.sessionId = parsed.sessionId;
|
|
865
|
+
}
|
|
866
|
+
ws.send(JSON.stringify(response));
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
if (parsed && parsed.method) {
|
|
871
|
+
ws.cdpTrace.push(parsed.method);
|
|
872
|
+
if (ws.cdpTrace.length > CONFIG.CDP_TRACE_MAX_LENGTH) {
|
|
873
|
+
ws.cdpTrace = ws.cdpTrace.slice(-CONFIG.CDP_TRACE_MAX_LENGTH);
|
|
874
|
+
}
|
|
875
|
+
if (shouldLog('debug')) {
|
|
876
|
+
console.log(`[CDP TRACE] ${id} -> ${parsed.method}`);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// 发送给配对的 plugin (或广播)
|
|
881
|
+
if (ws.pairedPlugin && ws.pairedPlugin.readyState === WebSocket.OPEN) {
|
|
882
|
+
console.log(`[SEND TO PLUGIN] id=${parsed?.id} method=${parsed?.method} sessionId=${parsed?.sessionId?.substring(0,8) || 'none'}`);
|
|
883
|
+
ws.pairedPlugin.send(modifiedData);
|
|
884
|
+
} else {
|
|
885
|
+
broadcastToPlugins(modifiedData, ws);
|
|
886
|
+
}
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
// 连接关闭
|
|
890
|
+
ws.on('close', async (code, reason) => {
|
|
891
|
+
// 记录断开事件到日志文件
|
|
892
|
+
logCDP('EVENT', `CLIENT DISCONNECTED id=${id} code=${code} reason=${reason.toString() || 'none'}`);
|
|
893
|
+
|
|
894
|
+
// 收集该 client 的所有 session
|
|
895
|
+
const sessionsToClean = [];
|
|
896
|
+
for (const [sessionId, clientId] of sessionToClientId.entries()) {
|
|
897
|
+
if (clientId === id) {
|
|
898
|
+
sessionsToClean.push(sessionId);
|
|
899
|
+
sessionToClientId.delete(sessionId);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
clientConnections.delete(ws);
|
|
904
|
+
clientById.delete(id);
|
|
905
|
+
if (shouldLog('info')) {
|
|
906
|
+
console.log(`\n[CLIENT DISCONNECTED] ${id}`);
|
|
907
|
+
console.log(` - Code: ${code}, Reason: ${reason || 'none'}`);
|
|
908
|
+
console.log(` - Sessions to clean: ${sessionsToClean.length}`);
|
|
909
|
+
console.log(` - Total client connections: ${clientConnections.size}`);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
logConnectionEvent('CLIENT_DISCONNECTED', {
|
|
913
|
+
id,
|
|
914
|
+
code,
|
|
915
|
+
reason: reason?.toString() || 'none',
|
|
916
|
+
sessionsCleaned: sessionsToClean.length,
|
|
917
|
+
totalPlugins: pluginConnections.size,
|
|
918
|
+
totalClients: clientConnections.size
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
if (ws.cdpTrace && ws.cdpTrace.length && shouldLog('debug')) {
|
|
922
|
+
const unique = [...new Set(ws.cdpTrace)];
|
|
923
|
+
console.log(`[CDP TRACE] ${id} methods (${ws.cdpTrace.length}): ${unique.join(', ')}`);
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// 向 plugin 发送清理命令
|
|
927
|
+
if (ws.pairedPlugin) {
|
|
928
|
+
safeSend(ws.pairedPlugin, JSON.stringify({
|
|
929
|
+
type: 'client-disconnected',
|
|
930
|
+
clientId: id,
|
|
931
|
+
sessions: sessionsToClean
|
|
932
|
+
}), 'plugin');
|
|
933
|
+
if (shouldLog('debug')) {
|
|
934
|
+
console.log(` - Notified plugin of client disconnect`);
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// 广播更新后的客户端列表
|
|
939
|
+
broadcastClientList();
|
|
940
|
+
|
|
941
|
+
// 清理配对关系
|
|
942
|
+
if (ws.pairedPlugin) {
|
|
943
|
+
ws.pairedPlugin.pairedClientId = null;
|
|
944
|
+
}
|
|
945
|
+
connectionPairs.delete(id);
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
// 错误处理
|
|
949
|
+
ws.on('error', (error) => {
|
|
950
|
+
console.error(`[CLIENT ERROR] ${id}:`, error.message);
|
|
951
|
+
|
|
952
|
+
logConnectionEvent('CLIENT_ERROR', {
|
|
953
|
+
id,
|
|
954
|
+
error: error.message,
|
|
955
|
+
totalPlugins: pluginConnections.size,
|
|
956
|
+
totalClients: clientConnections.size
|
|
957
|
+
});
|
|
958
|
+
|
|
959
|
+
clientConnections.delete(ws);
|
|
960
|
+
clientById.delete(id);
|
|
961
|
+
|
|
962
|
+
if (ws.pairedPlugin) {
|
|
963
|
+
ws.pairedPlugin.pairedClientId = null;
|
|
964
|
+
}
|
|
965
|
+
connectionPairs.delete(id);
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
function handlePageConnection(ws, clientInfo, targetId) {
|
|
970
|
+
clientConnections.add(ws);
|
|
971
|
+
const id = generateId('page');
|
|
972
|
+
if (shouldLog('info')) {
|
|
973
|
+
console.log(`\n[PAGE CONNECTED] ID: ${id}, targetId: ${targetId}`);
|
|
974
|
+
console.log(` - Remote: ${clientInfo.ip}:${clientInfo.port}`);
|
|
975
|
+
console.log(` - Total client connections: ${clientConnections.size}`);
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
ws.id = id;
|
|
979
|
+
ws.isAlive = true;
|
|
980
|
+
ws.cdpTrace = [];
|
|
981
|
+
ws.targetId = targetId;
|
|
982
|
+
ws.lastActivityTime = Date.now();
|
|
983
|
+
clientById.set(id, ws);
|
|
984
|
+
|
|
985
|
+
const plugin = pluginConnections.values().next().value;
|
|
986
|
+
if (plugin && plugin.readyState === WebSocket.OPEN) {
|
|
987
|
+
ws.pairedPlugin = plugin;
|
|
988
|
+
plugin.pairedClientId = id;
|
|
989
|
+
if (shouldLog('info')) {
|
|
990
|
+
console.log(` - Paired with plugin: ${plugin.id}`);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
ws.on('pong', () => {
|
|
995
|
+
ws.isAlive = true;
|
|
996
|
+
});
|
|
997
|
+
|
|
998
|
+
const pluginMessageHandler = (data) => {
|
|
999
|
+
if (ws.readyState !== WebSocket.OPEN) {
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
let msg;
|
|
1004
|
+
try {
|
|
1005
|
+
msg = JSON.parse(data.toString());
|
|
1006
|
+
} catch {
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
if (msg.type === 'event' && msg.method) {
|
|
1011
|
+
const cdpMsg = {
|
|
1012
|
+
method: msg.method,
|
|
1013
|
+
params: msg.params
|
|
1014
|
+
};
|
|
1015
|
+
if (msg.sessionId) {
|
|
1016
|
+
cdpMsg.sessionId = msg.sessionId;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
if (msg.method === 'Page.screencastFrame' && shouldLog('debug')) {
|
|
1020
|
+
console.log(`[PLUGIN -> PAGE] ${id}: Page.screencastFrame`);
|
|
1021
|
+
}
|
|
1022
|
+
ws.lastActivityTime = Date.now();
|
|
1023
|
+
ws.send(JSON.stringify(cdpMsg));
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
if (msg.id !== undefined || msg.method) {
|
|
1028
|
+
const messagePreview = msg.method || msg.id || 'response';
|
|
1029
|
+
if (shouldLog('debug')) {
|
|
1030
|
+
console.log(`[PLUGIN -> PAGE] ${id}: ${messagePreview}`);
|
|
1031
|
+
}
|
|
1032
|
+
ws.lastActivityTime = Date.now();
|
|
1033
|
+
ws.send(data.toString());
|
|
1034
|
+
}
|
|
1035
|
+
};
|
|
1036
|
+
|
|
1037
|
+
ws.pluginMessageHandler = pluginMessageHandler;
|
|
1038
|
+
|
|
1039
|
+
if (ws.pairedPlugin) {
|
|
1040
|
+
ws.pairedPlugin.on('message', pluginMessageHandler);
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
ws.on('message', (data) => {
|
|
1044
|
+
ws.lastActivityTime = Date.now();
|
|
1045
|
+
|
|
1046
|
+
let parsed;
|
|
1047
|
+
try {
|
|
1048
|
+
parsed = JSON.parse(data);
|
|
1049
|
+
} catch {
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
const messagePreview = parsed.method || parsed.id || 'response';
|
|
1054
|
+
if (shouldLog('debug')) {
|
|
1055
|
+
console.log(`[PAGE -> PLUGIN] ${id}: ${messagePreview}`);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
if (parsed && parsed.method) {
|
|
1059
|
+
ws.cdpTrace.push(parsed.method);
|
|
1060
|
+
if (ws.cdpTrace.length > CONFIG.CDP_TRACE_MAX_LENGTH) {
|
|
1061
|
+
ws.cdpTrace = ws.cdpTrace.slice(-CONFIG.CDP_TRACE_MAX_LENGTH);
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
if (ws.pairedPlugin) {
|
|
1066
|
+
const msg = { ...parsed, tabId: targetId };
|
|
1067
|
+
safeSend(ws.pairedPlugin, JSON.stringify(msg), 'plugin');
|
|
1068
|
+
}
|
|
1069
|
+
});
|
|
1070
|
+
|
|
1071
|
+
ws.on('close', (code, reason) => {
|
|
1072
|
+
clientConnections.delete(ws);
|
|
1073
|
+
clientById.delete(id);
|
|
1074
|
+
if (shouldLog('info')) {
|
|
1075
|
+
console.log(`\n[PAGE DISCONNECTED] ${id}`);
|
|
1076
|
+
console.log(` - Code: ${code}, Reason: ${reason || 'none'}`);
|
|
1077
|
+
console.log(` - Total client connections: ${clientConnections.size}`);
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
if (ws.pairedPlugin && ws.pluginMessageHandler) {
|
|
1081
|
+
ws.pairedPlugin.off('message', ws.pluginMessageHandler);
|
|
1082
|
+
ws.pairedPlugin.pairedClientId = null;
|
|
1083
|
+
|
|
1084
|
+
safeSend(ws.pairedPlugin, JSON.stringify({
|
|
1085
|
+
type: 'client-disconnected',
|
|
1086
|
+
clientId: id,
|
|
1087
|
+
sessions: []
|
|
1088
|
+
}), 'plugin');
|
|
1089
|
+
if (shouldLog('debug')) {
|
|
1090
|
+
console.log(` - Notified plugin of page disconnect`);
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
ws.pluginMessageHandler = null;
|
|
1095
|
+
});
|
|
1096
|
+
|
|
1097
|
+
ws.on('error', (error) => {
|
|
1098
|
+
console.error(`[PAGE ERROR] ${id}:`, error.message);
|
|
1099
|
+
|
|
1100
|
+
clientConnections.delete(ws);
|
|
1101
|
+
clientById.delete(id);
|
|
1102
|
+
|
|
1103
|
+
if (ws.pairedPlugin && ws.pluginMessageHandler) {
|
|
1104
|
+
ws.pairedPlugin.off('message', ws.pluginMessageHandler);
|
|
1105
|
+
ws.pairedPlugin.pairedClientId = null;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
ws.pluginMessageHandler = null;
|
|
1109
|
+
});
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
/**
|
|
1113
|
+
* 广播消息给所有客户端
|
|
1114
|
+
*/
|
|
1115
|
+
function broadcastToClients(data, excludeWs = null) {
|
|
1116
|
+
let sent = 0;
|
|
1117
|
+
clientConnections.forEach((client) => {
|
|
1118
|
+
if (client !== excludeWs && safeSend(client, data, 'client')) {
|
|
1119
|
+
sent++;
|
|
1120
|
+
}
|
|
1121
|
+
});
|
|
1122
|
+
return sent;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
function broadcastToPlugins(data, excludeWs = null) {
|
|
1126
|
+
let sent = 0;
|
|
1127
|
+
pluginConnections.forEach((plugin) => {
|
|
1128
|
+
if (plugin !== excludeWs && safeSend(plugin, data, 'plugin')) {
|
|
1129
|
+
sent++;
|
|
1130
|
+
}
|
|
1131
|
+
});
|
|
1132
|
+
return sent;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
function broadcastClientList() {
|
|
1136
|
+
const clients = [];
|
|
1137
|
+
clientConnections.forEach((client) => {
|
|
1138
|
+
clients.push({
|
|
1139
|
+
id: client.id,
|
|
1140
|
+
connectedAt: client.connectedAt,
|
|
1141
|
+
lastActivity: client.lastActivityTime
|
|
1142
|
+
});
|
|
1143
|
+
});
|
|
1144
|
+
|
|
1145
|
+
broadcastToPlugins(JSON.stringify({
|
|
1146
|
+
type: 'client-list',
|
|
1147
|
+
clients: clients
|
|
1148
|
+
}));
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
/**
|
|
1152
|
+
* 生成唯一 ID
|
|
1153
|
+
*/
|
|
1154
|
+
function generateId(prefix = 'conn') {
|
|
1155
|
+
return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
const BUFFER_THRESHOLD = 1024 * 1024;
|
|
1159
|
+
const MAX_QUEUE_SIZE = 100;
|
|
1160
|
+
const messageQueues = new Map();
|
|
1161
|
+
|
|
1162
|
+
function safeSend(ws, data, label = '') {
|
|
1163
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
1164
|
+
return false;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
const wsId = ws.id || label;
|
|
1168
|
+
|
|
1169
|
+
if (ws.bufferedAmount > BUFFER_THRESHOLD) {
|
|
1170
|
+
if (shouldLog('warn')) {
|
|
1171
|
+
console.warn(`[BACKPRESSURE] ${wsId} buffer full (${Math.round(ws.bufferedAmount / 1024)}KB), queuing message`);
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
if (!messageQueues.has(wsId)) {
|
|
1175
|
+
messageQueues.set(wsId, []);
|
|
1176
|
+
}
|
|
1177
|
+
const queue = messageQueues.get(wsId);
|
|
1178
|
+
if (queue.length < MAX_QUEUE_SIZE) {
|
|
1179
|
+
queue.push(data);
|
|
1180
|
+
if (shouldLog('debug')) {
|
|
1181
|
+
console.log(`[BACKPRESSURE] ${wsId} queue size: ${queue.length}`);
|
|
1182
|
+
}
|
|
1183
|
+
} else {
|
|
1184
|
+
if (shouldLog('warn')) {
|
|
1185
|
+
console.warn(`[BACKPRESSURE] ${wsId} queue full, dropping oldest message`);
|
|
1186
|
+
}
|
|
1187
|
+
queue.shift();
|
|
1188
|
+
queue.push(data);
|
|
1189
|
+
}
|
|
1190
|
+
return false;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
if (ws.lastActivityTime !== undefined) {
|
|
1194
|
+
ws.lastActivityTime = Date.now();
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
try {
|
|
1198
|
+
ws.send(data);
|
|
1199
|
+
return true;
|
|
1200
|
+
} catch (e) {
|
|
1201
|
+
console.error(`[SEND_ERROR] ${wsId}:`, e.message);
|
|
1202
|
+
return false;
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
setInterval(() => {
|
|
1207
|
+
messageQueues.forEach((queue, wsId) => {
|
|
1208
|
+
let ws = null;
|
|
1209
|
+
for (const conn of pluginConnections) {
|
|
1210
|
+
if (conn.id === wsId) { ws = conn; break; }
|
|
1211
|
+
}
|
|
1212
|
+
if (!ws) {
|
|
1213
|
+
for (const conn of clientConnections) {
|
|
1214
|
+
if (conn.id === wsId) { ws = conn; break; }
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
1219
|
+
messageQueues.delete(wsId);
|
|
1220
|
+
if (shouldLog('debug')) {
|
|
1221
|
+
console.log(`[QUEUE] ${wsId} cleaned up (connection closed)`);
|
|
1222
|
+
}
|
|
1223
|
+
return;
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
if (ws.bufferedAmount < BUFFER_THRESHOLD / 2) {
|
|
1227
|
+
const data = queue.shift();
|
|
1228
|
+
if (data) {
|
|
1229
|
+
try {
|
|
1230
|
+
ws.send(data);
|
|
1231
|
+
if (shouldLog('debug')) {
|
|
1232
|
+
console.log(`[QUEUE] ${wsId} sent queued message, remaining: ${queue.length}`);
|
|
1233
|
+
}
|
|
1234
|
+
} catch (e) {
|
|
1235
|
+
console.error(`[QUEUE_ERROR] ${wsId}:`, e.message);
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
if (queue.length === 0) {
|
|
1241
|
+
messageQueues.delete(wsId);
|
|
1242
|
+
}
|
|
1243
|
+
});
|
|
1244
|
+
}, 100);
|
|
1245
|
+
|
|
1246
|
+
/**
|
|
1247
|
+
* 心跳检测 - 每 30 秒检查一次
|
|
1248
|
+
*/
|
|
1249
|
+
const heartbeatInterval = setInterval(() => {
|
|
1250
|
+
const now = new Date().toISOString();
|
|
1251
|
+
const nowMs = Date.now();
|
|
1252
|
+
|
|
1253
|
+
// 检查 plugin 连接
|
|
1254
|
+
pluginConnections.forEach((ws) => {
|
|
1255
|
+
if (!ws.isAlive) {
|
|
1256
|
+
if (shouldLog('warn')) {
|
|
1257
|
+
console.log(`[${now}] Plugin ${ws.id} not responding, terminating...`);
|
|
1258
|
+
}
|
|
1259
|
+
logConnectionEvent('HEARTBEAT_TIMEOUT', { type: 'plugin', id: ws.id });
|
|
1260
|
+
pluginConnections.delete(ws);
|
|
1261
|
+
if (ws.pairedClientId) {
|
|
1262
|
+
connectionPairs.delete(ws.pairedClientId);
|
|
1263
|
+
const clientWs = clientById.get(ws.pairedClientId);
|
|
1264
|
+
if (clientWs) {
|
|
1265
|
+
clientWs.pairedPlugin = null;
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
return ws.terminate();
|
|
1269
|
+
}
|
|
1270
|
+
ws.isAlive = false;
|
|
1271
|
+
ws.ping();
|
|
1272
|
+
logConnectionEvent('HEARTBEAT_PING', { type: 'plugin', id: ws.id, bufferedAmount: ws.bufferedAmount });
|
|
1273
|
+
});
|
|
1274
|
+
|
|
1275
|
+
// 检查 client 连接
|
|
1276
|
+
clientConnections.forEach((ws) => {
|
|
1277
|
+
if (!ws.isAlive) {
|
|
1278
|
+
if (shouldLog('warn')) {
|
|
1279
|
+
console.log(`[${now}] Client ${ws.id} not responding, terminating...`);
|
|
1280
|
+
}
|
|
1281
|
+
logConnectionEvent('HEARTBEAT_TIMEOUT', { type: 'client', id: ws.id });
|
|
1282
|
+
clientConnections.delete(ws);
|
|
1283
|
+
clientById.delete(ws.id);
|
|
1284
|
+
if (ws.pairedPlugin) {
|
|
1285
|
+
ws.pairedPlugin.pairedClientId = null;
|
|
1286
|
+
}
|
|
1287
|
+
connectionPairs.delete(ws.id);
|
|
1288
|
+
return ws.terminate();
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
// 检查空闲超时
|
|
1292
|
+
if (ws.lastActivityTime && (nowMs - ws.lastActivityTime > CONFIG.CLIENT_IDLE_TIMEOUT)) {
|
|
1293
|
+
const idleSeconds = Math.round((nowMs - ws.lastActivityTime) / 1000);
|
|
1294
|
+
if (shouldLog('info')) {
|
|
1295
|
+
console.log(`[${now}] Client ${ws.id} idle for ${idleSeconds}s, closing...`);
|
|
1296
|
+
}
|
|
1297
|
+
logConnectionEvent('CLIENT_IDLE_TIMEOUT', {
|
|
1298
|
+
type: 'client',
|
|
1299
|
+
id: ws.id,
|
|
1300
|
+
idleSeconds,
|
|
1301
|
+
lastActivityTime: new Date(ws.lastActivityTime).toISOString()
|
|
1302
|
+
});
|
|
1303
|
+
ws.close(1001, `Idle timeout: no activity for ${idleSeconds} seconds`);
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
ws.isAlive = false;
|
|
1308
|
+
ws.ping();
|
|
1309
|
+
logConnectionEvent('HEARTBEAT_PING', { type: 'client', id: ws.id, bufferedAmount: ws.bufferedAmount });
|
|
1310
|
+
});
|
|
1311
|
+
}, CONFIG.HEARTBEAT_INTERVAL);
|
|
1312
|
+
|
|
1313
|
+
setInterval(() => {
|
|
1314
|
+
const toRemove = [];
|
|
1315
|
+
pluginConnections.forEach(ws => {
|
|
1316
|
+
if (ws.readyState !== WebSocket.OPEN) {
|
|
1317
|
+
toRemove.push(ws);
|
|
1318
|
+
}
|
|
1319
|
+
});
|
|
1320
|
+
toRemove.forEach(ws => {
|
|
1321
|
+
pluginConnections.delete(ws);
|
|
1322
|
+
if (shouldLog('debug')) {
|
|
1323
|
+
console.log(`[CLEANUP] Removed zombie plugin: ${ws.id}, state: ${ws.readyState}`);
|
|
1324
|
+
}
|
|
1325
|
+
});
|
|
1326
|
+
|
|
1327
|
+
toRemove.length = 0;
|
|
1328
|
+
clientConnections.forEach(ws => {
|
|
1329
|
+
if (ws.readyState !== WebSocket.OPEN) {
|
|
1330
|
+
toRemove.push(ws);
|
|
1331
|
+
}
|
|
1332
|
+
});
|
|
1333
|
+
toRemove.forEach(ws => {
|
|
1334
|
+
clientConnections.delete(ws);
|
|
1335
|
+
clientById.delete(ws.id);
|
|
1336
|
+
if (shouldLog('debug')) {
|
|
1337
|
+
console.log(`[CLEANUP] Removed zombie client: ${ws.id}, state: ${ws.readyState}`);
|
|
1338
|
+
}
|
|
1339
|
+
});
|
|
1340
|
+
}, 60000);
|
|
1341
|
+
|
|
1342
|
+
/**
|
|
1343
|
+
* 服务器关闭处理
|
|
1344
|
+
*/
|
|
1345
|
+
wss.on('close', () => {
|
|
1346
|
+
clearInterval(heartbeatInterval);
|
|
1347
|
+
console.log('\n[SERVER] Server closed');
|
|
1348
|
+
});
|
|
1349
|
+
|
|
1350
|
+
/**
|
|
1351
|
+
* 定期打印状态
|
|
1352
|
+
*/
|
|
1353
|
+
setInterval(() => {
|
|
1354
|
+
const now = new Date().toISOString();
|
|
1355
|
+
const nowMs = Date.now();
|
|
1356
|
+
|
|
1357
|
+
const validPlugins = Array.from(pluginConnections).filter(ws => ws.readyState === WebSocket.OPEN);
|
|
1358
|
+
const zombiePlugins = Array.from(pluginConnections).filter(ws => ws.readyState !== WebSocket.OPEN);
|
|
1359
|
+
const validClients = Array.from(clientConnections).filter(ws => ws.readyState === WebSocket.OPEN);
|
|
1360
|
+
const zombieClients = Array.from(clientConnections).filter(ws => ws.readyState !== WebSocket.OPEN);
|
|
1361
|
+
|
|
1362
|
+
if (shouldLog('info')) {
|
|
1363
|
+
console.log(`\n[${now}] Status:`);
|
|
1364
|
+
console.log(` - Plugin connections: ${validPlugins.length} valid / ${pluginConnections.size} total`);
|
|
1365
|
+
console.log(` - Client connections: ${validClients.length} valid / ${clientConnections.size} total`);
|
|
1366
|
+
console.log(` - Active pairs: ${connectionPairs.size}`);
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
if (zombiePlugins.length > 0 && shouldLog('warn')) {
|
|
1370
|
+
console.log(` - Zombie plugins: ${zombiePlugins.map(ws => `${ws.id}(${ws.readyState})`).join(', ')}`);
|
|
1371
|
+
}
|
|
1372
|
+
if (zombieClients.length > 0 && shouldLog('warn')) {
|
|
1373
|
+
console.log(` - Zombie clients: ${zombieClients.map(ws => `${ws.id}(${ws.readyState})`).join(', ')}`);
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
const pluginList = Array.from(pluginConnections).map(ws => ({
|
|
1377
|
+
id: ws.id,
|
|
1378
|
+
readyState: ws.readyState,
|
|
1379
|
+
pairedClientId: ws.pairedClientId,
|
|
1380
|
+
bufferedAmount: ws.bufferedAmount,
|
|
1381
|
+
isAlive: ws.isAlive
|
|
1382
|
+
}));
|
|
1383
|
+
|
|
1384
|
+
const clientList = Array.from(clientConnections).map(ws => {
|
|
1385
|
+
const idleMs = ws.lastActivityTime ? nowMs - ws.lastActivityTime : 0;
|
|
1386
|
+
return {
|
|
1387
|
+
id: ws.id,
|
|
1388
|
+
readyState: ws.readyState,
|
|
1389
|
+
hasPairedPlugin: !!ws.pairedPlugin,
|
|
1390
|
+
bufferedAmount: ws.bufferedAmount,
|
|
1391
|
+
isAlive: ws.isAlive,
|
|
1392
|
+
idleSeconds: Math.round(idleMs / 1000)
|
|
1393
|
+
};
|
|
1394
|
+
});
|
|
1395
|
+
|
|
1396
|
+
logStatus({
|
|
1397
|
+
timestamp: now,
|
|
1398
|
+
plugins: pluginConnections.size,
|
|
1399
|
+
validPlugins: validPlugins.length,
|
|
1400
|
+
clients: clientConnections.size,
|
|
1401
|
+
validClients: validClients.length,
|
|
1402
|
+
pairs: connectionPairs.size,
|
|
1403
|
+
pluginDetails: pluginList,
|
|
1404
|
+
clientDetails: clientList,
|
|
1405
|
+
sessions: sessionToClientId.size,
|
|
1406
|
+
pendingAttach: pendingAttachRequests.size
|
|
1407
|
+
});
|
|
1408
|
+
}, CONFIG.STATUS_PRINT_INTERVAL);
|
|
1409
|
+
|
|
1410
|
+
// 优雅关闭
|
|
1411
|
+
process.on('SIGINT', () => {
|
|
1412
|
+
console.log('\n[SERVER] Shutting down...');
|
|
1413
|
+
clearInterval(heartbeatInterval);
|
|
1414
|
+
|
|
1415
|
+
// 关闭所有连接
|
|
1416
|
+
pluginConnections.forEach(ws => ws.close(1001, 'Server shutting down'));
|
|
1417
|
+
clientConnections.forEach(ws => ws.close(1001, 'Server shutting down'));
|
|
1418
|
+
|
|
1419
|
+
wss.close(() => {
|
|
1420
|
+
console.log('[SERVER] Server closed');
|
|
1421
|
+
flushAllLogs();
|
|
1422
|
+
process.exit(0);
|
|
1423
|
+
});
|
|
1424
|
+
});
|
|
1425
|
+
|
|
1426
|
+
process.on('SIGTERM', () => {
|
|
1427
|
+
flushAllLogs();
|
|
1428
|
+
process.exit(0);
|
|
1429
|
+
});
|
|
1430
|
+
|
|
1431
|
+
server.listen(PORT, '0.0.0.0');
|