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.
- package/README.md +17 -0
- package/dist/cli.js +266 -140
- package/dist/server.js +132 -33
- package/extension/background.js +236 -81
- package/extension/manifest.json +1 -1
- package/extension/offscreen.js +64 -21
- package/extension/popup.html +72 -54
- package/extension/popup.js +72 -42
- package/package.json +1 -1
package/extension/background.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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 === "
|
|
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
|
-
|
|
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
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
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
|
-
|
|
1152
|
-
|
|
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
|
-
|
|
1183
|
-
|
|
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()
|
|
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
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
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
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
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
|
-
|
|
1285
|
-
|
|
1286
|
-
sendResponse({
|
|
1287
|
-
|
|
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
|
|
package/extension/manifest.json
CHANGED
package/extension/offscreen.js
CHANGED
|
@@ -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
|
-
|
|
36
|
-
ws
|
|
66
|
+
const socket = new WebSocket(url.toString())
|
|
67
|
+
ws = socket
|
|
68
|
+
socket.binaryType = 'blob' // 明确设置
|
|
37
69
|
|
|
38
70
|
const connectionTimeout = setTimeout(() => {
|
|
39
|
-
if (
|
|
40
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
156
|
+
scheduleReconnect(generation, 2000)
|
|
114
157
|
return
|
|
115
158
|
}
|
|
116
159
|
log(`固定端口 ${port} 未发现可用服务,2秒后重试...`)
|
|
117
|
-
chrome.runtime.sendMessage({
|
|
118
|
-
|
|
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
|
-
|
|
173
|
+
scheduleReconnect(generation, 1000)
|
|
126
174
|
}
|
|
127
175
|
|
|
128
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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 })
|