ghost-bridge 0.7.1 → 0.9.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 +32 -1
- package/dist/cli.js +266 -140
- package/dist/server.js +288 -69
- package/extension/background.js +692 -224
- package/extension/manifest.json +3 -2
- package/extension/offscreen.js +211 -29
- package/extension/popup.html +72 -54
- package/extension/popup.js +72 -42
- package/package.json +4 -4
package/extension/background.js
CHANGED
|
@@ -12,18 +12,83 @@ const CONFIG = {
|
|
|
12
12
|
maxRequestBodySize: 500000, // 提升至 500KB,容纳较大的 API 请求
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
let attachedTabId = null
|
|
16
|
-
let
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
15
|
+
let attachedTabId = null // Last touched attached tab, kept for popup/backward-compatible status.
|
|
16
|
+
let focusedTabId = null
|
|
17
|
+
const sessionsByTabId = new Map() // tabId -> per-tab debugger state
|
|
18
|
+
|
|
19
|
+
// 每个服务器侧 MCP 连接(会话)独立的 target 命名空间:
|
|
20
|
+
// pin 状态与 bind 的命名 target 互不可见,多 Agent / 同 Agent 多会话不会互相覆盖。
|
|
21
|
+
// 服务器在每条命令上带 clientId;不带时(本地会话)落到 'local' 桶
|
|
22
|
+
const clientSessions = new Map() // clientId -> { targetMode, pinnedTabId, namedTargets: Map }
|
|
23
|
+
const DEFAULT_CLIENT_ID = 'local'
|
|
24
|
+
|
|
25
|
+
function getClientSession(clientId) {
|
|
26
|
+
const key = clientId || DEFAULT_CLIENT_ID
|
|
27
|
+
let client = clientSessions.get(key)
|
|
28
|
+
if (!client) {
|
|
29
|
+
client = { targetMode: 'focused', pinnedTabId: null, namedTargets: new Map() }
|
|
30
|
+
clientSessions.set(key, client)
|
|
31
|
+
}
|
|
32
|
+
return client
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function cleanupClientSession(clientId) {
|
|
36
|
+
const client = clientSessions.get(clientId)
|
|
37
|
+
if (!client) return
|
|
38
|
+
clientSessions.delete(clientId)
|
|
39
|
+
// 释放仅被该会话 pin/bind 且无其他会话引用、也非当前跟随页的调试 session
|
|
40
|
+
const candidateTabIds = new Set([client.pinnedTabId, ...client.namedTargets.values()])
|
|
41
|
+
for (const tabId of candidateTabIds) {
|
|
42
|
+
if (tabId === null || tabId === undefined) continue
|
|
43
|
+
if (isTabReferenced(tabId)) continue
|
|
44
|
+
if (tabId === attachedTabId) continue
|
|
45
|
+
const session = getSession(tabId, { create: false })
|
|
46
|
+
if (session) {
|
|
47
|
+
await detachSession(session)
|
|
48
|
+
sessionsByTabId.delete(tabId)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
log(`已清理会话 "${clientId}" 的 target 命名空间`)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let state = { enabled: false, connected: false, port: null, currentPort: null, connectionStatus: 'disconnected', connectionError: '', serverInfo: null }
|
|
23
55
|
|
|
24
56
|
// 待处理的请求(等待 offscreen 响应)
|
|
25
57
|
const pendingRequests = new Map()
|
|
26
58
|
|
|
59
|
+
function createSession(tabId) {
|
|
60
|
+
return {
|
|
61
|
+
tabId,
|
|
62
|
+
attached: false,
|
|
63
|
+
scriptMap: new Map(),
|
|
64
|
+
scriptSourceCache: new Map(),
|
|
65
|
+
lastErrors: [],
|
|
66
|
+
lastErrorLocation: null,
|
|
67
|
+
requestMap: new Map(),
|
|
68
|
+
networkRequests: [],
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function getSession(tabId, { create = true } = {}) {
|
|
73
|
+
if (tabId === undefined || tabId === null) return null
|
|
74
|
+
const numericTabId = Number(tabId)
|
|
75
|
+
if (!Number.isInteger(numericTabId)) return null
|
|
76
|
+
let session = sessionsByTabId.get(numericTabId)
|
|
77
|
+
if (!session && create) {
|
|
78
|
+
session = createSession(numericTabId)
|
|
79
|
+
sessionsByTabId.set(numericTabId, session)
|
|
80
|
+
}
|
|
81
|
+
return session
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function resetDebuggerState(session) {
|
|
85
|
+
if (!session) return
|
|
86
|
+
session.scriptMap = new Map()
|
|
87
|
+
session.scriptSourceCache = new Map()
|
|
88
|
+
session.networkRequests = []
|
|
89
|
+
session.requestMap = new Map()
|
|
90
|
+
}
|
|
91
|
+
|
|
27
92
|
function setBadgeState(status) {
|
|
28
93
|
const map = {
|
|
29
94
|
connecting: { text: "…", color: "#999" },
|
|
@@ -88,14 +153,77 @@ async function closeOffscreenDocument() {
|
|
|
88
153
|
}
|
|
89
154
|
}
|
|
90
155
|
|
|
156
|
+
async function startBridgeConnection({ persist = false } = {}) {
|
|
157
|
+
state.enabled = true
|
|
158
|
+
state.connected = false
|
|
159
|
+
state.port = null
|
|
160
|
+
state.currentPort = CONFIG.basePort
|
|
161
|
+
state.connectionStatus = 'connecting'
|
|
162
|
+
state.connectionError = ''
|
|
163
|
+
state.serverInfo = null
|
|
164
|
+
setBadgeState('connecting')
|
|
165
|
+
|
|
166
|
+
if (persist) {
|
|
167
|
+
await chrome.storage.local.set({ bridgeEnabled: true, basePort: CONFIG.basePort })
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
await setupOffscreenDocument()
|
|
171
|
+
await chrome.runtime.sendMessage({
|
|
172
|
+
type: 'connect',
|
|
173
|
+
basePort: CONFIG.basePort,
|
|
174
|
+
token: CONFIG.token,
|
|
175
|
+
}).catch(() => {})
|
|
176
|
+
broadcastStatus()
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function stopBridgeConnection({ persist = false } = {}) {
|
|
180
|
+
state.enabled = false
|
|
181
|
+
state.connected = false
|
|
182
|
+
state.port = null
|
|
183
|
+
state.currentPort = null
|
|
184
|
+
state.connectionStatus = 'disconnected'
|
|
185
|
+
state.connectionError = ''
|
|
186
|
+
state.serverInfo = null
|
|
187
|
+
focusedTabId = null
|
|
188
|
+
clientSessions.clear()
|
|
189
|
+
setBadgeState('off')
|
|
190
|
+
|
|
191
|
+
if (persist) {
|
|
192
|
+
await chrome.storage.local.set({ bridgeEnabled: false })
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
await detachAllTargets().catch(() => {})
|
|
196
|
+
await chrome.runtime.sendMessage({ type: 'disconnect' }).catch(() => {})
|
|
197
|
+
await closeOffscreenDocument().catch(() => {})
|
|
198
|
+
broadcastStatus()
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function restoreBridgeConnection() {
|
|
202
|
+
try {
|
|
203
|
+
const result = await chrome.storage.local.get(['basePort', 'bridgeEnabled'])
|
|
204
|
+
if (result.basePort) {
|
|
205
|
+
CONFIG.basePort = result.basePort
|
|
206
|
+
}
|
|
207
|
+
if (result.bridgeEnabled) {
|
|
208
|
+
await startBridgeConnection()
|
|
209
|
+
} else {
|
|
210
|
+
setBadgeState('off')
|
|
211
|
+
}
|
|
212
|
+
} catch (e) {
|
|
213
|
+
log(`恢复连接状态失败:${e.message}`)
|
|
214
|
+
setBadgeState('off')
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
91
218
|
// ========== Chrome Debugger 事件处理 ==========
|
|
92
219
|
|
|
93
220
|
chrome.debugger.onEvent.addListener((source, method, params) => {
|
|
94
|
-
|
|
221
|
+
const session = getSession(source.tabId, { create: false })
|
|
222
|
+
if (!session) return
|
|
95
223
|
if (!state.enabled) return
|
|
96
224
|
|
|
97
225
|
if (method === "Debugger.scriptParsed") {
|
|
98
|
-
scriptMap.set(params.scriptId, { url: params.url || "(inline)" })
|
|
226
|
+
session.scriptMap.set(params.scriptId, { url: params.url || "(inline)" })
|
|
99
227
|
}
|
|
100
228
|
|
|
101
229
|
if (method === "Runtime.exceptionThrown") {
|
|
@@ -112,18 +240,18 @@ chrome.debugger.onEvent.addListener((source, method, params) => {
|
|
|
112
240
|
stack: compactStack(detail.stackTrace),
|
|
113
241
|
timestamp: Date.now(),
|
|
114
242
|
}
|
|
115
|
-
lastErrorLocation = {
|
|
243
|
+
session.lastErrorLocation = {
|
|
116
244
|
url: entry.url,
|
|
117
245
|
line: entry.line,
|
|
118
246
|
column: entry.column,
|
|
119
247
|
scriptId: entry.scriptId,
|
|
120
248
|
}
|
|
121
|
-
pushError(entry)
|
|
249
|
+
pushError(session, entry)
|
|
122
250
|
}
|
|
123
251
|
|
|
124
252
|
if (method === "Log.entryAdded") {
|
|
125
253
|
const entry = params?.entry || {}
|
|
126
|
-
pushError({
|
|
254
|
+
pushError(session, {
|
|
127
255
|
type: entry.level || "log",
|
|
128
256
|
severity: entry.level === "warning" ? "warn" : entry.level === "error" ? "error" : "info",
|
|
129
257
|
url: entry.source || entry.url,
|
|
@@ -136,7 +264,7 @@ chrome.debugger.onEvent.addListener((source, method, params) => {
|
|
|
136
264
|
|
|
137
265
|
if (method === "Runtime.consoleAPICalled") {
|
|
138
266
|
const args = (params.args || []).map((a) => a.description || a.value).filter(Boolean)
|
|
139
|
-
pushError({
|
|
267
|
+
pushError(session, {
|
|
140
268
|
type: params.type || "console",
|
|
141
269
|
severity: params.type === "error" ? "error" : params.type === "warning" ? "warn" : "info",
|
|
142
270
|
url: params.stackTrace?.callFrames?.[0]?.url,
|
|
@@ -151,6 +279,7 @@ chrome.debugger.onEvent.addListener((source, method, params) => {
|
|
|
151
279
|
if (method === "Network.requestWillBeSent") {
|
|
152
280
|
const req = params.request || {}
|
|
153
281
|
const entry = {
|
|
282
|
+
tabId: source.tabId,
|
|
154
283
|
requestId: params.requestId,
|
|
155
284
|
url: req.url,
|
|
156
285
|
method: req.method || "GET",
|
|
@@ -162,13 +291,13 @@ chrome.debugger.onEvent.addListener((source, method, params) => {
|
|
|
162
291
|
timestamp: Date.now(),
|
|
163
292
|
status: "pending",
|
|
164
293
|
}
|
|
165
|
-
requestMap.set(params.requestId, entry)
|
|
166
|
-
trimPendingRequests()
|
|
294
|
+
session.requestMap.set(params.requestId, entry)
|
|
295
|
+
trimPendingRequests(session)
|
|
167
296
|
}
|
|
168
297
|
|
|
169
298
|
if (method === "Network.responseReceived") {
|
|
170
299
|
const res = params.response || {}
|
|
171
|
-
const entry = requestMap.get(params.requestId)
|
|
300
|
+
const entry = session.requestMap.get(params.requestId)
|
|
172
301
|
if (entry) {
|
|
173
302
|
entry.status = res.status >= 400 ? "error" : "success"
|
|
174
303
|
entry.statusCode = res.status
|
|
@@ -181,7 +310,7 @@ chrome.debugger.onEvent.addListener((source, method, params) => {
|
|
|
181
310
|
entry.timing = res.timing
|
|
182
311
|
entry.encodedDataLength = params.encodedDataLength
|
|
183
312
|
if (res.status >= 400) {
|
|
184
|
-
pushError({
|
|
313
|
+
pushError(session, {
|
|
185
314
|
type: "network",
|
|
186
315
|
severity: "error",
|
|
187
316
|
url: res.url || entry.url,
|
|
@@ -197,7 +326,7 @@ chrome.debugger.onEvent.addListener((source, method, params) => {
|
|
|
197
326
|
}
|
|
198
327
|
|
|
199
328
|
if (method === "Network.loadingFinished") {
|
|
200
|
-
const entry = requestMap.get(params.requestId)
|
|
329
|
+
const entry = session.requestMap.get(params.requestId)
|
|
201
330
|
if (entry) {
|
|
202
331
|
entry.endTime = params.timestamp
|
|
203
332
|
entry.encodedDataLength = params.encodedDataLength
|
|
@@ -205,19 +334,19 @@ chrome.debugger.onEvent.addListener((source, method, params) => {
|
|
|
205
334
|
? Math.round((entry.endTime - entry.startTime) * 1000)
|
|
206
335
|
: null
|
|
207
336
|
if (entry.status === "pending") entry.status = "success"
|
|
208
|
-
pushNetworkRequest(entry)
|
|
209
|
-
requestMap.delete(params.requestId)
|
|
337
|
+
pushNetworkRequest(session, entry)
|
|
338
|
+
session.requestMap.delete(params.requestId)
|
|
210
339
|
}
|
|
211
340
|
}
|
|
212
341
|
|
|
213
342
|
if (method === "Network.loadingFailed") {
|
|
214
|
-
const entry = requestMap.get(params.requestId)
|
|
343
|
+
const entry = session.requestMap.get(params.requestId)
|
|
215
344
|
if (entry) {
|
|
216
345
|
entry.status = "failed"
|
|
217
346
|
entry.errorText = params.errorText
|
|
218
347
|
entry.canceled = params.canceled
|
|
219
348
|
entry.blockedReason = params.blockedReason
|
|
220
|
-
pushError({
|
|
349
|
+
pushError(session, {
|
|
221
350
|
type: "network",
|
|
222
351
|
severity: "error",
|
|
223
352
|
url: entry.url,
|
|
@@ -226,32 +355,31 @@ chrome.debugger.onEvent.addListener((source, method, params) => {
|
|
|
226
355
|
text: params.errorText,
|
|
227
356
|
timestamp: Date.now(),
|
|
228
357
|
})
|
|
229
|
-
pushNetworkRequest(entry)
|
|
230
|
-
requestMap.delete(params.requestId)
|
|
358
|
+
pushNetworkRequest(session, entry)
|
|
359
|
+
session.requestMap.delete(params.requestId)
|
|
231
360
|
}
|
|
232
361
|
}
|
|
233
362
|
})
|
|
234
363
|
|
|
235
|
-
function pushNetworkRequest(entry) {
|
|
236
|
-
networkRequests.unshift(entry)
|
|
237
|
-
trimNetworkRequests()
|
|
364
|
+
function pushNetworkRequest(session, entry) {
|
|
365
|
+
session.networkRequests.unshift(entry)
|
|
366
|
+
trimNetworkRequests(session)
|
|
238
367
|
}
|
|
239
368
|
|
|
240
|
-
function trimNetworkRequests() {
|
|
241
|
-
GhostBridgeNetwork.trimTrackedRequests(networkRequests, CONFIG.maxRequestsTracked)
|
|
369
|
+
function trimNetworkRequests(session) {
|
|
370
|
+
GhostBridgeNetwork.trimTrackedRequests(session.networkRequests, CONFIG.maxRequestsTracked)
|
|
242
371
|
}
|
|
243
372
|
|
|
244
|
-
function trimPendingRequests() {
|
|
245
|
-
GhostBridgeNetwork.trimPendingRequestMap(requestMap, CONFIG.maxRequestsTracked * 2)
|
|
373
|
+
function trimPendingRequests(session) {
|
|
374
|
+
GhostBridgeNetwork.trimPendingRequestMap(session.requestMap, CONFIG.maxRequestsTracked * 2)
|
|
246
375
|
}
|
|
247
376
|
|
|
248
377
|
chrome.debugger.onDetach.addListener((source, reason) => {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
requestMap = new Map()
|
|
378
|
+
const session = getSession(source.tabId, { create: false })
|
|
379
|
+
if (source.tabId && session) {
|
|
380
|
+
session.attached = false
|
|
381
|
+
if (source.tabId === attachedTabId) attachedTabId = null
|
|
382
|
+
resetDebuggerState(session)
|
|
255
383
|
|
|
256
384
|
if (!state.enabled) return
|
|
257
385
|
if (reason === "canceled_by_user") {
|
|
@@ -271,15 +399,15 @@ chrome.debugger.onDetach.addListener((source, reason) => {
|
|
|
271
399
|
}
|
|
272
400
|
})
|
|
273
401
|
|
|
274
|
-
function pushError(entry) {
|
|
275
|
-
lastErrors.unshift(entry)
|
|
276
|
-
if (lastErrors.length > CONFIG.maxErrors) {
|
|
277
|
-
const dropIdx = lastErrors
|
|
402
|
+
function pushError(session, entry) {
|
|
403
|
+
session.lastErrors.unshift(entry)
|
|
404
|
+
if (session.lastErrors.length > CONFIG.maxErrors) {
|
|
405
|
+
const dropIdx = session.lastErrors
|
|
278
406
|
.map((e, i) => ({ sev: e.severity || "info", i }))
|
|
279
407
|
.reverse()
|
|
280
408
|
.find((e) => e.sev !== "error")?.i
|
|
281
|
-
if (dropIdx !== undefined) lastErrors.splice(dropIdx, 1)
|
|
282
|
-
else lastErrors.pop()
|
|
409
|
+
if (dropIdx !== undefined) session.lastErrors.splice(dropIdx, 1)
|
|
410
|
+
else session.lastErrors.pop()
|
|
283
411
|
}
|
|
284
412
|
}
|
|
285
413
|
|
|
@@ -295,27 +423,129 @@ function compactStack(stackTrace) {
|
|
|
295
423
|
|
|
296
424
|
// ========== Debugger 操作 ==========
|
|
297
425
|
|
|
426
|
+
function summarizeTab(tab) {
|
|
427
|
+
if (!tab) return null
|
|
428
|
+
return {
|
|
429
|
+
id: tab.id,
|
|
430
|
+
windowId: tab.windowId,
|
|
431
|
+
index: tab.index,
|
|
432
|
+
active: !!tab.active,
|
|
433
|
+
title: tab.title || '',
|
|
434
|
+
url: tab.url || '',
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
async function getFocusedTabOrThrow() {
|
|
439
|
+
const [tab] = await chrome.tabs.query({ active: true, lastFocusedWindow: true })
|
|
440
|
+
if (!tab || tab.id === undefined) throw new Error("没有激活的标签页")
|
|
441
|
+
return tab
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
async function getTabOrThrow(tabId) {
|
|
445
|
+
if (!Number.isInteger(tabId) || tabId < 0) throw new Error("需要提供有效的 tabId")
|
|
446
|
+
try {
|
|
447
|
+
const tab = await chrome.tabs.get(tabId)
|
|
448
|
+
if (!tab || tab.id === undefined) throw new Error("标签页不可用")
|
|
449
|
+
return tab
|
|
450
|
+
} catch (e) {
|
|
451
|
+
throw new Error(`标签页 ${tabId} 不可用:${e.message}`)
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function normalizeTargetName(name, field = 'target') {
|
|
456
|
+
const value = String(name || '').trim()
|
|
457
|
+
if (!value) throw new Error(`需要提供 ${field}`)
|
|
458
|
+
if (!/^[A-Za-z0-9_-]{1,64}$/.test(value)) {
|
|
459
|
+
throw new Error(`${field} 只能包含字母、数字、下划线和连字符,长度 1-64`)
|
|
460
|
+
}
|
|
461
|
+
return value
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function getNamesForTab(tabId) {
|
|
465
|
+
const names = []
|
|
466
|
+
for (const client of clientSessions.values()) {
|
|
467
|
+
for (const [name, boundTabId] of client.namedTargets.entries()) {
|
|
468
|
+
if (boundTabId === tabId) names.push(name)
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return names
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function isTabReferenced(tabId) {
|
|
475
|
+
for (const client of clientSessions.values()) {
|
|
476
|
+
if (client.pinnedTabId === tabId) return true
|
|
477
|
+
for (const boundTabId of client.namedTargets.values()) {
|
|
478
|
+
if (boundTabId === tabId) return true
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return false
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
async function findTabByParams(params = {}) {
|
|
485
|
+
if (params.tabId !== undefined) {
|
|
486
|
+
return getTabOrThrow(Number(params.tabId))
|
|
487
|
+
}
|
|
488
|
+
const urlContains = String(params.urlContains || '')
|
|
489
|
+
const titleContains = String(params.titleContains || '')
|
|
490
|
+
if (!urlContains && !titleContains) {
|
|
491
|
+
throw new Error("需要提供 tabId、urlContains 或 titleContains")
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const tabs = await chrome.tabs.query({})
|
|
495
|
+
const matches = tabs.filter((tab) => {
|
|
496
|
+
const urlOk = !urlContains || (tab.url || '').includes(urlContains)
|
|
497
|
+
const titleOk = !titleContains || (tab.title || '').includes(titleContains)
|
|
498
|
+
return tab.id !== undefined && urlOk && titleOk
|
|
499
|
+
})
|
|
500
|
+
if (matches.length === 0) throw new Error("没有找到匹配的标签页")
|
|
501
|
+
|
|
502
|
+
matches.sort((a, b) => Number(b.active) - Number(a.active) || (a.windowId - b.windowId) || (a.index - b.index))
|
|
503
|
+
return matches[0]
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
async function resolveTargetTab(params = {}) {
|
|
507
|
+
const client = getClientSession(params.clientId)
|
|
508
|
+
if (params.target !== undefined && params.target !== null && String(params.target).trim() !== '') {
|
|
509
|
+
const targetName = normalizeTargetName(params.target)
|
|
510
|
+
const tabId = client.namedTargets.get(targetName)
|
|
511
|
+
if (tabId === undefined) throw new Error(`未绑定 target "${targetName}",请先调用 bind_tab`)
|
|
512
|
+
return getTabOrThrow(tabId)
|
|
513
|
+
}
|
|
514
|
+
if (params.tabId !== undefined) {
|
|
515
|
+
return getTabOrThrow(Number(params.tabId))
|
|
516
|
+
}
|
|
517
|
+
if (client.targetMode === 'pinned') {
|
|
518
|
+
if (client.pinnedTabId === null) throw new Error("锁定的标签页不可用,请重新 pin")
|
|
519
|
+
return getTabOrThrow(client.pinnedTabId)
|
|
520
|
+
}
|
|
521
|
+
return getFocusedTabOrThrow()
|
|
522
|
+
}
|
|
523
|
+
|
|
298
524
|
// attach 互斥锁:防止并发调用 ensureAttached 导致重复 attach / 状态竞态
|
|
299
525
|
let _attachLock = Promise.resolve()
|
|
300
526
|
|
|
301
|
-
async function
|
|
527
|
+
async function ensureAttachedSession(params = {}) {
|
|
302
528
|
let _release
|
|
303
529
|
const _prev = _attachLock
|
|
304
530
|
_attachLock = new Promise(r => _release = r)
|
|
305
531
|
await _prev
|
|
306
532
|
try {
|
|
307
533
|
if (!state.enabled) throw new Error("扩展已暂停,点击图标开启后再试")
|
|
308
|
-
const
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
534
|
+
const tab = await resolveTargetTab(params)
|
|
535
|
+
const client = getClientSession(params.clientId)
|
|
536
|
+
const isFocusedDefaultTarget = !params.target && params.tabId === undefined && client.targetMode === 'focused'
|
|
537
|
+
if (isFocusedDefaultTarget && focusedTabId !== null && focusedTabId !== tab.id && !isTabReferenced(focusedTabId)) {
|
|
538
|
+
const previousFocusedSession = getSession(focusedTabId, { create: false })
|
|
539
|
+
await detachSession(previousFocusedSession)
|
|
540
|
+
sessionsByTabId.delete(focusedTabId)
|
|
541
|
+
}
|
|
542
|
+
const session = getSession(tab.id)
|
|
543
|
+
if (!session.attached) {
|
|
314
544
|
try {
|
|
315
545
|
await chrome.debugger.attach({ tabId: tab.id }, "1.3")
|
|
316
546
|
setBadgeState("on")
|
|
317
547
|
} catch (e) {
|
|
318
|
-
attachedTabId = null
|
|
548
|
+
if (attachedTabId === tab.id) attachedTabId = null
|
|
319
549
|
if (state.connected) {
|
|
320
550
|
setBadgeState("on")
|
|
321
551
|
} else {
|
|
@@ -323,40 +553,57 @@ async function ensureAttached() {
|
|
|
323
553
|
}
|
|
324
554
|
throw e
|
|
325
555
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
await chrome.debugger.sendCommand({ tabId:
|
|
332
|
-
await chrome.debugger.sendCommand({ tabId:
|
|
333
|
-
await chrome.debugger.sendCommand({ tabId:
|
|
334
|
-
await chrome.debugger.sendCommand({ tabId: attachedTabId }, "Debugger.enable")
|
|
335
|
-
await chrome.debugger.sendCommand({ tabId: attachedTabId }, "Profiler.enable")
|
|
336
|
-
await chrome.debugger.sendCommand({ tabId: attachedTabId }, "Network.enable").catch(() => {})
|
|
556
|
+
session.attached = true
|
|
557
|
+
resetDebuggerState(session)
|
|
558
|
+
await chrome.debugger.sendCommand({ tabId: session.tabId }, "Runtime.enable")
|
|
559
|
+
await chrome.debugger.sendCommand({ tabId: session.tabId }, "Log.enable")
|
|
560
|
+
await chrome.debugger.sendCommand({ tabId: session.tabId }, "Console.enable").catch(() => {})
|
|
561
|
+
await chrome.debugger.sendCommand({ tabId: session.tabId }, "Debugger.enable")
|
|
562
|
+
await chrome.debugger.sendCommand({ tabId: session.tabId }, "Profiler.enable")
|
|
563
|
+
await chrome.debugger.sendCommand({ tabId: session.tabId }, "Network.enable").catch(() => {})
|
|
337
564
|
|
|
338
565
|
// Enable auto-attach to sub-targets (iframes, workers) for comprehensive capture
|
|
339
|
-
await chrome.debugger.sendCommand({ tabId:
|
|
566
|
+
await chrome.debugger.sendCommand({ tabId: session.tabId }, "Target.setAutoAttach", {
|
|
340
567
|
autoAttach: true,
|
|
341
568
|
waitForDebuggerOnStart: false,
|
|
342
569
|
flatten: true,
|
|
343
570
|
}).catch(() => {})
|
|
344
571
|
}
|
|
345
|
-
|
|
572
|
+
attachedTabId = session.tabId
|
|
573
|
+
if (isFocusedDefaultTarget) focusedTabId = session.tabId
|
|
574
|
+
return { target: { tabId: session.tabId }, session, tab }
|
|
346
575
|
} finally {
|
|
347
576
|
_release()
|
|
348
577
|
}
|
|
349
578
|
}
|
|
350
579
|
|
|
580
|
+
async function ensureAttached(params = {}) {
|
|
581
|
+
const attached = await ensureAttachedSession(params)
|
|
582
|
+
return attached.target
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
async function detachSession(session) {
|
|
586
|
+
if (!session) return
|
|
587
|
+
try {
|
|
588
|
+
if (session.attached) await chrome.debugger.detach({ tabId: session.tabId })
|
|
589
|
+
} catch (e) {
|
|
590
|
+
log(`detach 失败:${e.message}`)
|
|
591
|
+
} finally {
|
|
592
|
+
session.attached = false
|
|
593
|
+
if (attachedTabId === session.tabId) attachedTabId = null
|
|
594
|
+
resetDebuggerState(session)
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
351
598
|
async function maybeDetach(force = false) {
|
|
352
|
-
if (
|
|
353
|
-
|
|
354
|
-
await
|
|
355
|
-
} catch (e) {
|
|
356
|
-
log(`detach 失败:${e.message}`)
|
|
357
|
-
} finally {
|
|
358
|
-
attachedTabId = null
|
|
599
|
+
if (force) {
|
|
600
|
+
for (const session of sessionsByTabId.values()) {
|
|
601
|
+
await detachSession(session)
|
|
359
602
|
}
|
|
603
|
+
return
|
|
604
|
+
}
|
|
605
|
+
if (CONFIG.autoDetach && attachedTabId !== null) {
|
|
606
|
+
await detachSession(getSession(attachedTabId, { create: false }))
|
|
360
607
|
}
|
|
361
608
|
}
|
|
362
609
|
|
|
@@ -382,15 +629,185 @@ async function detachAllTargets() {
|
|
|
382
629
|
}
|
|
383
630
|
} catch {}
|
|
384
631
|
attachedTabId = null
|
|
632
|
+
focusedTabId = null
|
|
633
|
+
sessionsByTabId.clear()
|
|
385
634
|
}
|
|
386
635
|
|
|
387
636
|
// ========== 命令处理 ==========
|
|
388
637
|
|
|
638
|
+
async function buildTargetInfo(clientId) {
|
|
639
|
+
const client = getClientSession(clientId)
|
|
640
|
+
const viewingTab = await chrome.tabs.query({ active: true, lastFocusedWindow: true })
|
|
641
|
+
.then(([tab]) => tab || null)
|
|
642
|
+
.catch(() => null)
|
|
643
|
+
|
|
644
|
+
let targetTab = null
|
|
645
|
+
let targetError = ''
|
|
646
|
+
const targetTabId = client.targetMode === 'pinned' ? client.pinnedTabId : (viewingTab?.id ?? attachedTabId)
|
|
647
|
+
|
|
648
|
+
if (targetTabId !== null && targetTabId !== undefined) {
|
|
649
|
+
try {
|
|
650
|
+
targetTab = await chrome.tabs.get(targetTabId)
|
|
651
|
+
} catch (e) {
|
|
652
|
+
targetError = e.message
|
|
653
|
+
}
|
|
654
|
+
} else if (client.targetMode === 'focused') {
|
|
655
|
+
targetTab = viewingTab
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
return {
|
|
659
|
+
clientId: clientId || DEFAULT_CLIENT_ID,
|
|
660
|
+
targetMode: client.targetMode,
|
|
661
|
+
pinnedTabId: client.pinnedTabId,
|
|
662
|
+
focusedTabId,
|
|
663
|
+
attachedTabId,
|
|
664
|
+
attachedTabIds: [...sessionsByTabId.values()].filter((session) => session.attached).map((session) => session.tabId),
|
|
665
|
+
targetTab: summarizeTab(targetTab),
|
|
666
|
+
viewingTab: summarizeTab(viewingTab),
|
|
667
|
+
targets: await summarizeNamedTargets(),
|
|
668
|
+
targetError,
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function describeCommandTarget(params, session) {
|
|
673
|
+
if (params?.target) return String(params.target)
|
|
674
|
+
const names = getNamesForTab(session.tabId)
|
|
675
|
+
if (names.length) return names[0]
|
|
676
|
+
const client = getClientSession(params?.clientId)
|
|
677
|
+
if (client.targetMode === 'pinned' && client.pinnedTabId === session.tabId) return 'pinned'
|
|
678
|
+
return 'focused'
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
async function summarizeNamedTargets() {
|
|
682
|
+
const results = []
|
|
683
|
+
for (const [ownerId, client] of clientSessions.entries()) {
|
|
684
|
+
for (const [name, tabId] of client.namedTargets.entries()) {
|
|
685
|
+
const session = getSession(tabId, { create: false })
|
|
686
|
+
try {
|
|
687
|
+
const tab = await chrome.tabs.get(tabId)
|
|
688
|
+
results.push({
|
|
689
|
+
owner: ownerId,
|
|
690
|
+
name,
|
|
691
|
+
tabId,
|
|
692
|
+
tab: summarizeTab(tab),
|
|
693
|
+
attached: !!session?.attached,
|
|
694
|
+
errorCount: session?.lastErrors.filter((e) => e.severity === 'error').length || 0,
|
|
695
|
+
networkCount: (session?.networkRequests.length || 0) + (session?.requestMap.size || 0),
|
|
696
|
+
})
|
|
697
|
+
} catch (e) {
|
|
698
|
+
results.push({ owner: ownerId, name, tabId, attached: false, error: e.message })
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
return results
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
async function handleListTabs(params = {}) {
|
|
706
|
+
const tabs = await chrome.tabs.query({})
|
|
707
|
+
const targetInfo = await buildTargetInfo(params.clientId)
|
|
708
|
+
return {
|
|
709
|
+
...targetInfo,
|
|
710
|
+
tabs: tabs
|
|
711
|
+
.filter((tab) => tab.id !== undefined)
|
|
712
|
+
.sort((a, b) => (a.windowId - b.windowId) || (a.index - b.index))
|
|
713
|
+
.map(summarizeTab),
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
async function handleListTargets(params = {}) {
|
|
718
|
+
return buildTargetInfo(params.clientId)
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
async function bindTarget(name, tab, clientId) {
|
|
722
|
+
const client = getClientSession(clientId)
|
|
723
|
+
const previousTabId = client.namedTargets.get(name)
|
|
724
|
+
client.namedTargets.set(name, tab.id)
|
|
725
|
+
try {
|
|
726
|
+
await ensureAttachedSession({ target: name, clientId })
|
|
727
|
+
} catch (e) {
|
|
728
|
+
if (previousTabId !== undefined) client.namedTargets.set(name, previousTabId)
|
|
729
|
+
else client.namedTargets.delete(name)
|
|
730
|
+
throw e
|
|
731
|
+
}
|
|
732
|
+
if (previousTabId !== undefined && previousTabId !== tab.id && !isTabReferenced(previousTabId)) {
|
|
733
|
+
const previousSession = getSession(previousTabId, { create: false })
|
|
734
|
+
await detachSession(previousSession)
|
|
735
|
+
sessionsByTabId.delete(previousTabId)
|
|
736
|
+
}
|
|
737
|
+
broadcastStatus()
|
|
738
|
+
return {
|
|
739
|
+
bound: { name, tabId: tab.id, tab: summarizeTab(tab) },
|
|
740
|
+
...(await buildTargetInfo(clientId)),
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
async function handleBindTab(params = {}) {
|
|
745
|
+
const name = normalizeTargetName(params.name, 'name')
|
|
746
|
+
const tab = await findTabByParams(params)
|
|
747
|
+
return bindTarget(name, tab, params.clientId)
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
async function handleUnbindTab(params = {}) {
|
|
751
|
+
const client = getClientSession(params.clientId)
|
|
752
|
+
const name = normalizeTargetName(params.name || params.target, 'name')
|
|
753
|
+
const tabId = client.namedTargets.get(name)
|
|
754
|
+
if (tabId === undefined) throw new Error(`未绑定 target "${name}"`)
|
|
755
|
+
client.namedTargets.delete(name)
|
|
756
|
+
|
|
757
|
+
const session = getSession(tabId, { create: false })
|
|
758
|
+
if (session && !isTabReferenced(tabId)) {
|
|
759
|
+
await detachSession(session)
|
|
760
|
+
sessionsByTabId.delete(tabId)
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
broadcastStatus()
|
|
764
|
+
return {
|
|
765
|
+
unbound: { name, tabId },
|
|
766
|
+
...(await buildTargetInfo(params.clientId)),
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
async function pinTab(tab, clientId) {
|
|
771
|
+
const client = getClientSession(clientId)
|
|
772
|
+
const previousMode = client.targetMode
|
|
773
|
+
const previousPinnedTabId = client.pinnedTabId
|
|
774
|
+
client.targetMode = 'pinned'
|
|
775
|
+
client.pinnedTabId = tab.id
|
|
776
|
+
try {
|
|
777
|
+
await ensureAttachedSession({ tabId: tab.id, clientId })
|
|
778
|
+
} catch (e) {
|
|
779
|
+
client.targetMode = previousMode
|
|
780
|
+
client.pinnedTabId = previousPinnedTabId
|
|
781
|
+
throw e
|
|
782
|
+
}
|
|
783
|
+
broadcastStatus()
|
|
784
|
+
return buildTargetInfo(clientId)
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
async function handlePinCurrentTab(params = {}) {
|
|
788
|
+
return pinTab(await getFocusedTabOrThrow(), params.clientId)
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
async function handlePinTab(params = {}) {
|
|
792
|
+
return pinTab(await findTabByParams(params), params.clientId)
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
async function handleUnpinTab(params = {}) {
|
|
796
|
+
const client = getClientSession(params.clientId)
|
|
797
|
+
client.targetMode = 'focused'
|
|
798
|
+
client.pinnedTabId = null
|
|
799
|
+
if (state.enabled && state.connected) {
|
|
800
|
+
await ensureAttached({ clientId: params.clientId })
|
|
801
|
+
}
|
|
802
|
+
broadcastStatus()
|
|
803
|
+
return buildTargetInfo(params.clientId)
|
|
804
|
+
}
|
|
805
|
+
|
|
389
806
|
async function handleGetLastError(params = {}) {
|
|
390
|
-
await
|
|
807
|
+
const { session } = await ensureAttachedSession(params)
|
|
391
808
|
const severity = params.severity || "error"
|
|
392
809
|
const limit = Math.max(1, Math.min(params.limit || 20, CONFIG.maxErrors))
|
|
393
|
-
const allEvents = lastErrors.slice(0, CONFIG.maxErrors)
|
|
810
|
+
const allEvents = session.lastErrors.slice(0, CONFIG.maxErrors)
|
|
394
811
|
const filteredEvents = severity === "all"
|
|
395
812
|
? allEvents
|
|
396
813
|
: allEvents.filter((event) => (event.severity || "info") === severity)
|
|
@@ -404,7 +821,7 @@ async function handleGetLastError(params = {}) {
|
|
|
404
821
|
)
|
|
405
822
|
const events = filteredEvents.slice(0, limit)
|
|
406
823
|
return {
|
|
407
|
-
lastErrorLocation,
|
|
824
|
+
lastErrorLocation: session.lastErrorLocation,
|
|
408
825
|
summary: {
|
|
409
826
|
count: events.length,
|
|
410
827
|
cachedCount: allEvents.length,
|
|
@@ -418,17 +835,17 @@ async function handleGetLastError(params = {}) {
|
|
|
418
835
|
}
|
|
419
836
|
}
|
|
420
837
|
|
|
421
|
-
async function pickScriptId(preferUrlContains) {
|
|
838
|
+
async function pickScriptId(session, preferUrlContains) {
|
|
422
839
|
if (preferUrlContains) {
|
|
423
|
-
for (const [id, meta] of scriptMap.entries()) {
|
|
840
|
+
for (const [id, meta] of session.scriptMap.entries()) {
|
|
424
841
|
if (meta.url && meta.url.includes(preferUrlContains)) return { id, url: meta.url }
|
|
425
842
|
}
|
|
426
843
|
}
|
|
427
|
-
if (lastErrorLocation?.scriptId && scriptMap.has(lastErrorLocation.scriptId)) {
|
|
428
|
-
const meta = scriptMap.get(lastErrorLocation.scriptId)
|
|
429
|
-
return { id: lastErrorLocation.scriptId, url: meta.url }
|
|
844
|
+
if (session.lastErrorLocation?.scriptId && session.scriptMap.has(session.lastErrorLocation.scriptId)) {
|
|
845
|
+
const meta = session.scriptMap.get(session.lastErrorLocation.scriptId)
|
|
846
|
+
return { id: session.lastErrorLocation.scriptId, url: meta.url }
|
|
430
847
|
}
|
|
431
|
-
const first = scriptMap.entries().next().value
|
|
848
|
+
const first = session.scriptMap.entries().next().value
|
|
432
849
|
if (first) {
|
|
433
850
|
return { id: first[0], url: first[1].url }
|
|
434
851
|
}
|
|
@@ -436,15 +853,15 @@ async function pickScriptId(preferUrlContains) {
|
|
|
436
853
|
}
|
|
437
854
|
|
|
438
855
|
async function handleGetScriptSource(params = {}) {
|
|
439
|
-
const target = await
|
|
440
|
-
const chosen = await pickScriptId(params.scriptUrlContains)
|
|
856
|
+
const { target, session } = await ensureAttachedSession(params)
|
|
857
|
+
const chosen = await pickScriptId(session, params.scriptUrlContains)
|
|
441
858
|
const { scriptSource } = await chrome.debugger.sendCommand(target, "Debugger.getScriptSource", {
|
|
442
859
|
scriptId: chosen.id,
|
|
443
860
|
})
|
|
444
|
-
scriptSourceCache.set(chosen.id, scriptSource)
|
|
861
|
+
session.scriptSourceCache.set(chosen.id, scriptSource)
|
|
445
862
|
const location = {
|
|
446
|
-
line: params.line ?? lastErrorLocation?.line ?? null,
|
|
447
|
-
column: params.column ?? lastErrorLocation?.column ?? null,
|
|
863
|
+
line: params.line ?? session.lastErrorLocation?.line ?? null,
|
|
864
|
+
column: params.column ?? session.lastErrorLocation?.column ?? null,
|
|
448
865
|
}
|
|
449
866
|
return {
|
|
450
867
|
url: chosen.url,
|
|
@@ -456,7 +873,7 @@ async function handleGetScriptSource(params = {}) {
|
|
|
456
873
|
}
|
|
457
874
|
|
|
458
875
|
async function handleCoverageSnapshot(params = {}) {
|
|
459
|
-
const target = await ensureAttached()
|
|
876
|
+
const target = await ensureAttached(params)
|
|
460
877
|
const durationMs = params.durationMs || 1500
|
|
461
878
|
await chrome.debugger.sendCommand(target, "Profiler.startPreciseCoverage", {
|
|
462
879
|
callCount: true,
|
|
@@ -492,20 +909,20 @@ function findContexts(source, query, maxMatches) {
|
|
|
492
909
|
}
|
|
493
910
|
|
|
494
911
|
async function handleFindByString(params = {}) {
|
|
495
|
-
const target = await
|
|
912
|
+
const { target, session } = await ensureAttachedSession(params)
|
|
496
913
|
const query = params.query
|
|
497
914
|
const maxMatches = params.maxMatches || 5
|
|
498
915
|
const preferred = params.scriptUrlContains
|
|
499
916
|
|
|
500
917
|
const results = []
|
|
501
|
-
const entries = [...scriptMap.entries()]
|
|
918
|
+
const entries = [...session.scriptMap.entries()]
|
|
502
919
|
for (const [id, meta] of entries) {
|
|
503
920
|
if (preferred && (!meta.url || !meta.url.includes(preferred))) continue
|
|
504
|
-
if (!scriptSourceCache.has(id)) {
|
|
921
|
+
if (!session.scriptSourceCache.has(id)) {
|
|
505
922
|
const { scriptSource } = await chrome.debugger.sendCommand(target, "Debugger.getScriptSource", { scriptId: id })
|
|
506
|
-
scriptSourceCache.set(id, scriptSource)
|
|
923
|
+
session.scriptSourceCache.set(id, scriptSource)
|
|
507
924
|
}
|
|
508
|
-
const source = scriptSourceCache.get(id)
|
|
925
|
+
const source = session.scriptSourceCache.get(id)
|
|
509
926
|
const matches = findContexts(source, query, maxMatches - results.length)
|
|
510
927
|
if (matches.length) {
|
|
511
928
|
results.push({ url: meta.url, scriptId: id, matches })
|
|
@@ -516,8 +933,8 @@ async function handleFindByString(params = {}) {
|
|
|
516
933
|
return { query, results }
|
|
517
934
|
}
|
|
518
935
|
|
|
519
|
-
async function handleSymbolicHints() {
|
|
520
|
-
const target = await ensureAttached()
|
|
936
|
+
async function handleSymbolicHints(params = {}) {
|
|
937
|
+
const target = await ensureAttached(params)
|
|
521
938
|
const expression = `(function(){
|
|
522
939
|
try {
|
|
523
940
|
const resources = performance.getEntriesByType('resource').slice(-20).map(e => ({
|
|
@@ -540,7 +957,7 @@ async function handleSymbolicHints() {
|
|
|
540
957
|
}
|
|
541
958
|
|
|
542
959
|
async function handleEval(params = {}) {
|
|
543
|
-
const target = await ensureAttached()
|
|
960
|
+
const target = await ensureAttached(params)
|
|
544
961
|
const { result } = await chrome.debugger.sendCommand(target, "Runtime.evaluate", {
|
|
545
962
|
expression: params.code,
|
|
546
963
|
returnByValue: true,
|
|
@@ -549,11 +966,11 @@ async function handleEval(params = {}) {
|
|
|
549
966
|
}
|
|
550
967
|
|
|
551
968
|
async function handleListNetworkRequests(params = {}) {
|
|
552
|
-
await
|
|
969
|
+
const { session } = await ensureAttachedSession(params)
|
|
553
970
|
const { filter, method, status, resourceType, limit = 50, priorityMode = 'debug' } = params
|
|
554
971
|
|
|
555
|
-
let results = [...networkRequests]
|
|
556
|
-
const pending = [...requestMap.values()].map(r => ({ ...r, status: "pending" }))
|
|
972
|
+
let results = [...session.networkRequests]
|
|
973
|
+
const pending = [...session.requestMap.values()].map(r => ({ ...r, status: "pending" }))
|
|
557
974
|
results = [...pending, ...results]
|
|
558
975
|
|
|
559
976
|
if (filter) {
|
|
@@ -571,7 +988,7 @@ async function handleListNetworkRequests(params = {}) {
|
|
|
571
988
|
results = results.slice(0, limit)
|
|
572
989
|
|
|
573
990
|
return {
|
|
574
|
-
total: networkRequests.length + requestMap.size,
|
|
991
|
+
total: session.networkRequests.length + session.requestMap.size,
|
|
575
992
|
filtered: results.length,
|
|
576
993
|
priorityMode,
|
|
577
994
|
requests: results.map((entry) => GhostBridgeNetwork.buildNetworkRequestSummary(entry)),
|
|
@@ -579,12 +996,12 @@ async function handleListNetworkRequests(params = {}) {
|
|
|
579
996
|
}
|
|
580
997
|
|
|
581
998
|
async function handleGetNetworkDetail(params = {}) {
|
|
582
|
-
const target = await
|
|
999
|
+
const { target, session } = await ensureAttachedSession(params)
|
|
583
1000
|
const { requestId, includeBody = false } = params
|
|
584
1001
|
if (!requestId) throw new Error("需要提供 requestId")
|
|
585
1002
|
|
|
586
|
-
let entry = requestMap.get(requestId)
|
|
587
|
-
if (!entry) entry = networkRequests.find(r => r.requestId === requestId)
|
|
1003
|
+
let entry = session.requestMap.get(requestId)
|
|
1004
|
+
if (!entry) entry = session.networkRequests.find(r => r.requestId === requestId)
|
|
588
1005
|
if (!entry) throw new Error(`未找到请求: ${requestId}`)
|
|
589
1006
|
|
|
590
1007
|
const urlMeta = GhostBridgeNetwork.summarizeNetworkUrl(entry.url)
|
|
@@ -627,15 +1044,15 @@ async function handleGetNetworkDetail(params = {}) {
|
|
|
627
1044
|
return result
|
|
628
1045
|
}
|
|
629
1046
|
|
|
630
|
-
async function handleClearNetworkRequests() {
|
|
631
|
-
await
|
|
632
|
-
const count = networkRequests.length
|
|
633
|
-
networkRequests = []
|
|
1047
|
+
async function handleClearNetworkRequests(params = {}) {
|
|
1048
|
+
const { session } = await ensureAttachedSession(params)
|
|
1049
|
+
const count = session.networkRequests.length
|
|
1050
|
+
session.networkRequests = []
|
|
634
1051
|
return { cleared: count }
|
|
635
1052
|
}
|
|
636
1053
|
|
|
637
1054
|
async function handlePerfMetrics(params = {}) {
|
|
638
|
-
const target = await ensureAttached()
|
|
1055
|
+
const target = await ensureAttached(params)
|
|
639
1056
|
const { includeResources = true, includeTimings = true } = params
|
|
640
1057
|
|
|
641
1058
|
// 1. CDP Performance.getMetrics — 底层引擎指标
|
|
@@ -786,7 +1203,7 @@ function roundMs(seconds) {
|
|
|
786
1203
|
}
|
|
787
1204
|
|
|
788
1205
|
async function handleCaptureScreenshot(params = {}) {
|
|
789
|
-
const target = await ensureAttached()
|
|
1206
|
+
const target = await ensureAttached(params)
|
|
790
1207
|
const { format: requestedFormat, quality: requestedQuality, fullPage = false, clip } = params
|
|
791
1208
|
const format = requestedFormat || 'jpeg'
|
|
792
1209
|
const quality = format === 'jpeg'
|
|
@@ -866,7 +1283,7 @@ async function handleCaptureScreenshot(params = {}) {
|
|
|
866
1283
|
}
|
|
867
1284
|
|
|
868
1285
|
async function handleInspectPageSnapshot(params = {}) {
|
|
869
|
-
const target = await
|
|
1286
|
+
const { target, session } = await ensureAttachedSession(params)
|
|
870
1287
|
const { selector, includeInteractive = true, maxElements = 30 } = params
|
|
871
1288
|
const expression = GhostBridgeDom.buildInspectPageExpression({ selector, includeInteractive, maxElements })
|
|
872
1289
|
|
|
@@ -876,11 +1293,16 @@ async function handleInspectPageSnapshot(params = {}) {
|
|
|
876
1293
|
})
|
|
877
1294
|
|
|
878
1295
|
if (result?.value?.error) throw new Error(result.value.error)
|
|
879
|
-
|
|
1296
|
+
const value = result?.value
|
|
1297
|
+
if (value) {
|
|
1298
|
+
value.target = describeCommandTarget(params, session)
|
|
1299
|
+
value.tabId = session.tabId
|
|
1300
|
+
}
|
|
1301
|
+
return value
|
|
880
1302
|
}
|
|
881
1303
|
|
|
882
1304
|
async function handleGetPageContent(params = {}) {
|
|
883
|
-
const target = await ensureAttached()
|
|
1305
|
+
const target = await ensureAttached(params)
|
|
884
1306
|
const { mode = "text", selector, maxLength = 50000, includeMetadata = true } = params
|
|
885
1307
|
const expression = GhostBridgeDom.buildPageContentExpression({ mode, selector, maxLength, includeMetadata })
|
|
886
1308
|
|
|
@@ -896,7 +1318,7 @@ async function handleGetPageContent(params = {}) {
|
|
|
896
1318
|
// ========== DOM 交互:可交互元素快照 ==========
|
|
897
1319
|
|
|
898
1320
|
async function handleGetInteractiveSnapshot(params = {}) {
|
|
899
|
-
const target = await
|
|
1321
|
+
const { target, session } = await ensureAttachedSession(params)
|
|
900
1322
|
const { selector, includeText = true, maxElements = 100 } = params
|
|
901
1323
|
const expression = GhostBridgeDom.buildInteractiveSnapshotExpression({ selector, includeText, maxElements })
|
|
902
1324
|
|
|
@@ -906,13 +1328,22 @@ async function handleGetInteractiveSnapshot(params = {}) {
|
|
|
906
1328
|
})
|
|
907
1329
|
|
|
908
1330
|
if (result?.value?.error) throw new Error(result.value.error)
|
|
909
|
-
|
|
1331
|
+
const value = result?.value
|
|
1332
|
+
if (value) {
|
|
1333
|
+
value.target = describeCommandTarget(params, session)
|
|
1334
|
+
value.tabId = session.tabId
|
|
1335
|
+
}
|
|
1336
|
+
return value
|
|
910
1337
|
}
|
|
911
1338
|
|
|
912
1339
|
// ========== DOM 交互:动作分发器 ==========
|
|
913
1340
|
|
|
914
1341
|
async function handleDispatchAction(params = {}) {
|
|
915
|
-
const
|
|
1342
|
+
const anyNamedTargets = [...clientSessions.values()].some((client) => client.namedTargets.size > 0)
|
|
1343
|
+
if (anyNamedTargets && !params.target && params.tabId === undefined) {
|
|
1344
|
+
throw new Error("已绑定命名 target 时,dispatch_action 必须提供 target,避免跨页面误用 ref")
|
|
1345
|
+
}
|
|
1346
|
+
const target = await ensureAttached(params)
|
|
916
1347
|
const { ref, action, value, key, deltaX, deltaY, waitMs = 500 } = params
|
|
917
1348
|
|
|
918
1349
|
if (!ref) throw new Error("需要提供 ref(元素标识,如 'e1')")
|
|
@@ -1080,8 +1511,15 @@ async function handleDispatchAction(params = {}) {
|
|
|
1080
1511
|
|
|
1081
1512
|
// 处理来自服务器的命令
|
|
1082
1513
|
async function handleCommand(message) {
|
|
1083
|
-
const { id, command,
|
|
1514
|
+
const { id, command, token } = message
|
|
1084
1515
|
if (!id || !command) return
|
|
1516
|
+
|
|
1517
|
+
// 内部命令:服务器通知某个 MCP 会话断开,清理其 target 命名空间(不要求扩展已启用)
|
|
1518
|
+
if (command === "_clientDisconnected") {
|
|
1519
|
+
cleanupClientSession(message.params?.clientId)
|
|
1520
|
+
sendToServer({ id, result: { cleaned: true } })
|
|
1521
|
+
return
|
|
1522
|
+
}
|
|
1085
1523
|
if (!state.enabled) {
|
|
1086
1524
|
sendToServer({ id, error: "扩展已暂停,点击图标重新开启" })
|
|
1087
1525
|
return
|
|
@@ -1090,17 +1528,28 @@ async function handleCommand(message) {
|
|
|
1090
1528
|
sendToServer({ id, error: "token 校验失败" })
|
|
1091
1529
|
return
|
|
1092
1530
|
}
|
|
1531
|
+
// 命令所属的 MCP 会话:pin/bind 的命名空间按它隔离
|
|
1532
|
+
const clientId = message.clientId
|
|
1533
|
+
const params = { ...(message.params || {}), clientId }
|
|
1093
1534
|
try {
|
|
1094
1535
|
let result
|
|
1095
|
-
if (command === "
|
|
1536
|
+
if (command === "listTabs") result = await handleListTabs(params)
|
|
1537
|
+
else if (command === "getTargetTab") result = await buildTargetInfo(clientId)
|
|
1538
|
+
else if (command === "listTargets") result = await handleListTargets(params)
|
|
1539
|
+
else if (command === "bindTab") result = await handleBindTab(params)
|
|
1540
|
+
else if (command === "unbindTab") result = await handleUnbindTab(params)
|
|
1541
|
+
else if (command === "pinCurrentTab") result = await handlePinCurrentTab(params)
|
|
1542
|
+
else if (command === "pinTab") result = await handlePinTab(params)
|
|
1543
|
+
else if (command === "unpinTab") result = await handleUnpinTab(params)
|
|
1544
|
+
else if (command === "getLastError") result = await handleGetLastError(params)
|
|
1096
1545
|
else if (command === "getScriptSource") result = await handleGetScriptSource(params)
|
|
1097
1546
|
else if (command === "coverageSnapshot") result = await handleCoverageSnapshot(params)
|
|
1098
1547
|
else if (command === "findByString") result = await handleFindByString(params)
|
|
1099
|
-
else if (command === "symbolicHints") result = await handleSymbolicHints()
|
|
1548
|
+
else if (command === "symbolicHints") result = await handleSymbolicHints(params)
|
|
1100
1549
|
else if (command === "eval") result = await handleEval(params)
|
|
1101
1550
|
else if (command === "listNetworkRequests") result = await handleListNetworkRequests(params)
|
|
1102
1551
|
else if (command === "getNetworkDetail") result = await handleGetNetworkDetail(params)
|
|
1103
|
-
else if (command === "clearNetworkRequests") result = await handleClearNetworkRequests()
|
|
1552
|
+
else if (command === "clearNetworkRequests") result = await handleClearNetworkRequests(params)
|
|
1104
1553
|
else if (command === "perfMetrics") result = await handlePerfMetrics(params)
|
|
1105
1554
|
else if (command === "captureScreenshot") result = await handleCaptureScreenshot(params)
|
|
1106
1555
|
else if (command === "inspectPageSnapshot") result = await handleInspectPageSnapshot(params)
|
|
@@ -1124,8 +1573,7 @@ function sendToServer(data) {
|
|
|
1124
1573
|
|
|
1125
1574
|
// ========== 状态广播 ==========
|
|
1126
1575
|
|
|
1127
|
-
|
|
1128
|
-
function broadcastStatus() {
|
|
1576
|
+
function getConnectionStatus() {
|
|
1129
1577
|
let status
|
|
1130
1578
|
if (!state.enabled) {
|
|
1131
1579
|
status = 'disconnected'
|
|
@@ -1134,43 +1582,53 @@ function broadcastStatus() {
|
|
|
1134
1582
|
} else {
|
|
1135
1583
|
status = state.connectionStatus || 'connecting'
|
|
1136
1584
|
}
|
|
1585
|
+
return status
|
|
1586
|
+
}
|
|
1137
1587
|
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1588
|
+
async function buildPopupState() {
|
|
1589
|
+
const status = getConnectionStatus()
|
|
1590
|
+
const targetInfo = await buildTargetInfo()
|
|
1591
|
+
const targetTab = targetInfo.targetTab
|
|
1592
|
+
const viewingTab = targetInfo.viewingTab
|
|
1593
|
+
return {
|
|
1594
|
+
status,
|
|
1595
|
+
enabled: state.enabled,
|
|
1596
|
+
port: state.port,
|
|
1597
|
+
currentPort: state.currentPort,
|
|
1598
|
+
basePort: CONFIG.basePort,
|
|
1599
|
+
connectionError: state.connectionError,
|
|
1600
|
+
serverInfo: state.serverInfo,
|
|
1601
|
+
targetMode: targetInfo.targetMode,
|
|
1602
|
+
pinnedTabId: targetInfo.pinnedTabId,
|
|
1603
|
+
focusedTabId: targetInfo.focusedTabId,
|
|
1604
|
+
attachedTabId: targetInfo.attachedTabId,
|
|
1605
|
+
attachedTabIds: targetInfo.attachedTabIds,
|
|
1606
|
+
targets: targetInfo.targets,
|
|
1607
|
+
targetTab,
|
|
1608
|
+
viewingTab,
|
|
1609
|
+
targetError: targetInfo.targetError,
|
|
1610
|
+
tabTitle: targetTab?.title || '',
|
|
1611
|
+
tabUrl: targetTab?.url || '',
|
|
1612
|
+
viewingTitle: viewingTab?.title || '',
|
|
1613
|
+
viewingUrl: viewingTab?.url || '',
|
|
1149
1614
|
}
|
|
1615
|
+
}
|
|
1150
1616
|
|
|
1151
|
-
|
|
1152
|
-
|
|
1617
|
+
// 主动推送状态给 popup
|
|
1618
|
+
async function broadcastStatus() {
|
|
1619
|
+
try {
|
|
1153
1620
|
chrome.runtime.sendMessage({
|
|
1154
1621
|
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
|
-
}
|
|
1622
|
+
state: await buildPopupState()
|
|
1167
1623
|
}).catch(() => {}) // popup 可能未打开,忽略错误
|
|
1624
|
+
} catch (e) {
|
|
1625
|
+
log(`状态广播失败:${e.message}`)
|
|
1168
1626
|
}
|
|
1169
1627
|
}
|
|
1170
1628
|
|
|
1171
1629
|
// 监听被调试页面的导航变化,实时推送到 popup
|
|
1172
1630
|
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
|
|
1173
|
-
if (tabId
|
|
1631
|
+
if ((sessionsByTabId.has(tabId) || tab.active) && (changeInfo.title || changeInfo.url)) {
|
|
1174
1632
|
if (state.connected) broadcastStatus()
|
|
1175
1633
|
}
|
|
1176
1634
|
})
|
|
@@ -1179,8 +1637,12 @@ chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
|
|
|
1179
1637
|
chrome.tabs.onActivated.addListener(async (activeInfo) => {
|
|
1180
1638
|
if (state.enabled && state.connected) {
|
|
1181
1639
|
try {
|
|
1182
|
-
//
|
|
1183
|
-
|
|
1640
|
+
// focused 模式保持旧行为:切换 Tab 时自动跟随(focused 是共享语义,任一会话处于 focused 即跟随)
|
|
1641
|
+
const anyFocused = clientSessions.size === 0
|
|
1642
|
+
|| [...clientSessions.values()].some((client) => client.targetMode === 'focused')
|
|
1643
|
+
if (anyFocused) {
|
|
1644
|
+
await ensureAttached()
|
|
1645
|
+
}
|
|
1184
1646
|
broadcastStatus()
|
|
1185
1647
|
} catch (e) {
|
|
1186
1648
|
log(`自动跟随切换 Tab 失败:${e.message}`)
|
|
@@ -1188,6 +1650,26 @@ chrome.tabs.onActivated.addListener(async (activeInfo) => {
|
|
|
1188
1650
|
}
|
|
1189
1651
|
})
|
|
1190
1652
|
|
|
1653
|
+
chrome.tabs.onRemoved.addListener((tabId) => {
|
|
1654
|
+
for (const client of clientSessions.values()) {
|
|
1655
|
+
if (client.pinnedTabId === tabId) {
|
|
1656
|
+
client.targetMode = 'focused'
|
|
1657
|
+
client.pinnedTabId = null
|
|
1658
|
+
}
|
|
1659
|
+
for (const [name, boundTabId] of client.namedTargets.entries()) {
|
|
1660
|
+
if (boundTabId === tabId) client.namedTargets.delete(name)
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
if (tabId === attachedTabId) {
|
|
1664
|
+
attachedTabId = null
|
|
1665
|
+
}
|
|
1666
|
+
if (tabId === focusedTabId) {
|
|
1667
|
+
focusedTabId = null
|
|
1668
|
+
}
|
|
1669
|
+
sessionsByTabId.delete(tabId)
|
|
1670
|
+
if (state.connected) broadcastStatus()
|
|
1671
|
+
})
|
|
1672
|
+
|
|
1191
1673
|
|
|
1192
1674
|
// ========== 消息监听 ==========
|
|
1193
1675
|
|
|
@@ -1210,29 +1692,38 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|
|
1210
1692
|
state.currentPort = message.port
|
|
1211
1693
|
state.connectionStatus = 'connected'
|
|
1212
1694
|
state.connectionError = ''
|
|
1695
|
+
state.serverInfo = message.serverInfo || null
|
|
1213
1696
|
setBadgeState('on')
|
|
1697
|
+
// 服务器可能已重启,clientId 分配从零开始,旧会话桶全部作废
|
|
1698
|
+
clientSessions.clear()
|
|
1214
1699
|
log(`✅ 已连接到 ghost-bridge 服务 (端口 ${message.port})`)
|
|
1215
|
-
ensureAttached()
|
|
1700
|
+
ensureAttached()
|
|
1701
|
+
.then(() => broadcastStatus())
|
|
1702
|
+
.catch((e) => log(`attach 失败:${e.message}`))
|
|
1216
1703
|
} else if (message.status === 'disconnected') {
|
|
1217
1704
|
state.connected = false
|
|
1218
1705
|
state.port = null
|
|
1219
1706
|
state.connectionStatus = 'connecting'
|
|
1220
1707
|
state.connectionError = ''
|
|
1708
|
+
state.serverInfo = null
|
|
1221
1709
|
if (state.enabled) setBadgeState('connecting')
|
|
1222
1710
|
} else if (message.status === 'connecting') {
|
|
1223
1711
|
state.currentPort = message.currentPort
|
|
1224
1712
|
state.connectionStatus = 'connecting'
|
|
1225
1713
|
state.connectionError = ''
|
|
1714
|
+
state.serverInfo = null
|
|
1226
1715
|
setBadgeState('connecting')
|
|
1227
1716
|
} else if (message.status === 'error') {
|
|
1228
1717
|
state.currentPort = message.currentPort
|
|
1229
1718
|
state.connectionStatus = 'error'
|
|
1230
1719
|
state.connectionError = message.errorMessage || ''
|
|
1720
|
+
state.serverInfo = null
|
|
1231
1721
|
setBadgeState('err')
|
|
1232
1722
|
} else if (message.status === 'not_found') {
|
|
1233
1723
|
state.currentPort = message.currentPort
|
|
1234
1724
|
state.connectionStatus = 'not_found'
|
|
1235
|
-
state.connectionError = ''
|
|
1725
|
+
state.connectionError = message.errorMessage || ''
|
|
1726
|
+
state.serverInfo = null
|
|
1236
1727
|
setBadgeState('connecting')
|
|
1237
1728
|
}
|
|
1238
1729
|
broadcastStatus() // 状态变化时主动推送
|
|
@@ -1258,44 +1749,25 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|
|
1258
1749
|
|
|
1259
1750
|
// 来自 popup 的状态查询
|
|
1260
1751
|
if (message.type === 'getStatus') {
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
} else if (state.connected) {
|
|
1265
|
-
status = 'connected'
|
|
1266
|
-
} else {
|
|
1267
|
-
status = state.connectionStatus || 'connecting'
|
|
1268
|
-
}
|
|
1752
|
+
buildPopupState().then(sendResponse).catch((e) => sendResponse({ status: 'error', connectionError: e.message }))
|
|
1753
|
+
return true
|
|
1754
|
+
}
|
|
1269
1755
|
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
tabUrl = t.url
|
|
1275
|
-
tabTitle = t.title
|
|
1276
|
-
sendStatusResponse()
|
|
1277
|
-
}).catch(() => {
|
|
1278
|
-
sendStatusResponse()
|
|
1279
|
-
})
|
|
1280
|
-
} else {
|
|
1281
|
-
sendStatusResponse()
|
|
1756
|
+
if (message.type === 'pinCurrentTab') {
|
|
1757
|
+
if (!state.enabled || !state.connected) {
|
|
1758
|
+
sendResponse({ ok: false, error: "Ghost Bridge 尚未连接" })
|
|
1759
|
+
return true
|
|
1282
1760
|
}
|
|
1761
|
+
handlePinCurrentTab().then((result) => sendResponse({ ok: true, result })).catch((e) => sendResponse({ ok: false, error: e.message }))
|
|
1762
|
+
return true
|
|
1763
|
+
}
|
|
1283
1764
|
|
|
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
|
-
})
|
|
1765
|
+
if (message.type === 'unpinTab') {
|
|
1766
|
+
if (!state.enabled || !state.connected) {
|
|
1767
|
+
sendResponse({ ok: false, error: "Ghost Bridge 尚未连接" })
|
|
1768
|
+
return true
|
|
1298
1769
|
}
|
|
1770
|
+
handleUnpinTab().then((result) => sendResponse({ ok: true, result })).catch((e) => sendResponse({ ok: false, error: e.message }))
|
|
1299
1771
|
return true
|
|
1300
1772
|
}
|
|
1301
1773
|
|
|
@@ -1303,60 +1775,56 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|
|
1303
1775
|
if (message.type === 'connect') {
|
|
1304
1776
|
if (message.port) {
|
|
1305
1777
|
CONFIG.basePort = message.port
|
|
1306
|
-
chrome.storage.local.set({ basePort: message.port })
|
|
1307
1778
|
}
|
|
1308
|
-
state.enabled = true
|
|
1309
|
-
state.connected = false
|
|
1310
|
-
state.port = null
|
|
1311
|
-
state.currentPort = CONFIG.basePort
|
|
1312
|
-
state.connectionStatus = 'connecting'
|
|
1313
|
-
state.connectionError = ''
|
|
1314
|
-
setBadgeState('connecting')
|
|
1315
|
-
|
|
1316
|
-
// 启动 offscreen 并开始连接
|
|
1317
|
-
setupOffscreenDocument().then(() => {
|
|
1318
|
-
chrome.runtime.sendMessage({
|
|
1319
|
-
type: 'connect',
|
|
1320
|
-
basePort: CONFIG.basePort,
|
|
1321
|
-
token: CONFIG.token,
|
|
1322
|
-
}).catch(() => {})
|
|
1323
|
-
})
|
|
1324
1779
|
|
|
1325
|
-
|
|
1780
|
+
startBridgeConnection({ persist: true })
|
|
1781
|
+
.then(() => sendResponse({ ok: true }))
|
|
1782
|
+
.catch((e) => sendResponse({ ok: false, error: e.message }))
|
|
1326
1783
|
return true
|
|
1327
1784
|
}
|
|
1328
1785
|
|
|
1329
1786
|
// 来自 popup 的断开请求
|
|
1330
1787
|
if (message.type === 'disconnect') {
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
state.currentPort = null
|
|
1335
|
-
state.connectionStatus = 'disconnected'
|
|
1336
|
-
state.connectionError = ''
|
|
1337
|
-
setBadgeState('off')
|
|
1338
|
-
detachAllTargets().catch(() => {})
|
|
1339
|
-
|
|
1340
|
-
// 通知 offscreen 断开 (WebSocket 清除)
|
|
1341
|
-
chrome.runtime.sendMessage({ type: 'disconnect' }).catch(() => {})
|
|
1342
|
-
|
|
1343
|
-
// 关键修复:显式销毁 offscreen document 防止内存泄漏
|
|
1344
|
-
closeOffscreenDocument().catch(() => {})
|
|
1345
|
-
|
|
1346
|
-
sendResponse({ ok: true })
|
|
1788
|
+
stopBridgeConnection({ persist: true })
|
|
1789
|
+
.then(() => sendResponse({ ok: true }))
|
|
1790
|
+
.catch((e) => sendResponse({ ok: false, error: e.message }))
|
|
1347
1791
|
return true
|
|
1348
1792
|
}
|
|
1349
1793
|
|
|
1350
1794
|
return false
|
|
1351
1795
|
})
|
|
1352
1796
|
|
|
1353
|
-
//
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1797
|
+
// ========== 唤醒探活钩子 ==========
|
|
1798
|
+
// 系统锁屏/睡眠唤醒后,WebSocket 可能处于半开状态(onclose 不触发、徽章仍显示已连接),
|
|
1799
|
+
// 通知 offscreen 立即发一次心跳:无响应则关闭死链并马上重连,不等 15 秒周期心跳超时
|
|
1800
|
+
chrome.idle.onStateChanged.addListener(async (idleState) => {
|
|
1801
|
+
if (idleState !== 'active') return
|
|
1802
|
+
if (!state.enabled) return
|
|
1803
|
+
log('系统唤醒,触发连接探活...')
|
|
1804
|
+
try {
|
|
1805
|
+
await setupOffscreenDocument()
|
|
1806
|
+
const alive = await chrome.runtime.sendMessage({ type: 'healthCheck' }).catch(() => null)
|
|
1807
|
+
// alive === false 说明 offscreen 已自行触发重连,无需干预;
|
|
1808
|
+
// 仅当 offscreen 完全无响应(可能被回收)时才走完整连接流程兜底
|
|
1809
|
+
if (alive !== true && alive !== false) {
|
|
1810
|
+
const status = await chrome.runtime.sendMessage({ type: 'getOffscreenStatus' }).catch(() => null)
|
|
1811
|
+
if (!status || !status.connected) {
|
|
1812
|
+
log('唤醒探活无响应,重新建立连接...')
|
|
1813
|
+
await startBridgeConnection()
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
} catch (e) {
|
|
1817
|
+
log(`唤醒探活失败:${e.message}`)
|
|
1357
1818
|
}
|
|
1358
1819
|
})
|
|
1359
1820
|
|
|
1360
|
-
|
|
1361
|
-
|
|
1821
|
+
chrome.runtime.onStartup.addListener(() => {
|
|
1822
|
+
restoreBridgeConnection()
|
|
1823
|
+
})
|
|
1824
|
+
|
|
1825
|
+
chrome.runtime.onInstalled.addListener(() => {
|
|
1826
|
+
restoreBridgeConnection()
|
|
1827
|
+
})
|
|
1828
|
+
|
|
1829
|
+
restoreBridgeConnection()
|
|
1362
1830
|
log("Ghost Bridge background 已加载")
|