ghost-bridge 0.9.1 → 1.0.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.
@@ -524,6 +524,23 @@ async function resolveTargetTab(params = {}) {
524
524
  // attach 互斥锁:防止并发调用 ensureAttached 导致重复 attach / 状态竞态
525
525
  let _attachLock = Promise.resolve()
526
526
 
527
+ // Chrome 对冻结/丢弃/无响应标签页的 debugger 调用可能永不回调。
528
+ // 锁内的每次 debugger 操作都必须带超时,否则一次挂起会让锁永不释放,
529
+ // 所有会话的调试功能整体死锁(list_tabs 等非 debugger 命令不受影响)
530
+ const DBG_ATTACH_TIMEOUT_MS = 10000
531
+ const DBG_ENABLE_TIMEOUT_MS = 8000
532
+ const DBG_DETACH_TIMEOUT_MS = 5000
533
+
534
+ function withTimeout(promise, ms, label) {
535
+ return new Promise((resolve, reject) => {
536
+ const timer = setTimeout(() => reject(new Error(`${label} 超时(${ms}ms),标签页可能已被 Chrome 冻结或无响应`)), ms)
537
+ promise.then(
538
+ (v) => { clearTimeout(timer); resolve(v) },
539
+ (e) => { clearTimeout(timer); reject(e) }
540
+ )
541
+ })
542
+ }
543
+
527
544
  async function ensureAttachedSession(params = {}) {
528
545
  let _release
529
546
  const _prev = _attachLock
@@ -542,32 +559,44 @@ async function ensureAttachedSession(params = {}) {
542
559
  const session = getSession(tab.id)
543
560
  if (!session.attached) {
544
561
  try {
545
- await chrome.debugger.attach({ tabId: tab.id }, "1.3")
562
+ await withTimeout(chrome.debugger.attach({ tabId: tab.id }, "1.3"), DBG_ATTACH_TIMEOUT_MS, `attach 标签页 ${tab.id}`)
546
563
  setBadgeState("on")
547
564
  } catch (e) {
548
- if (attachedTabId === tab.id) attachedTabId = null
549
- if (state.connected) {
550
- setBadgeState("on")
551
- } else {
552
- setBadgeState("att")
565
+ // 本扩展重复 attach 视为成功(幂等):也覆盖超时放弃后迟到的 attach 成功等竞态;
566
+ // 其余错误(如 DevTools 正在调试该标签页)照常抛出
567
+ if (!/^already attached/i.test(String(e && e.message))) {
568
+ if (attachedTabId === tab.id) attachedTabId = null
569
+ if (state.connected) {
570
+ setBadgeState("on")
571
+ } else {
572
+ setBadgeState("att")
573
+ }
574
+ throw e
553
575
  }
554
- throw e
555
576
  }
556
577
  session.attached = true
557
578
  resetDebuggerState(session)
558
- await chrome.debugger.sendCommand({ tabId: session.tabId }, "Runtime.enable")
559
- await chrome.debugger.sendCommand({ tabId: session.tabId }, "Log.enable")
560
- await chrome.debugger.sendCommand({ tabId: session.tabId }, "Console.enable").catch(() => {})
561
- await chrome.debugger.sendCommand({ tabId: session.tabId }, "Debugger.enable")
562
- await chrome.debugger.sendCommand({ tabId: session.tabId }, "Profiler.enable")
563
- await chrome.debugger.sendCommand({ tabId: session.tabId }, "Network.enable").catch(() => {})
564
-
565
- // Enable auto-attach to sub-targets (iframes, workers) for comprehensive capture
566
- await chrome.debugger.sendCommand({ tabId: session.tabId }, "Target.setAutoAttach", {
567
- autoAttach: true,
568
- waitForDebuggerOnStart: false,
569
- flatten: true,
570
- }).catch(() => {})
579
+ try {
580
+ await withTimeout((async () => {
581
+ await chrome.debugger.sendCommand({ tabId: session.tabId }, "Runtime.enable")
582
+ await chrome.debugger.sendCommand({ tabId: session.tabId }, "Log.enable")
583
+ await chrome.debugger.sendCommand({ tabId: session.tabId }, "Console.enable").catch(() => {})
584
+ await chrome.debugger.sendCommand({ tabId: session.tabId }, "Debugger.enable")
585
+ await chrome.debugger.sendCommand({ tabId: session.tabId }, "Profiler.enable")
586
+ await chrome.debugger.sendCommand({ tabId: session.tabId }, "Network.enable").catch(() => {})
587
+
588
+ // Enable auto-attach to sub-targets (iframes, workers) for comprehensive capture
589
+ await chrome.debugger.sendCommand({ tabId: session.tabId }, "Target.setAutoAttach", {
590
+ autoAttach: true,
591
+ waitForDebuggerOnStart: false,
592
+ flatten: true,
593
+ }).catch(() => {})
594
+ })(), DBG_ENABLE_TIMEOUT_MS, `初始化调试器会话 ${session.tabId}`)
595
+ } catch (e) {
596
+ // 初始化阶段挂起/失败:放弃该会话,避免后续命令打到半初始化的调试器上
597
+ await detachSession(session)
598
+ throw e
599
+ }
571
600
  }
572
601
  attachedTabId = session.tabId
573
602
  if (isFocusedDefaultTarget) focusedTabId = session.tabId
@@ -585,7 +614,9 @@ async function ensureAttached(params = {}) {
585
614
  async function detachSession(session) {
586
615
  if (!session) return
587
616
  try {
588
- if (session.attached) await chrome.debugger.detach({ tabId: session.tabId })
617
+ if (session.attached) {
618
+ await withTimeout(chrome.debugger.detach({ tabId: session.tabId }), DBG_DETACH_TIMEOUT_MS, `detach 标签页 ${session.tabId}`)
619
+ }
589
620
  } catch (e) {
590
621
  log(`detach 失败:${e.message}`)
591
622
  } finally {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "Ghost Bridge",
4
- "version": "0.9.1",
4
+ "version": "1.0.0",
5
5
  "description": "Zero-restart Chrome debugger bridge for Claude MCP, optimized for no-sourcemap production debugging.",
6
6
  "permissions": [
7
7
  "debugger",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ghost-bridge",
3
- "version": "0.9.1",
3
+ "version": "1.0.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.",