fix(windows): hotkey toggle 二次按键失效 - 串行化 Pressed/Released edge (#489)

根因: 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

Co-authored-by: baiqing <lbx12309@icloud.com>
This commit is contained in:
TRIPandbaiqing authored and GitHub committed 2026-05-18 18:54:53 +08:00
1 parent cb2c233ebe
commit f30a787227
1 file changed
+28 -5
+28 -5
View File
@@ -1305,11 +1305,17 @@ fn combo_hotkey_bridge_loop(inner: Arc<Inner>, rx: mpsc::Receiver<ComboHotkeyEve
}
let inner_cloned = Arc::clone(&inner);
match evt {
// P0 #468/#475: 同 hotkey_bridge_loop —— Pressed/Released 必须串行 await,
// 否则 latch 竞态导致 combo 快捷键二次按键失效。
ComboHotkeyEvent::Pressed => {
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<Inner>, rx: mpsc::Receiver<HotkeyEvent>) {
}
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) {