ghost-bridge 0.2.5 → 0.3.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/dist/server.js +24 -2
- package/extension/background.js +152 -0
- package/extension/manifest.json +1 -1
- package/package.json +2 -2
package/dist/server.js
CHANGED
|
@@ -28896,7 +28896,7 @@ function buildSnippet(source, line, column, { beautifyEnabled = true, contextLin
|
|
|
28896
28896
|
return result;
|
|
28897
28897
|
}
|
|
28898
28898
|
var server = new Server(
|
|
28899
|
-
{ name: "ghost-bridge", version: "0.
|
|
28899
|
+
{ name: "ghost-bridge", version: "0.3.0" },
|
|
28900
28900
|
{ capabilities: { tools: {} } }
|
|
28901
28901
|
);
|
|
28902
28902
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
@@ -28993,6 +28993,23 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
28993
28993
|
description: "\u6E05\u7A7A\u5DF2\u6355\u83B7\u7684\u7F51\u7EDC\u8BF7\u6C42\u8BB0\u5F55",
|
|
28994
28994
|
inputSchema: { type: "object", properties: {} }
|
|
28995
28995
|
},
|
|
28996
|
+
{
|
|
28997
|
+
name: "perf_metrics",
|
|
28998
|
+
description: "\u83B7\u53D6\u9875\u9762\u6027\u80FD\u6307\u6807\uFF1A\u5305\u542B\u5F15\u64CE\u7EA7\u6307\u6807\uFF08JS\u5806\u5185\u5B58\u3001DOM\u8282\u70B9\u6570\u3001Layout\u6B21\u6570\u3001\u811A\u672C\u6267\u884C\u65F6\u95F4\uFF09\u3001Web Vitals\uFF08FCP\u3001TTFB\u3001DOMContentLoaded\u3001Long Tasks\uFF09\u548C\u8D44\u6E90\u52A0\u8F7D\u6458\u8981\u3002\u7528\u4E8E\u8BCA\u65AD\u9875\u9762\u5361\u987F\u3001\u5185\u5B58\u5360\u7528\u8FC7\u9AD8\u3001\u52A0\u8F7D\u7F13\u6162\u7B49\u6027\u80FD\u95EE\u9898\u3002",
|
|
28999
|
+
inputSchema: {
|
|
29000
|
+
type: "object",
|
|
29001
|
+
properties: {
|
|
29002
|
+
includeTimings: {
|
|
29003
|
+
type: "boolean",
|
|
29004
|
+
description: "\u662F\u5426\u5305\u542B Navigation Timing \u548C Web Vitals\uFF0C\u9ED8\u8BA4 true"
|
|
29005
|
+
},
|
|
29006
|
+
includeResources: {
|
|
29007
|
+
type: "boolean",
|
|
29008
|
+
description: "\u662F\u5426\u5305\u542B\u8D44\u6E90\u52A0\u8F7D\u6458\u8981\uFF08\u6309\u7C7B\u578B\u7EDF\u8BA1\u3001\u6700\u6162\u8D44\u6E90\uFF09\uFF0C\u9ED8\u8BA4 true"
|
|
29009
|
+
}
|
|
29010
|
+
}
|
|
29011
|
+
}
|
|
29012
|
+
},
|
|
28996
29013
|
{
|
|
28997
29014
|
name: "capture_screenshot",
|
|
28998
29015
|
description: "\u3010\u63A8\u8350\u7528\u4E8E\u89C6\u89C9\u5206\u6790\u3011\u622A\u53D6\u5F53\u524D\u9875\u9762\u7684\u622A\u56FE\uFF0C\u8FD4\u56DE base64 \u56FE\u7247\u3002\u9002\u7528\u4E8E\uFF1A1) \u67E5\u770B\u9875\u9762\u5B9E\u9645\u89C6\u89C9\u6548\u679C 2) \u6392\u67E5 UI/\u6837\u5F0F/\u5E03\u5C40/\u989C\u8272\u95EE\u9898 3) \u9A8C\u8BC1\u9875\u9762\u6E32\u67D3 4) \u5206\u6790\u5143\u7D20\u4F4D\u7F6E\u548C\u95F4\u8DDD 5) \u67E5\u770B\u56FE\u7247/\u56FE\u6807\u7B49\u89C6\u89C9\u5185\u5BB9\u3002\u5F53\u9700\u8981\u770B\u5230\u9875\u9762\u300C\u957F\u4EC0\u4E48\u6837\u300D\u65F6\u4F7F\u7528\u6B64\u5DE5\u5177\u3002\u5982\u4EC5\u9700\u6587\u672C/\u94FE\u63A5\u7B49\u4FE1\u606F\uFF0C\u5EFA\u8BAE\u4F7F\u7528\u66F4\u5FEB\u7684 get_page_content\u3002",
|
|
@@ -29078,7 +29095,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
29078
29095
|
type: "text",
|
|
29079
29096
|
text: jsonText({
|
|
29080
29097
|
service: "ghost-bridge",
|
|
29081
|
-
version: "0.
|
|
29098
|
+
version: "0.3.0",
|
|
29082
29099
|
role: isMainInstance ? "\u4E3B\u5B9E\u4F8B (WebSocket Server)" : "\u5BA2\u6237\u7AEF (\u8FDE\u63A5\u5230\u4E3B\u5B9E\u4F8B)",
|
|
29083
29100
|
wsPort: actualPort,
|
|
29084
29101
|
wsUrl: `ws://localhost:${actualPort}`,
|
|
@@ -29163,6 +29180,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
29163
29180
|
const res = await askChrome("clearNetworkRequests");
|
|
29164
29181
|
return { content: [{ type: "text", text: jsonText(res) }] };
|
|
29165
29182
|
}
|
|
29183
|
+
if (name === "perf_metrics") {
|
|
29184
|
+
const { includeTimings, includeResources } = args;
|
|
29185
|
+
const res = await askChrome("perfMetrics", { includeTimings, includeResources });
|
|
29186
|
+
return { content: [{ type: "text", text: jsonText(res) }] };
|
|
29187
|
+
}
|
|
29166
29188
|
if (name === "capture_screenshot") {
|
|
29167
29189
|
const { format, quality, fullPage, clip } = args;
|
|
29168
29190
|
const res = await askChrome("captureScreenshot", { format, quality, fullPage, clip }, { timeoutMs: 15e3 });
|
package/extension/background.js
CHANGED
|
@@ -590,6 +590,157 @@ async function handleClearNetworkRequests() {
|
|
|
590
590
|
return { cleared: count }
|
|
591
591
|
}
|
|
592
592
|
|
|
593
|
+
async function handlePerfMetrics(params = {}) {
|
|
594
|
+
const target = await ensureAttached()
|
|
595
|
+
const { includeResources = true, includeTimings = true } = params
|
|
596
|
+
|
|
597
|
+
// 1. CDP Performance.getMetrics — 底层引擎指标
|
|
598
|
+
await chrome.debugger.sendCommand(target, "Performance.enable")
|
|
599
|
+
const { metrics } = await chrome.debugger.sendCommand(target, "Performance.getMetrics")
|
|
600
|
+
await chrome.debugger.sendCommand(target, "Performance.disable")
|
|
601
|
+
|
|
602
|
+
// 整理为可读的分组
|
|
603
|
+
const metricsMap = {}
|
|
604
|
+
for (const m of metrics) {
|
|
605
|
+
metricsMap[m.name] = m.value
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const engineMetrics = {
|
|
609
|
+
memory: {
|
|
610
|
+
jsHeapUsedSize: formatBytes(metricsMap.JSHeapUsedSize),
|
|
611
|
+
jsHeapTotalSize: formatBytes(metricsMap.JSHeapTotalSize),
|
|
612
|
+
usagePercent: metricsMap.JSHeapTotalSize
|
|
613
|
+
? Math.round((metricsMap.JSHeapUsedSize / metricsMap.JSHeapTotalSize) * 100) + "%"
|
|
614
|
+
: "N/A",
|
|
615
|
+
},
|
|
616
|
+
dom: {
|
|
617
|
+
nodes: metricsMap.Nodes,
|
|
618
|
+
documents: metricsMap.Documents,
|
|
619
|
+
frames: metricsMap.Frames,
|
|
620
|
+
jsEventListeners: metricsMap.JSEventListeners,
|
|
621
|
+
},
|
|
622
|
+
layout: {
|
|
623
|
+
layoutCount: metricsMap.LayoutCount,
|
|
624
|
+
recalcStyleCount: metricsMap.RecalcStyleCount,
|
|
625
|
+
layoutDuration: roundMs(metricsMap.LayoutDuration),
|
|
626
|
+
recalcStyleDuration: roundMs(metricsMap.RecalcStyleDuration),
|
|
627
|
+
},
|
|
628
|
+
tasks: {
|
|
629
|
+
scriptDuration: roundMs(metricsMap.ScriptDuration),
|
|
630
|
+
taskDuration: roundMs(metricsMap.TaskDuration),
|
|
631
|
+
taskOtherDuration: roundMs(metricsMap.TaskOtherDuration),
|
|
632
|
+
},
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const result = { engineMetrics }
|
|
636
|
+
|
|
637
|
+
// 2. Web Vitals + Navigation Timing(通过 Runtime.evaluate)
|
|
638
|
+
if (includeTimings) {
|
|
639
|
+
const expression = `(function() {
|
|
640
|
+
try {
|
|
641
|
+
const result = {};
|
|
642
|
+
// Navigation Timing
|
|
643
|
+
const nav = performance.getEntriesByType('navigation')[0];
|
|
644
|
+
if (nav) {
|
|
645
|
+
result.navigation = {
|
|
646
|
+
type: nav.type,
|
|
647
|
+
redirectTime: Math.round(nav.redirectEnd - nav.redirectStart),
|
|
648
|
+
dnsTime: Math.round(nav.domainLookupEnd - nav.domainLookupStart),
|
|
649
|
+
connectTime: Math.round(nav.connectEnd - nav.connectStart),
|
|
650
|
+
ttfb: Math.round(nav.responseStart - nav.requestStart),
|
|
651
|
+
responseTime: Math.round(nav.responseEnd - nav.responseStart),
|
|
652
|
+
domInteractive: Math.round(nav.domInteractive),
|
|
653
|
+
domContentLoaded: Math.round(nav.domContentLoadedEventEnd),
|
|
654
|
+
loadComplete: Math.round(nav.loadEventEnd),
|
|
655
|
+
totalDuration: Math.round(nav.duration),
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
// Paint Timing (FP, FCP)
|
|
659
|
+
const paints = performance.getEntriesByType('paint');
|
|
660
|
+
result.paint = {};
|
|
661
|
+
for (const p of paints) {
|
|
662
|
+
if (p.name === 'first-paint') result.paint.firstPaint = Math.round(p.startTime);
|
|
663
|
+
if (p.name === 'first-contentful-paint') result.paint.firstContentfulPaint = Math.round(p.startTime);
|
|
664
|
+
}
|
|
665
|
+
// Long Tasks(如果有 PerformanceObserver 记录)
|
|
666
|
+
try {
|
|
667
|
+
const longTasks = performance.getEntriesByType('longtask');
|
|
668
|
+
if (longTasks && longTasks.length > 0) {
|
|
669
|
+
result.longTasks = {
|
|
670
|
+
count: longTasks.length,
|
|
671
|
+
totalDuration: Math.round(longTasks.reduce((s, t) => s + t.duration, 0)),
|
|
672
|
+
longest: Math.round(Math.max(...longTasks.map(t => t.duration))),
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
} catch(e) {}
|
|
676
|
+
// 基本信息
|
|
677
|
+
result.timing = {
|
|
678
|
+
now: Math.round(performance.now()),
|
|
679
|
+
timeOrigin: Math.round(performance.timeOrigin),
|
|
680
|
+
};
|
|
681
|
+
return result;
|
|
682
|
+
} catch (e) { return { error: e.message }; }
|
|
683
|
+
})()`
|
|
684
|
+
const { result: evalResult } = await chrome.debugger.sendCommand(target, "Runtime.evaluate", {
|
|
685
|
+
expression,
|
|
686
|
+
returnByValue: true,
|
|
687
|
+
})
|
|
688
|
+
if (evalResult?.value) {
|
|
689
|
+
result.webVitals = evalResult.value
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// 3. 资源加载摘要
|
|
694
|
+
if (includeResources) {
|
|
695
|
+
const resExpression = `(function() {
|
|
696
|
+
try {
|
|
697
|
+
const entries = performance.getEntriesByType('resource');
|
|
698
|
+
const byType = {};
|
|
699
|
+
let totalSize = 0, totalDuration = 0, slowest = null;
|
|
700
|
+
for (const e of entries) {
|
|
701
|
+
const type = e.initiatorType || 'other';
|
|
702
|
+
if (!byType[type]) byType[type] = { count: 0, totalSize: 0, totalDuration: 0 };
|
|
703
|
+
byType[type].count++;
|
|
704
|
+
byType[type].totalSize += e.transferSize || 0;
|
|
705
|
+
byType[type].totalDuration += e.duration || 0;
|
|
706
|
+
totalSize += e.transferSize || 0;
|
|
707
|
+
totalDuration += e.duration || 0;
|
|
708
|
+
if (!slowest || e.duration > slowest.duration) {
|
|
709
|
+
slowest = { name: e.name.split('/').pop().split('?')[0] || e.name.slice(0, 60), duration: Math.round(e.duration), size: e.transferSize || 0, type };
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
// 格式化 byType
|
|
713
|
+
const summary = {};
|
|
714
|
+
for (const [type, data] of Object.entries(byType)) {
|
|
715
|
+
summary[type] = { count: data.count, totalSize: data.totalSize, avgDuration: Math.round(data.totalDuration / data.count) };
|
|
716
|
+
}
|
|
717
|
+
return { totalResources: entries.length, totalTransferSize: totalSize, summary, slowest };
|
|
718
|
+
} catch (e) { return { error: e.message }; }
|
|
719
|
+
})()`
|
|
720
|
+
const { result: resResult } = await chrome.debugger.sendCommand(target, "Runtime.evaluate", {
|
|
721
|
+
expression: resExpression,
|
|
722
|
+
returnByValue: true,
|
|
723
|
+
})
|
|
724
|
+
if (resResult?.value) {
|
|
725
|
+
result.resources = resResult.value
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
return result
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function formatBytes(bytes) {
|
|
733
|
+
if (bytes == null) return "N/A"
|
|
734
|
+
if (bytes < 1024) return bytes + " B"
|
|
735
|
+
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB"
|
|
736
|
+
return (bytes / (1024 * 1024)).toFixed(1) + " MB"
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
function roundMs(seconds) {
|
|
740
|
+
if (seconds == null) return "N/A"
|
|
741
|
+
return Math.round(seconds * 1000) + "ms"
|
|
742
|
+
}
|
|
743
|
+
|
|
593
744
|
async function handleCaptureScreenshot(params = {}) {
|
|
594
745
|
const target = await ensureAttached()
|
|
595
746
|
const { format = 'png', quality, fullPage = false, clip } = params
|
|
@@ -784,6 +935,7 @@ async function handleCommand(message) {
|
|
|
784
935
|
else if (command === "listNetworkRequests") result = await handleListNetworkRequests(params)
|
|
785
936
|
else if (command === "getNetworkDetail") result = await handleGetNetworkDetail(params)
|
|
786
937
|
else if (command === "clearNetworkRequests") result = await handleClearNetworkRequests()
|
|
938
|
+
else if (command === "perfMetrics") result = await handlePerfMetrics(params)
|
|
787
939
|
else if (command === "captureScreenshot") result = await handleCaptureScreenshot(params)
|
|
788
940
|
else if (command === "getPageContent") result = await handleGetPageContent(params)
|
|
789
941
|
else throw new Error(`未知指令 ${command}`)
|
package/extension/manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ghost-bridge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Ghost Bridge: Zero-restart Chrome debugger bridge for Claude MCP. Includes CLI for easy setup.",
|
|
@@ -26,4 +26,4 @@
|
|
|
26
26
|
"js-beautify": "^1.14.11",
|
|
27
27
|
"ws": "^8.14.2"
|
|
28
28
|
}
|
|
29
|
-
}
|
|
29
|
+
}
|