From 83e4135175864193e4b24b06a77fa11a1bd69222 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=95=E6=9F=8F=E9=9D=92?= Date: Thu, 4 Jun 2026 20:45:47 +0800 Subject: [PATCH] =?UTF-8?q?chore(capsule):=20#470=20=E8=AF=8A=E6=96=AD=20v?= =?UTF-8?q?2=20=E2=80=94=E2=80=94=20=E7=BB=99=E8=83=B6=E5=9B=8A=E4=B8=8D?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=E7=9A=84=E6=9A=97=E7=82=B9=E8=A1=A5=E4=B8=80?= =?UTF-8?q?=E6=AC=A1=E6=80=A7=E6=97=A5=E5=BF=97=EF=BC=88=E9=9B=B6=E8=A1=8C?= =?UTF-8?q?=E4=B8=BA=E6=94=B9=E5=8A=A8=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #470「Windows 语音输入看不到录音胶囊」根因仍未定(PR #471 已明说只加诊断、需真机 日志;byte92 报 macOS 也复现,疑跨平台)。本 PR 不改任何 show/hide/position 行为, 仅在几个静默路径补日志,把根因收敛到 A/B/C 单一分支,帮助拿日志定位: - coordinator.rs show_capsule_window_no_activate(Windows):两处 window_handle() / RawWindowHandle::Win32 取不到时此前静默 return false,补 warn(Win32 show 失败最可能的暗点)。 - coordinator.rs emit_capsule:get_webview_window("capsule") 取不到时此前静默 return, 补一次性 warn(暗点 A0:窗口压根没创建/已销毁)。沿用 AtomicBool 一次性门,避免 ~30Hz 刷屏。 - lib.rs position_capsule_bottom_center(Windows):定位只夹了上边(.max(mon.top)), 多显示器/负坐标/异常 DPI 下可能算到屏幕外却无观测,补显示器几何 + 最终落点 debug log。 - Capsule.tsx:webview 收到第一个 capsule:state 事件时打一次 console.info,区分 「后端没 emit」与「emit 了但窗口没显示/没渲染」。 注:Windows-cfg 部分本机无法交叉编译验证(ring 的 C 代码需 MSVC),依赖 Windows CI。 --- openless-all/app/src-tauri/src/coordinator.rs | 14 ++++++++++++++ openless-all/app/src-tauri/src/lib.rs | 10 +++++++++- openless-all/app/src/components/Capsule.tsx | 9 +++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/openless-all/app/src-tauri/src/coordinator.rs b/openless-all/app/src-tauri/src/coordinator.rs index 72ce6dce..3dad786a 100644 --- a/openless-all/app/src-tauri/src/coordinator.rs +++ b/openless-all/app/src-tauri/src/coordinator.rs @@ -100,6 +100,8 @@ fn capsule_show_strategy_for_platform() -> CapsuleShowStrategy { static CAPSULE_NO_ACTIVATE_FALLBACK_WARNED: AtomicBool = AtomicBool::new(false); static CAPSULE_SUPPRESSED_BY_TOGGLE_LOGGED: AtomicBool = AtomicBool::new(false); static CAPSULE_FIRST_SHOW_LOGGED: AtomicBool = AtomicBool::new(false); +// #470 诊断 v2:capsule webview 句柄取不到时的一次性门,区分「窗口压根没创建」(A0)。 +static CAPSULE_WINDOW_MISSING_LOGGED: AtomicBool = AtomicBool::new(false); /// 给 #470 诊断日志用的 capsule 状态短名。显式枚举每个变体到 &'static str, /// 不走 `Debug` —— 哪天 CapsuleState 加了 `String` 字段,`:?` 会把 ASR / polish @@ -4989,9 +4991,13 @@ fn show_capsule_window_no_activate( }; let Ok(handle) = window.window_handle() else { + // #470 诊断 v2:Win32 show 路径最可能的暗点之一。此前静默 return, + // 无法观测「胶囊完全不显示」是否卡在这里。 + log::warn!("[capsule] no_activate failed: window_handle() unavailable — Win32 show skipped"); return false; }; let RawWindowHandle::Win32(raw) = handle.as_raw() else { + log::warn!("[capsule] no_activate failed: non-Win32 RawWindowHandle — Win32 show skipped"); return false; }; let hwnd = HWND(raw.hwnd.get() as *mut _); @@ -5226,6 +5232,14 @@ fn emit_capsule( let app_for_main = app.clone(); let _ = app.run_on_main_thread(move || { let Some(window) = app_for_main.get_webview_window("capsule") else { + // #470 诊断 v2:比 A/B/C 更靠前的暗点 A0 —— capsule webview 句柄取不到 + // (窗口未创建/已销毁)。此前静默 return,无法观测。一次性 warn。 + if !CAPSULE_WINDOW_MISSING_LOGGED.swap(true, Ordering::SeqCst) { + log::warn!( + "[capsule] capsule webview window not found — emit_capsule show path skipped (state={})", + capsule_state_log_name(state) + ); + } return; }; let show_capsule = inner_for_main.prefs.get().show_capsule; diff --git a/openless-all/app/src-tauri/src/lib.rs b/openless-all/app/src-tauri/src/lib.rs index 076e6c8e..4b65b3d8 100644 --- a/openless-all/app/src-tauri/src/lib.rs +++ b/openless-all/app/src-tauri/src/lib.rs @@ -1376,7 +1376,15 @@ pub(crate) fn position_capsule_bottom_center( let offset_from_bottom = (capsule_visual_height(translation_active) + 80.0 + bounds.bottom_inset) * scale; let y = ((mon.bottom as f64) - offset_from_bottom).round() as i32; - window.set_position(PhysicalPosition::new(x, y.max(mon.top)))?; + let clamped_y = y.max(mon.top); + // #470 诊断 v2:当前只夹了上边(.max(mon.top)),未夹下/左/右。多显示器、 + // 负坐标或异常 DPI 下胶囊可能被算到屏幕外却无任何观测。记录显示器几何与 + // 最终落点,用于证伪/证实「胶囊定位到屏幕外」(C 子嫌疑)。 + log::debug!( + "[capsule] win position: mon=({},{})..({},{}) scale={:.2} size=({}x{}) -> x={} y={} clamped_y={}", + mon.left, mon.top, mon.right, mon.bottom, scale, phys_w, phys_h, x, y, clamped_y + ); + window.set_position(PhysicalPosition::new(x, clamped_y))?; return Ok(()); } // 仅当 Win32 取不到前台显示器时,落回下面的 current_monitor 逻辑。 diff --git a/openless-all/app/src/components/Capsule.tsx b/openless-all/app/src/components/Capsule.tsx index e7ddb5d3..48065a57 100644 --- a/openless-all/app/src/components/Capsule.tsx +++ b/openless-all/app/src/components/Capsule.tsx @@ -286,6 +286,9 @@ function Pill({ os, state, level, insertedChars, message, onCancel, onConfirm }: // 动画结束就 unmount → 用户看到半截动画被截断。 // v1.3.1-6: 从 240ms 加到 360ms 让用户看清退出动画(240ms 太快感知不到)。 const EXIT_ANIM_MS = 360; +// #470 诊断 v2:模块级一次性门,只在 webview 收到第一个 capsule:state 事件时打 log。 +let capsuleStateFirstLogged = false; + // 初始可见 state:Tauri 内运行从 idle 开始(等后端 capsule:state 事件), // 浏览器 dev 模式从 recording 开始以便直接看到胶囊。 const INITIAL_VISIBLE_STATE: CapsuleState = isTauri ? 'idle' : 'recording'; @@ -321,6 +324,12 @@ export function Capsule() { const { listen } = await import('@tauri-apps/api/event'); const handle = await listen('capsule:state', event => { const p = event.payload; + if (!capsuleStateFirstLogged) { + capsuleStateFirstLogged = true; + // #470 诊断 v2:确认 capsule webview 确实收到了后端事件 —— 区分「后端没 + // emit」与「emit 了但窗口没显示/没渲染」。配合后端 [capsule] 日志定位根因。 + console.info('[capsule] first capsule:state received in webview, state=', p.state); + } setState(p.state); setLevel(p.level ?? 0); setMessage(p.message ?? undefined);