ghost-bridge 0.2.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 +85 -0
- package/dist/cli.js +5731 -0
- package/dist/server.js +29289 -0
- package/extension/background.js +956 -0
- package/extension/icon-128.png +0 -0
- package/extension/icon-16.png +0 -0
- package/extension/icon-32.png +0 -0
- package/extension/icon-48.png +0 -0
- package/extension/manifest.json +37 -0
- package/extension/offscreen.html +10 -0
- package/extension/offscreen.js +175 -0
- package/extension/popup.html +160 -0
- package/extension/popup.js +165 -0
- package/package.json +29 -0
|
@@ -0,0 +1,956 @@
|
|
|
1
|
+
// 使用当月1号0点的时间戳作为 token,确保同月内的服务器和插件自动匹配
|
|
2
|
+
function getMonthlyToken() {
|
|
3
|
+
const now = new Date()
|
|
4
|
+
const firstDayOfMonth = new Date(now.getFullYear(), now.getMonth(), 1, 0, 0, 0, 0)
|
|
5
|
+
return String(firstDayOfMonth.getTime())
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const CONFIG = {
|
|
9
|
+
basePort: 33333,
|
|
10
|
+
maxPortRetries: 10,
|
|
11
|
+
token: getMonthlyToken(),
|
|
12
|
+
autoDetach: false,
|
|
13
|
+
maxErrors: 40,
|
|
14
|
+
maxStackFrames: 5,
|
|
15
|
+
maxRequestsTracked: 200,
|
|
16
|
+
maxRequestBodySize: 100000,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let attachedTabId = null
|
|
20
|
+
let scriptMap = new Map()
|
|
21
|
+
let scriptSourceCache = new Map()
|
|
22
|
+
let lastErrors = []
|
|
23
|
+
let lastErrorLocation = null
|
|
24
|
+
let requestMap = new Map()
|
|
25
|
+
let networkRequests = []
|
|
26
|
+
let state = { enabled: false, connected: false, port: null, currentPort: null, scanRound: 0 }
|
|
27
|
+
|
|
28
|
+
// 待处理的请求(等待 offscreen 响应)
|
|
29
|
+
const pendingRequests = new Map()
|
|
30
|
+
|
|
31
|
+
function setBadgeState(status) {
|
|
32
|
+
const map = {
|
|
33
|
+
connecting: { text: "…", color: "#999" },
|
|
34
|
+
on: { text: "ON", color: "#34c759" },
|
|
35
|
+
off: { text: "OFF", color: "#999" },
|
|
36
|
+
err: { text: "ERR", color: "#ff3b30" },
|
|
37
|
+
att: { text: "ATT", color: "#ff9f0a" },
|
|
38
|
+
}
|
|
39
|
+
const cfg = map[status] || map.off
|
|
40
|
+
chrome.action.setBadgeText({ text: cfg.text })
|
|
41
|
+
chrome.action.setBadgeBackgroundColor({ color: cfg.color })
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function sleep(ms) {
|
|
45
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function log(msg) {
|
|
49
|
+
console.log(`[ghost-bridge] ${msg}`)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ========== Offscreen Document 管理 ==========
|
|
53
|
+
|
|
54
|
+
let offscreenCreating = null
|
|
55
|
+
|
|
56
|
+
async function setupOffscreenDocument() {
|
|
57
|
+
const offscreenUrl = chrome.runtime.getURL('offscreen.html')
|
|
58
|
+
|
|
59
|
+
// 检查是否已存在
|
|
60
|
+
const existingContexts = await chrome.runtime.getContexts({
|
|
61
|
+
contextTypes: ['OFFSCREEN_DOCUMENT'],
|
|
62
|
+
documentUrls: [offscreenUrl]
|
|
63
|
+
}).catch(() => [])
|
|
64
|
+
|
|
65
|
+
if (existingContexts.length > 0) {
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 防止并发创建
|
|
70
|
+
if (offscreenCreating) {
|
|
71
|
+
await offscreenCreating
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
offscreenCreating = chrome.offscreen.createDocument({
|
|
76
|
+
url: 'offscreen.html',
|
|
77
|
+
reasons: ['WORKERS'], // 使用 WORKERS 作为理由
|
|
78
|
+
justification: 'Maintain WebSocket connection to ghost-bridge server'
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
await offscreenCreating
|
|
82
|
+
offscreenCreating = null
|
|
83
|
+
log('Offscreen document 已创建')
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function closeOffscreenDocument() {
|
|
87
|
+
try {
|
|
88
|
+
await chrome.offscreen.closeDocument()
|
|
89
|
+
log('Offscreen document 已关闭')
|
|
90
|
+
} catch {
|
|
91
|
+
// 可能已关闭
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ========== Chrome Debugger 事件处理 ==========
|
|
96
|
+
|
|
97
|
+
chrome.debugger.onEvent.addListener((source, method, params) => {
|
|
98
|
+
if (source.tabId !== attachedTabId) return
|
|
99
|
+
if (!state.enabled) return
|
|
100
|
+
|
|
101
|
+
if (method === "Debugger.scriptParsed") {
|
|
102
|
+
scriptMap.set(params.scriptId, { url: params.url || "(inline)" })
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (method === "Runtime.exceptionThrown") {
|
|
106
|
+
const detail = params?.exceptionDetails || {}
|
|
107
|
+
const topFrame = detail.stackTrace?.callFrames?.[0]
|
|
108
|
+
const entry = {
|
|
109
|
+
type: "exception",
|
|
110
|
+
severity: "error",
|
|
111
|
+
url: topFrame?.url || detail.url,
|
|
112
|
+
line: topFrame?.lineNumber,
|
|
113
|
+
column: topFrame?.columnNumber,
|
|
114
|
+
text: detail.text,
|
|
115
|
+
scriptId: topFrame?.scriptId,
|
|
116
|
+
stack: compactStack(detail.stackTrace),
|
|
117
|
+
timestamp: Date.now(),
|
|
118
|
+
}
|
|
119
|
+
lastErrorLocation = {
|
|
120
|
+
url: entry.url,
|
|
121
|
+
line: entry.line,
|
|
122
|
+
column: entry.column,
|
|
123
|
+
scriptId: entry.scriptId,
|
|
124
|
+
}
|
|
125
|
+
pushError(entry)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (method === "Log.entryAdded") {
|
|
129
|
+
const entry = params?.entry || {}
|
|
130
|
+
pushError({
|
|
131
|
+
type: entry.level || "log",
|
|
132
|
+
severity: entry.level === "warning" ? "warn" : entry.level === "error" ? "error" : "info",
|
|
133
|
+
url: entry.source || entry.url,
|
|
134
|
+
line: entry.lineNumber,
|
|
135
|
+
text: entry.text,
|
|
136
|
+
stack: compactStack(entry.stackTrace),
|
|
137
|
+
timestamp: Date.now(),
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (method === "Runtime.consoleAPICalled") {
|
|
142
|
+
const args = (params.args || []).map((a) => a.value).filter(Boolean)
|
|
143
|
+
pushError({
|
|
144
|
+
type: params.type || "console",
|
|
145
|
+
severity: params.type === "error" ? "error" : params.type === "warning" ? "warn" : "info",
|
|
146
|
+
url: params.stackTrace?.callFrames?.[0]?.url,
|
|
147
|
+
line: params.stackTrace?.callFrames?.[0]?.lineNumber,
|
|
148
|
+
text: args.join(" "),
|
|
149
|
+
stack: compactStack(params.stackTrace),
|
|
150
|
+
timestamp: Date.now(),
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 网络事件处理
|
|
155
|
+
if (method === "Network.requestWillBeSent") {
|
|
156
|
+
const req = params.request || {}
|
|
157
|
+
const entry = {
|
|
158
|
+
requestId: params.requestId,
|
|
159
|
+
url: req.url,
|
|
160
|
+
method: req.method || "GET",
|
|
161
|
+
requestHeaders: req.headers || {},
|
|
162
|
+
postData: req.postData,
|
|
163
|
+
initiator: params.initiator?.type,
|
|
164
|
+
resourceType: params.type,
|
|
165
|
+
startTime: params.timestamp,
|
|
166
|
+
timestamp: Date.now(),
|
|
167
|
+
status: "pending",
|
|
168
|
+
}
|
|
169
|
+
requestMap.set(params.requestId, entry)
|
|
170
|
+
if (requestMap.size > CONFIG.maxRequestsTracked * 2) {
|
|
171
|
+
const firstKey = requestMap.keys().next().value
|
|
172
|
+
requestMap.delete(firstKey)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (method === "Network.responseReceived") {
|
|
177
|
+
const res = params.response || {}
|
|
178
|
+
const entry = requestMap.get(params.requestId)
|
|
179
|
+
if (entry) {
|
|
180
|
+
entry.status = res.status >= 400 ? "error" : "success"
|
|
181
|
+
entry.statusCode = res.status
|
|
182
|
+
entry.statusText = res.statusText
|
|
183
|
+
entry.mimeType = res.mimeType
|
|
184
|
+
entry.responseHeaders = res.headers || {}
|
|
185
|
+
entry.protocol = res.protocol
|
|
186
|
+
entry.remoteAddress = res.remoteIPAddress
|
|
187
|
+
entry.fromCache = res.fromDiskCache || res.fromServiceWorker
|
|
188
|
+
entry.timing = res.timing
|
|
189
|
+
entry.encodedDataLength = params.encodedDataLength
|
|
190
|
+
if (res.status >= 400) {
|
|
191
|
+
pushError({
|
|
192
|
+
type: "network",
|
|
193
|
+
severity: "error",
|
|
194
|
+
url: res.url || entry.url,
|
|
195
|
+
status: res.status,
|
|
196
|
+
statusText: res.statusText,
|
|
197
|
+
mimeType: res.mimeType,
|
|
198
|
+
requestId: params.requestId,
|
|
199
|
+
method: entry.method,
|
|
200
|
+
timestamp: Date.now(),
|
|
201
|
+
})
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (method === "Network.loadingFinished") {
|
|
207
|
+
const entry = requestMap.get(params.requestId)
|
|
208
|
+
if (entry) {
|
|
209
|
+
entry.endTime = params.timestamp
|
|
210
|
+
entry.encodedDataLength = params.encodedDataLength
|
|
211
|
+
entry.duration = entry.endTime && entry.startTime
|
|
212
|
+
? Math.round((entry.endTime - entry.startTime) * 1000)
|
|
213
|
+
: null
|
|
214
|
+
if (entry.status === "pending") entry.status = "success"
|
|
215
|
+
pushNetworkRequest(entry)
|
|
216
|
+
requestMap.delete(params.requestId)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (method === "Network.loadingFailed") {
|
|
221
|
+
const entry = requestMap.get(params.requestId)
|
|
222
|
+
if (entry) {
|
|
223
|
+
entry.status = "failed"
|
|
224
|
+
entry.errorText = params.errorText
|
|
225
|
+
entry.canceled = params.canceled
|
|
226
|
+
entry.blockedReason = params.blockedReason
|
|
227
|
+
pushError({
|
|
228
|
+
type: "network",
|
|
229
|
+
severity: "error",
|
|
230
|
+
url: entry.url,
|
|
231
|
+
requestId: params.requestId,
|
|
232
|
+
method: entry.method,
|
|
233
|
+
text: params.errorText,
|
|
234
|
+
timestamp: Date.now(),
|
|
235
|
+
})
|
|
236
|
+
pushNetworkRequest(entry)
|
|
237
|
+
requestMap.delete(params.requestId)
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
function pushNetworkRequest(entry) {
|
|
243
|
+
networkRequests.unshift(entry)
|
|
244
|
+
if (networkRequests.length > CONFIG.maxRequestsTracked) {
|
|
245
|
+
networkRequests.pop()
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
chrome.debugger.onDetach.addListener((source, reason) => {
|
|
250
|
+
if (source.tabId && source.tabId === attachedTabId) {
|
|
251
|
+
attachedTabId = null
|
|
252
|
+
scriptMap = new Map()
|
|
253
|
+
scriptSourceCache = new Map()
|
|
254
|
+
networkRequests = []
|
|
255
|
+
requestMap = new Map()
|
|
256
|
+
}
|
|
257
|
+
if (!state.enabled) return
|
|
258
|
+
if (reason === "canceled_by_user") {
|
|
259
|
+
log("调试被用户取消,已关闭")
|
|
260
|
+
state.enabled = false
|
|
261
|
+
setBadgeState("off")
|
|
262
|
+
} else {
|
|
263
|
+
log(`调试已断开:${reason}`)
|
|
264
|
+
setBadgeState("att")
|
|
265
|
+
}
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
function pushError(entry) {
|
|
269
|
+
lastErrors.unshift(entry)
|
|
270
|
+
if (lastErrors.length > CONFIG.maxErrors) {
|
|
271
|
+
const dropIdx = lastErrors
|
|
272
|
+
.map((e, i) => ({ sev: e.severity || "info", i }))
|
|
273
|
+
.reverse()
|
|
274
|
+
.find((e) => e.sev !== "error")?.i
|
|
275
|
+
if (dropIdx !== undefined) lastErrors.splice(dropIdx, 1)
|
|
276
|
+
else lastErrors.pop()
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function compactStack(stackTrace) {
|
|
281
|
+
const frames = stackTrace?.callFrames || []
|
|
282
|
+
return frames.slice(0, CONFIG.maxStackFrames).map((f) => ({
|
|
283
|
+
functionName: f.functionName || "",
|
|
284
|
+
url: f.url || "(inline)",
|
|
285
|
+
line: f.lineNumber,
|
|
286
|
+
column: f.columnNumber,
|
|
287
|
+
}))
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ========== Debugger 操作 ==========
|
|
291
|
+
|
|
292
|
+
async function ensureAttached() {
|
|
293
|
+
if (!state.enabled) throw new Error("扩展已暂停,点击图标开启后再试")
|
|
294
|
+
const [tab] = await chrome.tabs.query({ active: true, lastFocusedWindow: true })
|
|
295
|
+
if (!tab) throw new Error("没有激活的标签页")
|
|
296
|
+
if (attachedTabId !== tab.id) {
|
|
297
|
+
try {
|
|
298
|
+
await chrome.debugger.attach({ tabId: tab.id }, "1.3")
|
|
299
|
+
setBadgeState("on")
|
|
300
|
+
} catch (e) {
|
|
301
|
+
setBadgeState("att")
|
|
302
|
+
throw e
|
|
303
|
+
}
|
|
304
|
+
attachedTabId = tab.id
|
|
305
|
+
scriptMap = new Map()
|
|
306
|
+
scriptSourceCache = new Map()
|
|
307
|
+
networkRequests = []
|
|
308
|
+
requestMap = new Map()
|
|
309
|
+
await chrome.debugger.sendCommand({ tabId: attachedTabId }, "Runtime.enable")
|
|
310
|
+
await chrome.debugger.sendCommand({ tabId: attachedTabId }, "Log.enable")
|
|
311
|
+
await chrome.debugger.sendCommand({ tabId: attachedTabId }, "Console.enable").catch(() => {})
|
|
312
|
+
await chrome.debugger.sendCommand({ tabId: attachedTabId }, "Debugger.enable")
|
|
313
|
+
await chrome.debugger.sendCommand({ tabId: attachedTabId }, "Profiler.enable")
|
|
314
|
+
await chrome.debugger.sendCommand({ tabId: attachedTabId }, "Network.enable").catch(() => {})
|
|
315
|
+
}
|
|
316
|
+
return { tabId: attachedTabId }
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async function maybeDetach(force = false) {
|
|
320
|
+
if ((CONFIG.autoDetach || force) && attachedTabId) {
|
|
321
|
+
try {
|
|
322
|
+
await chrome.debugger.detach({ tabId: attachedTabId })
|
|
323
|
+
} catch (e) {
|
|
324
|
+
log(`detach 失败:${e.message}`)
|
|
325
|
+
} finally {
|
|
326
|
+
attachedTabId = null
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async function detachAllTargets() {
|
|
332
|
+
try {
|
|
333
|
+
const targets = await chrome.debugger.getTargets()
|
|
334
|
+
for (const t of targets) {
|
|
335
|
+
if (!t.attached) continue
|
|
336
|
+
try {
|
|
337
|
+
if (t.tabId !== undefined) {
|
|
338
|
+
await chrome.debugger.detach({ tabId: t.tabId })
|
|
339
|
+
} else {
|
|
340
|
+
await chrome.debugger.detach({ targetId: t.id })
|
|
341
|
+
}
|
|
342
|
+
} catch {}
|
|
343
|
+
}
|
|
344
|
+
const tabs = await chrome.tabs.query({})
|
|
345
|
+
for (const tab of tabs) {
|
|
346
|
+
if (!tab.id) continue
|
|
347
|
+
try {
|
|
348
|
+
await chrome.debugger.detach({ tabId: tab.id })
|
|
349
|
+
} catch {}
|
|
350
|
+
}
|
|
351
|
+
} catch {}
|
|
352
|
+
attachedTabId = null
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// ========== 命令处理 ==========
|
|
356
|
+
|
|
357
|
+
async function handleGetLastError() {
|
|
358
|
+
await ensureAttached()
|
|
359
|
+
const events = lastErrors.slice(0, CONFIG.maxErrors)
|
|
360
|
+
const counts = events.reduce(
|
|
361
|
+
(acc, e) => {
|
|
362
|
+
acc.total++
|
|
363
|
+
acc[e.severity || "info"] = (acc[e.severity || "info"] || 0) + 1
|
|
364
|
+
return acc
|
|
365
|
+
},
|
|
366
|
+
{ total: 0 }
|
|
367
|
+
)
|
|
368
|
+
return {
|
|
369
|
+
lastErrorLocation,
|
|
370
|
+
summary: {
|
|
371
|
+
count: events.length,
|
|
372
|
+
severityCount: counts,
|
|
373
|
+
lastTimestamp: events[0]?.timestamp,
|
|
374
|
+
},
|
|
375
|
+
recent: events,
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
async function pickScriptId(preferUrlContains) {
|
|
380
|
+
if (preferUrlContains) {
|
|
381
|
+
for (const [id, meta] of scriptMap.entries()) {
|
|
382
|
+
if (meta.url && meta.url.includes(preferUrlContains)) return { id, url: meta.url }
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
if (lastErrorLocation?.scriptId && scriptMap.has(lastErrorLocation.scriptId)) {
|
|
386
|
+
const meta = scriptMap.get(lastErrorLocation.scriptId)
|
|
387
|
+
return { id: lastErrorLocation.scriptId, url: meta.url }
|
|
388
|
+
}
|
|
389
|
+
const first = scriptMap.entries().next().value
|
|
390
|
+
if (first) {
|
|
391
|
+
return { id: first[0], url: first[1].url }
|
|
392
|
+
}
|
|
393
|
+
throw new Error("未找到可用脚本,确认页面已加载脚本")
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async function handleGetScriptSource(params = {}) {
|
|
397
|
+
const target = await ensureAttached()
|
|
398
|
+
const chosen = await pickScriptId(params.scriptUrlContains)
|
|
399
|
+
const { scriptSource } = await chrome.debugger.sendCommand(target, "Debugger.getScriptSource", {
|
|
400
|
+
scriptId: chosen.id,
|
|
401
|
+
})
|
|
402
|
+
scriptSourceCache.set(chosen.id, scriptSource)
|
|
403
|
+
const location = {
|
|
404
|
+
line: params.line ?? lastErrorLocation?.line ?? null,
|
|
405
|
+
column: params.column ?? lastErrorLocation?.column ?? null,
|
|
406
|
+
}
|
|
407
|
+
return {
|
|
408
|
+
url: chosen.url,
|
|
409
|
+
scriptId: chosen.id,
|
|
410
|
+
location,
|
|
411
|
+
source: scriptSource,
|
|
412
|
+
note: "若为单行压缩脚本,可结合 column 提取片段",
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
async function handleCoverageSnapshot(params = {}) {
|
|
417
|
+
const target = await ensureAttached()
|
|
418
|
+
const durationMs = params.durationMs || 1500
|
|
419
|
+
await chrome.debugger.sendCommand(target, "Profiler.startPreciseCoverage", {
|
|
420
|
+
callCount: true,
|
|
421
|
+
detailed: true,
|
|
422
|
+
})
|
|
423
|
+
await sleep(durationMs)
|
|
424
|
+
const { result } = await chrome.debugger.sendCommand(target, "Profiler.takePreciseCoverage")
|
|
425
|
+
await chrome.debugger.sendCommand(target, "Profiler.stopPreciseCoverage")
|
|
426
|
+
|
|
427
|
+
const simplified = result
|
|
428
|
+
.map((item) => {
|
|
429
|
+
const totalCount = item.functions.reduce((sum, f) => sum + (f.callCount || 0), 0)
|
|
430
|
+
return { url: item.url || "(inline)", scriptId: item.scriptId, totalCount }
|
|
431
|
+
})
|
|
432
|
+
.sort((a, b) => b.totalCount - a.totalCount)
|
|
433
|
+
.slice(0, 20)
|
|
434
|
+
|
|
435
|
+
return { topScripts: simplified, rawCount: result.length }
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function findContexts(source, query, maxMatches) {
|
|
439
|
+
const lower = source.toLowerCase()
|
|
440
|
+
const q = query.toLowerCase()
|
|
441
|
+
const matches = []
|
|
442
|
+
let idx = lower.indexOf(q)
|
|
443
|
+
while (idx !== -1 && matches.length < maxMatches) {
|
|
444
|
+
const start = Math.max(0, idx - 200)
|
|
445
|
+
const end = Math.min(source.length, idx + q.length + 200)
|
|
446
|
+
matches.push({ start, end, context: source.slice(start, end) })
|
|
447
|
+
idx = lower.indexOf(q, idx + q.length)
|
|
448
|
+
}
|
|
449
|
+
return matches
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
async function handleFindByString(params = {}) {
|
|
453
|
+
const target = await ensureAttached()
|
|
454
|
+
const query = params.query
|
|
455
|
+
const maxMatches = params.maxMatches || 5
|
|
456
|
+
const preferred = params.scriptUrlContains
|
|
457
|
+
|
|
458
|
+
const results = []
|
|
459
|
+
const entries = [...scriptMap.entries()]
|
|
460
|
+
for (const [id, meta] of entries) {
|
|
461
|
+
if (preferred && (!meta.url || !meta.url.includes(preferred))) continue
|
|
462
|
+
if (!scriptSourceCache.has(id)) {
|
|
463
|
+
const { scriptSource } = await chrome.debugger.sendCommand(target, "Debugger.getScriptSource", { scriptId: id })
|
|
464
|
+
scriptSourceCache.set(id, scriptSource)
|
|
465
|
+
}
|
|
466
|
+
const source = scriptSourceCache.get(id)
|
|
467
|
+
const matches = findContexts(source, query, maxMatches - results.length)
|
|
468
|
+
if (matches.length) {
|
|
469
|
+
results.push({ url: meta.url, scriptId: id, matches })
|
|
470
|
+
}
|
|
471
|
+
if (results.length >= maxMatches) break
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return { query, results }
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
async function handleSymbolicHints() {
|
|
478
|
+
const target = await ensureAttached()
|
|
479
|
+
const expression = `(function(){
|
|
480
|
+
try {
|
|
481
|
+
const resources = performance.getEntriesByType('resource').slice(-20).map(e => ({
|
|
482
|
+
name: e.name, type: e.initiatorType || '', size: e.transferSize || 0
|
|
483
|
+
}));
|
|
484
|
+
const globals = Object.keys(window).filter(k => k.length < 30).slice(0, 60);
|
|
485
|
+
const ls = Object.keys(localStorage || {}).slice(0, 20);
|
|
486
|
+
return {
|
|
487
|
+
location: window.location.href,
|
|
488
|
+
ua: navigator.userAgent,
|
|
489
|
+
resources, globals, localStorageKeys: ls
|
|
490
|
+
};
|
|
491
|
+
} catch (e) { return { error: e.message }; }
|
|
492
|
+
})()`
|
|
493
|
+
const { result } = await chrome.debugger.sendCommand(target, "Runtime.evaluate", {
|
|
494
|
+
expression,
|
|
495
|
+
returnByValue: true,
|
|
496
|
+
})
|
|
497
|
+
return result?.value
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
async function handleEval(params = {}) {
|
|
501
|
+
const target = await ensureAttached()
|
|
502
|
+
const { result } = await chrome.debugger.sendCommand(target, "Runtime.evaluate", {
|
|
503
|
+
expression: params.code,
|
|
504
|
+
returnByValue: true,
|
|
505
|
+
})
|
|
506
|
+
return result?.value
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
async function handleListNetworkRequests(params = {}) {
|
|
510
|
+
await ensureAttached()
|
|
511
|
+
const { filter, method, status, resourceType, limit = 50 } = params
|
|
512
|
+
|
|
513
|
+
let results = [...networkRequests]
|
|
514
|
+
const pending = [...requestMap.values()].map(r => ({ ...r, status: "pending" }))
|
|
515
|
+
results = [...pending, ...results]
|
|
516
|
+
|
|
517
|
+
if (filter) {
|
|
518
|
+
const lowerFilter = filter.toLowerCase()
|
|
519
|
+
results = results.filter(r => r.url?.toLowerCase().includes(lowerFilter))
|
|
520
|
+
}
|
|
521
|
+
if (method) results = results.filter(r => r.method?.toUpperCase() === method.toUpperCase())
|
|
522
|
+
if (status) results = results.filter(r => r.status === status)
|
|
523
|
+
if (resourceType) {
|
|
524
|
+
const lowerType = resourceType.toLowerCase()
|
|
525
|
+
results = results.filter(r => r.resourceType?.toLowerCase() === lowerType)
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
results = results.slice(0, limit)
|
|
529
|
+
|
|
530
|
+
return {
|
|
531
|
+
total: networkRequests.length + requestMap.size,
|
|
532
|
+
filtered: results.length,
|
|
533
|
+
requests: results.map(r => ({
|
|
534
|
+
requestId: r.requestId,
|
|
535
|
+
url: r.url,
|
|
536
|
+
method: r.method,
|
|
537
|
+
status: r.status,
|
|
538
|
+
statusCode: r.statusCode,
|
|
539
|
+
resourceType: r.resourceType,
|
|
540
|
+
mimeType: r.mimeType,
|
|
541
|
+
duration: r.duration,
|
|
542
|
+
encodedDataLength: r.encodedDataLength,
|
|
543
|
+
fromCache: r.fromCache,
|
|
544
|
+
timestamp: r.timestamp,
|
|
545
|
+
errorText: r.errorText,
|
|
546
|
+
})),
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
async function handleGetNetworkDetail(params = {}) {
|
|
551
|
+
const target = await ensureAttached()
|
|
552
|
+
const { requestId, includeBody = false } = params
|
|
553
|
+
if (!requestId) throw new Error("需要提供 requestId")
|
|
554
|
+
|
|
555
|
+
let entry = requestMap.get(requestId)
|
|
556
|
+
if (!entry) entry = networkRequests.find(r => r.requestId === requestId)
|
|
557
|
+
if (!entry) throw new Error(`未找到请求: ${requestId}`)
|
|
558
|
+
|
|
559
|
+
const result = { ...entry }
|
|
560
|
+
|
|
561
|
+
if (includeBody && entry.status !== "pending" && entry.status !== "failed") {
|
|
562
|
+
try {
|
|
563
|
+
const { body, base64Encoded } = await chrome.debugger.sendCommand(
|
|
564
|
+
target, "Network.getResponseBody", { requestId }
|
|
565
|
+
)
|
|
566
|
+
if (base64Encoded) {
|
|
567
|
+
result.bodyInfo = { type: "binary", base64Length: body.length, note: "二进制内容,已 base64 编码" }
|
|
568
|
+
if (body.length < CONFIG.maxRequestBodySize) result.bodyBase64 = body
|
|
569
|
+
} else {
|
|
570
|
+
if (body.length > CONFIG.maxRequestBodySize) {
|
|
571
|
+
result.body = body.slice(0, CONFIG.maxRequestBodySize)
|
|
572
|
+
result.bodyTruncated = true
|
|
573
|
+
result.bodyTotalLength = body.length
|
|
574
|
+
} else {
|
|
575
|
+
result.body = body
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
} catch (e) {
|
|
579
|
+
result.bodyError = e.message
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
return result
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
async function handleClearNetworkRequests() {
|
|
587
|
+
await ensureAttached()
|
|
588
|
+
const count = networkRequests.length
|
|
589
|
+
networkRequests = []
|
|
590
|
+
return { cleared: count }
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
async function handleCaptureScreenshot(params = {}) {
|
|
594
|
+
const target = await ensureAttached()
|
|
595
|
+
const { format = 'png', quality, fullPage = false, clip } = params
|
|
596
|
+
|
|
597
|
+
await chrome.debugger.sendCommand(target, 'Page.enable')
|
|
598
|
+
|
|
599
|
+
let captureParams = {
|
|
600
|
+
format,
|
|
601
|
+
...(quality !== undefined && format === 'jpeg' ? { quality } : {}),
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (clip) {
|
|
605
|
+
captureParams.clip = { x: clip.x || 0, y: clip.y || 0, width: clip.width, height: clip.height, scale: clip.scale || 1 }
|
|
606
|
+
} else if (fullPage) {
|
|
607
|
+
const { result } = await chrome.debugger.sendCommand(target, 'Runtime.evaluate', {
|
|
608
|
+
expression: `(function() {
|
|
609
|
+
return {
|
|
610
|
+
width: Math.max(document.body.scrollWidth, document.documentElement.scrollWidth, document.body.offsetWidth, document.documentElement.offsetWidth, document.body.clientWidth, document.documentElement.clientWidth),
|
|
611
|
+
height: Math.max(document.body.scrollHeight, document.documentElement.scrollHeight, document.body.offsetHeight, document.documentElement.offsetHeight, document.body.clientHeight, document.documentElement.clientHeight)
|
|
612
|
+
};
|
|
613
|
+
})()`,
|
|
614
|
+
returnByValue: true,
|
|
615
|
+
})
|
|
616
|
+
|
|
617
|
+
const pageSize = result?.value
|
|
618
|
+
if (pageSize && pageSize.width && pageSize.height) {
|
|
619
|
+
const maxWidth = Math.min(pageSize.width, 4096)
|
|
620
|
+
const maxHeight = Math.min(pageSize.height, 16384)
|
|
621
|
+
|
|
622
|
+
captureParams.clip = { x: 0, y: 0, width: maxWidth, height: maxHeight, scale: 1 }
|
|
623
|
+
|
|
624
|
+
const { result: viewportResult } = await chrome.debugger.sendCommand(target, 'Runtime.evaluate', {
|
|
625
|
+
expression: `({ width: window.innerWidth, height: window.innerHeight })`,
|
|
626
|
+
returnByValue: true,
|
|
627
|
+
})
|
|
628
|
+
const originalViewport = viewportResult?.value
|
|
629
|
+
|
|
630
|
+
await chrome.debugger.sendCommand(target, 'Emulation.setDeviceMetricsOverride', {
|
|
631
|
+
width: maxWidth, height: maxHeight, deviceScaleFactor: 1, mobile: false,
|
|
632
|
+
})
|
|
633
|
+
|
|
634
|
+
try {
|
|
635
|
+
const { data } = await chrome.debugger.sendCommand(target, 'Page.captureScreenshot', captureParams)
|
|
636
|
+
if (originalViewport) {
|
|
637
|
+
await chrome.debugger.sendCommand(target, 'Emulation.setDeviceMetricsOverride', {
|
|
638
|
+
width: originalViewport.width, height: originalViewport.height, deviceScaleFactor: 1, mobile: false,
|
|
639
|
+
})
|
|
640
|
+
}
|
|
641
|
+
await chrome.debugger.sendCommand(target, 'Emulation.clearDeviceMetricsOverride').catch(() => {})
|
|
642
|
+
return {
|
|
643
|
+
imageData: data, format, fullPage: true, width: maxWidth, height: maxHeight,
|
|
644
|
+
note: pageSize.height > maxHeight ? `页面高度 ${pageSize.height}px 超过限制,已截取前 ${maxHeight}px` : undefined,
|
|
645
|
+
}
|
|
646
|
+
} catch (e) {
|
|
647
|
+
await chrome.debugger.sendCommand(target, 'Emulation.clearDeviceMetricsOverride').catch(() => {})
|
|
648
|
+
throw e
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
const { data } = await chrome.debugger.sendCommand(target, 'Page.captureScreenshot', captureParams)
|
|
654
|
+
const { result: sizeResult } = await chrome.debugger.sendCommand(target, 'Runtime.evaluate', {
|
|
655
|
+
expression: `({ width: window.innerWidth, height: window.innerHeight })`,
|
|
656
|
+
returnByValue: true,
|
|
657
|
+
})
|
|
658
|
+
|
|
659
|
+
return { imageData: data, format, fullPage: false, width: sizeResult?.value?.width, height: sizeResult?.value?.height }
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
async function handleGetPageContent(params = {}) {
|
|
663
|
+
const target = await ensureAttached()
|
|
664
|
+
const { mode = "text", selector, maxLength = 50000, includeMetadata = true } = params
|
|
665
|
+
|
|
666
|
+
const selectorStr = selector ? JSON.stringify(selector) : 'null'
|
|
667
|
+
const modeStr = JSON.stringify(mode)
|
|
668
|
+
|
|
669
|
+
const expression = `(function() {
|
|
670
|
+
try {
|
|
671
|
+
const result = {};
|
|
672
|
+
if (document.readyState === 'loading') {
|
|
673
|
+
return { error: '页面尚未加载完成,请稍后重试', readyState: document.readyState };
|
|
674
|
+
}
|
|
675
|
+
let targetElement = document.body;
|
|
676
|
+
const selector = ${selectorStr};
|
|
677
|
+
if (selector) {
|
|
678
|
+
try {
|
|
679
|
+
targetElement = document.querySelector(selector);
|
|
680
|
+
if (!targetElement) {
|
|
681
|
+
return { error: '选择器未匹配到任何元素', selector: selector, suggestion: '请检查选择器是否正确' };
|
|
682
|
+
}
|
|
683
|
+
result.selector = selector;
|
|
684
|
+
result.matchedTag = targetElement.tagName.toLowerCase();
|
|
685
|
+
} catch (e) {
|
|
686
|
+
return { error: '无效的 CSS 选择器: ' + e.message, selector: selector };
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
const includeMetadata = ${includeMetadata};
|
|
690
|
+
if (includeMetadata) {
|
|
691
|
+
result.metadata = {
|
|
692
|
+
title: document.title || '',
|
|
693
|
+
url: window.location.href,
|
|
694
|
+
description: document.querySelector('meta[name="description"]')?.content || '',
|
|
695
|
+
keywords: document.querySelector('meta[name="keywords"]')?.content || '',
|
|
696
|
+
charset: document.characterSet,
|
|
697
|
+
language: document.documentElement.lang || '',
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
const mode = ${modeStr};
|
|
701
|
+
const maxLength = ${maxLength};
|
|
702
|
+
if (mode === 'text') {
|
|
703
|
+
let text = targetElement.innerText || targetElement.textContent || '';
|
|
704
|
+
text = text.replace(/\\n{3,}/g, '\\n\\n').trim();
|
|
705
|
+
result.contentLength = text.length;
|
|
706
|
+
if (text.length > maxLength) {
|
|
707
|
+
result.content = text.slice(0, maxLength);
|
|
708
|
+
result.truncated = true;
|
|
709
|
+
} else {
|
|
710
|
+
result.content = text;
|
|
711
|
+
result.truncated = false;
|
|
712
|
+
}
|
|
713
|
+
} else if (mode === 'html') {
|
|
714
|
+
let html = targetElement.outerHTML || '';
|
|
715
|
+
result.contentLength = html.length;
|
|
716
|
+
if (html.length > maxLength) {
|
|
717
|
+
result.content = html.slice(0, maxLength);
|
|
718
|
+
result.truncated = true;
|
|
719
|
+
result.note = 'HTML 已截断,可能不完整';
|
|
720
|
+
} else {
|
|
721
|
+
result.content = html;
|
|
722
|
+
result.truncated = false;
|
|
723
|
+
}
|
|
724
|
+
} else if (mode === 'structured') {
|
|
725
|
+
const structured = {};
|
|
726
|
+
const headings = targetElement.querySelectorAll('h1,h2,h3,h4,h5,h6');
|
|
727
|
+
structured.headings = Array.from(headings).slice(0, 50).map(h => ({ level: parseInt(h.tagName[1]), text: h.innerText.trim().slice(0, 200) }));
|
|
728
|
+
const links = targetElement.querySelectorAll('a[href]');
|
|
729
|
+
structured.links = Array.from(links).slice(0, 100).map(a => ({ text: (a.innerText || '').trim().slice(0, 100), href: a.href })).filter(l => l.href && !l.href.startsWith('javascript:'));
|
|
730
|
+
const buttons = targetElement.querySelectorAll('button, input[type="button"], input[type="submit"], [role="button"]');
|
|
731
|
+
structured.buttons = Array.from(buttons).slice(0, 50).map(b => ({ text: (b.innerText || b.value || b.getAttribute('aria-label') || '').trim().slice(0, 100), type: b.type || 'button', disabled: b.disabled || false }));
|
|
732
|
+
const forms = targetElement.querySelectorAll('form');
|
|
733
|
+
structured.forms = Array.from(forms).slice(0, 20).map(f => {
|
|
734
|
+
const fields = Array.from(f.querySelectorAll('input, select, textarea')).slice(0, 30);
|
|
735
|
+
return { action: f.action || '', method: (f.method || 'GET').toUpperCase(), fieldCount: fields.length, fields: fields.map(field => ({ tag: field.tagName.toLowerCase(), type: field.type || '', name: field.name || '', placeholder: field.placeholder || '', required: field.required || false })) };
|
|
736
|
+
});
|
|
737
|
+
const images = targetElement.querySelectorAll('img');
|
|
738
|
+
structured.images = Array.from(images).slice(0, 50).map(img => ({ alt: img.alt || '', src: img.src ? img.src.slice(0, 200) : '' })).filter(img => img.src);
|
|
739
|
+
const tables = targetElement.querySelectorAll('table');
|
|
740
|
+
structured.tables = Array.from(tables).slice(0, 10).map(table => {
|
|
741
|
+
const headers = Array.from(table.querySelectorAll('th')).map(th => th.innerText.trim().slice(0, 50));
|
|
742
|
+
const rows = table.querySelectorAll('tr');
|
|
743
|
+
return { headers: headers.slice(0, 20), rowCount: rows.length };
|
|
744
|
+
});
|
|
745
|
+
result.structured = structured;
|
|
746
|
+
result.counts = { headings: structured.headings.length, links: structured.links.length, buttons: structured.buttons.length, forms: structured.forms.length, images: structured.images.length, tables: structured.tables.length };
|
|
747
|
+
}
|
|
748
|
+
result.mode = mode;
|
|
749
|
+
return result;
|
|
750
|
+
} catch (e) {
|
|
751
|
+
return { error: e.message };
|
|
752
|
+
}
|
|
753
|
+
})()`
|
|
754
|
+
|
|
755
|
+
const { result } = await chrome.debugger.sendCommand(target, "Runtime.evaluate", {
|
|
756
|
+
expression,
|
|
757
|
+
returnByValue: true,
|
|
758
|
+
})
|
|
759
|
+
|
|
760
|
+
if (result?.value?.error) throw new Error(result.value.error)
|
|
761
|
+
return result?.value
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// 处理来自服务器的命令
|
|
765
|
+
async function handleCommand(message) {
|
|
766
|
+
const { id, command, params, token } = message
|
|
767
|
+
if (!id || !command) return
|
|
768
|
+
if (!state.enabled) {
|
|
769
|
+
sendToServer({ id, error: "扩展已暂停,点击图标重新开启" })
|
|
770
|
+
return
|
|
771
|
+
}
|
|
772
|
+
if (CONFIG.token && CONFIG.token !== token) {
|
|
773
|
+
sendToServer({ id, error: "token 校验失败" })
|
|
774
|
+
return
|
|
775
|
+
}
|
|
776
|
+
try {
|
|
777
|
+
let result
|
|
778
|
+
if (command === "getLastError") result = await handleGetLastError()
|
|
779
|
+
else if (command === "getScriptSource") result = await handleGetScriptSource(params)
|
|
780
|
+
else if (command === "coverageSnapshot") result = await handleCoverageSnapshot(params)
|
|
781
|
+
else if (command === "findByString") result = await handleFindByString(params)
|
|
782
|
+
else if (command === "symbolicHints") result = await handleSymbolicHints()
|
|
783
|
+
else if (command === "eval") result = await handleEval(params)
|
|
784
|
+
else if (command === "listNetworkRequests") result = await handleListNetworkRequests(params)
|
|
785
|
+
else if (command === "getNetworkDetail") result = await handleGetNetworkDetail(params)
|
|
786
|
+
else if (command === "clearNetworkRequests") result = await handleClearNetworkRequests()
|
|
787
|
+
else if (command === "captureScreenshot") result = await handleCaptureScreenshot(params)
|
|
788
|
+
else if (command === "getPageContent") result = await handleGetPageContent(params)
|
|
789
|
+
else throw new Error(`未知指令 ${command}`)
|
|
790
|
+
|
|
791
|
+
sendToServer({ id, result })
|
|
792
|
+
} catch (e) {
|
|
793
|
+
sendToServer({ id, error: e.message })
|
|
794
|
+
} finally {
|
|
795
|
+
await maybeDetach()
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// 发送消息到服务器(通过 offscreen)
|
|
800
|
+
function sendToServer(data) {
|
|
801
|
+
chrome.runtime.sendMessage({ type: 'send', data }).catch(() => {})
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// ========== 消息监听 ==========
|
|
805
|
+
|
|
806
|
+
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|
807
|
+
// 判断消息来源
|
|
808
|
+
const senderUrl = sender.url || ''
|
|
809
|
+
const isFromOffscreen = senderUrl.includes('offscreen.html')
|
|
810
|
+
const isFromBackground = !sender.url // background 发的消息没有 url
|
|
811
|
+
|
|
812
|
+
// background 自己发出的消息不处理(避免循环)
|
|
813
|
+
if (isFromBackground) {
|
|
814
|
+
return
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// 主动推送状态给 popup
|
|
818
|
+
function broadcastStatus() {
|
|
819
|
+
let status
|
|
820
|
+
if (!state.enabled) {
|
|
821
|
+
status = 'disconnected'
|
|
822
|
+
} else if (state.connected) {
|
|
823
|
+
status = 'connected'
|
|
824
|
+
} else if (state.scanRound >= 2) {
|
|
825
|
+
status = 'not_found'
|
|
826
|
+
} else {
|
|
827
|
+
status = 'scanning'
|
|
828
|
+
}
|
|
829
|
+
chrome.runtime.sendMessage({
|
|
830
|
+
type: 'statusUpdate',
|
|
831
|
+
state: {
|
|
832
|
+
status,
|
|
833
|
+
enabled: state.enabled,
|
|
834
|
+
port: state.port,
|
|
835
|
+
currentPort: state.currentPort,
|
|
836
|
+
basePort: CONFIG.basePort,
|
|
837
|
+
scanRound: state.scanRound,
|
|
838
|
+
}
|
|
839
|
+
}).catch(() => {}) // popup 可能未打开,忽略错误
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// 来自 offscreen 的状态更新
|
|
843
|
+
if (message.type === 'status' && isFromOffscreen) {
|
|
844
|
+
if (message.status === 'connected') {
|
|
845
|
+
state.connected = true
|
|
846
|
+
state.port = message.port
|
|
847
|
+
setBadgeState('on')
|
|
848
|
+
log(`✅ 已连接到 ghost-bridge 服务 (端口 ${message.port})`)
|
|
849
|
+
ensureAttached().catch((e) => log(`attach 失败:${e.message}`))
|
|
850
|
+
} else if (message.status === 'disconnected') {
|
|
851
|
+
state.connected = false
|
|
852
|
+
state.port = null
|
|
853
|
+
if (state.enabled) setBadgeState('connecting')
|
|
854
|
+
} else if (message.status === 'scanning') {
|
|
855
|
+
state.currentPort = message.currentPort
|
|
856
|
+
setBadgeState('connecting')
|
|
857
|
+
} else if (message.status === 'not_found') {
|
|
858
|
+
state.scanRound++
|
|
859
|
+
setBadgeState('connecting')
|
|
860
|
+
}
|
|
861
|
+
broadcastStatus() // 状态变化时主动推送
|
|
862
|
+
return
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// 来自 offscreen 的日志
|
|
866
|
+
if (message.type === 'log' && isFromOffscreen) {
|
|
867
|
+
console.log(`[offscreen] ${message.msg}`)
|
|
868
|
+
return
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// 来自 offscreen 的命令(从服务器转发)
|
|
872
|
+
if (message.type === 'command' && isFromOffscreen) {
|
|
873
|
+
handleCommand(message.data)
|
|
874
|
+
return
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// send 消息是 background 发给 offscreen 的,这里不处理
|
|
878
|
+
if (message.type === 'send') {
|
|
879
|
+
return
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// 来自 popup 的状态查询
|
|
883
|
+
if (message.type === 'getStatus') {
|
|
884
|
+
let status
|
|
885
|
+
if (!state.enabled) {
|
|
886
|
+
status = 'disconnected'
|
|
887
|
+
} else if (state.connected) {
|
|
888
|
+
status = 'connected'
|
|
889
|
+
} else if (state.scanRound >= 2) {
|
|
890
|
+
status = 'not_found'
|
|
891
|
+
} else {
|
|
892
|
+
status = 'scanning'
|
|
893
|
+
}
|
|
894
|
+
sendResponse({
|
|
895
|
+
status,
|
|
896
|
+
enabled: state.enabled,
|
|
897
|
+
port: state.port,
|
|
898
|
+
currentPort: state.currentPort,
|
|
899
|
+
basePort: CONFIG.basePort,
|
|
900
|
+
scanRound: state.scanRound,
|
|
901
|
+
})
|
|
902
|
+
return true
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// 来自 popup 的连接请求
|
|
906
|
+
if (message.type === 'connect') {
|
|
907
|
+
if (message.port) {
|
|
908
|
+
CONFIG.basePort = message.port
|
|
909
|
+
chrome.storage.local.set({ basePort: message.port })
|
|
910
|
+
}
|
|
911
|
+
state.enabled = true
|
|
912
|
+
state.scanRound = 0
|
|
913
|
+
setBadgeState('connecting')
|
|
914
|
+
|
|
915
|
+
// 启动 offscreen 并开始连接
|
|
916
|
+
setupOffscreenDocument().then(() => {
|
|
917
|
+
chrome.runtime.sendMessage({
|
|
918
|
+
type: 'connect',
|
|
919
|
+
basePort: CONFIG.basePort,
|
|
920
|
+
token: CONFIG.token,
|
|
921
|
+
maxPortRetries: CONFIG.maxPortRetries,
|
|
922
|
+
}).catch(() => {})
|
|
923
|
+
})
|
|
924
|
+
|
|
925
|
+
sendResponse({ ok: true })
|
|
926
|
+
return true
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
// 来自 popup 的断开请求
|
|
930
|
+
if (message.type === 'disconnect') {
|
|
931
|
+
state.enabled = false
|
|
932
|
+
state.connected = false
|
|
933
|
+
state.scanRound = 0
|
|
934
|
+
setBadgeState('off')
|
|
935
|
+
detachAllTargets().catch(() => {})
|
|
936
|
+
|
|
937
|
+
// 通知 offscreen 断开
|
|
938
|
+
chrome.runtime.sendMessage({ type: 'disconnect' }).catch(() => {})
|
|
939
|
+
|
|
940
|
+
sendResponse({ ok: true })
|
|
941
|
+
return true
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
return false
|
|
945
|
+
})
|
|
946
|
+
|
|
947
|
+
// 启动时从 storage 加载端口配置
|
|
948
|
+
chrome.storage.local.get(['basePort'], (result) => {
|
|
949
|
+
if (result.basePort) {
|
|
950
|
+
CONFIG.basePort = result.basePort
|
|
951
|
+
}
|
|
952
|
+
})
|
|
953
|
+
|
|
954
|
+
// 默认暂停
|
|
955
|
+
setBadgeState("off")
|
|
956
|
+
log("Ghost Bridge background 已加载")
|