ghost-bridge 0.8.0 → 0.9.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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "Ghost Bridge",
4
- "version": "0.8.0",
4
+ "version": "0.9.0",
5
5
  "description": "Zero-restart Chrome debugger bridge for Claude MCP, optimized for no-sourcemap production debugging.",
6
6
  "permissions": [
7
7
  "debugger",
@@ -9,7 +9,8 @@
9
9
  "scripting",
10
10
  "storage",
11
11
  "tabs",
12
- "offscreen"
12
+ "offscreen",
13
+ "idle"
13
14
  ],
14
15
  "host_permissions": [
15
16
  "ws://localhost/*",
@@ -3,6 +3,7 @@
3
3
 
4
4
  let ws = null
5
5
  let reconnectTimer = null
6
+ let heartbeatTimer = null
6
7
  let manualDisconnect = false // 用户主动断开标志,防止 onclose 触发重连
7
8
  let connectionGeneration = 0
8
9
  let config = {
@@ -10,14 +11,16 @@ let config = {
10
11
  token: '',
11
12
  }
12
13
 
14
+ const HEARTBEAT_INTERVAL_MS = 15000
15
+ const HEARTBEAT_TIMEOUT_MS = 45000
16
+ const DEFAULT_TOKEN = 'ghost-bridge-local'
17
+
13
18
  function log(msg) {
14
19
  console.log(`[ghost-bridge offscreen] ${msg}`)
15
20
  // 转发日志到 service worker
16
21
  chrome.runtime.sendMessage({ type: 'log', msg }).catch(() => {})
17
22
  }
18
23
 
19
- const DEFAULT_TOKEN = 'ghost-bridge-local'
20
-
21
24
  function clearReconnectTimer() {
22
25
  if (reconnectTimer) {
23
26
  clearTimeout(reconnectTimer)
@@ -25,6 +28,13 @@ function clearReconnectTimer() {
25
28
  }
26
29
  }
27
30
 
31
+ function clearHeartbeatTimer() {
32
+ if (heartbeatTimer) {
33
+ clearInterval(heartbeatTimer)
34
+ heartbeatTimer = null
35
+ }
36
+ }
37
+
28
38
  function isCurrentConnection(generation, socket) {
29
39
  return generation === connectionGeneration && socket === ws
30
40
  }
@@ -33,11 +43,13 @@ function scheduleReconnect(generation, delay) {
33
43
  if (generation !== connectionGeneration) return
34
44
  clearReconnectTimer()
35
45
  reconnectTimer = setTimeout(() => {
36
- if (generation === connectionGeneration) connect()
46
+ reconnectTimer = null
47
+ if (!manualDisconnect && generation === connectionGeneration) connect()
37
48
  }, delay)
38
49
  }
39
50
 
40
51
  function closeCurrentSocket() {
52
+ clearHeartbeatTimer()
41
53
  if (!ws) return
42
54
  try {
43
55
  ws.close()
@@ -45,6 +57,50 @@ function closeCurrentSocket() {
45
57
  ws = null
46
58
  }
47
59
 
60
+ function describeCloseEvent(event) {
61
+ const reason = event.reason ? ` reason="${event.reason}"` : ''
62
+ return `code=${event.code}${reason} clean=${event.wasClean}`
63
+ }
64
+
65
+ function describeReadyState(socket) {
66
+ const states = ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED']
67
+ return states[socket.readyState] || String(socket.readyState)
68
+ }
69
+
70
+ function startHeartbeat(generation, socket, port, requireAck) {
71
+ clearHeartbeatTimer()
72
+ let lastAckAt = Date.now()
73
+
74
+ heartbeatTimer = setInterval(() => {
75
+ if (!isCurrentConnection(generation, socket) || socket.readyState !== WebSocket.OPEN) {
76
+ clearHeartbeatTimer()
77
+ return
78
+ }
79
+
80
+ const now = Date.now()
81
+ if (requireAck && now - lastAckAt > HEARTBEAT_TIMEOUT_MS) {
82
+ log(`端口 ${port} 心跳超时,关闭连接后重连...`)
83
+ try {
84
+ socket.close(4000, 'Heartbeat timeout')
85
+ } catch {}
86
+ return
87
+ }
88
+
89
+ try {
90
+ socket.send(JSON.stringify({ type: 'heartbeat', token: config.token, ts: now }))
91
+ } catch (e) {
92
+ log(`端口 ${port} 心跳发送失败:${e.message}`)
93
+ try {
94
+ socket.close(4001, 'Heartbeat send failed')
95
+ } catch {}
96
+ }
97
+ }, HEARTBEAT_INTERVAL_MS)
98
+
99
+ return () => {
100
+ lastAckAt = Date.now()
101
+ }
102
+ }
103
+
48
104
  // 连接到服务器
49
105
  function connect() {
50
106
  // 如果已手动断开,不再尝试连接
@@ -76,6 +132,7 @@ function connect() {
76
132
  let identityVerified = false
77
133
  let socketOpened = false
78
134
  let terminalErrorMessage = ''
135
+ let markHeartbeatAck = null
79
136
 
80
137
  socket.onopen = () => {
81
138
  if (!isCurrentConnection(generation, socket)) return
@@ -98,6 +155,12 @@ function connect() {
98
155
  if (msg.type === 'identity') {
99
156
  if (msg.service === 'ghost-bridge' && msg.token === config.token) {
100
157
  identityVerified = true
158
+ markHeartbeatAck = startHeartbeat(
159
+ generation,
160
+ socket,
161
+ port,
162
+ Array.isArray(msg.capabilities) && msg.capabilities.includes('heartbeat')
163
+ )
101
164
  log(`✅ 已连接到 ghost-bridge 服务 (端口 ${port})`)
102
165
  chrome.runtime.sendMessage({
103
166
  type: 'status',
@@ -122,11 +185,17 @@ function connect() {
122
185
  currentPort: port,
123
186
  errorMessage: terminalErrorMessage,
124
187
  }).catch(() => {})
125
- ws.close()
188
+ socket.close()
126
189
  }
127
190
  return
128
191
  }
129
192
 
193
+ if (msg.type === 'heartbeat_ack') {
194
+ if (markHeartbeatAck) markHeartbeatAck()
195
+ if (pendingHealthCheck) resolveHealthCheck(true)
196
+ return
197
+ }
198
+
130
199
  // 转发命令到 service worker
131
200
  if (identityVerified && msg.id) {
132
201
  chrome.runtime.sendMessage({ type: 'command', data: msg }).catch(() => {})
@@ -139,14 +208,20 @@ function connect() {
139
208
  socket.onclose = (event) => {
140
209
  if (!isCurrentConnection(generation, socket)) return
141
210
  clearTimeout(connectionTimeout)
211
+ clearHeartbeatTimer()
212
+ ws = null
213
+ const closeInfo = describeCloseEvent(event)
142
214
 
143
215
  // 用户主动断开,不重连
144
- if (manualDisconnect) return
216
+ if (manualDisconnect) {
217
+ log(`连接已按用户请求关闭 (${closeInfo})`)
218
+ return
219
+ }
145
220
 
146
221
  if (!identityVerified) {
147
222
  if (terminalErrorMessage || socketOpened) {
148
223
  const errorMessage = terminalErrorMessage || `Port ${port} is occupied or responding with a non-ghost-bridge protocol.`
149
- log(`${errorMessage} 2秒后重试...`)
224
+ log(`${errorMessage} (${closeInfo}) 2秒后重试...`)
150
225
  chrome.runtime.sendMessage({
151
226
  type: 'status',
152
227
  status: 'error',
@@ -168,26 +243,79 @@ function connect() {
168
243
  }
169
244
 
170
245
  // 连接断开,重试
171
- log(`端口 ${port} 连接断开,尝试重连...`)
172
- chrome.runtime.sendMessage({ type: 'status', status: 'disconnected' }).catch(() => {})
246
+ log(`端口 ${port} 连接断开 (${closeInfo}),尝试重连...`)
247
+ chrome.runtime.sendMessage({
248
+ type: 'status',
249
+ status: 'disconnected',
250
+ currentPort: port,
251
+ errorMessage: `Connection closed: ${closeInfo}`,
252
+ }).catch(() => {})
173
253
  scheduleReconnect(generation, 1000)
174
254
  }
175
255
 
176
256
  socket.onerror = () => {
177
257
  if (!isCurrentConnection(generation, socket)) return
178
258
  clearTimeout(connectionTimeout)
259
+ log(`端口 ${port} WebSocket 错误,readyState=${describeReadyState(socket)}`)
179
260
  }
180
261
  }
181
262
 
182
263
  // 发送消息到服务器
183
264
  function sendToServer(data) {
184
265
  if (ws && ws.readyState === WebSocket.OPEN) {
185
- ws.send(JSON.stringify(data))
186
- return true
266
+ try {
267
+ ws.send(JSON.stringify(data))
268
+ return true
269
+ } catch (e) {
270
+ log(`发送消息到服务器失败:${e.message}`)
271
+ }
187
272
  }
188
273
  return false
189
274
  }
190
275
 
276
+ // ========== 唤醒探活 ==========
277
+ // 系统睡眠/锁屏唤醒后连接可能处于半开状态且 onclose 不会触发,
278
+ // 由 background 的 idle 钩子通知这里立即发一次心跳,短超时内无响应就主动重连
279
+ let pendingHealthCheck = null
280
+
281
+ function resolveHealthCheck(alive) {
282
+ if (!pendingHealthCheck) return
283
+ clearTimeout(pendingHealthCheck.timer)
284
+ const resolve = pendingHealthCheck.resolve
285
+ pendingHealthCheck = null
286
+ resolve(alive)
287
+ }
288
+
289
+ function requestHealthCheck() {
290
+ return new Promise((resolve) => {
291
+ if (manualDisconnect) return resolve(false)
292
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
293
+ log('唤醒探活:连接未打开,立即重连')
294
+ connect()
295
+ return resolve(false)
296
+ }
297
+ // 收敛上一次未完成的探活
298
+ if (pendingHealthCheck) resolveHealthCheck(false)
299
+
300
+ const timer = setTimeout(() => {
301
+ log('唤醒探活:心跳无响应,判定为死连接,关闭后重连')
302
+ resolveHealthCheck(false)
303
+ try {
304
+ ws.close(4002, 'Health check timeout')
305
+ } catch {}
306
+ }, 5000)
307
+ pendingHealthCheck = { timer, resolve }
308
+
309
+ try {
310
+ ws.send(JSON.stringify({ type: 'heartbeat', token: config.token, ts: Date.now() }))
311
+ } catch (e) {
312
+ log(`唤醒探活:心跳发送失败 (${e.message}),立即重连`)
313
+ resolveHealthCheck(false)
314
+ connect()
315
+ }
316
+ })
317
+ }
318
+
191
319
  // 断开连接
192
320
  function disconnect() {
193
321
  manualDisconnect = true // 标记为手动断开,阻止 onclose 重连
@@ -223,6 +351,17 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
223
351
  if (message.type === 'getOffscreenStatus') {
224
352
  sendResponse({
225
353
  connected: ws && ws.readyState === WebSocket.OPEN,
354
+ readyState: ws ? describeReadyState(ws) : 'CLOSED',
355
+ port: config.basePort,
356
+ })
357
+ return true
358
+ }
359
+
360
+ // 唤醒钩子触发的立即探活(background 的 chrome.idle 监听转发)
361
+ if (message.type === 'healthCheck') {
362
+ requestHealthCheck().then((alive) => {
363
+ if (!alive) log('唤醒探活完成:连接已重建/重连中')
364
+ sendResponse({ alive })
226
365
  })
227
366
  return true
228
367
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ghost-bridge",
3
- "version": "0.8.0",
3
+ "version": "0.9.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.",
@@ -17,12 +17,12 @@
17
17
  "author": "<Author>",
18
18
  "repository": {
19
19
  "type": "git",
20
- "url": "https://github.com/<Username>/ghost-bridge.git"
20
+ "url": "https://github.com/learnStop163/ghost-bridge.git"
21
21
  },
22
22
  "bugs": {
23
- "url": "https://github.com/<Username>/ghost-bridge/issues"
23
+ "url": "https://github.com/learnStop163/ghost-bridge/issues"
24
24
  },
25
- "homepage": "https://github.com/<Username>/ghost-bridge#readme",
25
+ "homepage": "https://github.com/learnStop163/ghost-bridge#readme",
26
26
  "license": "MIT",
27
27
  "engines": {
28
28
  "node": ">=18.0.0"