From 60ab2418bdf7ee3ed40bb2e0547dff28ce79f6ee Mon Sep 17 00:00:00 2001 From: baiqing Date: Mon, 11 May 2026 23:39:43 +0800 Subject: [PATCH] fix(hotkey): 250ms debounce on pressed-edge dispatch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 避免微动开关回弹 / 用户手抖双击造成的空转写报错和 ASR session 抢资源: 相邻 < 250ms 的 press 边沿直接丢弃,记 log。 - coordinator.rs: Inner 新增 last_hotkey_dispatch_at: Mutex> - coordinator/dictation.rs: HOTKEY_DEBOUNCE 常量 + handle_pressed_edge 入口检查 与 hotkey_trigger_held 互补:held 防 press-without-release,本检查防 press-release-press 三连过快。每个有效边沿都会更新时间戳;用户正常使用 (不会快于 4 次/秒)不会触发。 --- openless-all/app/src-tauri/src/coordinator.rs | 7 ++++++ .../src-tauri/src/coordinator/dictation.rs | 24 +++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/openless-all/app/src-tauri/src/coordinator.rs b/openless-all/app/src-tauri/src/coordinator.rs index c2322174..e321d495 100644 --- a/openless-all/app/src-tauri/src/coordinator.rs +++ b/openless-all/app/src-tauri/src/coordinator.rs @@ -119,6 +119,11 @@ struct Inner { hotkey: Mutex>, hotkey_status: Mutex, hotkey_trigger_held: AtomicBool, + /// 防抖时间戳:handle_pressed_edge 入口检查与本字段的距离,< 250ms 的边沿直接 + /// 丢弃(误触双击 / 微动开关回弹 / 用户连点过快造成的空转写报错)。 + /// 与 `hotkey_trigger_held` 互补 —— held 防 press-without-release,本字段防 + /// press-release-press 三连过快。 + last_hotkey_dispatch_at: Mutex>, shortcut_recording_active: AtomicBool, /// 自定义组合键监听器(global-hotkey crate)。当 `prefs.hotkey.trigger == Custom` 时 /// 代替 modifier-only 的 hotkey monitor。`None` 表示不使用自定义组合键或还没成功安装。 @@ -199,6 +204,7 @@ impl Coordinator { hotkey: Mutex::new(None), hotkey_status: Mutex::new(HotkeyStatus::default()), hotkey_trigger_held: AtomicBool::new(false), + last_hotkey_dispatch_at: Mutex::new(None), shortcut_recording_active: AtomicBool::new(false), combo_hotkey: Mutex::new(None), translation_hotkey: Mutex::new(None), @@ -245,6 +251,7 @@ impl Coordinator { hotkey: Mutex::new(None), hotkey_status: Mutex::new(HotkeyStatus::default()), hotkey_trigger_held: AtomicBool::new(false), + last_hotkey_dispatch_at: Mutex::new(None), shortcut_recording_active: AtomicBool::new(false), combo_hotkey: Mutex::new(None), translation_hotkey: Mutex::new(None), diff --git a/openless-all/app/src-tauri/src/coordinator/dictation.rs b/openless-all/app/src-tauri/src/coordinator/dictation.rs index 0a92b475..a506d35c 100644 --- a/openless-all/app/src-tauri/src/coordinator/dictation.rs +++ b/openless-all/app/src-tauri/src/coordinator/dictation.rs @@ -9,9 +9,33 @@ use super::qa::handle_qa_option_edge; use super::resources::*; use super::*; +/// 同一个 hotkey 边沿之间的最小间隔。低于此阈值的连按整体作为误触丢弃 —— +/// 避免微动开关回弹 / 用户手抖双击造成的空转写报错和 ASR session 抢资源。 +const HOTKEY_DEBOUNCE: std::time::Duration = std::time::Duration::from_millis(250); + pub(super) async fn handle_pressed_edge(inner: &Arc) { let was_held = inner.hotkey_trigger_held.swap(true, Ordering::SeqCst); if !was_held { + // 防抖:相邻 < HOTKEY_DEBOUNCE 的边沿直接丢弃,记到 log 方便排查。 + // 与 `hotkey_trigger_held` 互补:held 防 press-without-release,本检查防 + // press-release-press 三连过快。每个有效边沿都会更新时间戳。 + let now = std::time::Instant::now(); + let too_soon = { + let mut last = inner.last_hotkey_dispatch_at.lock(); + let drop = matches!(*last, Some(t) if now.duration_since(t) < HOTKEY_DEBOUNCE); + if !drop { + *last = Some(now); + } + drop + }; + if too_soon { + log::info!( + "[coord] hotkey pressed edge debounced (< {} ms since last dispatch)", + HOTKEY_DEBOUNCE.as_millis() + ); + return; + } + // 路由:QA 浮窗可见时,rightOption 边沿走 QA;否则走主听写。详见 issue #118 v2。 // 例外:dictation session 已经在跑(Starting / Listening / Processing / Inserting), // 即使 QA 浮窗被打开了,这条边沿也必须先走 dictation。否则 begin_qa_session 会