cdp-tunnel 3.2.5 → 3.2.7

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.
@@ -65,8 +65,30 @@ var SpecialHandler = (function() {
65
65
  }
66
66
 
67
67
  var isAlreadyAttached = state.isTabAttached(tabId);
68
-
69
- if (isAlreadyAttached) {
68
+ var isAttaching = state.attachingTabs && state.attachingTabs.has(tabId);
69
+
70
+ if (isAlreadyAttached || isAttaching) {
71
+ // 已 attach 或正在 attach——等待正在进行的 attach 完成后返回新 session
72
+ if (isAttaching && state.attachingTabs) {
73
+ // 等待 attach 完成
74
+ var waitAttach = function(cb, retries) {
75
+ if (state.isTabAttached(tabId)) {
76
+ var sid = CDPUtils.generateSessionId();
77
+ state.mapSession(sid, tabId, targetId);
78
+ muteTabIfNeeded(tabId);
79
+ cb({ sessionId: sid });
80
+ } else if (retries > 0) {
81
+ setTimeout(function() { waitAttach(cb, retries - 1); }, 100);
82
+ } else {
83
+ // 超时——直接生成 session(attach 可能成功了但标记慢了)
84
+ var sid2 = CDPUtils.generateSessionId();
85
+ state.mapSession(sid2, tabId, targetId);
86
+ muteTabIfNeeded(tabId);
87
+ cb({ sessionId: sid2 });
88
+ }
89
+ };
90
+ return new Promise(function(resolve) { waitAttach(resolve, 30); });
91
+ }
70
92
  var newSessionId = CDPUtils.generateSessionId();
71
93
  state.mapSession(newSessionId, tabId, targetId);
72
94
  muteTabIfNeeded(tabId);
@@ -74,7 +96,14 @@ var SpecialHandler = (function() {
74
96
  return { sessionId: newSessionId };
75
97
  }
76
98
 
99
+ // 标记为正在 attach(防止并发 attach 同一个 tab)
100
+ if (!state.attachingTabs) state.attachingTabs = new Set();
101
+ state.attachingTabs.add(tabId);
102
+
77
103
  return DebuggerManager.attach(tabId, state).then(function(attached) {
104
+ // 清除"正在 attach"标记
105
+ if (state.attachingTabs) state.attachingTabs.delete(tabId);
106
+
78
107
  if (!attached) {
79
108
  throw new Error('Failed to attach');
80
109
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "CDP Bridge",
4
- "version": "3.2.5",
4
+ "version": "3.2.7",
5
5
  "description": "Chrome DevTools Protocol Bridge for Playwright/Puppeteer automation",
6
6
  "permissions": [
7
7
  "debugger",
@@ -1,5 +1,5 @@
1
1
  var Config = {
2
- WS_URL: 'ws://localhost:9221/plugin',
2
+ WS_URL: 'ws://localhost:29931/plugin',
3
3
  RECONNECT_DELAY: 3000,
4
4
  DEBUGGER_VERSION: '1.3',
5
5
  HEARTBEAT_INTERVAL: 25000,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cdp-tunnel",
3
- "version": "3.2.5",
3
+ "version": "3.2.7",
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",
@@ -204,38 +204,57 @@ class PortPoolManager {
204
204
  const SYNTHETIC_INPUT = ['Input.dispatchKeyEvent', 'Input.dispatchMouseEvent'];
205
205
 
206
206
  // 消息处理:client → plugin(带 portIndex 标记)
207
- ws.on('message', async (data) => {
207
+ // session 级别的命令队列:保证跨 client 的命令串行处理
208
+ const handleMessage = async (data) => {
208
209
  let msg;
209
210
  try { msg = JSON.parse(data.toString()); } catch { return; }
210
211
 
211
212
  if (msg.id !== undefined) {
212
- // createTarget 串行化(避免并发 chrome.tabs.create 回调串)
213
- if (msg.method === 'Target.createTarget') {
214
- while (session.createTargetLock) { await session.createTargetLock; }
215
- session.createTargetLock = new Promise(r => { session._releaseCreateTarget = r; });
213
+ // createTarget:等前一个 createTarget 的响应回来
214
+ if (msg.method === 'Target.createTarget' && session.createTargetChain) {
215
+ await session.createTargetChain;
216
+ }
217
+
218
+ // attachToTarget:已 attach 的 target 返回虚拟 session
219
+ if (msg.method === 'Target.attachToTarget' && msg.params && msg.params.targetId) {
220
+ const targetId = msg.params.targetId;
221
+ if (session.attachedTargets && session.attachedTargets.has(targetId)) {
222
+ const vsid = `vs_${session.portIndex}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
223
+ session.virtualSessions.set(vsid, targetId);
224
+ this.sessionToPort.set(vsid, session.portIndex);
225
+ ws.send(JSON.stringify({ id: msg.id, result: { sessionId: vsid } }));
226
+ return;
227
+ }
228
+ if (!session.attachedTargets) session.attachedTargets = new Set();
229
+ session.attachedTargets.add(targetId);
216
230
  }
217
231
 
218
- // 合成输入命令:先 bringToFront + 等待,再发原命令
219
232
  if (SYNTHETIC_INPUT.indexOf(msg.method) >= 0 && msg.sessionId) {
220
233
  await this._ensureVisible(session, pluginWs, msg.sessionId);
221
234
  }
222
235
 
223
- // 命令:分配新 id,记录映射(含 method 名供响应后处理),转发给 plugin
236
+ // createTarget:设置串行链
237
+ if (msg.method === 'Target.createTarget') {
238
+ let release;
239
+ session.createTargetChain = new Promise(r => { release = r; });
240
+ session._releaseCT = release;
241
+ }
242
+
224
243
  const newId = `pool${session.portIndex}_${msg.id}`;
225
244
  session.pendingRequests.set(newId, {
226
- originalId: msg.id,
227
- method: msg.method,
228
- clientWs: ws,
229
- params: msg.params,
230
- sessionId: msg.sessionId
245
+ originalId: msg.id, method: msg.method, clientWs: ws,
246
+ params: msg.params, sessionId: msg.sessionId
231
247
  });
232
-
233
248
  const forwarded = { ...msg, id: newId, __portIndex: session.portIndex, __clientId: `pool_${session.port}` };
234
249
  pluginWs.send(JSON.stringify(forwarded));
235
250
  } else {
236
- // 无 id 的消息(事件),直接转发
237
251
  pluginWs.send(JSON.stringify({ ...msg, __portIndex: session.portIndex }));
238
252
  }
253
+ };
254
+
255
+ ws.on('message', (data) => {
256
+ if (!session.cmdQueue) session.cmdQueue = Promise.resolve();
257
+ session.cmdQueue = session.cmdQueue.then(() => handleMessage(data)).catch(() => {});
239
258
  });
240
259
 
241
260
  ws.on('close', () => {
@@ -256,8 +275,8 @@ class PortPoolManager {
256
275
  handlePluginMessage(msg, pluginWs) {
257
276
  if (!msg) return false;
258
277
 
259
- // 1. 响应消息:id 以 pool 开头
260
- if (msg.id && typeof msg.id === 'string' && msg.id.startsWith('pool')) {
278
+ // 1. 响应消息:id 以 pool 开头(排除事件——事件有 method 字段)
279
+ if (msg.id && typeof msg.id === 'string' && msg.id.startsWith('pool') && !msg.method) {
261
280
  // 内部命令(ensureVisible 用的)响应直接丢弃
262
281
  if (msg.id.includes('_internal')) {
263
282
  return true;
@@ -273,18 +292,18 @@ class PortPoolManager {
273
292
  const pending = session.pendingRequests.get(msg.id);
274
293
  session.pendingRequests.delete(msg.id);
275
294
 
276
- // 如果是 createTarget 响应,记录 targetId → portIndex 归属 + 初始 url + 释放锁
295
+ // 如果是 createTarget 响应,记录 targetId + 释放串行链
277
296
  if (msg.result && msg.result.targetId) {
278
297
  const tid = msg.result.targetId;
279
298
  session.targetIds.add(tid);
280
299
  session.targetUrls.set(tid, pending?.params?.url || 'about:blank');
281
300
  this.targetToPort.set(tid, portIndex);
282
- // 释放 createTarget 锁
283
- if (session._releaseCreateTarget) {
284
- session._releaseCreateTarget();
285
- session._releaseCreateTarget = null;
286
- session.createTargetLock = null;
287
- }
301
+ }
302
+ // 释放 createTarget 串行链
303
+ if (pending && pending.method === 'Target.createTarget' && session._releaseCT) {
304
+ session._releaseCT();
305
+ session._releaseCT = null;
306
+ session.createTargetChain = null;
288
307
  }
289
308
 
290
309
  // 如果是 Page.navigate 响应,更新 targetId 的 url