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
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
(function initGhostBridgeDomHelpers(global) {
|
|
2
|
+
function buildInspectPageExpression({ selector, includeInteractive, maxElements }) {
|
|
3
|
+
const selectorStr = selector ? JSON.stringify(selector) : 'null'
|
|
4
|
+
|
|
5
|
+
return `(function() {
|
|
6
|
+
try {
|
|
7
|
+
if (document.readyState === 'loading') {
|
|
8
|
+
return { error: '页面尚未加载完成,请稍后重试', readyState: document.readyState };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const includeInteractive = ${includeInteractive};
|
|
12
|
+
const maxEls = ${maxElements};
|
|
13
|
+
const selector = ${selectorStr};
|
|
14
|
+
const result = {};
|
|
15
|
+
let targetElement = document.body;
|
|
16
|
+
|
|
17
|
+
function getMetadata() {
|
|
18
|
+
return {
|
|
19
|
+
title: document.title || '',
|
|
20
|
+
url: window.location.href,
|
|
21
|
+
description: document.querySelector('meta[name="description"]')?.content || '',
|
|
22
|
+
keywords: document.querySelector('meta[name="keywords"]')?.content || '',
|
|
23
|
+
charset: document.characterSet,
|
|
24
|
+
language: document.documentElement.lang || '',
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function resolveTargetElement() {
|
|
29
|
+
if (!selector) return document.body;
|
|
30
|
+
try {
|
|
31
|
+
const matched = document.querySelector(selector);
|
|
32
|
+
if (!matched) {
|
|
33
|
+
return { error: '选择器未匹配到任何元素', selector: selector, suggestion: '请检查选择器是否正确' };
|
|
34
|
+
}
|
|
35
|
+
result.selector = selector;
|
|
36
|
+
result.matchedTag = matched.tagName.toLowerCase();
|
|
37
|
+
return matched;
|
|
38
|
+
} catch (e) {
|
|
39
|
+
return { error: '无效的 CSS 选择器: ' + e.message, selector: selector };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function buildStructuredContent(root) {
|
|
44
|
+
const structured = {};
|
|
45
|
+
const headings = root.querySelectorAll('h1,h2,h3,h4,h5,h6');
|
|
46
|
+
structured.headings = Array.from(headings).slice(0, 50).map(h => ({
|
|
47
|
+
level: parseInt(h.tagName[1]),
|
|
48
|
+
text: h.innerText.trim().slice(0, 200)
|
|
49
|
+
}));
|
|
50
|
+
const links = root.querySelectorAll('a[href]');
|
|
51
|
+
structured.links = Array.from(links).slice(0, 100).map(a => ({
|
|
52
|
+
text: (a.innerText || '').trim().slice(0, 100),
|
|
53
|
+
href: a.href
|
|
54
|
+
})).filter(l => l.href && !l.href.startsWith('javascript:'));
|
|
55
|
+
const buttons = root.querySelectorAll('button, input[type="button"], input[type="submit"], [role="button"]');
|
|
56
|
+
structured.buttons = Array.from(buttons).slice(0, 50).map(b => ({
|
|
57
|
+
text: (b.innerText || b.value || b.getAttribute('aria-label') || '').trim().slice(0, 100),
|
|
58
|
+
type: b.type || 'button',
|
|
59
|
+
disabled: b.disabled || false
|
|
60
|
+
}));
|
|
61
|
+
const forms = root.querySelectorAll('form');
|
|
62
|
+
structured.forms = Array.from(forms).slice(0, 20).map(f => {
|
|
63
|
+
const fields = Array.from(f.querySelectorAll('input, select, textarea')).slice(0, 30);
|
|
64
|
+
return {
|
|
65
|
+
action: f.action || '',
|
|
66
|
+
method: (f.method || 'GET').toUpperCase(),
|
|
67
|
+
fieldCount: fields.length,
|
|
68
|
+
fields: fields.map(field => ({
|
|
69
|
+
tag: field.tagName.toLowerCase(),
|
|
70
|
+
type: field.type || '',
|
|
71
|
+
name: field.name || '',
|
|
72
|
+
placeholder: field.placeholder || '',
|
|
73
|
+
required: field.required || false
|
|
74
|
+
}))
|
|
75
|
+
};
|
|
76
|
+
});
|
|
77
|
+
const images = root.querySelectorAll('img');
|
|
78
|
+
structured.images = Array.from(images).slice(0, 50).map(img => ({
|
|
79
|
+
alt: img.alt || '',
|
|
80
|
+
src: img.src ? img.src.slice(0, 200) : ''
|
|
81
|
+
})).filter(img => img.src);
|
|
82
|
+
const tables = root.querySelectorAll('table');
|
|
83
|
+
structured.tables = Array.from(tables).slice(0, 10).map(table => {
|
|
84
|
+
const headers = Array.from(table.querySelectorAll('th')).map(th => th.innerText.trim().slice(0, 50));
|
|
85
|
+
const rows = table.querySelectorAll('tr');
|
|
86
|
+
return { headers: headers.slice(0, 20), rowCount: rows.length };
|
|
87
|
+
});
|
|
88
|
+
return structured;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function buildCounts(structured) {
|
|
92
|
+
return {
|
|
93
|
+
headings: structured.headings.length,
|
|
94
|
+
links: structured.links.length,
|
|
95
|
+
buttons: structured.buttons.length,
|
|
96
|
+
forms: structured.forms.length,
|
|
97
|
+
images: structured.images.length,
|
|
98
|
+
tables: structured.tables.length
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function buildInteractiveSnapshot(root, includeText, maxEls) {
|
|
103
|
+
let refCounter = 0;
|
|
104
|
+
const elements = [];
|
|
105
|
+
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]';
|
|
106
|
+
|
|
107
|
+
function isVisible(el) {
|
|
108
|
+
const style = window.getComputedStyle(el);
|
|
109
|
+
if (style.display === 'none' || style.visibility === 'hidden' || parseFloat(style.opacity) === 0) return null;
|
|
110
|
+
if (!el.offsetParent && el.tagName !== 'HTML' && el.tagName !== 'BODY' &&
|
|
111
|
+
style.position !== 'fixed' && style.position !== 'sticky') return null;
|
|
112
|
+
const rect = el.getBoundingClientRect();
|
|
113
|
+
if (rect.width === 0 && rect.height === 0) return null;
|
|
114
|
+
return rect;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function buildEntry(el, rect) {
|
|
118
|
+
refCounter++;
|
|
119
|
+
const ref = 'e' + refCounter;
|
|
120
|
+
el.setAttribute('data-ghost-ref', ref);
|
|
121
|
+
const tag = el.tagName.toLowerCase();
|
|
122
|
+
const entry = { ref, tag, cx: Math.round(rect.left + rect.width / 2), cy: Math.round(rect.top + rect.height / 2) };
|
|
123
|
+
if (el.type) entry.type = el.type;
|
|
124
|
+
if (el.name) entry.name = el.name;
|
|
125
|
+
if (el.getAttribute('role')) entry.role = el.getAttribute('role');
|
|
126
|
+
if (includeText) {
|
|
127
|
+
if (el.placeholder) entry.placeholder = el.placeholder.slice(0, 80);
|
|
128
|
+
if (el.value && tag !== 'textarea') entry.value = el.value.slice(0, 80);
|
|
129
|
+
if (tag === 'a') entry.href = (el.href || '').slice(0, 150);
|
|
130
|
+
if (tag === 'select') {
|
|
131
|
+
entry.options = Array.from(el.options).slice(0, 10).map(o => ({
|
|
132
|
+
value: o.value, text: o.text.slice(0, 50), selected: o.selected
|
|
133
|
+
}));
|
|
134
|
+
}
|
|
135
|
+
const text = (el.innerText || el.textContent || el.getAttribute('aria-label') || '').trim();
|
|
136
|
+
if (text && text.length <= 100) entry.text = text;
|
|
137
|
+
else if (text) entry.text = text.slice(0, 97) + '...';
|
|
138
|
+
}
|
|
139
|
+
if (el.disabled) entry.disabled = true;
|
|
140
|
+
return entry;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function scanRoot(scanTarget) {
|
|
144
|
+
const candidates = scanTarget.querySelectorAll(INTERACTIVE_SELECTOR);
|
|
145
|
+
for (let i = 0; i < candidates.length && elements.length < maxEls; i++) {
|
|
146
|
+
const rect = isVisible(candidates[i]);
|
|
147
|
+
if (rect) elements.push(buildEntry(candidates[i], rect));
|
|
148
|
+
}
|
|
149
|
+
if (elements.length < maxEls) {
|
|
150
|
+
const all = scanTarget.querySelectorAll('*');
|
|
151
|
+
for (let i = 0; i < all.length && elements.length < maxEls; i++) {
|
|
152
|
+
const el = all[i];
|
|
153
|
+
if (el.shadowRoot) scanRoot(el.shadowRoot);
|
|
154
|
+
if (el.onclick && !el.hasAttribute('data-ghost-ref')) {
|
|
155
|
+
const rect = isVisible(el);
|
|
156
|
+
if (rect) elements.push(buildEntry(el, rect));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
document.querySelectorAll('[data-ghost-ref]').forEach(el => el.removeAttribute('data-ghost-ref'));
|
|
163
|
+
scanRoot(root);
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
url: window.location.href,
|
|
167
|
+
title: document.title,
|
|
168
|
+
elementCount: elements.length,
|
|
169
|
+
viewport: {
|
|
170
|
+
width: window.innerWidth,
|
|
171
|
+
height: window.innerHeight,
|
|
172
|
+
scrollX: Math.round(window.scrollX),
|
|
173
|
+
scrollY: Math.round(window.scrollY),
|
|
174
|
+
},
|
|
175
|
+
elements
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
targetElement = resolveTargetElement();
|
|
180
|
+
if (targetElement?.error) return targetElement;
|
|
181
|
+
|
|
182
|
+
result.metadata = getMetadata();
|
|
183
|
+
const structured = buildStructuredContent(targetElement);
|
|
184
|
+
|
|
185
|
+
result.page = {
|
|
186
|
+
metadata: result.metadata,
|
|
187
|
+
...(result.selector ? { selector: result.selector, matchedTag: result.matchedTag } : {}),
|
|
188
|
+
structured,
|
|
189
|
+
counts: buildCounts(structured),
|
|
190
|
+
mode: 'structured'
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
if (!includeInteractive) {
|
|
194
|
+
result.interactive = null;
|
|
195
|
+
return result;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
result.interactive = buildInteractiveSnapshot(targetElement, true, maxEls);
|
|
199
|
+
return result;
|
|
200
|
+
} catch (e) {
|
|
201
|
+
return { error: e.message };
|
|
202
|
+
}
|
|
203
|
+
})()`
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function buildPageContentExpression({ mode, selector, maxLength, includeMetadata }) {
|
|
207
|
+
const selectorStr = selector ? JSON.stringify(selector) : 'null'
|
|
208
|
+
const modeStr = JSON.stringify(mode)
|
|
209
|
+
|
|
210
|
+
return `(function() {
|
|
211
|
+
try {
|
|
212
|
+
const result = {};
|
|
213
|
+
if (document.readyState === 'loading') {
|
|
214
|
+
return { error: '页面尚未加载完成,请稍后重试', readyState: document.readyState };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const selector = ${selectorStr};
|
|
218
|
+
const mode = ${modeStr};
|
|
219
|
+
const maxLength = ${maxLength};
|
|
220
|
+
const includeMetadata = ${includeMetadata};
|
|
221
|
+
|
|
222
|
+
function getMetadata() {
|
|
223
|
+
return {
|
|
224
|
+
title: document.title || '',
|
|
225
|
+
url: window.location.href,
|
|
226
|
+
description: document.querySelector('meta[name="description"]')?.content || '',
|
|
227
|
+
keywords: document.querySelector('meta[name="keywords"]')?.content || '',
|
|
228
|
+
charset: document.characterSet,
|
|
229
|
+
language: document.documentElement.lang || '',
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function resolveTargetElement() {
|
|
234
|
+
if (!selector) return document.body;
|
|
235
|
+
try {
|
|
236
|
+
const matched = document.querySelector(selector);
|
|
237
|
+
if (!matched) {
|
|
238
|
+
return { error: '选择器未匹配到任何元素', selector: selector, suggestion: '请检查选择器是否正确' };
|
|
239
|
+
}
|
|
240
|
+
result.selector = selector;
|
|
241
|
+
result.matchedTag = matched.tagName.toLowerCase();
|
|
242
|
+
return matched;
|
|
243
|
+
} catch (e) {
|
|
244
|
+
return { error: '无效的 CSS 选择器: ' + e.message, selector: selector };
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function buildStructuredContent(root) {
|
|
249
|
+
const structured = {};
|
|
250
|
+
const headings = root.querySelectorAll('h1,h2,h3,h4,h5,h6');
|
|
251
|
+
structured.headings = Array.from(headings).slice(0, 50).map(h => ({ level: parseInt(h.tagName[1]), text: h.innerText.trim().slice(0, 200) }));
|
|
252
|
+
const links = root.querySelectorAll('a[href]');
|
|
253
|
+
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:'));
|
|
254
|
+
const buttons = root.querySelectorAll('button, input[type="button"], input[type="submit"], [role="button"]');
|
|
255
|
+
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 }));
|
|
256
|
+
const forms = root.querySelectorAll('form');
|
|
257
|
+
structured.forms = Array.from(forms).slice(0, 20).map(f => {
|
|
258
|
+
const fields = Array.from(f.querySelectorAll('input, select, textarea')).slice(0, 30);
|
|
259
|
+
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 })) };
|
|
260
|
+
});
|
|
261
|
+
const images = root.querySelectorAll('img');
|
|
262
|
+
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);
|
|
263
|
+
const tables = root.querySelectorAll('table');
|
|
264
|
+
structured.tables = Array.from(tables).slice(0, 10).map(table => {
|
|
265
|
+
const headers = Array.from(table.querySelectorAll('th')).map(th => th.innerText.trim().slice(0, 50));
|
|
266
|
+
const rows = table.querySelectorAll('tr');
|
|
267
|
+
return { headers: headers.slice(0, 20), rowCount: rows.length };
|
|
268
|
+
});
|
|
269
|
+
return structured;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function smartTruncateText(text, limit) {
|
|
273
|
+
if (text.length <= limit) {
|
|
274
|
+
return { content: text, truncated: false };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (limit < 400) {
|
|
278
|
+
return { content: text.slice(0, limit), truncated: true, note: '内容过长,已截断' };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const headLength = Math.max(200, Math.floor(limit * 0.8));
|
|
282
|
+
const tailLength = Math.max(120, limit - headLength - 80);
|
|
283
|
+
const head = text.slice(0, headLength).trimEnd();
|
|
284
|
+
const tail = text.slice(-tailLength).trimStart();
|
|
285
|
+
const omittedChars = Math.max(0, text.length - head.length - tail.length);
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
content: head + '\\n\\n... [已省略 ' + omittedChars + ' 个字符] ...\\n\\n' + tail,
|
|
289
|
+
truncated: true,
|
|
290
|
+
note: '内容过长,已保留开头与结尾片段'
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const targetElement = resolveTargetElement();
|
|
295
|
+
if (targetElement?.error) return targetElement;
|
|
296
|
+
|
|
297
|
+
if (includeMetadata) {
|
|
298
|
+
result.metadata = getMetadata();
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (mode === 'text') {
|
|
302
|
+
let text = targetElement.innerText || targetElement.textContent || '';
|
|
303
|
+
text = text.replace(/\\n{3,}/g, '\\n\\n').trim();
|
|
304
|
+
result.contentLength = text.length;
|
|
305
|
+
const truncated = smartTruncateText(text, maxLength);
|
|
306
|
+
result.content = truncated.content;
|
|
307
|
+
result.truncated = truncated.truncated;
|
|
308
|
+
if (truncated.note) result.note = truncated.note;
|
|
309
|
+
} else if (mode === 'html') {
|
|
310
|
+
let html = targetElement.outerHTML || '';
|
|
311
|
+
result.contentLength = html.length;
|
|
312
|
+
if (html.length > maxLength) {
|
|
313
|
+
result.content = html.slice(0, maxLength);
|
|
314
|
+
result.truncated = true;
|
|
315
|
+
result.note = 'HTML 已截断,可能不完整';
|
|
316
|
+
} else {
|
|
317
|
+
result.content = html;
|
|
318
|
+
result.truncated = false;
|
|
319
|
+
}
|
|
320
|
+
} else if (mode === 'structured') {
|
|
321
|
+
const structured = buildStructuredContent(targetElement);
|
|
322
|
+
result.structured = structured;
|
|
323
|
+
result.counts = {
|
|
324
|
+
headings: structured.headings.length,
|
|
325
|
+
links: structured.links.length,
|
|
326
|
+
buttons: structured.buttons.length,
|
|
327
|
+
forms: structured.forms.length,
|
|
328
|
+
images: structured.images.length,
|
|
329
|
+
tables: structured.tables.length
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
result.mode = mode;
|
|
334
|
+
return result;
|
|
335
|
+
} catch (e) {
|
|
336
|
+
return { error: e.message };
|
|
337
|
+
}
|
|
338
|
+
})()`
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function buildInteractiveSnapshotExpression({ selector, includeText, maxElements }) {
|
|
342
|
+
const selectorStr = selector ? JSON.stringify(selector) : 'null'
|
|
343
|
+
|
|
344
|
+
return `(function() {
|
|
345
|
+
try {
|
|
346
|
+
let refCounter = 0;
|
|
347
|
+
const elements = [];
|
|
348
|
+
const maxEls = ${maxElements};
|
|
349
|
+
const includeText = ${includeText};
|
|
350
|
+
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]';
|
|
351
|
+
|
|
352
|
+
function isVisible(el) {
|
|
353
|
+
const style = window.getComputedStyle(el);
|
|
354
|
+
if (style.display === 'none' || style.visibility === 'hidden' || parseFloat(style.opacity) === 0) return null;
|
|
355
|
+
if (!el.offsetParent && el.tagName !== 'HTML' && el.tagName !== 'BODY' &&
|
|
356
|
+
style.position !== 'fixed' && style.position !== 'sticky') return null;
|
|
357
|
+
const rect = el.getBoundingClientRect();
|
|
358
|
+
if (rect.width === 0 && rect.height === 0) return null;
|
|
359
|
+
return rect;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function buildEntry(el, rect) {
|
|
363
|
+
refCounter++;
|
|
364
|
+
const ref = 'e' + refCounter;
|
|
365
|
+
el.setAttribute('data-ghost-ref', ref);
|
|
366
|
+
const tag = el.tagName.toLowerCase();
|
|
367
|
+
const entry = { ref, tag, cx: Math.round(rect.left + rect.width / 2), cy: Math.round(rect.top + rect.height / 2) };
|
|
368
|
+
if (el.type) entry.type = el.type;
|
|
369
|
+
if (el.name) entry.name = el.name;
|
|
370
|
+
if (el.getAttribute('role')) entry.role = el.getAttribute('role');
|
|
371
|
+
if (includeText) {
|
|
372
|
+
if (el.placeholder) entry.placeholder = el.placeholder.slice(0, 80);
|
|
373
|
+
if (el.value && tag !== 'textarea') entry.value = el.value.slice(0, 80);
|
|
374
|
+
if (tag === 'a') entry.href = (el.href || '').slice(0, 150);
|
|
375
|
+
if (tag === 'select') {
|
|
376
|
+
entry.options = Array.from(el.options).slice(0, 10).map(o => ({
|
|
377
|
+
value: o.value, text: o.text.slice(0, 50), selected: o.selected
|
|
378
|
+
}));
|
|
379
|
+
}
|
|
380
|
+
const text = (el.innerText || el.textContent || el.getAttribute('aria-label') || '').trim();
|
|
381
|
+
if (text && text.length <= 100) entry.text = text;
|
|
382
|
+
else if (text) entry.text = text.slice(0, 97) + '...';
|
|
383
|
+
}
|
|
384
|
+
if (el.disabled) entry.disabled = true;
|
|
385
|
+
return entry;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function scanRoot(root) {
|
|
389
|
+
const candidates = root.querySelectorAll(INTERACTIVE_SELECTOR);
|
|
390
|
+
for (let i = 0; i < candidates.length && elements.length < maxEls; i++) {
|
|
391
|
+
const rect = isVisible(candidates[i]);
|
|
392
|
+
if (rect) elements.push(buildEntry(candidates[i], rect));
|
|
393
|
+
}
|
|
394
|
+
if (elements.length < maxEls) {
|
|
395
|
+
const all = root.querySelectorAll('*');
|
|
396
|
+
for (let i = 0; i < all.length && elements.length < maxEls; i++) {
|
|
397
|
+
const el = all[i];
|
|
398
|
+
if (el.shadowRoot) scanRoot(el.shadowRoot);
|
|
399
|
+
if (el.onclick && !el.hasAttribute('data-ghost-ref')) {
|
|
400
|
+
const rect = isVisible(el);
|
|
401
|
+
if (rect) elements.push(buildEntry(el, rect));
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
document.querySelectorAll('[data-ghost-ref]').forEach(el => el.removeAttribute('data-ghost-ref'));
|
|
408
|
+
|
|
409
|
+
let rootEl = document.body;
|
|
410
|
+
const sel = ${selectorStr};
|
|
411
|
+
if (sel) {
|
|
412
|
+
rootEl = document.querySelector(sel);
|
|
413
|
+
if (!rootEl) return { error: '选择器未匹配到任何元素', selector: sel };
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
scanRoot(rootEl);
|
|
417
|
+
|
|
418
|
+
return {
|
|
419
|
+
url: window.location.href,
|
|
420
|
+
title: document.title,
|
|
421
|
+
elementCount: elements.length,
|
|
422
|
+
viewport: {
|
|
423
|
+
width: window.innerWidth,
|
|
424
|
+
height: window.innerHeight,
|
|
425
|
+
scrollX: Math.round(window.scrollX),
|
|
426
|
+
scrollY: Math.round(window.scrollY),
|
|
427
|
+
},
|
|
428
|
+
elements
|
|
429
|
+
};
|
|
430
|
+
} catch (e) {
|
|
431
|
+
return { error: e.message };
|
|
432
|
+
}
|
|
433
|
+
})()`
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
global.GhostBridgeDom = {
|
|
437
|
+
buildInspectPageExpression,
|
|
438
|
+
buildPageContentExpression,
|
|
439
|
+
buildInteractiveSnapshotExpression,
|
|
440
|
+
}
|
|
441
|
+
})(self)
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
(function initGhostBridgeNetworkHelpers(global) {
|
|
2
|
+
const MAX_NETWORK_URL_OUTPUT_LENGTH = 240
|
|
3
|
+
const NETWORK_URL_HEAD_LENGTH = 180
|
|
4
|
+
const NETWORK_URL_TAIL_LENGTH = 40
|
|
5
|
+
const MAX_DATA_URL_OUTPUT_LENGTH = 256
|
|
6
|
+
|
|
7
|
+
function getApiSignalScore(entry) {
|
|
8
|
+
const url = (entry.url || '').toLowerCase()
|
|
9
|
+
let score = 0
|
|
10
|
+
if (url.includes('/api/')) score += 80
|
|
11
|
+
if (url.includes('graphql')) score += 80
|
|
12
|
+
if (url.includes('/rpc/')) score += 60
|
|
13
|
+
if (url.includes('/rest/')) score += 40
|
|
14
|
+
if ((entry.method || 'GET').toUpperCase() !== 'GET') score += 25
|
|
15
|
+
return score
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function getResourceTypeScore(entry, mode = 'debug') {
|
|
19
|
+
const type = (entry.resourceType || '').toLowerCase()
|
|
20
|
+
const debugScores = {
|
|
21
|
+
fetch: 140,
|
|
22
|
+
xhr: 140,
|
|
23
|
+
websocket: 120,
|
|
24
|
+
document: 90,
|
|
25
|
+
script: 45,
|
|
26
|
+
stylesheet: 25,
|
|
27
|
+
other: 0,
|
|
28
|
+
image: -40,
|
|
29
|
+
font: -40,
|
|
30
|
+
media: -50,
|
|
31
|
+
}
|
|
32
|
+
const apiScores = {
|
|
33
|
+
fetch: 220,
|
|
34
|
+
xhr: 220,
|
|
35
|
+
websocket: 160,
|
|
36
|
+
document: 40,
|
|
37
|
+
script: -10,
|
|
38
|
+
stylesheet: -20,
|
|
39
|
+
other: 0,
|
|
40
|
+
image: -80,
|
|
41
|
+
font: -80,
|
|
42
|
+
media: -90,
|
|
43
|
+
}
|
|
44
|
+
const table = mode === 'api' ? apiScores : debugScores
|
|
45
|
+
return table[type] ?? table.other
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function getStatusScore(entry) {
|
|
49
|
+
if (entry.status === 'failed') return 360
|
|
50
|
+
if (entry.status === 'error') return 330
|
|
51
|
+
if (entry.status === 'pending') return 280
|
|
52
|
+
if ((entry.statusCode || 0) >= 500) return 340
|
|
53
|
+
if ((entry.statusCode || 0) >= 400) return 300
|
|
54
|
+
return 80
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function getNetworkPriorityScore(entry, mode = 'debug') {
|
|
58
|
+
if (mode === 'recent') {
|
|
59
|
+
return entry.timestamp || 0
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let score = getStatusScore(entry)
|
|
63
|
+
score += getResourceTypeScore(entry, mode)
|
|
64
|
+
score += getApiSignalScore(entry)
|
|
65
|
+
|
|
66
|
+
if (entry.fromCache) score -= 20
|
|
67
|
+
if ((entry.encodedDataLength || 0) === 0 && entry.status === 'success') score -= 10
|
|
68
|
+
|
|
69
|
+
return score
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function compareNetworkEntries(a, b, mode = 'debug') {
|
|
73
|
+
if (mode === 'recent') {
|
|
74
|
+
return (b.timestamp || 0) - (a.timestamp || 0)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const scoreDiff = getNetworkPriorityScore(b, mode) - getNetworkPriorityScore(a, mode)
|
|
78
|
+
if (scoreDiff !== 0) return scoreDiff
|
|
79
|
+
return (b.timestamp || 0) - (a.timestamp || 0)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function summarizeNetworkUrl(url) {
|
|
83
|
+
if (!url) return { displayUrl: url }
|
|
84
|
+
|
|
85
|
+
const urlOriginalLength = url.length
|
|
86
|
+
const schemeMatch = /^([a-z][a-z0-9+.-]*):/i.exec(url)
|
|
87
|
+
const urlScheme = schemeMatch?.[1]?.toLowerCase()
|
|
88
|
+
|
|
89
|
+
if (urlScheme === 'data') {
|
|
90
|
+
const commaIndex = url.indexOf(',')
|
|
91
|
+
const meta = commaIndex >= 0 ? url.slice(5, commaIndex) : url.slice(5)
|
|
92
|
+
const dataUrlMimeType = (meta.split(';')[0] || 'text/plain').toLowerCase()
|
|
93
|
+
const isBase64 = meta.includes(';base64')
|
|
94
|
+
|
|
95
|
+
if (!isBase64 && urlOriginalLength <= MAX_DATA_URL_OUTPUT_LENGTH) {
|
|
96
|
+
return {
|
|
97
|
+
displayUrl: url,
|
|
98
|
+
urlOriginalLength,
|
|
99
|
+
urlScheme,
|
|
100
|
+
urlTruncated: false,
|
|
101
|
+
dataUrlMimeType,
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
displayUrl: `data:${dataUrlMimeType}${isBase64 ? ';base64' : ''},<omitted ${urlOriginalLength} chars>`,
|
|
107
|
+
urlOriginalLength,
|
|
108
|
+
urlScheme,
|
|
109
|
+
urlTruncated: true,
|
|
110
|
+
dataUrlMimeType,
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (urlOriginalLength > MAX_NETWORK_URL_OUTPUT_LENGTH) {
|
|
115
|
+
return {
|
|
116
|
+
displayUrl: `${url.slice(0, NETWORK_URL_HEAD_LENGTH)}...${url.slice(-NETWORK_URL_TAIL_LENGTH)}`,
|
|
117
|
+
urlOriginalLength,
|
|
118
|
+
urlScheme,
|
|
119
|
+
urlTruncated: true,
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
displayUrl: url,
|
|
125
|
+
urlOriginalLength,
|
|
126
|
+
urlScheme,
|
|
127
|
+
urlTruncated: false,
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function buildNetworkRequestSummary(entry) {
|
|
132
|
+
const urlMeta = summarizeNetworkUrl(entry.url)
|
|
133
|
+
return {
|
|
134
|
+
requestId: entry.requestId,
|
|
135
|
+
url: urlMeta.displayUrl,
|
|
136
|
+
...(urlMeta.urlTruncated ? { urlTruncated: true, urlOriginalLength: urlMeta.urlOriginalLength } : {}),
|
|
137
|
+
...(urlMeta.urlScheme ? { urlScheme: urlMeta.urlScheme } : {}),
|
|
138
|
+
...(urlMeta.dataUrlMimeType ? { dataUrlMimeType: urlMeta.dataUrlMimeType } : {}),
|
|
139
|
+
method: entry.method,
|
|
140
|
+
status: entry.status,
|
|
141
|
+
statusCode: entry.statusCode,
|
|
142
|
+
resourceType: entry.resourceType,
|
|
143
|
+
mimeType: entry.mimeType,
|
|
144
|
+
duration: entry.duration,
|
|
145
|
+
encodedDataLength: entry.encodedDataLength,
|
|
146
|
+
fromCache: entry.fromCache,
|
|
147
|
+
timestamp: entry.timestamp,
|
|
148
|
+
errorText: entry.errorText,
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function trimTrackedRequests(list, maxRequestsTracked) {
|
|
153
|
+
while (list.length > maxRequestsTracked) {
|
|
154
|
+
let worstIndex = 0
|
|
155
|
+
for (let i = 1; i < list.length; i++) {
|
|
156
|
+
const candidate = list[i]
|
|
157
|
+
const worst = list[worstIndex]
|
|
158
|
+
const cmp = compareNetworkEntries(candidate, worst, 'debug')
|
|
159
|
+
if (cmp < 0 || (cmp === 0 && (candidate.timestamp || 0) < (worst.timestamp || 0))) {
|
|
160
|
+
worstIndex = i
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
list.splice(worstIndex, 1)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function trimPendingRequestMap(requestMap, maxPendingRequests) {
|
|
168
|
+
while (requestMap.size > maxPendingRequests) {
|
|
169
|
+
const entries = [...requestMap.entries()]
|
|
170
|
+
let worstKey = entries[0]?.[0]
|
|
171
|
+
let worstValue = entries[0]?.[1]
|
|
172
|
+
|
|
173
|
+
for (let i = 1; i < entries.length; i++) {
|
|
174
|
+
const [key, value] = entries[i]
|
|
175
|
+
const cmp = compareNetworkEntries(value, worstValue, 'debug')
|
|
176
|
+
if (cmp < 0 || (cmp === 0 && (value.timestamp || 0) < (worstValue.timestamp || 0))) {
|
|
177
|
+
worstKey = key
|
|
178
|
+
worstValue = value
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (!worstKey) break
|
|
183
|
+
requestMap.delete(worstKey)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
global.GhostBridgeNetwork = {
|
|
188
|
+
compareNetworkEntries,
|
|
189
|
+
summarizeNetworkUrl,
|
|
190
|
+
buildNetworkRequestSummary,
|
|
191
|
+
trimTrackedRequests,
|
|
192
|
+
trimPendingRequestMap,
|
|
193
|
+
}
|
|
194
|
+
})(self)
|
package/extension/manifest.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": 3,
|
|
3
3
|
"name": "Ghost Bridge",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.7.1",
|
|
5
5
|
"description": "Zero-restart Chrome debugger bridge for Claude MCP, optimized for no-sourcemap production debugging.",
|
|
6
6
|
"permissions": [
|
|
7
7
|
"debugger",
|
|
@@ -20,14 +20,12 @@
|
|
|
20
20
|
"default_popup": "popup.html",
|
|
21
21
|
"default_icon": {
|
|
22
22
|
"16": "icon-16.png",
|
|
23
|
-
"32": "icon-32.png",
|
|
24
23
|
"48": "icon-48.png",
|
|
25
24
|
"128": "icon-128.png"
|
|
26
25
|
}
|
|
27
26
|
},
|
|
28
27
|
"icons": {
|
|
29
28
|
"16": "icon-16.png",
|
|
30
|
-
"32": "icon-32.png",
|
|
31
29
|
"48": "icon-48.png",
|
|
32
30
|
"128": "icon-128.png"
|
|
33
31
|
},
|
package/extension/offscreen.js
CHANGED
|
@@ -15,11 +15,7 @@ function log(msg) {
|
|
|
15
15
|
chrome.runtime.sendMessage({ type: 'log', msg }).catch(() => {})
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
const now = new Date()
|
|
20
|
-
const firstDayOfMonth = new Date(now.getFullYear(), now.getMonth(), 1, 0, 0, 0, 0)
|
|
21
|
-
return String(firstDayOfMonth.getTime())
|
|
22
|
-
}
|
|
18
|
+
const DEFAULT_TOKEN = 'ghost-bridge-local'
|
|
23
19
|
|
|
24
20
|
// 连接到服务器
|
|
25
21
|
function connect() {
|
|
@@ -74,7 +70,9 @@ function connect() {
|
|
|
74
70
|
port: port,
|
|
75
71
|
}).catch(() => {})
|
|
76
72
|
} else {
|
|
77
|
-
terminalErrorMessage =
|
|
73
|
+
terminalErrorMessage = msg.service === 'ghost-bridge'
|
|
74
|
+
? `Port ${port} is running ghost-bridge, but the token does not match.`
|
|
75
|
+
: `Port ${port} is occupied by a non-matching service.`
|
|
78
76
|
log('身份验证失败,将在固定端口上重试...')
|
|
79
77
|
chrome.runtime.sendMessage({
|
|
80
78
|
type: 'status',
|
|
@@ -159,7 +157,7 @@ function disconnect() {
|
|
|
159
157
|
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|
160
158
|
if (message.type === 'connect') {
|
|
161
159
|
config.basePort = message.basePort || 33333
|
|
162
|
-
config.token = message.token ||
|
|
160
|
+
config.token = message.token || DEFAULT_TOKEN
|
|
163
161
|
disconnect()
|
|
164
162
|
manualDisconnect = false // 用户重新连接,清除断开标志
|
|
165
163
|
connect()
|