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