ghost-bridge 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +85 -0
- package/dist/cli.js +5731 -0
- package/dist/server.js +29289 -0
- package/extension/background.js +956 -0
- package/extension/icon-128.png +0 -0
- package/extension/icon-16.png +0 -0
- package/extension/icon-32.png +0 -0
- package/extension/icon-48.png +0 -0
- package/extension/manifest.json +37 -0
- package/extension/offscreen.html +10 -0
- package/extension/offscreen.js +175 -0
- package/extension/popup.html +160 -0
- package/extension/popup.js +165 -0
- package/package.json +29 -0
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"manifest_version": 3,
|
|
3
|
+
"name": "Ghost Bridge",
|
|
4
|
+
"version": "0.2.0",
|
|
5
|
+
"description": "Zero-restart Chrome debugger bridge for Claude MCP, optimized for no-sourcemap production debugging.",
|
|
6
|
+
"permissions": [
|
|
7
|
+
"debugger",
|
|
8
|
+
"activeTab",
|
|
9
|
+
"scripting",
|
|
10
|
+
"storage",
|
|
11
|
+
"tabs",
|
|
12
|
+
"offscreen"
|
|
13
|
+
],
|
|
14
|
+
"host_permissions": [
|
|
15
|
+
"ws://localhost/*",
|
|
16
|
+
"ws://127.0.0.1/*"
|
|
17
|
+
],
|
|
18
|
+
"action": {
|
|
19
|
+
"default_title": "Ghost Bridge",
|
|
20
|
+
"default_popup": "popup.html",
|
|
21
|
+
"default_icon": {
|
|
22
|
+
"16": "icon-16.png",
|
|
23
|
+
"32": "icon-32.png",
|
|
24
|
+
"48": "icon-48.png",
|
|
25
|
+
"128": "icon-128.png"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"icons": {
|
|
29
|
+
"16": "icon-16.png",
|
|
30
|
+
"32": "icon-32.png",
|
|
31
|
+
"48": "icon-48.png",
|
|
32
|
+
"128": "icon-128.png"
|
|
33
|
+
},
|
|
34
|
+
"background": {
|
|
35
|
+
"service_worker": "background.js"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
// Offscreen document 用于维持 WebSocket 长连接
|
|
2
|
+
// 不受 MV3 service worker 暂停的影响
|
|
3
|
+
|
|
4
|
+
let ws = null
|
|
5
|
+
let reconnectTimer = null
|
|
6
|
+
let config = {
|
|
7
|
+
basePort: 33333,
|
|
8
|
+
token: '',
|
|
9
|
+
maxPortRetries: 10,
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function log(msg) {
|
|
13
|
+
console.log(`[ghost-bridge offscreen] ${msg}`)
|
|
14
|
+
// 转发日志到 service worker
|
|
15
|
+
chrome.runtime.sendMessage({ type: 'log', msg }).catch(() => {})
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function getMonthlyToken() {
|
|
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
|
+
}
|
|
23
|
+
|
|
24
|
+
// 连接到服务器
|
|
25
|
+
function connect(portIndex = 0, isNewRound = false) {
|
|
26
|
+
if (portIndex >= config.maxPortRetries) {
|
|
27
|
+
log(`扫描完毕,未找到服务,2秒后重试...`)
|
|
28
|
+
reconnectTimer = setTimeout(() => connect(0, true), 2000)
|
|
29
|
+
chrome.runtime.sendMessage({ type: 'status', status: 'not_found' }).catch(() => {})
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const port = config.basePort + portIndex
|
|
34
|
+
const url = new URL(`ws://localhost:${port}`)
|
|
35
|
+
url.searchParams.set('token', config.token)
|
|
36
|
+
|
|
37
|
+
if (portIndex === 0 && isNewRound) {
|
|
38
|
+
log(`开始扫描端口 ${config.basePort}-${config.basePort + config.maxPortRetries - 1}`)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
log(`尝试连接端口 ${port}...`)
|
|
42
|
+
chrome.runtime.sendMessage({
|
|
43
|
+
type: 'status',
|
|
44
|
+
status: 'scanning',
|
|
45
|
+
currentPort: port,
|
|
46
|
+
}).catch(() => {})
|
|
47
|
+
|
|
48
|
+
ws = new WebSocket(url.toString())
|
|
49
|
+
ws.binaryType = 'blob' // 明确设置
|
|
50
|
+
|
|
51
|
+
const connectionTimeout = setTimeout(() => {
|
|
52
|
+
if (ws && ws.readyState === WebSocket.CONNECTING) {
|
|
53
|
+
ws.close()
|
|
54
|
+
}
|
|
55
|
+
}, 2000) // 增加到 2 秒
|
|
56
|
+
|
|
57
|
+
let identityVerified = false
|
|
58
|
+
|
|
59
|
+
ws.onopen = () => {
|
|
60
|
+
clearTimeout(connectionTimeout)
|
|
61
|
+
log(`WebSocket 已连接端口 ${port},等待身份验证...`)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
ws.onmessage = async (event) => {
|
|
65
|
+
try {
|
|
66
|
+
// 处理 Blob 类型的消息
|
|
67
|
+
let data = event.data
|
|
68
|
+
if (data instanceof Blob) {
|
|
69
|
+
data = await data.text()
|
|
70
|
+
}
|
|
71
|
+
const msg = JSON.parse(data)
|
|
72
|
+
|
|
73
|
+
if (msg.type === 'identity') {
|
|
74
|
+
if (msg.service === 'ghost-bridge' && msg.token === config.token) {
|
|
75
|
+
identityVerified = true
|
|
76
|
+
log(`✅ 已连接到 ghost-bridge 服务 (端口 ${port})`)
|
|
77
|
+
chrome.runtime.sendMessage({
|
|
78
|
+
type: 'status',
|
|
79
|
+
status: 'connected',
|
|
80
|
+
port: port,
|
|
81
|
+
}).catch(() => {})
|
|
82
|
+
} else {
|
|
83
|
+
log(`身份验证失败,尝试下一个端口...`)
|
|
84
|
+
ws.close()
|
|
85
|
+
setTimeout(() => connect(portIndex + 1), 50)
|
|
86
|
+
}
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 转发命令到 service worker
|
|
91
|
+
if (identityVerified && msg.id) {
|
|
92
|
+
chrome.runtime.sendMessage({ type: 'command', data: msg }).catch(() => {})
|
|
93
|
+
}
|
|
94
|
+
} catch (e) {
|
|
95
|
+
log(`解析消息失败:${e.message}`)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
ws.onclose = (event) => {
|
|
100
|
+
clearTimeout(connectionTimeout)
|
|
101
|
+
|
|
102
|
+
if (!identityVerified) {
|
|
103
|
+
// 连接失败,尝试下一个端口
|
|
104
|
+
setTimeout(() => connect(portIndex + 1), 50)
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 连接断开,重试
|
|
109
|
+
log('连接断开,尝试重连...')
|
|
110
|
+
chrome.runtime.sendMessage({ type: 'status', status: 'disconnected' }).catch(() => {})
|
|
111
|
+
reconnectTimer = setTimeout(() => connect(0, true), 1000)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
ws.onerror = () => {
|
|
115
|
+
clearTimeout(connectionTimeout)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// 发送消息到服务器
|
|
120
|
+
function sendToServer(data) {
|
|
121
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
122
|
+
ws.send(JSON.stringify(data))
|
|
123
|
+
return true
|
|
124
|
+
}
|
|
125
|
+
return false
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 断开连接
|
|
129
|
+
function disconnect() {
|
|
130
|
+
if (reconnectTimer) {
|
|
131
|
+
clearTimeout(reconnectTimer)
|
|
132
|
+
reconnectTimer = null
|
|
133
|
+
}
|
|
134
|
+
if (ws) {
|
|
135
|
+
ws.close()
|
|
136
|
+
ws = null
|
|
137
|
+
}
|
|
138
|
+
log('已断开连接')
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// 监听来自 service worker 的消息
|
|
142
|
+
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|
143
|
+
if (message.type === 'connect') {
|
|
144
|
+
config.basePort = message.basePort || 33333
|
|
145
|
+
config.token = message.token || getMonthlyToken()
|
|
146
|
+
config.maxPortRetries = message.maxPortRetries || 10
|
|
147
|
+
disconnect()
|
|
148
|
+
connect(0, true)
|
|
149
|
+
sendResponse({ ok: true })
|
|
150
|
+
return true
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (message.type === 'disconnect') {
|
|
154
|
+
disconnect()
|
|
155
|
+
sendResponse({ ok: true })
|
|
156
|
+
return true
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (message.type === 'send') {
|
|
160
|
+
const ok = sendToServer(message.data)
|
|
161
|
+
sendResponse({ ok })
|
|
162
|
+
return true
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (message.type === 'getStatus') {
|
|
166
|
+
sendResponse({
|
|
167
|
+
connected: ws && ws.readyState === WebSocket.OPEN,
|
|
168
|
+
})
|
|
169
|
+
return true
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return false
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
log('Offscreen document 已加载')
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<style>
|
|
6
|
+
* {
|
|
7
|
+
margin: 0;
|
|
8
|
+
padding: 0;
|
|
9
|
+
box-sizing: border-box;
|
|
10
|
+
}
|
|
11
|
+
body {
|
|
12
|
+
width: 280px;
|
|
13
|
+
padding: 16px;
|
|
14
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
15
|
+
background: #1a1a2e;
|
|
16
|
+
color: #eee;
|
|
17
|
+
border-radius: 12px;
|
|
18
|
+
overflow: hidden;
|
|
19
|
+
box-shadow: inset 0 0 0 1px rgba(255,255,255,0.1);
|
|
20
|
+
}
|
|
21
|
+
.header {
|
|
22
|
+
display: flex;
|
|
23
|
+
align-items: center;
|
|
24
|
+
gap: 10px;
|
|
25
|
+
margin-bottom: 16px;
|
|
26
|
+
}
|
|
27
|
+
.header img {
|
|
28
|
+
width: 32px;
|
|
29
|
+
height: 32px;
|
|
30
|
+
}
|
|
31
|
+
.header h1 {
|
|
32
|
+
font-size: 16px;
|
|
33
|
+
font-weight: 600;
|
|
34
|
+
}
|
|
35
|
+
.status-card {
|
|
36
|
+
background: #16213e;
|
|
37
|
+
border-radius: 8px;
|
|
38
|
+
padding: 14px;
|
|
39
|
+
margin-bottom: 12px;
|
|
40
|
+
}
|
|
41
|
+
.status-row {
|
|
42
|
+
display: flex;
|
|
43
|
+
align-items: center;
|
|
44
|
+
gap: 10px;
|
|
45
|
+
}
|
|
46
|
+
.status-dot {
|
|
47
|
+
width: 10px;
|
|
48
|
+
height: 10px;
|
|
49
|
+
border-radius: 50%;
|
|
50
|
+
flex-shrink: 0;
|
|
51
|
+
}
|
|
52
|
+
.status-dot.connected { background: #34c759; box-shadow: 0 0 8px #34c759; }
|
|
53
|
+
.status-dot.connecting { background: #ff9f0a; animation: pulse 1s infinite; }
|
|
54
|
+
.status-dot.disconnected { background: #666; }
|
|
55
|
+
.status-dot.error { background: #ff3b30; }
|
|
56
|
+
@keyframes pulse {
|
|
57
|
+
0%, 100% { opacity: 1; }
|
|
58
|
+
50% { opacity: 0.4; }
|
|
59
|
+
}
|
|
60
|
+
.status-text {
|
|
61
|
+
font-size: 14px;
|
|
62
|
+
font-weight: 500;
|
|
63
|
+
}
|
|
64
|
+
.status-detail {
|
|
65
|
+
font-size: 12px;
|
|
66
|
+
color: #888;
|
|
67
|
+
margin-top: 8px;
|
|
68
|
+
padding-left: 20px;
|
|
69
|
+
}
|
|
70
|
+
.config-section {
|
|
71
|
+
background: #16213e;
|
|
72
|
+
border-radius: 8px;
|
|
73
|
+
padding: 14px;
|
|
74
|
+
margin-bottom: 12px;
|
|
75
|
+
}
|
|
76
|
+
.config-label {
|
|
77
|
+
font-size: 12px;
|
|
78
|
+
color: #888;
|
|
79
|
+
margin-bottom: 6px;
|
|
80
|
+
}
|
|
81
|
+
.config-input {
|
|
82
|
+
width: 100%;
|
|
83
|
+
padding: 8px 10px;
|
|
84
|
+
border: 1px solid #333;
|
|
85
|
+
border-radius: 6px;
|
|
86
|
+
background: #0f0f23;
|
|
87
|
+
color: #eee;
|
|
88
|
+
font-size: 13px;
|
|
89
|
+
outline: none;
|
|
90
|
+
}
|
|
91
|
+
.config-input:focus {
|
|
92
|
+
border-color: #4a9eff;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.btn-row {
|
|
96
|
+
display: flex;
|
|
97
|
+
gap: 8px;
|
|
98
|
+
}
|
|
99
|
+
.btn {
|
|
100
|
+
flex: 1;
|
|
101
|
+
padding: 10px;
|
|
102
|
+
border: none;
|
|
103
|
+
border-radius: 6px;
|
|
104
|
+
font-size: 13px;
|
|
105
|
+
font-weight: 500;
|
|
106
|
+
cursor: pointer;
|
|
107
|
+
transition: all 0.2s;
|
|
108
|
+
}
|
|
109
|
+
.btn-primary {
|
|
110
|
+
background: #4a9eff;
|
|
111
|
+
color: #fff;
|
|
112
|
+
}
|
|
113
|
+
.btn-primary:hover {
|
|
114
|
+
background: #3a8eef;
|
|
115
|
+
}
|
|
116
|
+
.btn-primary:disabled {
|
|
117
|
+
background: #333;
|
|
118
|
+
cursor: not-allowed;
|
|
119
|
+
}
|
|
120
|
+
.btn-secondary {
|
|
121
|
+
background: #333;
|
|
122
|
+
color: #ccc;
|
|
123
|
+
}
|
|
124
|
+
.btn-secondary:hover {
|
|
125
|
+
background: #444;
|
|
126
|
+
}
|
|
127
|
+
.scan-info {
|
|
128
|
+
font-size: 11px;
|
|
129
|
+
color: #666;
|
|
130
|
+
text-align: center;
|
|
131
|
+
margin-top: 8px;
|
|
132
|
+
}
|
|
133
|
+
</style>
|
|
134
|
+
</head>
|
|
135
|
+
<body>
|
|
136
|
+
<div class="header">
|
|
137
|
+
<img src="icon-32.png" alt="Ghost Bridge">
|
|
138
|
+
<h1>Ghost Bridge</h1>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
<div class="status-card">
|
|
142
|
+
<div class="status-row">
|
|
143
|
+
<div class="status-dot disconnected" id="statusDot"></div>
|
|
144
|
+
<span class="status-text" id="statusText">检查连接状态...</span>
|
|
145
|
+
</div>
|
|
146
|
+
<div class="status-detail" id="statusDetail"></div>
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
<div class="btn-row">
|
|
152
|
+
<button class="btn btn-primary" id="connectBtn">连接</button>
|
|
153
|
+
<button class="btn btn-secondary" id="disconnectBtn">断开</button>
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
<div class="scan-info" id="scanInfo"></div>
|
|
157
|
+
|
|
158
|
+
<script src="popup.js"></script>
|
|
159
|
+
</body>
|
|
160
|
+
</html>
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// popup.js - Ghost Bridge 弹窗逻辑
|
|
2
|
+
|
|
3
|
+
const statusDot = document.getElementById('statusDot')
|
|
4
|
+
const statusText = document.getElementById('statusText')
|
|
5
|
+
const statusDetail = document.getElementById('statusDetail')
|
|
6
|
+
const connectBtn = document.getElementById('connectBtn')
|
|
7
|
+
const disconnectBtn = document.getElementById('disconnectBtn')
|
|
8
|
+
const scanInfo = document.getElementById('scanInfo')
|
|
9
|
+
|
|
10
|
+
// 状态稳定性控制:防止闪烁
|
|
11
|
+
let lastStableStatus = null
|
|
12
|
+
let pendingStatus = null
|
|
13
|
+
let statusChangeTimer = null
|
|
14
|
+
const STATUS_DEBOUNCE_MS = 300 // 状态变化需要持续 300ms 才生效
|
|
15
|
+
|
|
16
|
+
// 状态映射
|
|
17
|
+
const STATUS_MAP = {
|
|
18
|
+
connected: {
|
|
19
|
+
dotClass: 'connected',
|
|
20
|
+
text: '✅ 已连接',
|
|
21
|
+
},
|
|
22
|
+
connecting: {
|
|
23
|
+
dotClass: 'connecting',
|
|
24
|
+
text: '🔍 正在扫描...',
|
|
25
|
+
},
|
|
26
|
+
verifying: {
|
|
27
|
+
dotClass: 'connecting',
|
|
28
|
+
text: '🔐 验证身份...',
|
|
29
|
+
},
|
|
30
|
+
scanning: {
|
|
31
|
+
dotClass: 'connecting',
|
|
32
|
+
text: '📡 搜索服务...',
|
|
33
|
+
},
|
|
34
|
+
not_found: {
|
|
35
|
+
dotClass: 'disconnected',
|
|
36
|
+
text: '🔴 未找到服务',
|
|
37
|
+
},
|
|
38
|
+
disconnected: {
|
|
39
|
+
dotClass: 'disconnected',
|
|
40
|
+
text: '未连接',
|
|
41
|
+
},
|
|
42
|
+
error: {
|
|
43
|
+
dotClass: 'error',
|
|
44
|
+
text: '连接失败',
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 实际执行 UI 更新
|
|
49
|
+
function renderUI(state) {
|
|
50
|
+
const { status, port, scanRound, enabled, currentPort, basePort } = state
|
|
51
|
+
const config = STATUS_MAP[status] || STATUS_MAP.disconnected
|
|
52
|
+
|
|
53
|
+
statusDot.className = `status-dot ${config.dotClass}`
|
|
54
|
+
statusText.textContent = config.text
|
|
55
|
+
|
|
56
|
+
// 状态详情
|
|
57
|
+
if (status === 'connected' && port) {
|
|
58
|
+
statusDetail.textContent = `端口 ${port} · WebSocket 已建立`
|
|
59
|
+
} else if ((status === 'connecting' || status === 'verifying' || status === 'scanning') && currentPort) {
|
|
60
|
+
const roundText = scanRound > 0 ? `(第 ${scanRound + 1} 轮)` : ''
|
|
61
|
+
statusDetail.textContent = `正在扫描 ${basePort}-${basePort + 9}${roundText}`
|
|
62
|
+
} else if (status === 'not_found') {
|
|
63
|
+
statusDetail.textContent = '请确保 Claude Code 已启动'
|
|
64
|
+
} else {
|
|
65
|
+
statusDetail.textContent = ''
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 按钮状态
|
|
69
|
+
connectBtn.textContent = enabled ? '重新连接' : '连接'
|
|
70
|
+
connectBtn.disabled = false
|
|
71
|
+
|
|
72
|
+
// 扫描轮次提示
|
|
73
|
+
if ((status === 'connecting' || status === 'scanning') && scanRound > 2) {
|
|
74
|
+
scanInfo.textContent = `已扫描 ${scanRound} 轮,请确保 Claude Code 已启动`
|
|
75
|
+
scanInfo.style.color = '#ff9f0a'
|
|
76
|
+
} else {
|
|
77
|
+
scanInfo.textContent = ''
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 更新 UI 状态(带防抖,防止闪烁)
|
|
82
|
+
function updateUI(state) {
|
|
83
|
+
const newStatus = state.status
|
|
84
|
+
|
|
85
|
+
// 如果是首次加载或状态相同,直接更新
|
|
86
|
+
if (lastStableStatus === null || newStatus === lastStableStatus) {
|
|
87
|
+
lastStableStatus = newStatus
|
|
88
|
+
pendingStatus = null
|
|
89
|
+
if (statusChangeTimer) {
|
|
90
|
+
clearTimeout(statusChangeTimer)
|
|
91
|
+
statusChangeTimer = null
|
|
92
|
+
}
|
|
93
|
+
renderUI(state)
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 状态变化:从 connected 变为其他状态时需要防抖
|
|
98
|
+
// 防止短暂的状态波动导致 UI 闪烁
|
|
99
|
+
if (lastStableStatus === 'connected' && newStatus !== 'connected') {
|
|
100
|
+
// 需要持续一段时间才确认断开
|
|
101
|
+
if (pendingStatus !== newStatus) {
|
|
102
|
+
pendingStatus = newStatus
|
|
103
|
+
if (statusChangeTimer) clearTimeout(statusChangeTimer)
|
|
104
|
+
statusChangeTimer = setTimeout(() => {
|
|
105
|
+
lastStableStatus = pendingStatus
|
|
106
|
+
pendingStatus = null
|
|
107
|
+
statusChangeTimer = null
|
|
108
|
+
renderUI(state)
|
|
109
|
+
}, STATUS_DEBOUNCE_MS)
|
|
110
|
+
}
|
|
111
|
+
// 暂不更新 UI,等待确认
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 其他状态变化(如从 scanning 到 connected)立即更新
|
|
116
|
+
lastStableStatus = newStatus
|
|
117
|
+
pendingStatus = null
|
|
118
|
+
if (statusChangeTimer) {
|
|
119
|
+
clearTimeout(statusChangeTimer)
|
|
120
|
+
statusChangeTimer = null
|
|
121
|
+
}
|
|
122
|
+
renderUI(state)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// 从 background 获取状态
|
|
126
|
+
async function fetchStatus() {
|
|
127
|
+
try {
|
|
128
|
+
const response = await chrome.runtime.sendMessage({ type: 'getStatus' })
|
|
129
|
+
if (response) {
|
|
130
|
+
updateUI(response)
|
|
131
|
+
}
|
|
132
|
+
} catch (e) {
|
|
133
|
+
console.error('获取状态失败:', e)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 启用连接(自动扫描端口)
|
|
138
|
+
connectBtn.addEventListener('click', async () => {
|
|
139
|
+
try {
|
|
140
|
+
await chrome.runtime.sendMessage({ type: 'connect' })
|
|
141
|
+
setTimeout(fetchStatus, 100)
|
|
142
|
+
} catch (e) {
|
|
143
|
+
console.error('连接失败:', e)
|
|
144
|
+
}
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
// 断开连接
|
|
148
|
+
disconnectBtn.addEventListener('click', async () => {
|
|
149
|
+
try {
|
|
150
|
+
await chrome.runtime.sendMessage({ type: 'disconnect' })
|
|
151
|
+
setTimeout(fetchStatus, 100)
|
|
152
|
+
} catch (e) {
|
|
153
|
+
console.error('断开失败:', e)
|
|
154
|
+
}
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
// 初始加载
|
|
158
|
+
fetchStatus()
|
|
159
|
+
|
|
160
|
+
// 监听 background 主动推送的状态变化
|
|
161
|
+
chrome.runtime.onMessage.addListener((message) => {
|
|
162
|
+
if (message.type === 'statusUpdate') {
|
|
163
|
+
updateUI(message.state)
|
|
164
|
+
}
|
|
165
|
+
})
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ghost-bridge",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "Ghost Bridge: Zero-restart Chrome debugger bridge for Claude MCP. Includes CLI for easy setup.",
|
|
7
|
+
"bin": {
|
|
8
|
+
"ghost-bridge": "./dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"main": "./dist/server.js",
|
|
11
|
+
"files": [
|
|
12
|
+
"dist/",
|
|
13
|
+
"extension/"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"start": "node dist/server.js",
|
|
17
|
+
"build": "node scripts/build.js",
|
|
18
|
+
"prepublishOnly": "npm run build"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@modelcontextprotocol/sdk": "^1.5.0",
|
|
22
|
+
"chalk": "^5.3.0",
|
|
23
|
+
"commander": "^11.0.0",
|
|
24
|
+
"esbuild": "^0.27.3",
|
|
25
|
+
"fs-extra": "^11.1.1",
|
|
26
|
+
"js-beautify": "^1.14.11",
|
|
27
|
+
"ws": "^8.14.2"
|
|
28
|
+
}
|
|
29
|
+
}
|