From a6999f1e5d1f4ccfe66054d156478b8eeb96cfe9 Mon Sep 17 00:00:00 2001 From: baiqing Date: Thu, 30 Apr 2026 13:36:15 +0800 Subject: [PATCH] =?UTF-8?q?fix(coordinator):=20=E4=BC=9A=E8=AF=9D=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E6=9C=BA=E4=BF=AE=E5=A4=8D=20=E2=80=94=20Starting=20?= =?UTF-8?q?=E9=98=B6=E6=AE=B5=E8=A1=A5=E5=81=BF=E7=83=AD=E9=94=AE=E8=BE=B9?= =?UTF-8?q?=E6=B2=BF=20+=20Esc=20=E5=8F=96=E6=B6=88=E6=94=AF=E6=8C=81=20Pr?= =?UTF-8?q?ocessing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit closes #51, #52 ## #51 — Starting 阶段忽略热键边沿 握手 ASR 期间(phase=Starting)的"停止"边沿(toggle 第二次按 / hold 松开) 之前被直接 _ => {} 掉,会话锁死直到用户再按一次或重启 app。 修: - SessionState 加 pending_stop: bool - handle_pressed Toggle/Starting → pending_stop = true - handle_released Hold/Starting → pending_stop = true - begin_session 转 phase=Listening 时 mem::replace 取出 pending_stop, 若为 true 立即 await end_session ## #52 — Esc 取消在 Processing 阶段无效 用户按 Esc 时若 end_session 已进入 Processing(润色 / 插入进行中),cancel 信号被忽略,文本仍插入到光标位置 → 数据无意写入用户当前 app。 修: - SessionState 加 cancelled: bool - cancel_session 设 cancelled=true(不再仅 Idle 之外 = 转 Idle) Processing 阶段保持 phase=Processing 让 end_session 自己走完检查 + 收尾 - end_session 在 ASR 完成后、polish 之前 + polish 完成后、insert 之前 各检查一次 cancelled,命中即跳过 insert/history.append 新会话 begin_session 入口清两个 flag,避免遗留触发奇怪行为。 --- openless-all/app/src-tauri/src/coordinator.rs | 67 +++++++++++++++++-- 1 file changed, 62 insertions(+), 5 deletions(-) diff --git a/openless-all/app/src-tauri/src/coordinator.rs b/openless-all/app/src-tauri/src/coordinator.rs index 4b8236b0..0c56626e 100644 --- a/openless-all/app/src-tauri/src/coordinator.rs +++ b/openless-all/app/src-tauri/src/coordinator.rs @@ -47,6 +47,12 @@ enum ActiveAsr { struct SessionState { phase: SessionPhase, started_at: Instant, + /// Starting 阶段(ASR 握手中)按下 stop 边沿(toggle 第二次按 / hold 松开)→ + /// 等握手完成 phase=Listening 后立刻 end_session,不丢边沿。issue #51。 + pending_stop: bool, + /// 用户在 Processing 阶段按 Esc 取消:end_session 在 polish/insert 检查点跳过插入 + + /// 跳过 history.append。issue #52。 + cancelled: bool, } impl Default for SessionState { @@ -54,6 +60,8 @@ impl Default for SessionState { Self { phase: SessionPhase::Idle, started_at: Instant::now(), + pending_stop: false, + cancelled: false, } } } @@ -261,6 +269,12 @@ async fn handle_pressed(inner: &Arc) { (HotkeyMode::Hold, SessionPhase::Idle) => { let _ = begin_session(inner).await; } + // Toggle 模式 Starting 阶段第二次按 → 用户想停。 + // 不能直接 end_session(ASR session 还没建好),存边沿,握手完成后立即触发。 + (HotkeyMode::Toggle, SessionPhase::Starting) => { + inner.state.lock().pending_stop = true; + log::info!("[coord] toggle stop edge during Starting — queued"); + } _ => {} } } @@ -270,8 +284,16 @@ async fn handle_released(inner: &Arc) { let phase = inner.state.lock().phase; log::info!("[coord] hotkey released (mode={mode:?}, phase={phase:?})"); if mode == HotkeyMode::Hold { - if phase == SessionPhase::Listening { - let _ = end_session(inner).await; + match phase { + SessionPhase::Listening => { + let _ = end_session(inner).await; + } + // Hold 模式 Starting 阶段松开 → 用户想停。同上:握手完成后再 end。 + SessionPhase::Starting => { + inner.state.lock().pending_stop = true; + log::info!("[coord] hold release edge during Starting — queued"); + } + _ => {} } } } @@ -286,6 +308,9 @@ async fn begin_session(inner: &Arc) -> Result<(), String> { } state.phase = SessionPhase::Starting; state.started_at = Instant::now(); + // 新会话清掉旧 pending_stop / cancelled,避免上一会话遗留触发奇怪行为 + state.pending_stop = false; + state.cancelled = false; } #[cfg(any(debug_assertions, test))] @@ -384,8 +409,18 @@ async fn begin_session(inner: &Arc) -> Result<(), String> { match Recorder::start(consumer, level_handler) { Ok(rec) => { *inner.recorder.lock() = Some(rec); - inner.state.lock().phase = SessionPhase::Listening; + // 转 Listening 同时检查 Starting 期间是否积累了 pending_stop 边沿。 + // hold 模式快速松开 / toggle 快速双击会到这里:握手刚完就要立即停。 + let should_stop_immediately = { + let mut state = inner.state.lock(); + state.phase = SessionPhase::Listening; + std::mem::replace(&mut state.pending_stop, false) + }; log::info!("[coord] session started (asr={})", active_asr); + if should_stop_immediately { + log::info!("[coord] applying pending_stop edge → end_session immediately"); + let _ = end_session(inner).await; + } } Err(e) => { log::error!("[coord] recorder start failed: {e}"); @@ -476,6 +511,13 @@ async fn end_session(inner: &Arc) -> Result<(), String> { }, }; + // ASR 完成后 cancel 检查:用户在 transcribe 进行中按 Esc 时,这里就会命中。 + if inner.state.lock().cancelled { + log::info!("[coord] cancel detected after ASR — discarding transcript"); + inner.state.lock().phase = SessionPhase::Idle; + return Ok(()); + } + emit_capsule(inner, CapsuleState::Polishing, 0.0, elapsed, None, None); let prefs = inner.prefs.get(); @@ -483,6 +525,13 @@ async fn end_session(inner: &Arc) -> Result<(), String> { let hotword_strs = enabled_phrases(inner); let polished = polish_or_passthrough(&raw, mode, &hotword_strs).await; + // Polish 完成后再 check 一次:即使 polish 已经返回,只要还没插入,仍可丢弃。 + if inner.state.lock().cancelled { + log::info!("[coord] cancel detected after polish — discarding output (chars={})", polished.chars().count()); + inner.state.lock().phase = SessionPhase::Idle; + return Ok(()); + } + let status = inner.inserter.insert(&polished); let inserted_chars = polished.chars().count() as u32; @@ -557,6 +606,10 @@ fn cancel_session(inner: &Arc) { if phase == SessionPhase::Idle { return; } + // Processing 阶段 cancel 不能直接干掉 in-flight polish task(已经 await 了), + // 但可以打 cancelled 标记,让 end_session 在插入前检查并丢弃结果。 + inner.state.lock().cancelled = true; + if let Some(rec) = inner.recorder.lock().take() { rec.stop(); } @@ -566,9 +619,13 @@ fn cancel_session(inner: &Arc) { ActiveAsr::Whisper(w) => w.cancel(), } } - inner.state.lock().phase = SessionPhase::Idle; + // Processing 阶段保持 phase=Processing 让 end_session 自己走完检查 + 收尾; + // 其他阶段直接转 Idle。 + if phase != SessionPhase::Processing { + inner.state.lock().phase = SessionPhase::Idle; + } emit_capsule(inner, CapsuleState::Cancelled, 0.0, 0, None, None); - log::info!("[coord] session cancelled"); + log::info!("[coord] session cancelled (was {phase:?})"); } // ─────────────────────────── helpers ───────────────────────────