ghost-bridge 0.7.1 → 0.8.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.
@@ -13,13 +13,15 @@ const CONFIG = {
13
13
  }
14
14
 
15
15
  let attachedTabId = null
16
+ let targetMode = 'focused' // focused | pinned
17
+ let pinnedTabId = null
16
18
  let scriptMap = new Map()
17
19
  let scriptSourceCache = new Map()
18
20
  let lastErrors = []
19
21
  let lastErrorLocation = null
20
22
  let requestMap = new Map()
21
23
  let networkRequests = []
22
- let state = { enabled: false, connected: false, port: null, currentPort: null, connectionStatus: 'disconnected', connectionError: '' }
24
+ let state = { enabled: false, connected: false, port: null, currentPort: null, connectionStatus: 'disconnected', connectionError: '', serverInfo: null }
23
25
 
24
26
  // 待处理的请求(等待 offscreen 响应)
25
27
  const pendingRequests = new Map()
@@ -248,10 +250,7 @@ function trimPendingRequests() {
248
250
  chrome.debugger.onDetach.addListener((source, reason) => {
249
251
  if (source.tabId && source.tabId === attachedTabId) {
250
252
  attachedTabId = null
251
- scriptMap = new Map()
252
- scriptSourceCache = new Map()
253
- networkRequests = []
254
- requestMap = new Map()
253
+ resetDebuggerState()
255
254
 
256
255
  if (!state.enabled) return
257
256
  if (reason === "canceled_by_user") {
@@ -295,20 +294,66 @@ function compactStack(stackTrace) {
295
294
 
296
295
  // ========== Debugger 操作 ==========
297
296
 
297
+ function resetDebuggerState() {
298
+ scriptMap = new Map()
299
+ scriptSourceCache = new Map()
300
+ networkRequests = []
301
+ requestMap = new Map()
302
+ }
303
+
304
+ function summarizeTab(tab) {
305
+ if (!tab) return null
306
+ return {
307
+ id: tab.id,
308
+ windowId: tab.windowId,
309
+ index: tab.index,
310
+ active: !!tab.active,
311
+ title: tab.title || '',
312
+ url: tab.url || '',
313
+ }
314
+ }
315
+
316
+ async function getFocusedTabOrThrow() {
317
+ const [tab] = await chrome.tabs.query({ active: true, lastFocusedWindow: true })
318
+ if (!tab || tab.id === undefined) throw new Error("没有激活的标签页")
319
+ return tab
320
+ }
321
+
322
+ async function getTabOrThrow(tabId) {
323
+ if (!Number.isInteger(tabId) || tabId < 0) throw new Error("需要提供有效的 tabId")
324
+ try {
325
+ const tab = await chrome.tabs.get(tabId)
326
+ if (!tab || tab.id === undefined) throw new Error("标签页不可用")
327
+ return tab
328
+ } catch (e) {
329
+ throw new Error(`标签页 ${tabId} 不可用:${e.message}`)
330
+ }
331
+ }
332
+
333
+ async function resolveTargetTab(params = {}) {
334
+ if (params.tabId !== undefined) {
335
+ return getTabOrThrow(Number(params.tabId))
336
+ }
337
+ if (targetMode === 'pinned') {
338
+ if (pinnedTabId === null) throw new Error("锁定的标签页不可用,请重新 pin")
339
+ return getTabOrThrow(pinnedTabId)
340
+ }
341
+ return getFocusedTabOrThrow()
342
+ }
343
+
298
344
  // attach 互斥锁:防止并发调用 ensureAttached 导致重复 attach / 状态竞态
299
345
  let _attachLock = Promise.resolve()
300
346
 
301
- async function ensureAttached() {
347
+ async function ensureAttached(params = {}) {
302
348
  let _release
303
349
  const _prev = _attachLock
304
350
  _attachLock = new Promise(r => _release = r)
305
351
  await _prev
306
352
  try {
307
353
  if (!state.enabled) throw new Error("扩展已暂停,点击图标开启后再试")
308
- const [tab] = await chrome.tabs.query({ active: true, lastFocusedWindow: true })
309
- if (!tab) throw new Error("没有激活的标签页")
354
+ const tab = await resolveTargetTab(params)
310
355
  if (attachedTabId !== tab.id) {
311
- if (attachedTabId) {
356
+ if (attachedTabId !== null) {
312
357
  try { await chrome.debugger.detach({ tabId: attachedTabId }) } catch (e) {}
313
358
  }
314
359
  try {
@@ -324,10 +369,7 @@ async function ensureAttached() {
324
369
  throw e
325
370
  }
326
371
  attachedTabId = tab.id
327
- scriptMap = new Map()
328
- scriptSourceCache = new Map()
329
- networkRequests = []
330
- requestMap = new Map()
372
+ resetDebuggerState()
331
373
  await chrome.debugger.sendCommand({ tabId: attachedTabId }, "Runtime.enable")
332
374
  await chrome.debugger.sendCommand({ tabId: attachedTabId }, "Log.enable")
333
375
  await chrome.debugger.sendCommand({ tabId: attachedTabId }, "Console.enable").catch(() => {})
@@ -349,13 +391,14 @@ async function ensureAttached() {
349
391
  }
350
392
 
351
393
  async function maybeDetach(force = false) {
352
- if ((CONFIG.autoDetach || force) && attachedTabId) {
394
+ if ((CONFIG.autoDetach || force) && attachedTabId !== null) {
353
395
  try {
354
396
  await chrome.debugger.detach({ tabId: attachedTabId })
355
397
  } catch (e) {
356
398
  log(`detach 失败:${e.message}`)
357
399
  } finally {
358
400
  attachedTabId = null
401
+ resetDebuggerState()
359
402
  }
360
403
  }
361
404
  }
@@ -382,10 +425,105 @@ async function detachAllTargets() {
382
425
  }
383
426
  } catch {}
384
427
  attachedTabId = null
428
+ resetDebuggerState()
385
429
  }
386
430
 
387
431
  // ========== 命令处理 ==========
388
432
 
433
+ async function buildTargetInfo() {
434
+ const viewingTab = await chrome.tabs.query({ active: true, lastFocusedWindow: true })
435
+ .then(([tab]) => tab || null)
436
+ .catch(() => null)
437
+
438
+ let targetTab = null
439
+ let targetError = ''
440
+ const targetTabId = targetMode === 'pinned' ? pinnedTabId : attachedTabId
441
+
442
+ if (targetTabId !== null && targetTabId !== undefined) {
443
+ try {
444
+ targetTab = await chrome.tabs.get(targetTabId)
445
+ } catch (e) {
446
+ targetError = e.message
447
+ }
448
+ } else if (targetMode === 'focused') {
449
+ targetTab = viewingTab
450
+ }
451
+
452
+ return {
453
+ targetMode,
454
+ pinnedTabId,
455
+ attachedTabId,
456
+ targetTab: summarizeTab(targetTab),
457
+ viewingTab: summarizeTab(viewingTab),
458
+ targetError,
459
+ }
460
+ }
461
+
462
+ async function handleListTabs() {
463
+ const tabs = await chrome.tabs.query({})
464
+ const targetInfo = await buildTargetInfo()
465
+ return {
466
+ ...targetInfo,
467
+ tabs: tabs
468
+ .filter((tab) => tab.id !== undefined)
469
+ .sort((a, b) => (a.windowId - b.windowId) || (a.index - b.index))
470
+ .map(summarizeTab),
471
+ }
472
+ }
473
+
474
+ async function pinTab(tab) {
475
+ const previousMode = targetMode
476
+ const previousPinnedTabId = pinnedTabId
477
+ targetMode = 'pinned'
478
+ pinnedTabId = tab.id
479
+ try {
480
+ await ensureAttached({ tabId: tab.id })
481
+ } catch (e) {
482
+ targetMode = previousMode
483
+ pinnedTabId = previousPinnedTabId
484
+ throw e
485
+ }
486
+ broadcastStatus()
487
+ return buildTargetInfo()
488
+ }
489
+
490
+ async function handlePinCurrentTab() {
491
+ return pinTab(await getFocusedTabOrThrow())
492
+ }
493
+
494
+ async function handlePinTab(params = {}) {
495
+ if (params.tabId !== undefined) {
496
+ return pinTab(await getTabOrThrow(Number(params.tabId)))
497
+ }
498
+
499
+ const urlContains = String(params.urlContains || '')
500
+ const titleContains = String(params.titleContains || '')
501
+ if (!urlContains && !titleContains) {
502
+ throw new Error("需要提供 tabId、urlContains 或 titleContains")
503
+ }
504
+
505
+ const tabs = await chrome.tabs.query({})
506
+ const matches = tabs.filter((tab) => {
507
+ const urlOk = !urlContains || (tab.url || '').includes(urlContains)
508
+ const titleOk = !titleContains || (tab.title || '').includes(titleContains)
509
+ return tab.id !== undefined && urlOk && titleOk
510
+ })
511
+ if (matches.length === 0) throw new Error("没有找到匹配的标签页")
512
+
513
+ matches.sort((a, b) => Number(b.active) - Number(a.active) || (a.windowId - b.windowId) || (a.index - b.index))
514
+ return pinTab(matches[0])
515
+ }
516
+
517
+ async function handleUnpinTab() {
518
+ targetMode = 'focused'
519
+ pinnedTabId = null
520
+ if (state.enabled && state.connected) {
521
+ await ensureAttached()
522
+ }
523
+ broadcastStatus()
524
+ return buildTargetInfo()
525
+ }
526
+
389
527
  async function handleGetLastError(params = {}) {
390
528
  await ensureAttached()
391
529
  const severity = params.severity || "error"
@@ -1092,7 +1230,12 @@ async function handleCommand(message) {
1092
1230
  }
1093
1231
  try {
1094
1232
  let result
1095
- if (command === "getLastError") result = await handleGetLastError(params)
1233
+ if (command === "listTabs") result = await handleListTabs()
1234
+ else if (command === "getTargetTab") result = await buildTargetInfo()
1235
+ else if (command === "pinCurrentTab") result = await handlePinCurrentTab()
1236
+ else if (command === "pinTab") result = await handlePinTab(params)
1237
+ else if (command === "unpinTab") result = await handleUnpinTab()
1238
+ else if (command === "getLastError") result = await handleGetLastError(params)
1096
1239
  else if (command === "getScriptSource") result = await handleGetScriptSource(params)
1097
1240
  else if (command === "coverageSnapshot") result = await handleCoverageSnapshot(params)
1098
1241
  else if (command === "findByString") result = await handleFindByString(params)
@@ -1124,8 +1267,7 @@ function sendToServer(data) {
1124
1267
 
1125
1268
  // ========== 状态广播 ==========
1126
1269
 
1127
- // 主动推送状态给 popup
1128
- function broadcastStatus() {
1270
+ function getConnectionStatus() {
1129
1271
  let status
1130
1272
  if (!state.enabled) {
1131
1273
  status = 'disconnected'
@@ -1134,43 +1276,50 @@ function broadcastStatus() {
1134
1276
  } else {
1135
1277
  status = state.connectionStatus || 'connecting'
1136
1278
  }
1279
+ return status
1280
+ }
1137
1281
 
1138
- let tabUrl = ''
1139
- let tabTitle = ''
1140
-
1141
- if (attachedTabId) {
1142
- chrome.tabs.get(attachedTabId).then(t => {
1143
- tabUrl = t.url
1144
- tabTitle = t.title
1145
- doBroadcast()
1146
- }).catch(() => doBroadcast())
1147
- } else {
1148
- doBroadcast()
1282
+ async function buildPopupState() {
1283
+ const status = getConnectionStatus()
1284
+ const targetInfo = await buildTargetInfo()
1285
+ const targetTab = targetInfo.targetTab
1286
+ const viewingTab = targetInfo.viewingTab
1287
+ return {
1288
+ status,
1289
+ enabled: state.enabled,
1290
+ port: state.port,
1291
+ currentPort: state.currentPort,
1292
+ basePort: CONFIG.basePort,
1293
+ connectionError: state.connectionError,
1294
+ serverInfo: state.serverInfo,
1295
+ targetMode: targetInfo.targetMode,
1296
+ pinnedTabId: targetInfo.pinnedTabId,
1297
+ attachedTabId: targetInfo.attachedTabId,
1298
+ targetTab,
1299
+ viewingTab,
1300
+ targetError: targetInfo.targetError,
1301
+ tabTitle: targetTab?.title || '',
1302
+ tabUrl: targetTab?.url || '',
1303
+ viewingTitle: viewingTab?.title || '',
1304
+ viewingUrl: viewingTab?.url || '',
1149
1305
  }
1306
+ }
1150
1307
 
1151
- function doBroadcast() {
1152
- const actualErrors = lastErrors.filter(e => e.severity === 'error')
1308
+ // 主动推送状态给 popup
1309
+ async function broadcastStatus() {
1310
+ try {
1153
1311
  chrome.runtime.sendMessage({
1154
1312
  type: 'statusUpdate',
1155
- state: {
1156
- status,
1157
- enabled: state.enabled,
1158
- port: state.port,
1159
- currentPort: state.currentPort,
1160
- basePort: CONFIG.basePort,
1161
- connectionError: state.connectionError,
1162
- errorCount: actualErrors.length,
1163
- recentErrors: actualErrors.slice(0, 5),
1164
- tabTitle,
1165
- tabUrl,
1166
- }
1313
+ state: await buildPopupState()
1167
1314
  }).catch(() => {}) // popup 可能未打开,忽略错误
1315
+ } catch (e) {
1316
+ log(`状态广播失败:${e.message}`)
1168
1317
  }
1169
1318
  }
1170
1319
 
1171
1320
  // 监听被调试页面的导航变化,实时推送到 popup
1172
1321
  chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
1173
- if (tabId === attachedTabId && (changeInfo.title || changeInfo.url)) {
1322
+ if ((tabId === attachedTabId || tab.active) && (changeInfo.title || changeInfo.url)) {
1174
1323
  if (state.connected) broadcastStatus()
1175
1324
  }
1176
1325
  })
@@ -1179,8 +1328,10 @@ chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
1179
1328
  chrome.tabs.onActivated.addListener(async (activeInfo) => {
1180
1329
  if (state.enabled && state.connected) {
1181
1330
  try {
1182
- // 这里的 ensureAttached() 会自动处理从旧 Tab detach 并 attach 到新 Tab
1183
- await ensureAttached()
1331
+ if (targetMode === 'focused') {
1332
+ // focused 模式保持旧行为:切换 Tab 时自动跟随
1333
+ await ensureAttached()
1334
+ }
1184
1335
  broadcastStatus()
1185
1336
  } catch (e) {
1186
1337
  log(`自动跟随切换 Tab 失败:${e.message}`)
@@ -1188,6 +1339,18 @@ chrome.tabs.onActivated.addListener(async (activeInfo) => {
1188
1339
  }
1189
1340
  })
1190
1341
 
1342
+ chrome.tabs.onRemoved.addListener((tabId) => {
1343
+ if (tabId === pinnedTabId) {
1344
+ targetMode = 'focused'
1345
+ pinnedTabId = null
1346
+ }
1347
+ if (tabId === attachedTabId) {
1348
+ attachedTabId = null
1349
+ resetDebuggerState()
1350
+ }
1351
+ if (state.connected) broadcastStatus()
1352
+ })
1353
+
1191
1354
 
1192
1355
  // ========== 消息监听 ==========
1193
1356
 
@@ -1210,29 +1373,36 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
1210
1373
  state.currentPort = message.port
1211
1374
  state.connectionStatus = 'connected'
1212
1375
  state.connectionError = ''
1376
+ state.serverInfo = message.serverInfo || null
1213
1377
  setBadgeState('on')
1214
1378
  log(`✅ 已连接到 ghost-bridge 服务 (端口 ${message.port})`)
1215
- ensureAttached().catch((e) => log(`attach 失败:${e.message}`))
1379
+ ensureAttached()
1380
+ .then(() => broadcastStatus())
1381
+ .catch((e) => log(`attach 失败:${e.message}`))
1216
1382
  } else if (message.status === 'disconnected') {
1217
1383
  state.connected = false
1218
1384
  state.port = null
1219
1385
  state.connectionStatus = 'connecting'
1220
1386
  state.connectionError = ''
1387
+ state.serverInfo = null
1221
1388
  if (state.enabled) setBadgeState('connecting')
1222
1389
  } else if (message.status === 'connecting') {
1223
1390
  state.currentPort = message.currentPort
1224
1391
  state.connectionStatus = 'connecting'
1225
1392
  state.connectionError = ''
1393
+ state.serverInfo = null
1226
1394
  setBadgeState('connecting')
1227
1395
  } else if (message.status === 'error') {
1228
1396
  state.currentPort = message.currentPort
1229
1397
  state.connectionStatus = 'error'
1230
1398
  state.connectionError = message.errorMessage || ''
1399
+ state.serverInfo = null
1231
1400
  setBadgeState('err')
1232
1401
  } else if (message.status === 'not_found') {
1233
1402
  state.currentPort = message.currentPort
1234
1403
  state.connectionStatus = 'not_found'
1235
- state.connectionError = ''
1404
+ state.connectionError = message.errorMessage || ''
1405
+ state.serverInfo = null
1236
1406
  setBadgeState('connecting')
1237
1407
  }
1238
1408
  broadcastStatus() // 状态变化时主动推送
@@ -1258,44 +1428,25 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
1258
1428
 
1259
1429
  // 来自 popup 的状态查询
1260
1430
  if (message.type === 'getStatus') {
1261
- let status
1262
- if (!state.enabled) {
1263
- status = 'disconnected'
1264
- } else if (state.connected) {
1265
- status = 'connected'
1266
- } else {
1267
- status = state.connectionStatus || 'connecting'
1268
- }
1431
+ buildPopupState().then(sendResponse).catch((e) => sendResponse({ status: 'error', connectionError: e.message }))
1432
+ return true
1433
+ }
1269
1434
 
1270
- let tabUrl = ''
1271
- let tabTitle = ''
1272
- if (attachedTabId) {
1273
- chrome.tabs.get(attachedTabId).then(t => {
1274
- tabUrl = t.url
1275
- tabTitle = t.title
1276
- sendStatusResponse()
1277
- }).catch(() => {
1278
- sendStatusResponse()
1279
- })
1280
- } else {
1281
- sendStatusResponse()
1435
+ if (message.type === 'pinCurrentTab') {
1436
+ if (!state.enabled || !state.connected) {
1437
+ sendResponse({ ok: false, error: "Ghost Bridge 尚未连接" })
1438
+ return true
1282
1439
  }
1440
+ handlePinCurrentTab().then((result) => sendResponse({ ok: true, result })).catch((e) => sendResponse({ ok: false, error: e.message }))
1441
+ return true
1442
+ }
1283
1443
 
1284
- function sendStatusResponse() {
1285
- const actualErrors = lastErrors.filter(e => e.severity === 'error')
1286
- sendResponse({
1287
- status,
1288
- enabled: state.enabled,
1289
- port: state.port,
1290
- currentPort: state.currentPort,
1291
- basePort: CONFIG.basePort,
1292
- connectionError: state.connectionError,
1293
- errorCount: actualErrors.length,
1294
- recentErrors: actualErrors.slice(0, 5),
1295
- tabTitle,
1296
- tabUrl,
1297
- })
1444
+ if (message.type === 'unpinTab') {
1445
+ if (!state.enabled || !state.connected) {
1446
+ sendResponse({ ok: false, error: "Ghost Bridge 尚未连接" })
1447
+ return true
1298
1448
  }
1449
+ handleUnpinTab().then((result) => sendResponse({ ok: true, result })).catch((e) => sendResponse({ ok: false, error: e.message }))
1299
1450
  return true
1300
1451
  }
1301
1452
 
@@ -1311,6 +1462,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
1311
1462
  state.currentPort = CONFIG.basePort
1312
1463
  state.connectionStatus = 'connecting'
1313
1464
  state.connectionError = ''
1465
+ state.serverInfo = null
1314
1466
  setBadgeState('connecting')
1315
1467
 
1316
1468
  // 启动 offscreen 并开始连接
@@ -1334,6 +1486,9 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
1334
1486
  state.currentPort = null
1335
1487
  state.connectionStatus = 'disconnected'
1336
1488
  state.connectionError = ''
1489
+ state.serverInfo = null
1490
+ targetMode = 'focused'
1491
+ pinnedTabId = null
1337
1492
  setBadgeState('off')
1338
1493
  detachAllTargets().catch(() => {})
1339
1494
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "Ghost Bridge",
4
- "version": "0.7.1",
4
+ "version": "0.8.0",
5
5
  "description": "Zero-restart Chrome debugger bridge for Claude MCP, optimized for no-sourcemap production debugging.",
6
6
  "permissions": [
7
7
  "debugger",
@@ -4,6 +4,7 @@
4
4
  let ws = null
5
5
  let reconnectTimer = null
6
6
  let manualDisconnect = false // 用户主动断开标志,防止 onclose 触发重连
7
+ let connectionGeneration = 0
7
8
  let config = {
8
9
  basePort: 33333,
9
10
  token: '',
@@ -17,12 +18,42 @@ function log(msg) {
17
18
 
18
19
  const DEFAULT_TOKEN = 'ghost-bridge-local'
19
20
 
21
+ function clearReconnectTimer() {
22
+ if (reconnectTimer) {
23
+ clearTimeout(reconnectTimer)
24
+ reconnectTimer = null
25
+ }
26
+ }
27
+
28
+ function isCurrentConnection(generation, socket) {
29
+ return generation === connectionGeneration && socket === ws
30
+ }
31
+
32
+ function scheduleReconnect(generation, delay) {
33
+ if (generation !== connectionGeneration) return
34
+ clearReconnectTimer()
35
+ reconnectTimer = setTimeout(() => {
36
+ if (generation === connectionGeneration) connect()
37
+ }, delay)
38
+ }
39
+
40
+ function closeCurrentSocket() {
41
+ if (!ws) return
42
+ try {
43
+ ws.close()
44
+ } catch {}
45
+ ws = null
46
+ }
47
+
20
48
  // 连接到服务器
21
49
  function connect() {
22
50
  // 如果已手动断开,不再尝试连接
23
51
  if (manualDisconnect) return
52
+ clearReconnectTimer()
53
+ closeCurrentSocket()
24
54
 
25
55
  const port = config.basePort
56
+ const generation = ++connectionGeneration
26
57
  const url = new URL(`ws://localhost:${port}`)
27
58
  url.searchParams.set('token', config.token)
28
59
  log(`尝试连接固定端口 ${port}...`)
@@ -32,12 +63,13 @@ function connect() {
32
63
  currentPort: port,
33
64
  }).catch(() => {})
34
65
 
35
- ws = new WebSocket(url.toString())
36
- ws.binaryType = 'blob' // 明确设置
66
+ const socket = new WebSocket(url.toString())
67
+ ws = socket
68
+ socket.binaryType = 'blob' // 明确设置
37
69
 
38
70
  const connectionTimeout = setTimeout(() => {
39
- if (ws && ws.readyState === WebSocket.CONNECTING) {
40
- ws.close()
71
+ if (isCurrentConnection(generation, socket) && socket.readyState === WebSocket.CONNECTING) {
72
+ socket.close()
41
73
  }
42
74
  }, 2000) // 增加到 2 秒
43
75
 
@@ -45,19 +77,22 @@ function connect() {
45
77
  let socketOpened = false
46
78
  let terminalErrorMessage = ''
47
79
 
48
- ws.onopen = () => {
80
+ socket.onopen = () => {
81
+ if (!isCurrentConnection(generation, socket)) return
49
82
  socketOpened = true
50
83
  clearTimeout(connectionTimeout)
51
84
  log(`WebSocket 已连接端口 ${port},等待身份验证...`)
52
85
  }
53
86
 
54
- ws.onmessage = async (event) => {
87
+ socket.onmessage = async (event) => {
55
88
  try {
89
+ if (!isCurrentConnection(generation, socket)) return
56
90
  // 处理 Blob 类型的消息
57
91
  let data = event.data
58
92
  if (data instanceof Blob) {
59
93
  data = await data.text()
60
94
  }
95
+ if (!isCurrentConnection(generation, socket)) return
61
96
  const msg = JSON.parse(data)
62
97
 
63
98
  if (msg.type === 'identity') {
@@ -68,6 +103,13 @@ function connect() {
68
103
  type: 'status',
69
104
  status: 'connected',
70
105
  port: port,
106
+ serverInfo: {
107
+ version: msg.version,
108
+ pid: msg.pid,
109
+ port: msg.port,
110
+ startedAt: msg.startedAt,
111
+ serverPath: msg.serverPath,
112
+ },
71
113
  }).catch(() => {})
72
114
  } else {
73
115
  terminalErrorMessage = msg.service === 'ghost-bridge'
@@ -94,7 +136,8 @@ function connect() {
94
136
  }
95
137
  }
96
138
 
97
- ws.onclose = (event) => {
139
+ socket.onclose = (event) => {
140
+ if (!isCurrentConnection(generation, socket)) return
98
141
  clearTimeout(connectionTimeout)
99
142
 
100
143
  // 用户主动断开,不重连
@@ -110,22 +153,28 @@ function connect() {
110
153
  currentPort: port,
111
154
  errorMessage,
112
155
  }).catch(() => {})
113
- reconnectTimer = setTimeout(() => connect(), 2000)
156
+ scheduleReconnect(generation, 2000)
114
157
  return
115
158
  }
116
159
  log(`固定端口 ${port} 未发现可用服务,2秒后重试...`)
117
- chrome.runtime.sendMessage({ type: 'status', status: 'not_found', currentPort: port }).catch(() => {})
118
- reconnectTimer = setTimeout(() => connect(), 2000)
160
+ chrome.runtime.sendMessage({
161
+ type: 'status',
162
+ status: 'not_found',
163
+ currentPort: port,
164
+ errorMessage: `No ghost-bridge WebSocket service was found on port ${port}.`,
165
+ }).catch(() => {})
166
+ scheduleReconnect(generation, 2000)
119
167
  return
120
168
  }
121
169
 
122
170
  // 连接断开,重试
123
171
  log(`端口 ${port} 连接断开,尝试重连...`)
124
172
  chrome.runtime.sendMessage({ type: 'status', status: 'disconnected' }).catch(() => {})
125
- reconnectTimer = setTimeout(() => connect(), 1000)
173
+ scheduleReconnect(generation, 1000)
126
174
  }
127
175
 
128
- ws.onerror = () => {
176
+ socket.onerror = () => {
177
+ if (!isCurrentConnection(generation, socket)) return
129
178
  clearTimeout(connectionTimeout)
130
179
  }
131
180
  }
@@ -142,14 +191,9 @@ function sendToServer(data) {
142
191
  // 断开连接
143
192
  function disconnect() {
144
193
  manualDisconnect = true // 标记为手动断开,阻止 onclose 重连
145
- if (reconnectTimer) {
146
- clearTimeout(reconnectTimer)
147
- reconnectTimer = null
148
- }
149
- if (ws) {
150
- ws.close()
151
- ws = null
152
- }
194
+ connectionGeneration++
195
+ clearReconnectTimer()
196
+ closeCurrentSocket()
153
197
  log('已断开连接')
154
198
  }
155
199
 
@@ -158,7 +202,6 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
158
202
  if (message.type === 'connect') {
159
203
  config.basePort = message.basePort || 33333
160
204
  config.token = message.token || DEFAULT_TOKEN
161
- disconnect()
162
205
  manualDisconnect = false // 用户重新连接,清除断开标志
163
206
  connect()
164
207
  sendResponse({ ok: true })