ghost-bridge 0.6.2 → 0.7.1
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 +1 -12
- package/dist/cli.js +4 -4
- package/dist/server.js +111 -41
- package/extension/background.js +12 -553
- package/extension/bg-dom.js +441 -0
- package/extension/bg-network.js +194 -0
- package/extension/manifest.json +1 -3
- package/extension/offscreen.js +5 -7
- package/package.json +1 -1
- package/extension/icon-32.png +0 -0
package/extension/background.js
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
const firstDayOfMonth = new Date(now.getFullYear(), now.getMonth(), 1, 0, 0, 0, 0)
|
|
5
|
-
return String(firstDayOfMonth.getTime())
|
|
6
|
-
}
|
|
1
|
+
importScripts('bg-network.js', 'bg-dom.js')
|
|
2
|
+
|
|
3
|
+
const DEFAULT_TOKEN = 'ghost-bridge-local'
|
|
7
4
|
|
|
8
5
|
const CONFIG = {
|
|
9
6
|
basePort: 33333,
|
|
10
|
-
token:
|
|
7
|
+
token: DEFAULT_TOKEN,
|
|
11
8
|
autoDetach: false,
|
|
12
9
|
maxErrors: 100,
|
|
13
10
|
maxStackFrames: 20,
|
|
@@ -240,187 +237,12 @@ function pushNetworkRequest(entry) {
|
|
|
240
237
|
trimNetworkRequests()
|
|
241
238
|
}
|
|
242
239
|
|
|
243
|
-
function getApiSignalScore(entry) {
|
|
244
|
-
const url = (entry.url || '').toLowerCase()
|
|
245
|
-
let score = 0
|
|
246
|
-
if (url.includes('/api/')) score += 80
|
|
247
|
-
if (url.includes('graphql')) score += 80
|
|
248
|
-
if (url.includes('/rpc/')) score += 60
|
|
249
|
-
if (url.includes('/rest/')) score += 40
|
|
250
|
-
if ((entry.method || 'GET').toUpperCase() !== 'GET') score += 25
|
|
251
|
-
return score
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
function getResourceTypeScore(entry, mode = 'debug') {
|
|
255
|
-
const type = (entry.resourceType || '').toLowerCase()
|
|
256
|
-
const debugScores = {
|
|
257
|
-
fetch: 140,
|
|
258
|
-
xhr: 140,
|
|
259
|
-
websocket: 120,
|
|
260
|
-
document: 90,
|
|
261
|
-
script: 45,
|
|
262
|
-
stylesheet: 25,
|
|
263
|
-
other: 0,
|
|
264
|
-
image: -40,
|
|
265
|
-
font: -40,
|
|
266
|
-
media: -50,
|
|
267
|
-
}
|
|
268
|
-
const apiScores = {
|
|
269
|
-
fetch: 220,
|
|
270
|
-
xhr: 220,
|
|
271
|
-
websocket: 160,
|
|
272
|
-
document: 40,
|
|
273
|
-
script: -10,
|
|
274
|
-
stylesheet: -20,
|
|
275
|
-
other: 0,
|
|
276
|
-
image: -80,
|
|
277
|
-
font: -80,
|
|
278
|
-
media: -90,
|
|
279
|
-
}
|
|
280
|
-
const table = mode === 'api' ? apiScores : debugScores
|
|
281
|
-
return table[type] ?? table.other
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
function getStatusScore(entry) {
|
|
285
|
-
if (entry.status === 'failed') return 360
|
|
286
|
-
if (entry.status === 'error') return 330
|
|
287
|
-
if (entry.status === 'pending') return 280
|
|
288
|
-
if ((entry.statusCode || 0) >= 500) return 340
|
|
289
|
-
if ((entry.statusCode || 0) >= 400) return 300
|
|
290
|
-
return 80
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
function getNetworkPriorityScore(entry, mode = 'debug') {
|
|
294
|
-
if (mode === 'recent') {
|
|
295
|
-
return entry.timestamp || 0
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
let score = getStatusScore(entry)
|
|
299
|
-
score += getResourceTypeScore(entry, mode)
|
|
300
|
-
score += getApiSignalScore(entry)
|
|
301
|
-
|
|
302
|
-
if (entry.fromCache) score -= 20
|
|
303
|
-
if ((entry.encodedDataLength || 0) === 0 && entry.status === 'success') score -= 10
|
|
304
|
-
|
|
305
|
-
return score
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
function compareNetworkEntries(a, b, mode = 'debug') {
|
|
309
|
-
if (mode === 'recent') {
|
|
310
|
-
return (b.timestamp || 0) - (a.timestamp || 0)
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
const scoreDiff = getNetworkPriorityScore(b, mode) - getNetworkPriorityScore(a, mode)
|
|
314
|
-
if (scoreDiff !== 0) return scoreDiff
|
|
315
|
-
return (b.timestamp || 0) - (a.timestamp || 0)
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
const MAX_NETWORK_URL_OUTPUT_LENGTH = 240
|
|
319
|
-
const NETWORK_URL_HEAD_LENGTH = 180
|
|
320
|
-
const NETWORK_URL_TAIL_LENGTH = 40
|
|
321
|
-
const MAX_DATA_URL_OUTPUT_LENGTH = 256
|
|
322
|
-
|
|
323
|
-
function summarizeNetworkUrl(url) {
|
|
324
|
-
if (!url) return { displayUrl: url }
|
|
325
|
-
|
|
326
|
-
const urlOriginalLength = url.length
|
|
327
|
-
const schemeMatch = /^([a-z][a-z0-9+.-]*):/i.exec(url)
|
|
328
|
-
const urlScheme = schemeMatch?.[1]?.toLowerCase()
|
|
329
|
-
|
|
330
|
-
if (urlScheme === 'data') {
|
|
331
|
-
const commaIndex = url.indexOf(',')
|
|
332
|
-
const meta = commaIndex >= 0 ? url.slice(5, commaIndex) : url.slice(5)
|
|
333
|
-
const dataUrlMimeType = (meta.split(';')[0] || 'text/plain').toLowerCase()
|
|
334
|
-
const isBase64 = meta.includes(';base64')
|
|
335
|
-
|
|
336
|
-
if (!isBase64 && urlOriginalLength <= MAX_DATA_URL_OUTPUT_LENGTH) {
|
|
337
|
-
return {
|
|
338
|
-
displayUrl: url,
|
|
339
|
-
urlOriginalLength,
|
|
340
|
-
urlScheme,
|
|
341
|
-
urlTruncated: false,
|
|
342
|
-
dataUrlMimeType,
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
return {
|
|
347
|
-
displayUrl: `data:${dataUrlMimeType}${isBase64 ? ';base64' : ''},<omitted ${urlOriginalLength} chars>`,
|
|
348
|
-
urlOriginalLength,
|
|
349
|
-
urlScheme,
|
|
350
|
-
urlTruncated: true,
|
|
351
|
-
dataUrlMimeType,
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
if (urlOriginalLength > MAX_NETWORK_URL_OUTPUT_LENGTH) {
|
|
356
|
-
return {
|
|
357
|
-
displayUrl: `${url.slice(0, NETWORK_URL_HEAD_LENGTH)}...${url.slice(-NETWORK_URL_TAIL_LENGTH)}`,
|
|
358
|
-
urlOriginalLength,
|
|
359
|
-
urlScheme,
|
|
360
|
-
urlTruncated: true,
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
return {
|
|
365
|
-
displayUrl: url,
|
|
366
|
-
urlOriginalLength,
|
|
367
|
-
urlScheme,
|
|
368
|
-
urlTruncated: false,
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
function buildNetworkRequestSummary(entry) {
|
|
373
|
-
const urlMeta = summarizeNetworkUrl(entry.url)
|
|
374
|
-
return {
|
|
375
|
-
requestId: entry.requestId,
|
|
376
|
-
url: urlMeta.displayUrl,
|
|
377
|
-
...(urlMeta.urlTruncated ? { urlTruncated: true, urlOriginalLength: urlMeta.urlOriginalLength } : {}),
|
|
378
|
-
...(urlMeta.urlScheme ? { urlScheme: urlMeta.urlScheme } : {}),
|
|
379
|
-
...(urlMeta.dataUrlMimeType ? { dataUrlMimeType: urlMeta.dataUrlMimeType } : {}),
|
|
380
|
-
method: entry.method,
|
|
381
|
-
status: entry.status,
|
|
382
|
-
statusCode: entry.statusCode,
|
|
383
|
-
resourceType: entry.resourceType,
|
|
384
|
-
mimeType: entry.mimeType,
|
|
385
|
-
duration: entry.duration,
|
|
386
|
-
encodedDataLength: entry.encodedDataLength,
|
|
387
|
-
fromCache: entry.fromCache,
|
|
388
|
-
timestamp: entry.timestamp,
|
|
389
|
-
errorText: entry.errorText,
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
|
|
393
240
|
function trimNetworkRequests() {
|
|
394
|
-
|
|
395
|
-
let worstIndex = 0
|
|
396
|
-
for (let i = 1; i < networkRequests.length; i++) {
|
|
397
|
-
const candidate = networkRequests[i]
|
|
398
|
-
const worst = networkRequests[worstIndex]
|
|
399
|
-
const cmp = compareNetworkEntries(candidate, worst, 'debug')
|
|
400
|
-
if (cmp < 0 || (cmp === 0 && (candidate.timestamp || 0) < (worst.timestamp || 0))) {
|
|
401
|
-
worstIndex = i
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
networkRequests.splice(worstIndex, 1)
|
|
405
|
-
}
|
|
241
|
+
GhostBridgeNetwork.trimTrackedRequests(networkRequests, CONFIG.maxRequestsTracked)
|
|
406
242
|
}
|
|
407
243
|
|
|
408
244
|
function trimPendingRequests() {
|
|
409
|
-
|
|
410
|
-
const entries = [...requestMap.entries()]
|
|
411
|
-
let worstKey = entries[0]?.[0]
|
|
412
|
-
let worstValue = entries[0]?.[1]
|
|
413
|
-
for (let i = 1; i < entries.length; i++) {
|
|
414
|
-
const [key, value] = entries[i]
|
|
415
|
-
const cmp = compareNetworkEntries(value, worstValue, 'debug')
|
|
416
|
-
if (cmp < 0 || (cmp === 0 && (value.timestamp || 0) < (worstValue.timestamp || 0))) {
|
|
417
|
-
worstKey = key
|
|
418
|
-
worstValue = value
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
if (!worstKey) break
|
|
422
|
-
requestMap.delete(worstKey)
|
|
423
|
-
}
|
|
245
|
+
GhostBridgeNetwork.trimPendingRequestMap(requestMap, CONFIG.maxRequestsTracked * 2)
|
|
424
246
|
}
|
|
425
247
|
|
|
426
248
|
chrome.debugger.onDetach.addListener((source, reason) => {
|
|
@@ -745,14 +567,14 @@ async function handleListNetworkRequests(params = {}) {
|
|
|
745
567
|
results = results.filter(r => r.resourceType?.toLowerCase() === lowerType)
|
|
746
568
|
}
|
|
747
569
|
|
|
748
|
-
results.sort((a, b) => compareNetworkEntries(a, b, priorityMode))
|
|
570
|
+
results.sort((a, b) => GhostBridgeNetwork.compareNetworkEntries(a, b, priorityMode))
|
|
749
571
|
results = results.slice(0, limit)
|
|
750
572
|
|
|
751
573
|
return {
|
|
752
574
|
total: networkRequests.length + requestMap.size,
|
|
753
575
|
filtered: results.length,
|
|
754
576
|
priorityMode,
|
|
755
|
-
requests: results.map(buildNetworkRequestSummary),
|
|
577
|
+
requests: results.map((entry) => GhostBridgeNetwork.buildNetworkRequestSummary(entry)),
|
|
756
578
|
}
|
|
757
579
|
}
|
|
758
580
|
|
|
@@ -765,7 +587,7 @@ async function handleGetNetworkDetail(params = {}) {
|
|
|
765
587
|
if (!entry) entry = networkRequests.find(r => r.requestId === requestId)
|
|
766
588
|
if (!entry) throw new Error(`未找到请求: ${requestId}`)
|
|
767
589
|
|
|
768
|
-
const urlMeta = summarizeNetworkUrl(entry.url)
|
|
590
|
+
const urlMeta = GhostBridgeNetwork.summarizeNetworkUrl(entry.url)
|
|
769
591
|
const result = {
|
|
770
592
|
...entry,
|
|
771
593
|
url: urlMeta.displayUrl,
|
|
@@ -1046,186 +868,7 @@ async function handleCaptureScreenshot(params = {}) {
|
|
|
1046
868
|
async function handleInspectPageSnapshot(params = {}) {
|
|
1047
869
|
const target = await ensureAttached()
|
|
1048
870
|
const { selector, includeInteractive = true, maxElements = 30 } = params
|
|
1049
|
-
|
|
1050
|
-
const selectorStr = selector ? JSON.stringify(selector) : 'null'
|
|
1051
|
-
|
|
1052
|
-
const expression = `(function() {
|
|
1053
|
-
try {
|
|
1054
|
-
if (document.readyState === 'loading') {
|
|
1055
|
-
return { error: '页面尚未加载完成,请稍后重试', readyState: document.readyState };
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
const includeInteractive = ${includeInteractive};
|
|
1059
|
-
const maxEls = ${maxElements};
|
|
1060
|
-
const selector = ${selectorStr};
|
|
1061
|
-
const result = {};
|
|
1062
|
-
let targetElement = document.body;
|
|
1063
|
-
|
|
1064
|
-
if (selector) {
|
|
1065
|
-
try {
|
|
1066
|
-
targetElement = document.querySelector(selector);
|
|
1067
|
-
if (!targetElement) {
|
|
1068
|
-
return { error: '选择器未匹配到任何元素', selector: selector, suggestion: '请检查选择器是否正确' };
|
|
1069
|
-
}
|
|
1070
|
-
result.selector = selector;
|
|
1071
|
-
result.matchedTag = targetElement.tagName.toLowerCase();
|
|
1072
|
-
} catch (e) {
|
|
1073
|
-
return { error: '无效的 CSS 选择器: ' + e.message, selector: selector };
|
|
1074
|
-
}
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
|
-
result.metadata = {
|
|
1078
|
-
title: document.title || '',
|
|
1079
|
-
url: window.location.href,
|
|
1080
|
-
description: document.querySelector('meta[name="description"]')?.content || '',
|
|
1081
|
-
keywords: document.querySelector('meta[name="keywords"]')?.content || '',
|
|
1082
|
-
charset: document.characterSet,
|
|
1083
|
-
language: document.documentElement.lang || '',
|
|
1084
|
-
};
|
|
1085
|
-
|
|
1086
|
-
const structured = {};
|
|
1087
|
-
const headings = targetElement.querySelectorAll('h1,h2,h3,h4,h5,h6');
|
|
1088
|
-
structured.headings = Array.from(headings).slice(0, 50).map(h => ({
|
|
1089
|
-
level: parseInt(h.tagName[1]),
|
|
1090
|
-
text: h.innerText.trim().slice(0, 200)
|
|
1091
|
-
}));
|
|
1092
|
-
const links = targetElement.querySelectorAll('a[href]');
|
|
1093
|
-
structured.links = Array.from(links).slice(0, 100).map(a => ({
|
|
1094
|
-
text: (a.innerText || '').trim().slice(0, 100),
|
|
1095
|
-
href: a.href
|
|
1096
|
-
})).filter(l => l.href && !l.href.startsWith('javascript:'));
|
|
1097
|
-
const buttons = targetElement.querySelectorAll('button, input[type="button"], input[type="submit"], [role="button"]');
|
|
1098
|
-
structured.buttons = Array.from(buttons).slice(0, 50).map(b => ({
|
|
1099
|
-
text: (b.innerText || b.value || b.getAttribute('aria-label') || '').trim().slice(0, 100),
|
|
1100
|
-
type: b.type || 'button',
|
|
1101
|
-
disabled: b.disabled || false
|
|
1102
|
-
}));
|
|
1103
|
-
const forms = targetElement.querySelectorAll('form');
|
|
1104
|
-
structured.forms = Array.from(forms).slice(0, 20).map(f => {
|
|
1105
|
-
const fields = Array.from(f.querySelectorAll('input, select, textarea')).slice(0, 30);
|
|
1106
|
-
return {
|
|
1107
|
-
action: f.action || '',
|
|
1108
|
-
method: (f.method || 'GET').toUpperCase(),
|
|
1109
|
-
fieldCount: fields.length,
|
|
1110
|
-
fields: fields.map(field => ({
|
|
1111
|
-
tag: field.tagName.toLowerCase(),
|
|
1112
|
-
type: field.type || '',
|
|
1113
|
-
name: field.name || '',
|
|
1114
|
-
placeholder: field.placeholder || '',
|
|
1115
|
-
required: field.required || false
|
|
1116
|
-
}))
|
|
1117
|
-
};
|
|
1118
|
-
});
|
|
1119
|
-
const images = targetElement.querySelectorAll('img');
|
|
1120
|
-
structured.images = Array.from(images).slice(0, 50).map(img => ({
|
|
1121
|
-
alt: img.alt || '',
|
|
1122
|
-
src: img.src ? img.src.slice(0, 200) : ''
|
|
1123
|
-
})).filter(img => img.src);
|
|
1124
|
-
const tables = targetElement.querySelectorAll('table');
|
|
1125
|
-
structured.tables = Array.from(tables).slice(0, 10).map(table => {
|
|
1126
|
-
const headers = Array.from(table.querySelectorAll('th')).map(th => th.innerText.trim().slice(0, 50));
|
|
1127
|
-
const rows = table.querySelectorAll('tr');
|
|
1128
|
-
return { headers: headers.slice(0, 20), rowCount: rows.length };
|
|
1129
|
-
});
|
|
1130
|
-
|
|
1131
|
-
result.page = {
|
|
1132
|
-
metadata: result.metadata,
|
|
1133
|
-
...(result.selector ? { selector: result.selector, matchedTag: result.matchedTag } : {}),
|
|
1134
|
-
structured,
|
|
1135
|
-
counts: {
|
|
1136
|
-
headings: structured.headings.length,
|
|
1137
|
-
links: structured.links.length,
|
|
1138
|
-
buttons: structured.buttons.length,
|
|
1139
|
-
forms: structured.forms.length,
|
|
1140
|
-
images: structured.images.length,
|
|
1141
|
-
tables: structured.tables.length
|
|
1142
|
-
},
|
|
1143
|
-
mode: 'structured'
|
|
1144
|
-
};
|
|
1145
|
-
|
|
1146
|
-
if (!includeInteractive) {
|
|
1147
|
-
result.interactive = null;
|
|
1148
|
-
return result;
|
|
1149
|
-
}
|
|
1150
|
-
|
|
1151
|
-
let refCounter = 0;
|
|
1152
|
-
const elements = [];
|
|
1153
|
-
const INTERACTIVE_SELECTOR = 'a,button,input,select,textarea,[role="button"],[role="link"],[role="tab"],[role="menuitem"],[role="checkbox"],[role="radio"],[role="switch"],[role="combobox"],[tabindex]:not([tabindex="-1"]),[contenteditable="true"],[onclick]';
|
|
1154
|
-
|
|
1155
|
-
function isVisible(el) {
|
|
1156
|
-
const style = window.getComputedStyle(el);
|
|
1157
|
-
if (style.display === 'none' || style.visibility === 'hidden' || parseFloat(style.opacity) === 0) return null;
|
|
1158
|
-
if (!el.offsetParent && el.tagName !== 'HTML' && el.tagName !== 'BODY' &&
|
|
1159
|
-
style.position !== 'fixed' && style.position !== 'sticky') return null;
|
|
1160
|
-
const rect = el.getBoundingClientRect();
|
|
1161
|
-
if (rect.width === 0 && rect.height === 0) return null;
|
|
1162
|
-
return rect;
|
|
1163
|
-
}
|
|
1164
|
-
|
|
1165
|
-
function buildEntry(el, rect) {
|
|
1166
|
-
refCounter++;
|
|
1167
|
-
const ref = 'e' + refCounter;
|
|
1168
|
-
el.setAttribute('data-ghost-ref', ref);
|
|
1169
|
-
const tag = el.tagName.toLowerCase();
|
|
1170
|
-
const entry = { ref, tag, cx: Math.round(rect.left + rect.width / 2), cy: Math.round(rect.top + rect.height / 2) };
|
|
1171
|
-
if (el.type) entry.type = el.type;
|
|
1172
|
-
if (el.name) entry.name = el.name;
|
|
1173
|
-
if (el.getAttribute('role')) entry.role = el.getAttribute('role');
|
|
1174
|
-
if (el.placeholder) entry.placeholder = el.placeholder.slice(0, 80);
|
|
1175
|
-
if (el.value && tag !== 'textarea') entry.value = el.value.slice(0, 80);
|
|
1176
|
-
if (tag === 'a') entry.href = (el.href || '').slice(0, 150);
|
|
1177
|
-
if (tag === 'select') {
|
|
1178
|
-
entry.options = Array.from(el.options).slice(0, 10).map(o => ({
|
|
1179
|
-
value: o.value, text: o.text.slice(0, 50), selected: o.selected
|
|
1180
|
-
}));
|
|
1181
|
-
}
|
|
1182
|
-
const text = (el.innerText || el.textContent || el.getAttribute('aria-label') || '').trim();
|
|
1183
|
-
if (text && text.length <= 100) entry.text = text;
|
|
1184
|
-
else if (text) entry.text = text.slice(0, 97) + '...';
|
|
1185
|
-
if (el.disabled) entry.disabled = true;
|
|
1186
|
-
return entry;
|
|
1187
|
-
}
|
|
1188
|
-
|
|
1189
|
-
function scanRoot(root) {
|
|
1190
|
-
const candidates = root.querySelectorAll(INTERACTIVE_SELECTOR);
|
|
1191
|
-
for (let i = 0; i < candidates.length && elements.length < maxEls; i++) {
|
|
1192
|
-
const rect = isVisible(candidates[i]);
|
|
1193
|
-
if (rect) elements.push(buildEntry(candidates[i], rect));
|
|
1194
|
-
}
|
|
1195
|
-
if (elements.length < maxEls) {
|
|
1196
|
-
const all = root.querySelectorAll('*');
|
|
1197
|
-
for (let i = 0; i < all.length && elements.length < maxEls; i++) {
|
|
1198
|
-
const el = all[i];
|
|
1199
|
-
if (el.shadowRoot) scanRoot(el.shadowRoot);
|
|
1200
|
-
if (el.onclick && !el.hasAttribute('data-ghost-ref')) {
|
|
1201
|
-
const rect = isVisible(el);
|
|
1202
|
-
if (rect) elements.push(buildEntry(el, rect));
|
|
1203
|
-
}
|
|
1204
|
-
}
|
|
1205
|
-
}
|
|
1206
|
-
}
|
|
1207
|
-
|
|
1208
|
-
document.querySelectorAll('[data-ghost-ref]').forEach(el => el.removeAttribute('data-ghost-ref'));
|
|
1209
|
-
scanRoot(targetElement);
|
|
1210
|
-
|
|
1211
|
-
result.interactive = {
|
|
1212
|
-
url: window.location.href,
|
|
1213
|
-
title: document.title,
|
|
1214
|
-
elementCount: elements.length,
|
|
1215
|
-
viewport: {
|
|
1216
|
-
width: window.innerWidth,
|
|
1217
|
-
height: window.innerHeight,
|
|
1218
|
-
scrollX: Math.round(window.scrollX),
|
|
1219
|
-
scrollY: Math.round(window.scrollY),
|
|
1220
|
-
},
|
|
1221
|
-
elements
|
|
1222
|
-
};
|
|
1223
|
-
|
|
1224
|
-
return result;
|
|
1225
|
-
} catch (e) {
|
|
1226
|
-
return { error: e.message };
|
|
1227
|
-
}
|
|
1228
|
-
})()`
|
|
871
|
+
const expression = GhostBridgeDom.buildInspectPageExpression({ selector, includeInteractive, maxElements })
|
|
1229
872
|
|
|
1230
873
|
const { result } = await chrome.debugger.sendCommand(target, "Runtime.evaluate", {
|
|
1231
874
|
expression,
|
|
@@ -1239,95 +882,7 @@ async function handleInspectPageSnapshot(params = {}) {
|
|
|
1239
882
|
async function handleGetPageContent(params = {}) {
|
|
1240
883
|
const target = await ensureAttached()
|
|
1241
884
|
const { mode = "text", selector, maxLength = 50000, includeMetadata = true } = params
|
|
1242
|
-
|
|
1243
|
-
const selectorStr = selector ? JSON.stringify(selector) : 'null'
|
|
1244
|
-
const modeStr = JSON.stringify(mode)
|
|
1245
|
-
|
|
1246
|
-
const expression = `(function() {
|
|
1247
|
-
try {
|
|
1248
|
-
const result = {};
|
|
1249
|
-
if (document.readyState === 'loading') {
|
|
1250
|
-
return { error: '页面尚未加载完成,请稍后重试', readyState: document.readyState };
|
|
1251
|
-
}
|
|
1252
|
-
let targetElement = document.body;
|
|
1253
|
-
const selector = ${selectorStr};
|
|
1254
|
-
if (selector) {
|
|
1255
|
-
try {
|
|
1256
|
-
targetElement = document.querySelector(selector);
|
|
1257
|
-
if (!targetElement) {
|
|
1258
|
-
return { error: '选择器未匹配到任何元素', selector: selector, suggestion: '请检查选择器是否正确' };
|
|
1259
|
-
}
|
|
1260
|
-
result.selector = selector;
|
|
1261
|
-
result.matchedTag = targetElement.tagName.toLowerCase();
|
|
1262
|
-
} catch (e) {
|
|
1263
|
-
return { error: '无效的 CSS 选择器: ' + e.message, selector: selector };
|
|
1264
|
-
}
|
|
1265
|
-
}
|
|
1266
|
-
const includeMetadata = ${includeMetadata};
|
|
1267
|
-
if (includeMetadata) {
|
|
1268
|
-
result.metadata = {
|
|
1269
|
-
title: document.title || '',
|
|
1270
|
-
url: window.location.href,
|
|
1271
|
-
description: document.querySelector('meta[name="description"]')?.content || '',
|
|
1272
|
-
keywords: document.querySelector('meta[name="keywords"]')?.content || '',
|
|
1273
|
-
charset: document.characterSet,
|
|
1274
|
-
language: document.documentElement.lang || '',
|
|
1275
|
-
};
|
|
1276
|
-
}
|
|
1277
|
-
const mode = ${modeStr};
|
|
1278
|
-
const maxLength = ${maxLength};
|
|
1279
|
-
if (mode === 'text') {
|
|
1280
|
-
let text = targetElement.innerText || targetElement.textContent || '';
|
|
1281
|
-
text = text.replace(/\\n{3,}/g, '\\n\\n').trim();
|
|
1282
|
-
result.contentLength = text.length;
|
|
1283
|
-
if (text.length > maxLength) {
|
|
1284
|
-
result.content = text.slice(0, maxLength);
|
|
1285
|
-
result.truncated = true;
|
|
1286
|
-
} else {
|
|
1287
|
-
result.content = text;
|
|
1288
|
-
result.truncated = false;
|
|
1289
|
-
}
|
|
1290
|
-
} else if (mode === 'html') {
|
|
1291
|
-
let html = targetElement.outerHTML || '';
|
|
1292
|
-
result.contentLength = html.length;
|
|
1293
|
-
if (html.length > maxLength) {
|
|
1294
|
-
result.content = html.slice(0, maxLength);
|
|
1295
|
-
result.truncated = true;
|
|
1296
|
-
result.note = 'HTML 已截断,可能不完整';
|
|
1297
|
-
} else {
|
|
1298
|
-
result.content = html;
|
|
1299
|
-
result.truncated = false;
|
|
1300
|
-
}
|
|
1301
|
-
} else if (mode === 'structured') {
|
|
1302
|
-
const structured = {};
|
|
1303
|
-
const headings = targetElement.querySelectorAll('h1,h2,h3,h4,h5,h6');
|
|
1304
|
-
structured.headings = Array.from(headings).slice(0, 50).map(h => ({ level: parseInt(h.tagName[1]), text: h.innerText.trim().slice(0, 200) }));
|
|
1305
|
-
const links = targetElement.querySelectorAll('a[href]');
|
|
1306
|
-
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:'));
|
|
1307
|
-
const buttons = targetElement.querySelectorAll('button, input[type="button"], input[type="submit"], [role="button"]');
|
|
1308
|
-
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 }));
|
|
1309
|
-
const forms = targetElement.querySelectorAll('form');
|
|
1310
|
-
structured.forms = Array.from(forms).slice(0, 20).map(f => {
|
|
1311
|
-
const fields = Array.from(f.querySelectorAll('input, select, textarea')).slice(0, 30);
|
|
1312
|
-
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 })) };
|
|
1313
|
-
});
|
|
1314
|
-
const images = targetElement.querySelectorAll('img');
|
|
1315
|
-
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);
|
|
1316
|
-
const tables = targetElement.querySelectorAll('table');
|
|
1317
|
-
structured.tables = Array.from(tables).slice(0, 10).map(table => {
|
|
1318
|
-
const headers = Array.from(table.querySelectorAll('th')).map(th => th.innerText.trim().slice(0, 50));
|
|
1319
|
-
const rows = table.querySelectorAll('tr');
|
|
1320
|
-
return { headers: headers.slice(0, 20), rowCount: rows.length };
|
|
1321
|
-
});
|
|
1322
|
-
result.structured = structured;
|
|
1323
|
-
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 };
|
|
1324
|
-
}
|
|
1325
|
-
result.mode = mode;
|
|
1326
|
-
return result;
|
|
1327
|
-
} catch (e) {
|
|
1328
|
-
return { error: e.message };
|
|
1329
|
-
}
|
|
1330
|
-
})()`
|
|
885
|
+
const expression = GhostBridgeDom.buildPageContentExpression({ mode, selector, maxLength, includeMetadata })
|
|
1331
886
|
|
|
1332
887
|
const { result } = await chrome.debugger.sendCommand(target, "Runtime.evaluate", {
|
|
1333
888
|
expression,
|
|
@@ -1343,103 +898,7 @@ async function handleGetPageContent(params = {}) {
|
|
|
1343
898
|
async function handleGetInteractiveSnapshot(params = {}) {
|
|
1344
899
|
const target = await ensureAttached()
|
|
1345
900
|
const { selector, includeText = true, maxElements = 100 } = params
|
|
1346
|
-
|
|
1347
|
-
const selectorStr = selector ? JSON.stringify(selector) : 'null'
|
|
1348
|
-
|
|
1349
|
-
const expression = `(function() {
|
|
1350
|
-
try {
|
|
1351
|
-
let refCounter = 0;
|
|
1352
|
-
const elements = [];
|
|
1353
|
-
|
|
1354
|
-
const maxEls = ${maxElements};
|
|
1355
|
-
// 候选集选择器——用浏览器原生选择器引擎代替全树 JS 递归
|
|
1356
|
-
const INTERACTIVE_SELECTOR = 'a,button,input,select,textarea,[role="button"],[role="link"],[role="tab"],[role="menuitem"],[role="checkbox"],[role="radio"],[role="switch"],[role="combobox"],[tabindex]:not([tabindex="-1"]),[contenteditable="true"],[onclick]';
|
|
1357
|
-
|
|
1358
|
-
// 可见性检测(单次 getComputedStyle,返回 rect 复用)
|
|
1359
|
-
function isVisible(el) {
|
|
1360
|
-
const style = window.getComputedStyle(el);
|
|
1361
|
-
if (style.display === 'none' || style.visibility === 'hidden' || parseFloat(style.opacity) === 0) return null;
|
|
1362
|
-
if (!el.offsetParent && el.tagName !== 'HTML' && el.tagName !== 'BODY' &&
|
|
1363
|
-
style.position !== 'fixed' && style.position !== 'sticky') return null;
|
|
1364
|
-
const rect = el.getBoundingClientRect();
|
|
1365
|
-
if (rect.width === 0 && rect.height === 0) return null;
|
|
1366
|
-
return rect;
|
|
1367
|
-
}
|
|
1368
|
-
|
|
1369
|
-
function buildEntry(el, rect) {
|
|
1370
|
-
refCounter++;
|
|
1371
|
-
const ref = 'e' + refCounter;
|
|
1372
|
-
el.setAttribute('data-ghost-ref', ref);
|
|
1373
|
-
const tag = el.tagName.toLowerCase();
|
|
1374
|
-
const entry = { ref, tag, cx: Math.round(rect.left + rect.width / 2), cy: Math.round(rect.top + rect.height / 2) };
|
|
1375
|
-
if (el.type) entry.type = el.type;
|
|
1376
|
-
if (el.name) entry.name = el.name;
|
|
1377
|
-
if (el.getAttribute('role')) entry.role = el.getAttribute('role');
|
|
1378
|
-
if (${includeText}) {
|
|
1379
|
-
if (el.placeholder) entry.placeholder = el.placeholder.slice(0, 80);
|
|
1380
|
-
if (el.value && tag !== 'textarea') entry.value = el.value.slice(0, 80);
|
|
1381
|
-
if (tag === 'a') entry.href = (el.href || '').slice(0, 150);
|
|
1382
|
-
if (tag === 'select') {
|
|
1383
|
-
entry.options = Array.from(el.options).slice(0, 10).map(o => ({
|
|
1384
|
-
value: o.value, text: o.text.slice(0, 50), selected: o.selected
|
|
1385
|
-
}));
|
|
1386
|
-
}
|
|
1387
|
-
const text = (el.innerText || el.textContent || el.getAttribute('aria-label') || '').trim();
|
|
1388
|
-
if (text && text.length <= 100) entry.text = text;
|
|
1389
|
-
else if (text) entry.text = text.slice(0, 97) + '...';
|
|
1390
|
-
}
|
|
1391
|
-
if (el.disabled) entry.disabled = true;
|
|
1392
|
-
return entry;
|
|
1393
|
-
}
|
|
1394
|
-
|
|
1395
|
-
// 候选集扫描(含 Shadow DOM 穿透)
|
|
1396
|
-
function scanRoot(root) {
|
|
1397
|
-
const candidates = root.querySelectorAll(INTERACTIVE_SELECTOR);
|
|
1398
|
-
for (let i = 0; i < candidates.length && elements.length < maxEls; i++) {
|
|
1399
|
-
const rect = isVisible(candidates[i]);
|
|
1400
|
-
if (rect) elements.push(buildEntry(candidates[i], rect));
|
|
1401
|
-
}
|
|
1402
|
-
// 穿透 Shadow DOM + 兜底检测 el.onclick = fn 形式的 JS 属性绑定
|
|
1403
|
-
if (elements.length < maxEls) {
|
|
1404
|
-
const all = root.querySelectorAll('*');
|
|
1405
|
-
for (let i = 0; i < all.length && elements.length < maxEls; i++) {
|
|
1406
|
-
const el = all[i];
|
|
1407
|
-
if (el.shadowRoot) scanRoot(el.shadowRoot);
|
|
1408
|
-
// CSS 选择器只能匹配 [onclick] 属性,这里兜住 el.onclick = fn 的情况
|
|
1409
|
-
if (el.onclick && !el.hasAttribute('data-ghost-ref')) {
|
|
1410
|
-
const rect = isVisible(el);
|
|
1411
|
-
if (rect) elements.push(buildEntry(el, rect));
|
|
1412
|
-
}
|
|
1413
|
-
}
|
|
1414
|
-
}
|
|
1415
|
-
}
|
|
1416
|
-
|
|
1417
|
-
// 清理旧的 ref 标记
|
|
1418
|
-
document.querySelectorAll('[data-ghost-ref]').forEach(el => el.removeAttribute('data-ghost-ref'));
|
|
1419
|
-
|
|
1420
|
-
let rootEl = document.body;
|
|
1421
|
-
const sel = ${selectorStr};
|
|
1422
|
-
if (sel) {
|
|
1423
|
-
rootEl = document.querySelector(sel);
|
|
1424
|
-
if (!rootEl) return { error: '选择器未匹配到任何元素', selector: sel };
|
|
1425
|
-
}
|
|
1426
|
-
|
|
1427
|
-
scanRoot(rootEl);
|
|
1428
|
-
|
|
1429
|
-
return {
|
|
1430
|
-
url: window.location.href,
|
|
1431
|
-
title: document.title,
|
|
1432
|
-
elementCount: elements.length,
|
|
1433
|
-
viewport: {
|
|
1434
|
-
width: window.innerWidth,
|
|
1435
|
-
height: window.innerHeight,
|
|
1436
|
-
scrollX: Math.round(window.scrollX),
|
|
1437
|
-
scrollY: Math.round(window.scrollY),
|
|
1438
|
-
},
|
|
1439
|
-
elements: elements,
|
|
1440
|
-
};
|
|
1441
|
-
} catch (e) { return { error: e.message }; }
|
|
1442
|
-
})()`
|
|
901
|
+
const expression = GhostBridgeDom.buildInteractiveSnapshotExpression({ selector, includeText, maxElements })
|
|
1443
902
|
|
|
1444
903
|
const { result } = await chrome.debugger.sendCommand(target, "Runtime.evaluate", {
|
|
1445
904
|
expression,
|