ghost-bridge 0.8.0 → 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 +15 -1
- package/dist/server.js +158 -38
- package/extension/background.js +529 -216
- package/extension/manifest.json +3 -2
- package/extension/offscreen.js +149 -10
- package/package.json +4 -4
package/extension/background.js
CHANGED
|
@@ -12,20 +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
|
-
|
|
23
|
-
|
|
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
|
+
|
|
24
54
|
let state = { enabled: false, connected: false, port: null, currentPort: null, connectionStatus: 'disconnected', connectionError: '', serverInfo: null }
|
|
25
55
|
|
|
26
56
|
// 待处理的请求(等待 offscreen 响应)
|
|
27
57
|
const pendingRequests = new Map()
|
|
28
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
|
+
|
|
29
92
|
function setBadgeState(status) {
|
|
30
93
|
const map = {
|
|
31
94
|
connecting: { text: "…", color: "#999" },
|
|
@@ -90,14 +153,77 @@ async function closeOffscreenDocument() {
|
|
|
90
153
|
}
|
|
91
154
|
}
|
|
92
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
|
+
|
|
93
218
|
// ========== Chrome Debugger 事件处理 ==========
|
|
94
219
|
|
|
95
220
|
chrome.debugger.onEvent.addListener((source, method, params) => {
|
|
96
|
-
|
|
221
|
+
const session = getSession(source.tabId, { create: false })
|
|
222
|
+
if (!session) return
|
|
97
223
|
if (!state.enabled) return
|
|
98
224
|
|
|
99
225
|
if (method === "Debugger.scriptParsed") {
|
|
100
|
-
scriptMap.set(params.scriptId, { url: params.url || "(inline)" })
|
|
226
|
+
session.scriptMap.set(params.scriptId, { url: params.url || "(inline)" })
|
|
101
227
|
}
|
|
102
228
|
|
|
103
229
|
if (method === "Runtime.exceptionThrown") {
|
|
@@ -114,18 +240,18 @@ chrome.debugger.onEvent.addListener((source, method, params) => {
|
|
|
114
240
|
stack: compactStack(detail.stackTrace),
|
|
115
241
|
timestamp: Date.now(),
|
|
116
242
|
}
|
|
117
|
-
lastErrorLocation = {
|
|
243
|
+
session.lastErrorLocation = {
|
|
118
244
|
url: entry.url,
|
|
119
245
|
line: entry.line,
|
|
120
246
|
column: entry.column,
|
|
121
247
|
scriptId: entry.scriptId,
|
|
122
248
|
}
|
|
123
|
-
pushError(entry)
|
|
249
|
+
pushError(session, entry)
|
|
124
250
|
}
|
|
125
251
|
|
|
126
252
|
if (method === "Log.entryAdded") {
|
|
127
253
|
const entry = params?.entry || {}
|
|
128
|
-
pushError({
|
|
254
|
+
pushError(session, {
|
|
129
255
|
type: entry.level || "log",
|
|
130
256
|
severity: entry.level === "warning" ? "warn" : entry.level === "error" ? "error" : "info",
|
|
131
257
|
url: entry.source || entry.url,
|
|
@@ -138,7 +264,7 @@ chrome.debugger.onEvent.addListener((source, method, params) => {
|
|
|
138
264
|
|
|
139
265
|
if (method === "Runtime.consoleAPICalled") {
|
|
140
266
|
const args = (params.args || []).map((a) => a.description || a.value).filter(Boolean)
|
|
141
|
-
pushError({
|
|
267
|
+
pushError(session, {
|
|
142
268
|
type: params.type || "console",
|
|
143
269
|
severity: params.type === "error" ? "error" : params.type === "warning" ? "warn" : "info",
|
|
144
270
|
url: params.stackTrace?.callFrames?.[0]?.url,
|
|
@@ -153,6 +279,7 @@ chrome.debugger.onEvent.addListener((source, method, params) => {
|
|
|
153
279
|
if (method === "Network.requestWillBeSent") {
|
|
154
280
|
const req = params.request || {}
|
|
155
281
|
const entry = {
|
|
282
|
+
tabId: source.tabId,
|
|
156
283
|
requestId: params.requestId,
|
|
157
284
|
url: req.url,
|
|
158
285
|
method: req.method || "GET",
|
|
@@ -164,13 +291,13 @@ chrome.debugger.onEvent.addListener((source, method, params) => {
|
|
|
164
291
|
timestamp: Date.now(),
|
|
165
292
|
status: "pending",
|
|
166
293
|
}
|
|
167
|
-
requestMap.set(params.requestId, entry)
|
|
168
|
-
trimPendingRequests()
|
|
294
|
+
session.requestMap.set(params.requestId, entry)
|
|
295
|
+
trimPendingRequests(session)
|
|
169
296
|
}
|
|
170
297
|
|
|
171
298
|
if (method === "Network.responseReceived") {
|
|
172
299
|
const res = params.response || {}
|
|
173
|
-
const entry = requestMap.get(params.requestId)
|
|
300
|
+
const entry = session.requestMap.get(params.requestId)
|
|
174
301
|
if (entry) {
|
|
175
302
|
entry.status = res.status >= 400 ? "error" : "success"
|
|
176
303
|
entry.statusCode = res.status
|
|
@@ -183,7 +310,7 @@ chrome.debugger.onEvent.addListener((source, method, params) => {
|
|
|
183
310
|
entry.timing = res.timing
|
|
184
311
|
entry.encodedDataLength = params.encodedDataLength
|
|
185
312
|
if (res.status >= 400) {
|
|
186
|
-
pushError({
|
|
313
|
+
pushError(session, {
|
|
187
314
|
type: "network",
|
|
188
315
|
severity: "error",
|
|
189
316
|
url: res.url || entry.url,
|
|
@@ -199,7 +326,7 @@ chrome.debugger.onEvent.addListener((source, method, params) => {
|
|
|
199
326
|
}
|
|
200
327
|
|
|
201
328
|
if (method === "Network.loadingFinished") {
|
|
202
|
-
const entry = requestMap.get(params.requestId)
|
|
329
|
+
const entry = session.requestMap.get(params.requestId)
|
|
203
330
|
if (entry) {
|
|
204
331
|
entry.endTime = params.timestamp
|
|
205
332
|
entry.encodedDataLength = params.encodedDataLength
|
|
@@ -207,19 +334,19 @@ chrome.debugger.onEvent.addListener((source, method, params) => {
|
|
|
207
334
|
? Math.round((entry.endTime - entry.startTime) * 1000)
|
|
208
335
|
: null
|
|
209
336
|
if (entry.status === "pending") entry.status = "success"
|
|
210
|
-
pushNetworkRequest(entry)
|
|
211
|
-
requestMap.delete(params.requestId)
|
|
337
|
+
pushNetworkRequest(session, entry)
|
|
338
|
+
session.requestMap.delete(params.requestId)
|
|
212
339
|
}
|
|
213
340
|
}
|
|
214
341
|
|
|
215
342
|
if (method === "Network.loadingFailed") {
|
|
216
|
-
const entry = requestMap.get(params.requestId)
|
|
343
|
+
const entry = session.requestMap.get(params.requestId)
|
|
217
344
|
if (entry) {
|
|
218
345
|
entry.status = "failed"
|
|
219
346
|
entry.errorText = params.errorText
|
|
220
347
|
entry.canceled = params.canceled
|
|
221
348
|
entry.blockedReason = params.blockedReason
|
|
222
|
-
pushError({
|
|
349
|
+
pushError(session, {
|
|
223
350
|
type: "network",
|
|
224
351
|
severity: "error",
|
|
225
352
|
url: entry.url,
|
|
@@ -228,29 +355,31 @@ chrome.debugger.onEvent.addListener((source, method, params) => {
|
|
|
228
355
|
text: params.errorText,
|
|
229
356
|
timestamp: Date.now(),
|
|
230
357
|
})
|
|
231
|
-
pushNetworkRequest(entry)
|
|
232
|
-
requestMap.delete(params.requestId)
|
|
358
|
+
pushNetworkRequest(session, entry)
|
|
359
|
+
session.requestMap.delete(params.requestId)
|
|
233
360
|
}
|
|
234
361
|
}
|
|
235
362
|
})
|
|
236
363
|
|
|
237
|
-
function pushNetworkRequest(entry) {
|
|
238
|
-
networkRequests.unshift(entry)
|
|
239
|
-
trimNetworkRequests()
|
|
364
|
+
function pushNetworkRequest(session, entry) {
|
|
365
|
+
session.networkRequests.unshift(entry)
|
|
366
|
+
trimNetworkRequests(session)
|
|
240
367
|
}
|
|
241
368
|
|
|
242
|
-
function trimNetworkRequests() {
|
|
243
|
-
GhostBridgeNetwork.trimTrackedRequests(networkRequests, CONFIG.maxRequestsTracked)
|
|
369
|
+
function trimNetworkRequests(session) {
|
|
370
|
+
GhostBridgeNetwork.trimTrackedRequests(session.networkRequests, CONFIG.maxRequestsTracked)
|
|
244
371
|
}
|
|
245
372
|
|
|
246
|
-
function trimPendingRequests() {
|
|
247
|
-
GhostBridgeNetwork.trimPendingRequestMap(requestMap, CONFIG.maxRequestsTracked * 2)
|
|
373
|
+
function trimPendingRequests(session) {
|
|
374
|
+
GhostBridgeNetwork.trimPendingRequestMap(session.requestMap, CONFIG.maxRequestsTracked * 2)
|
|
248
375
|
}
|
|
249
376
|
|
|
250
377
|
chrome.debugger.onDetach.addListener((source, reason) => {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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)
|
|
254
383
|
|
|
255
384
|
if (!state.enabled) return
|
|
256
385
|
if (reason === "canceled_by_user") {
|
|
@@ -270,15 +399,15 @@ chrome.debugger.onDetach.addListener((source, reason) => {
|
|
|
270
399
|
}
|
|
271
400
|
})
|
|
272
401
|
|
|
273
|
-
function pushError(entry) {
|
|
274
|
-
lastErrors.unshift(entry)
|
|
275
|
-
if (lastErrors.length > CONFIG.maxErrors) {
|
|
276
|
-
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
|
|
277
406
|
.map((e, i) => ({ sev: e.severity || "info", i }))
|
|
278
407
|
.reverse()
|
|
279
408
|
.find((e) => e.sev !== "error")?.i
|
|
280
|
-
if (dropIdx !== undefined) lastErrors.splice(dropIdx, 1)
|
|
281
|
-
else lastErrors.pop()
|
|
409
|
+
if (dropIdx !== undefined) session.lastErrors.splice(dropIdx, 1)
|
|
410
|
+
else session.lastErrors.pop()
|
|
282
411
|
}
|
|
283
412
|
}
|
|
284
413
|
|
|
@@ -294,13 +423,6 @@ function compactStack(stackTrace) {
|
|
|
294
423
|
|
|
295
424
|
// ========== Debugger 操作 ==========
|
|
296
425
|
|
|
297
|
-
function resetDebuggerState() {
|
|
298
|
-
scriptMap = new Map()
|
|
299
|
-
scriptSourceCache = new Map()
|
|
300
|
-
networkRequests = []
|
|
301
|
-
requestMap = new Map()
|
|
302
|
-
}
|
|
303
|
-
|
|
304
426
|
function summarizeTab(tab) {
|
|
305
427
|
if (!tab) return null
|
|
306
428
|
return {
|
|
@@ -330,13 +452,71 @@ async function getTabOrThrow(tabId) {
|
|
|
330
452
|
}
|
|
331
453
|
}
|
|
332
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
|
+
|
|
333
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
|
+
}
|
|
334
514
|
if (params.tabId !== undefined) {
|
|
335
515
|
return getTabOrThrow(Number(params.tabId))
|
|
336
516
|
}
|
|
337
|
-
if (targetMode === 'pinned') {
|
|
338
|
-
if (pinnedTabId === null) throw new Error("锁定的标签页不可用,请重新 pin")
|
|
339
|
-
return getTabOrThrow(pinnedTabId)
|
|
517
|
+
if (client.targetMode === 'pinned') {
|
|
518
|
+
if (client.pinnedTabId === null) throw new Error("锁定的标签页不可用,请重新 pin")
|
|
519
|
+
return getTabOrThrow(client.pinnedTabId)
|
|
340
520
|
}
|
|
341
521
|
return getFocusedTabOrThrow()
|
|
342
522
|
}
|
|
@@ -344,7 +524,7 @@ async function resolveTargetTab(params = {}) {
|
|
|
344
524
|
// attach 互斥锁:防止并发调用 ensureAttached 导致重复 attach / 状态竞态
|
|
345
525
|
let _attachLock = Promise.resolve()
|
|
346
526
|
|
|
347
|
-
async function
|
|
527
|
+
async function ensureAttachedSession(params = {}) {
|
|
348
528
|
let _release
|
|
349
529
|
const _prev = _attachLock
|
|
350
530
|
_attachLock = new Promise(r => _release = r)
|
|
@@ -352,15 +532,20 @@ async function ensureAttached(params = {}) {
|
|
|
352
532
|
try {
|
|
353
533
|
if (!state.enabled) throw new Error("扩展已暂停,点击图标开启后再试")
|
|
354
534
|
const tab = await resolveTargetTab(params)
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
}
|
|
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) {
|
|
359
544
|
try {
|
|
360
545
|
await chrome.debugger.attach({ tabId: tab.id }, "1.3")
|
|
361
546
|
setBadgeState("on")
|
|
362
547
|
} catch (e) {
|
|
363
|
-
attachedTabId = null
|
|
548
|
+
if (attachedTabId === tab.id) attachedTabId = null
|
|
364
549
|
if (state.connected) {
|
|
365
550
|
setBadgeState("on")
|
|
366
551
|
} else {
|
|
@@ -368,38 +553,57 @@ async function ensureAttached(params = {}) {
|
|
|
368
553
|
}
|
|
369
554
|
throw e
|
|
370
555
|
}
|
|
371
|
-
|
|
372
|
-
resetDebuggerState()
|
|
373
|
-
await chrome.debugger.sendCommand({ tabId:
|
|
374
|
-
await chrome.debugger.sendCommand({ tabId:
|
|
375
|
-
await chrome.debugger.sendCommand({ tabId:
|
|
376
|
-
await chrome.debugger.sendCommand({ tabId:
|
|
377
|
-
await chrome.debugger.sendCommand({ tabId:
|
|
378
|
-
await chrome.debugger.sendCommand({ tabId:
|
|
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(() => {})
|
|
379
564
|
|
|
380
565
|
// Enable auto-attach to sub-targets (iframes, workers) for comprehensive capture
|
|
381
|
-
await chrome.debugger.sendCommand({ tabId:
|
|
566
|
+
await chrome.debugger.sendCommand({ tabId: session.tabId }, "Target.setAutoAttach", {
|
|
382
567
|
autoAttach: true,
|
|
383
568
|
waitForDebuggerOnStart: false,
|
|
384
569
|
flatten: true,
|
|
385
570
|
}).catch(() => {})
|
|
386
571
|
}
|
|
387
|
-
|
|
572
|
+
attachedTabId = session.tabId
|
|
573
|
+
if (isFocusedDefaultTarget) focusedTabId = session.tabId
|
|
574
|
+
return { target: { tabId: session.tabId }, session, tab }
|
|
388
575
|
} finally {
|
|
389
576
|
_release()
|
|
390
577
|
}
|
|
391
578
|
}
|
|
392
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
|
+
|
|
393
598
|
async function maybeDetach(force = false) {
|
|
394
|
-
if (
|
|
395
|
-
|
|
396
|
-
await
|
|
397
|
-
} catch (e) {
|
|
398
|
-
log(`detach 失败:${e.message}`)
|
|
399
|
-
} finally {
|
|
400
|
-
attachedTabId = null
|
|
401
|
-
resetDebuggerState()
|
|
599
|
+
if (force) {
|
|
600
|
+
for (const session of sessionsByTabId.values()) {
|
|
601
|
+
await detachSession(session)
|
|
402
602
|
}
|
|
603
|
+
return
|
|
604
|
+
}
|
|
605
|
+
if (CONFIG.autoDetach && attachedTabId !== null) {
|
|
606
|
+
await detachSession(getSession(attachedTabId, { create: false }))
|
|
403
607
|
}
|
|
404
608
|
}
|
|
405
609
|
|
|
@@ -425,19 +629,21 @@ async function detachAllTargets() {
|
|
|
425
629
|
}
|
|
426
630
|
} catch {}
|
|
427
631
|
attachedTabId = null
|
|
428
|
-
|
|
632
|
+
focusedTabId = null
|
|
633
|
+
sessionsByTabId.clear()
|
|
429
634
|
}
|
|
430
635
|
|
|
431
636
|
// ========== 命令处理 ==========
|
|
432
637
|
|
|
433
|
-
async function buildTargetInfo() {
|
|
638
|
+
async function buildTargetInfo(clientId) {
|
|
639
|
+
const client = getClientSession(clientId)
|
|
434
640
|
const viewingTab = await chrome.tabs.query({ active: true, lastFocusedWindow: true })
|
|
435
641
|
.then(([tab]) => tab || null)
|
|
436
642
|
.catch(() => null)
|
|
437
643
|
|
|
438
644
|
let targetTab = null
|
|
439
645
|
let targetError = ''
|
|
440
|
-
const targetTabId = targetMode === 'pinned' ? pinnedTabId : attachedTabId
|
|
646
|
+
const targetTabId = client.targetMode === 'pinned' ? client.pinnedTabId : (viewingTab?.id ?? attachedTabId)
|
|
441
647
|
|
|
442
648
|
if (targetTabId !== null && targetTabId !== undefined) {
|
|
443
649
|
try {
|
|
@@ -445,23 +651,60 @@ async function buildTargetInfo() {
|
|
|
445
651
|
} catch (e) {
|
|
446
652
|
targetError = e.message
|
|
447
653
|
}
|
|
448
|
-
} else if (targetMode === 'focused') {
|
|
654
|
+
} else if (client.targetMode === 'focused') {
|
|
449
655
|
targetTab = viewingTab
|
|
450
656
|
}
|
|
451
657
|
|
|
452
658
|
return {
|
|
453
|
-
|
|
454
|
-
|
|
659
|
+
clientId: clientId || DEFAULT_CLIENT_ID,
|
|
660
|
+
targetMode: client.targetMode,
|
|
661
|
+
pinnedTabId: client.pinnedTabId,
|
|
662
|
+
focusedTabId,
|
|
455
663
|
attachedTabId,
|
|
664
|
+
attachedTabIds: [...sessionsByTabId.values()].filter((session) => session.attached).map((session) => session.tabId),
|
|
456
665
|
targetTab: summarizeTab(targetTab),
|
|
457
666
|
viewingTab: summarizeTab(viewingTab),
|
|
667
|
+
targets: await summarizeNamedTargets(),
|
|
458
668
|
targetError,
|
|
459
669
|
}
|
|
460
670
|
}
|
|
461
671
|
|
|
462
|
-
|
|
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 = {}) {
|
|
463
706
|
const tabs = await chrome.tabs.query({})
|
|
464
|
-
const targetInfo = await buildTargetInfo()
|
|
707
|
+
const targetInfo = await buildTargetInfo(params.clientId)
|
|
465
708
|
return {
|
|
466
709
|
...targetInfo,
|
|
467
710
|
tabs: tabs
|
|
@@ -471,64 +714,100 @@ async function handleListTabs() {
|
|
|
471
714
|
}
|
|
472
715
|
}
|
|
473
716
|
|
|
474
|
-
async function
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
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)
|
|
479
725
|
try {
|
|
480
|
-
await
|
|
726
|
+
await ensureAttachedSession({ target: name, clientId })
|
|
481
727
|
} catch (e) {
|
|
482
|
-
|
|
483
|
-
|
|
728
|
+
if (previousTabId !== undefined) client.namedTargets.set(name, previousTabId)
|
|
729
|
+
else client.namedTargets.delete(name)
|
|
484
730
|
throw e
|
|
485
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
|
+
}
|
|
486
737
|
broadcastStatus()
|
|
487
|
-
return
|
|
738
|
+
return {
|
|
739
|
+
bound: { name, tabId: tab.id, tab: summarizeTab(tab) },
|
|
740
|
+
...(await buildTargetInfo(clientId)),
|
|
741
|
+
}
|
|
488
742
|
}
|
|
489
743
|
|
|
490
|
-
async function
|
|
491
|
-
|
|
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)
|
|
492
748
|
}
|
|
493
749
|
|
|
494
|
-
async function
|
|
495
|
-
|
|
496
|
-
|
|
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)
|
|
497
761
|
}
|
|
498
762
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
763
|
+
broadcastStatus()
|
|
764
|
+
return {
|
|
765
|
+
unbound: { name, tabId },
|
|
766
|
+
...(await buildTargetInfo(params.clientId)),
|
|
503
767
|
}
|
|
768
|
+
}
|
|
504
769
|
|
|
505
|
-
|
|
506
|
-
const
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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
|
+
}
|
|
512
786
|
|
|
513
|
-
|
|
514
|
-
return pinTab(
|
|
787
|
+
async function handlePinCurrentTab(params = {}) {
|
|
788
|
+
return pinTab(await getFocusedTabOrThrow(), params.clientId)
|
|
515
789
|
}
|
|
516
790
|
|
|
517
|
-
async function
|
|
518
|
-
|
|
519
|
-
|
|
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
|
|
520
799
|
if (state.enabled && state.connected) {
|
|
521
|
-
await ensureAttached()
|
|
800
|
+
await ensureAttached({ clientId: params.clientId })
|
|
522
801
|
}
|
|
523
802
|
broadcastStatus()
|
|
524
|
-
return buildTargetInfo()
|
|
803
|
+
return buildTargetInfo(params.clientId)
|
|
525
804
|
}
|
|
526
805
|
|
|
527
806
|
async function handleGetLastError(params = {}) {
|
|
528
|
-
await
|
|
807
|
+
const { session } = await ensureAttachedSession(params)
|
|
529
808
|
const severity = params.severity || "error"
|
|
530
809
|
const limit = Math.max(1, Math.min(params.limit || 20, CONFIG.maxErrors))
|
|
531
|
-
const allEvents = lastErrors.slice(0, CONFIG.maxErrors)
|
|
810
|
+
const allEvents = session.lastErrors.slice(0, CONFIG.maxErrors)
|
|
532
811
|
const filteredEvents = severity === "all"
|
|
533
812
|
? allEvents
|
|
534
813
|
: allEvents.filter((event) => (event.severity || "info") === severity)
|
|
@@ -542,7 +821,7 @@ async function handleGetLastError(params = {}) {
|
|
|
542
821
|
)
|
|
543
822
|
const events = filteredEvents.slice(0, limit)
|
|
544
823
|
return {
|
|
545
|
-
lastErrorLocation,
|
|
824
|
+
lastErrorLocation: session.lastErrorLocation,
|
|
546
825
|
summary: {
|
|
547
826
|
count: events.length,
|
|
548
827
|
cachedCount: allEvents.length,
|
|
@@ -556,17 +835,17 @@ async function handleGetLastError(params = {}) {
|
|
|
556
835
|
}
|
|
557
836
|
}
|
|
558
837
|
|
|
559
|
-
async function pickScriptId(preferUrlContains) {
|
|
838
|
+
async function pickScriptId(session, preferUrlContains) {
|
|
560
839
|
if (preferUrlContains) {
|
|
561
|
-
for (const [id, meta] of scriptMap.entries()) {
|
|
840
|
+
for (const [id, meta] of session.scriptMap.entries()) {
|
|
562
841
|
if (meta.url && meta.url.includes(preferUrlContains)) return { id, url: meta.url }
|
|
563
842
|
}
|
|
564
843
|
}
|
|
565
|
-
if (lastErrorLocation?.scriptId && scriptMap.has(lastErrorLocation.scriptId)) {
|
|
566
|
-
const meta = scriptMap.get(lastErrorLocation.scriptId)
|
|
567
|
-
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 }
|
|
568
847
|
}
|
|
569
|
-
const first = scriptMap.entries().next().value
|
|
848
|
+
const first = session.scriptMap.entries().next().value
|
|
570
849
|
if (first) {
|
|
571
850
|
return { id: first[0], url: first[1].url }
|
|
572
851
|
}
|
|
@@ -574,15 +853,15 @@ async function pickScriptId(preferUrlContains) {
|
|
|
574
853
|
}
|
|
575
854
|
|
|
576
855
|
async function handleGetScriptSource(params = {}) {
|
|
577
|
-
const target = await
|
|
578
|
-
const chosen = await pickScriptId(params.scriptUrlContains)
|
|
856
|
+
const { target, session } = await ensureAttachedSession(params)
|
|
857
|
+
const chosen = await pickScriptId(session, params.scriptUrlContains)
|
|
579
858
|
const { scriptSource } = await chrome.debugger.sendCommand(target, "Debugger.getScriptSource", {
|
|
580
859
|
scriptId: chosen.id,
|
|
581
860
|
})
|
|
582
|
-
scriptSourceCache.set(chosen.id, scriptSource)
|
|
861
|
+
session.scriptSourceCache.set(chosen.id, scriptSource)
|
|
583
862
|
const location = {
|
|
584
|
-
line: params.line ?? lastErrorLocation?.line ?? null,
|
|
585
|
-
column: params.column ?? lastErrorLocation?.column ?? null,
|
|
863
|
+
line: params.line ?? session.lastErrorLocation?.line ?? null,
|
|
864
|
+
column: params.column ?? session.lastErrorLocation?.column ?? null,
|
|
586
865
|
}
|
|
587
866
|
return {
|
|
588
867
|
url: chosen.url,
|
|
@@ -594,7 +873,7 @@ async function handleGetScriptSource(params = {}) {
|
|
|
594
873
|
}
|
|
595
874
|
|
|
596
875
|
async function handleCoverageSnapshot(params = {}) {
|
|
597
|
-
const target = await ensureAttached()
|
|
876
|
+
const target = await ensureAttached(params)
|
|
598
877
|
const durationMs = params.durationMs || 1500
|
|
599
878
|
await chrome.debugger.sendCommand(target, "Profiler.startPreciseCoverage", {
|
|
600
879
|
callCount: true,
|
|
@@ -630,20 +909,20 @@ function findContexts(source, query, maxMatches) {
|
|
|
630
909
|
}
|
|
631
910
|
|
|
632
911
|
async function handleFindByString(params = {}) {
|
|
633
|
-
const target = await
|
|
912
|
+
const { target, session } = await ensureAttachedSession(params)
|
|
634
913
|
const query = params.query
|
|
635
914
|
const maxMatches = params.maxMatches || 5
|
|
636
915
|
const preferred = params.scriptUrlContains
|
|
637
916
|
|
|
638
917
|
const results = []
|
|
639
|
-
const entries = [...scriptMap.entries()]
|
|
918
|
+
const entries = [...session.scriptMap.entries()]
|
|
640
919
|
for (const [id, meta] of entries) {
|
|
641
920
|
if (preferred && (!meta.url || !meta.url.includes(preferred))) continue
|
|
642
|
-
if (!scriptSourceCache.has(id)) {
|
|
921
|
+
if (!session.scriptSourceCache.has(id)) {
|
|
643
922
|
const { scriptSource } = await chrome.debugger.sendCommand(target, "Debugger.getScriptSource", { scriptId: id })
|
|
644
|
-
scriptSourceCache.set(id, scriptSource)
|
|
923
|
+
session.scriptSourceCache.set(id, scriptSource)
|
|
645
924
|
}
|
|
646
|
-
const source = scriptSourceCache.get(id)
|
|
925
|
+
const source = session.scriptSourceCache.get(id)
|
|
647
926
|
const matches = findContexts(source, query, maxMatches - results.length)
|
|
648
927
|
if (matches.length) {
|
|
649
928
|
results.push({ url: meta.url, scriptId: id, matches })
|
|
@@ -654,8 +933,8 @@ async function handleFindByString(params = {}) {
|
|
|
654
933
|
return { query, results }
|
|
655
934
|
}
|
|
656
935
|
|
|
657
|
-
async function handleSymbolicHints() {
|
|
658
|
-
const target = await ensureAttached()
|
|
936
|
+
async function handleSymbolicHints(params = {}) {
|
|
937
|
+
const target = await ensureAttached(params)
|
|
659
938
|
const expression = `(function(){
|
|
660
939
|
try {
|
|
661
940
|
const resources = performance.getEntriesByType('resource').slice(-20).map(e => ({
|
|
@@ -678,7 +957,7 @@ async function handleSymbolicHints() {
|
|
|
678
957
|
}
|
|
679
958
|
|
|
680
959
|
async function handleEval(params = {}) {
|
|
681
|
-
const target = await ensureAttached()
|
|
960
|
+
const target = await ensureAttached(params)
|
|
682
961
|
const { result } = await chrome.debugger.sendCommand(target, "Runtime.evaluate", {
|
|
683
962
|
expression: params.code,
|
|
684
963
|
returnByValue: true,
|
|
@@ -687,11 +966,11 @@ async function handleEval(params = {}) {
|
|
|
687
966
|
}
|
|
688
967
|
|
|
689
968
|
async function handleListNetworkRequests(params = {}) {
|
|
690
|
-
await
|
|
969
|
+
const { session } = await ensureAttachedSession(params)
|
|
691
970
|
const { filter, method, status, resourceType, limit = 50, priorityMode = 'debug' } = params
|
|
692
971
|
|
|
693
|
-
let results = [...networkRequests]
|
|
694
|
-
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" }))
|
|
695
974
|
results = [...pending, ...results]
|
|
696
975
|
|
|
697
976
|
if (filter) {
|
|
@@ -709,7 +988,7 @@ async function handleListNetworkRequests(params = {}) {
|
|
|
709
988
|
results = results.slice(0, limit)
|
|
710
989
|
|
|
711
990
|
return {
|
|
712
|
-
total: networkRequests.length + requestMap.size,
|
|
991
|
+
total: session.networkRequests.length + session.requestMap.size,
|
|
713
992
|
filtered: results.length,
|
|
714
993
|
priorityMode,
|
|
715
994
|
requests: results.map((entry) => GhostBridgeNetwork.buildNetworkRequestSummary(entry)),
|
|
@@ -717,12 +996,12 @@ async function handleListNetworkRequests(params = {}) {
|
|
|
717
996
|
}
|
|
718
997
|
|
|
719
998
|
async function handleGetNetworkDetail(params = {}) {
|
|
720
|
-
const target = await
|
|
999
|
+
const { target, session } = await ensureAttachedSession(params)
|
|
721
1000
|
const { requestId, includeBody = false } = params
|
|
722
1001
|
if (!requestId) throw new Error("需要提供 requestId")
|
|
723
1002
|
|
|
724
|
-
let entry = requestMap.get(requestId)
|
|
725
|
-
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)
|
|
726
1005
|
if (!entry) throw new Error(`未找到请求: ${requestId}`)
|
|
727
1006
|
|
|
728
1007
|
const urlMeta = GhostBridgeNetwork.summarizeNetworkUrl(entry.url)
|
|
@@ -765,15 +1044,15 @@ async function handleGetNetworkDetail(params = {}) {
|
|
|
765
1044
|
return result
|
|
766
1045
|
}
|
|
767
1046
|
|
|
768
|
-
async function handleClearNetworkRequests() {
|
|
769
|
-
await
|
|
770
|
-
const count = networkRequests.length
|
|
771
|
-
networkRequests = []
|
|
1047
|
+
async function handleClearNetworkRequests(params = {}) {
|
|
1048
|
+
const { session } = await ensureAttachedSession(params)
|
|
1049
|
+
const count = session.networkRequests.length
|
|
1050
|
+
session.networkRequests = []
|
|
772
1051
|
return { cleared: count }
|
|
773
1052
|
}
|
|
774
1053
|
|
|
775
1054
|
async function handlePerfMetrics(params = {}) {
|
|
776
|
-
const target = await ensureAttached()
|
|
1055
|
+
const target = await ensureAttached(params)
|
|
777
1056
|
const { includeResources = true, includeTimings = true } = params
|
|
778
1057
|
|
|
779
1058
|
// 1. CDP Performance.getMetrics — 底层引擎指标
|
|
@@ -924,7 +1203,7 @@ function roundMs(seconds) {
|
|
|
924
1203
|
}
|
|
925
1204
|
|
|
926
1205
|
async function handleCaptureScreenshot(params = {}) {
|
|
927
|
-
const target = await ensureAttached()
|
|
1206
|
+
const target = await ensureAttached(params)
|
|
928
1207
|
const { format: requestedFormat, quality: requestedQuality, fullPage = false, clip } = params
|
|
929
1208
|
const format = requestedFormat || 'jpeg'
|
|
930
1209
|
const quality = format === 'jpeg'
|
|
@@ -1004,7 +1283,7 @@ async function handleCaptureScreenshot(params = {}) {
|
|
|
1004
1283
|
}
|
|
1005
1284
|
|
|
1006
1285
|
async function handleInspectPageSnapshot(params = {}) {
|
|
1007
|
-
const target = await
|
|
1286
|
+
const { target, session } = await ensureAttachedSession(params)
|
|
1008
1287
|
const { selector, includeInteractive = true, maxElements = 30 } = params
|
|
1009
1288
|
const expression = GhostBridgeDom.buildInspectPageExpression({ selector, includeInteractive, maxElements })
|
|
1010
1289
|
|
|
@@ -1014,11 +1293,16 @@ async function handleInspectPageSnapshot(params = {}) {
|
|
|
1014
1293
|
})
|
|
1015
1294
|
|
|
1016
1295
|
if (result?.value?.error) throw new Error(result.value.error)
|
|
1017
|
-
|
|
1296
|
+
const value = result?.value
|
|
1297
|
+
if (value) {
|
|
1298
|
+
value.target = describeCommandTarget(params, session)
|
|
1299
|
+
value.tabId = session.tabId
|
|
1300
|
+
}
|
|
1301
|
+
return value
|
|
1018
1302
|
}
|
|
1019
1303
|
|
|
1020
1304
|
async function handleGetPageContent(params = {}) {
|
|
1021
|
-
const target = await ensureAttached()
|
|
1305
|
+
const target = await ensureAttached(params)
|
|
1022
1306
|
const { mode = "text", selector, maxLength = 50000, includeMetadata = true } = params
|
|
1023
1307
|
const expression = GhostBridgeDom.buildPageContentExpression({ mode, selector, maxLength, includeMetadata })
|
|
1024
1308
|
|
|
@@ -1034,7 +1318,7 @@ async function handleGetPageContent(params = {}) {
|
|
|
1034
1318
|
// ========== DOM 交互:可交互元素快照 ==========
|
|
1035
1319
|
|
|
1036
1320
|
async function handleGetInteractiveSnapshot(params = {}) {
|
|
1037
|
-
const target = await
|
|
1321
|
+
const { target, session } = await ensureAttachedSession(params)
|
|
1038
1322
|
const { selector, includeText = true, maxElements = 100 } = params
|
|
1039
1323
|
const expression = GhostBridgeDom.buildInteractiveSnapshotExpression({ selector, includeText, maxElements })
|
|
1040
1324
|
|
|
@@ -1044,13 +1328,22 @@ async function handleGetInteractiveSnapshot(params = {}) {
|
|
|
1044
1328
|
})
|
|
1045
1329
|
|
|
1046
1330
|
if (result?.value?.error) throw new Error(result.value.error)
|
|
1047
|
-
|
|
1331
|
+
const value = result?.value
|
|
1332
|
+
if (value) {
|
|
1333
|
+
value.target = describeCommandTarget(params, session)
|
|
1334
|
+
value.tabId = session.tabId
|
|
1335
|
+
}
|
|
1336
|
+
return value
|
|
1048
1337
|
}
|
|
1049
1338
|
|
|
1050
1339
|
// ========== DOM 交互:动作分发器 ==========
|
|
1051
1340
|
|
|
1052
1341
|
async function handleDispatchAction(params = {}) {
|
|
1053
|
-
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)
|
|
1054
1347
|
const { ref, action, value, key, deltaX, deltaY, waitMs = 500 } = params
|
|
1055
1348
|
|
|
1056
1349
|
if (!ref) throw new Error("需要提供 ref(元素标识,如 'e1')")
|
|
@@ -1218,8 +1511,15 @@ async function handleDispatchAction(params = {}) {
|
|
|
1218
1511
|
|
|
1219
1512
|
// 处理来自服务器的命令
|
|
1220
1513
|
async function handleCommand(message) {
|
|
1221
|
-
const { id, command,
|
|
1514
|
+
const { id, command, token } = message
|
|
1222
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
|
+
}
|
|
1223
1523
|
if (!state.enabled) {
|
|
1224
1524
|
sendToServer({ id, error: "扩展已暂停,点击图标重新开启" })
|
|
1225
1525
|
return
|
|
@@ -1228,22 +1528,28 @@ async function handleCommand(message) {
|
|
|
1228
1528
|
sendToServer({ id, error: "token 校验失败" })
|
|
1229
1529
|
return
|
|
1230
1530
|
}
|
|
1531
|
+
// 命令所属的 MCP 会话:pin/bind 的命名空间按它隔离
|
|
1532
|
+
const clientId = message.clientId
|
|
1533
|
+
const params = { ...(message.params || {}), clientId }
|
|
1231
1534
|
try {
|
|
1232
1535
|
let result
|
|
1233
|
-
if (command === "listTabs") result = await handleListTabs()
|
|
1234
|
-
else if (command === "getTargetTab") result = await buildTargetInfo()
|
|
1235
|
-
else 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)
|
|
1236
1542
|
else if (command === "pinTab") result = await handlePinTab(params)
|
|
1237
|
-
else if (command === "unpinTab") result = await handleUnpinTab()
|
|
1543
|
+
else if (command === "unpinTab") result = await handleUnpinTab(params)
|
|
1238
1544
|
else if (command === "getLastError") result = await handleGetLastError(params)
|
|
1239
1545
|
else if (command === "getScriptSource") result = await handleGetScriptSource(params)
|
|
1240
1546
|
else if (command === "coverageSnapshot") result = await handleCoverageSnapshot(params)
|
|
1241
1547
|
else if (command === "findByString") result = await handleFindByString(params)
|
|
1242
|
-
else if (command === "symbolicHints") result = await handleSymbolicHints()
|
|
1548
|
+
else if (command === "symbolicHints") result = await handleSymbolicHints(params)
|
|
1243
1549
|
else if (command === "eval") result = await handleEval(params)
|
|
1244
1550
|
else if (command === "listNetworkRequests") result = await handleListNetworkRequests(params)
|
|
1245
1551
|
else if (command === "getNetworkDetail") result = await handleGetNetworkDetail(params)
|
|
1246
|
-
else if (command === "clearNetworkRequests") result = await handleClearNetworkRequests()
|
|
1552
|
+
else if (command === "clearNetworkRequests") result = await handleClearNetworkRequests(params)
|
|
1247
1553
|
else if (command === "perfMetrics") result = await handlePerfMetrics(params)
|
|
1248
1554
|
else if (command === "captureScreenshot") result = await handleCaptureScreenshot(params)
|
|
1249
1555
|
else if (command === "inspectPageSnapshot") result = await handleInspectPageSnapshot(params)
|
|
@@ -1294,7 +1600,10 @@ async function buildPopupState() {
|
|
|
1294
1600
|
serverInfo: state.serverInfo,
|
|
1295
1601
|
targetMode: targetInfo.targetMode,
|
|
1296
1602
|
pinnedTabId: targetInfo.pinnedTabId,
|
|
1603
|
+
focusedTabId: targetInfo.focusedTabId,
|
|
1297
1604
|
attachedTabId: targetInfo.attachedTabId,
|
|
1605
|
+
attachedTabIds: targetInfo.attachedTabIds,
|
|
1606
|
+
targets: targetInfo.targets,
|
|
1298
1607
|
targetTab,
|
|
1299
1608
|
viewingTab,
|
|
1300
1609
|
targetError: targetInfo.targetError,
|
|
@@ -1319,7 +1628,7 @@ async function broadcastStatus() {
|
|
|
1319
1628
|
|
|
1320
1629
|
// 监听被调试页面的导航变化,实时推送到 popup
|
|
1321
1630
|
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
|
|
1322
|
-
if ((tabId
|
|
1631
|
+
if ((sessionsByTabId.has(tabId) || tab.active) && (changeInfo.title || changeInfo.url)) {
|
|
1323
1632
|
if (state.connected) broadcastStatus()
|
|
1324
1633
|
}
|
|
1325
1634
|
})
|
|
@@ -1328,8 +1637,10 @@ chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
|
|
|
1328
1637
|
chrome.tabs.onActivated.addListener(async (activeInfo) => {
|
|
1329
1638
|
if (state.enabled && state.connected) {
|
|
1330
1639
|
try {
|
|
1331
|
-
|
|
1332
|
-
|
|
1640
|
+
// focused 模式保持旧行为:切换 Tab 时自动跟随(focused 是共享语义,任一会话处于 focused 即跟随)
|
|
1641
|
+
const anyFocused = clientSessions.size === 0
|
|
1642
|
+
|| [...clientSessions.values()].some((client) => client.targetMode === 'focused')
|
|
1643
|
+
if (anyFocused) {
|
|
1333
1644
|
await ensureAttached()
|
|
1334
1645
|
}
|
|
1335
1646
|
broadcastStatus()
|
|
@@ -1340,14 +1651,22 @@ chrome.tabs.onActivated.addListener(async (activeInfo) => {
|
|
|
1340
1651
|
})
|
|
1341
1652
|
|
|
1342
1653
|
chrome.tabs.onRemoved.addListener((tabId) => {
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
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
|
+
}
|
|
1346
1662
|
}
|
|
1347
1663
|
if (tabId === attachedTabId) {
|
|
1348
1664
|
attachedTabId = null
|
|
1349
|
-
resetDebuggerState()
|
|
1350
1665
|
}
|
|
1666
|
+
if (tabId === focusedTabId) {
|
|
1667
|
+
focusedTabId = null
|
|
1668
|
+
}
|
|
1669
|
+
sessionsByTabId.delete(tabId)
|
|
1351
1670
|
if (state.connected) broadcastStatus()
|
|
1352
1671
|
})
|
|
1353
1672
|
|
|
@@ -1375,6 +1694,8 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|
|
1375
1694
|
state.connectionError = ''
|
|
1376
1695
|
state.serverInfo = message.serverInfo || null
|
|
1377
1696
|
setBadgeState('on')
|
|
1697
|
+
// 服务器可能已重启,clientId 分配从零开始,旧会话桶全部作废
|
|
1698
|
+
clientSessions.clear()
|
|
1378
1699
|
log(`✅ 已连接到 ghost-bridge 服务 (端口 ${message.port})`)
|
|
1379
1700
|
ensureAttached()
|
|
1380
1701
|
.then(() => broadcastStatus())
|
|
@@ -1454,64 +1775,56 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|
|
1454
1775
|
if (message.type === 'connect') {
|
|
1455
1776
|
if (message.port) {
|
|
1456
1777
|
CONFIG.basePort = message.port
|
|
1457
|
-
chrome.storage.local.set({ basePort: message.port })
|
|
1458
1778
|
}
|
|
1459
|
-
state.enabled = true
|
|
1460
|
-
state.connected = false
|
|
1461
|
-
state.port = null
|
|
1462
|
-
state.currentPort = CONFIG.basePort
|
|
1463
|
-
state.connectionStatus = 'connecting'
|
|
1464
|
-
state.connectionError = ''
|
|
1465
|
-
state.serverInfo = null
|
|
1466
|
-
setBadgeState('connecting')
|
|
1467
|
-
|
|
1468
|
-
// 启动 offscreen 并开始连接
|
|
1469
|
-
setupOffscreenDocument().then(() => {
|
|
1470
|
-
chrome.runtime.sendMessage({
|
|
1471
|
-
type: 'connect',
|
|
1472
|
-
basePort: CONFIG.basePort,
|
|
1473
|
-
token: CONFIG.token,
|
|
1474
|
-
}).catch(() => {})
|
|
1475
|
-
})
|
|
1476
1779
|
|
|
1477
|
-
|
|
1780
|
+
startBridgeConnection({ persist: true })
|
|
1781
|
+
.then(() => sendResponse({ ok: true }))
|
|
1782
|
+
.catch((e) => sendResponse({ ok: false, error: e.message }))
|
|
1478
1783
|
return true
|
|
1479
1784
|
}
|
|
1480
1785
|
|
|
1481
1786
|
// 来自 popup 的断开请求
|
|
1482
1787
|
if (message.type === 'disconnect') {
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
state.currentPort = null
|
|
1487
|
-
state.connectionStatus = 'disconnected'
|
|
1488
|
-
state.connectionError = ''
|
|
1489
|
-
state.serverInfo = null
|
|
1490
|
-
targetMode = 'focused'
|
|
1491
|
-
pinnedTabId = null
|
|
1492
|
-
setBadgeState('off')
|
|
1493
|
-
detachAllTargets().catch(() => {})
|
|
1494
|
-
|
|
1495
|
-
// 通知 offscreen 断开 (WebSocket 清除)
|
|
1496
|
-
chrome.runtime.sendMessage({ type: 'disconnect' }).catch(() => {})
|
|
1497
|
-
|
|
1498
|
-
// 关键修复:显式销毁 offscreen document 防止内存泄漏
|
|
1499
|
-
closeOffscreenDocument().catch(() => {})
|
|
1500
|
-
|
|
1501
|
-
sendResponse({ ok: true })
|
|
1788
|
+
stopBridgeConnection({ persist: true })
|
|
1789
|
+
.then(() => sendResponse({ ok: true }))
|
|
1790
|
+
.catch((e) => sendResponse({ ok: false, error: e.message }))
|
|
1502
1791
|
return true
|
|
1503
1792
|
}
|
|
1504
1793
|
|
|
1505
1794
|
return false
|
|
1506
1795
|
})
|
|
1507
1796
|
|
|
1508
|
-
//
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
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}`)
|
|
1512
1818
|
}
|
|
1513
1819
|
})
|
|
1514
1820
|
|
|
1515
|
-
|
|
1516
|
-
|
|
1821
|
+
chrome.runtime.onStartup.addListener(() => {
|
|
1822
|
+
restoreBridgeConnection()
|
|
1823
|
+
})
|
|
1824
|
+
|
|
1825
|
+
chrome.runtime.onInstalled.addListener(() => {
|
|
1826
|
+
restoreBridgeConnection()
|
|
1827
|
+
})
|
|
1828
|
+
|
|
1829
|
+
restoreBridgeConnection()
|
|
1517
1830
|
log("Ghost Bridge background 已加载")
|