From 532cceabb9ec0feb24ad6a6cd8457742822b9057 Mon Sep 17 00:00:00 2001 From: baiqing Date: Mon, 18 May 2026 18:30:38 +0800 Subject: [PATCH] =?UTF-8?q?fix(windows):=20hotkey=20toggle=20=E4=BA=8C?= =?UTF-8?q?=E6=AC=A1=E6=8C=89=E9=94=AE=E5=A4=B1=E6=95=88=20-=20=E4=B8=B2?= =?UTF-8?q?=E8=A1=8C=E5=8C=96=20Pressed/Released=20edge?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因: hotkey_bridge_loop 和 combo_hotkey_bridge_loop 把每条 Pressed/Released 边沿独立 async_runtime::spawn 成两个 task,被 tokio work-stealing 调度器 并行执行。Windows WH_KEYBOARD_LL 边沿间隔微秒级,Released task 经常先 swap(true→false),导致下一次 Pressed 看到 was_held=true → toggle 被静默吞掉。 macOS 上 CGEventTap 边沿间隔几十毫秒,race window 极窄,肉眼不可见, 所以仅 Windows 必现。ESC 走 HotkeyEvent::Cancelled 不经过 latch, 是用户唯一能逃出 stuck-latch 的路径。 修复: 用 tauri::async_runtime::block_on 顺序 await 两个 handler, bridge 线程的 mpsc::Receiver::recv() FIFO 保证 swap 顺序正确。 bridge 在独立 OS 线程而非 tokio worker,block_on 安全。 代价: Hold 模式短按 stop 多 ≤500ms 延迟,被 request_stop_during_starting 覆盖,可接受。qa / translation bridge 不走 handle_*_edge 不受影响。 附带清理 coordinator.rs unreachable_code warning。 Refs #468 --- openless-all/app/src-tauri/src/coordinator.rs | 33 ++++++++++++++++--- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/openless-all/app/src-tauri/src/coordinator.rs b/openless-all/app/src-tauri/src/coordinator.rs index da3da4f8..a0a86d00 100644 --- a/openless-all/app/src-tauri/src/coordinator.rs +++ b/openless-all/app/src-tauri/src/coordinator.rs @@ -1305,11 +1305,17 @@ fn combo_hotkey_bridge_loop(inner: Arc, rx: mpsc::Receiver { - async_runtime::spawn(async move { handle_pressed_edge(&inner_cloned).await }); + async_runtime::block_on(async { + handle_pressed_edge(&inner_cloned).await; + }); } ComboHotkeyEvent::Released => { - async_runtime::spawn(async move { handle_released_edge(&inner_cloned).await }); + async_runtime::block_on(async { + handle_released_edge(&inner_cloned).await; + }); } } } @@ -1697,11 +1703,25 @@ fn hotkey_bridge_loop(inner: Arc, rx: mpsc::Receiver) { } let inner_cloned = Arc::clone(&inner); match evt { + // P0 #468/#475: Pressed/Released 必须串行处理,否则在 Windows 上 WH_KEYBOARD_LL + // 边沿间隔微秒级 → 两个独立 spawn 的 task 被 work-stealing 调度器并行执行 → + // `hotkey_trigger_held` latch 翻转顺序错乱 → 下次按键被静默吞掉 + // (UI 关不掉 / 录音停不下来)。改为 bridge 线程内 block_on 顺序 await, + // recv 的 FIFO 顺序就是 handler 执行顺序。 + // 注意:handle_pressed_edge / handle_released_edge 内部走 .await(含网络 + // 握手),会暂时阻塞本 bridge 线程;Hold 模式短按时 Released 会排队在 channel + // 里直到 begin_session 完成,但 SessionPhase::Starting 已经有 + // request_stop_during_starting 兜底,begin_session 完成进 Listening 后 + // bridge 立刻 recv Released → end_session,行为正确,仅有短暂 stop 延迟。 HotkeyEvent::Pressed => { - async_runtime::spawn(async move { handle_pressed_edge(&inner_cloned).await }); + async_runtime::block_on(async { + handle_pressed_edge(&inner_cloned).await; + }); } HotkeyEvent::Released => { - async_runtime::spawn(async move { handle_released_edge(&inner_cloned).await }); + async_runtime::block_on(async { + handle_released_edge(&inner_cloned).await; + }); } HotkeyEvent::Cancelled => { cancel_session(&inner_cloned); @@ -2091,7 +2111,10 @@ fn ensure_asr_credentials() -> Result<(), String> { { return Err("Foundry Local Whisper 当前仅支持 Windows".to_string()); } - return Ok(()); + #[cfg(target_os = "windows")] + { + return Ok(()); + } } if is_whisper_compatible_provider(&active_asr) || is_bailian_provider(&active_asr) {