From d33282a000d4610faae1e5a0121c144918f2a5d5 Mon Sep 17 00:00:00 2001 From: Ivan Hanloth Date: Sun, 26 Jul 2026 15:06:54 +0100 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=B7=B2=E9=9A=90?= =?UTF-8?q?=E8=97=8F=E7=9A=84=E7=AA=97=E5=8F=A3=E4=BC=9A=E8=A2=AB=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/core/src/agent.rs | 11 ++- crates/core/src/effects_worker.rs | 5 +- crates/core/src/hide.rs | 108 +++++++++++++++++++-- crates/core/src/i18n.rs | 16 ++- crates/core/src/logging.rs | 5 +- crates/core/src/recovery.rs | 5 +- crates/core/src/win_event.rs | 14 ++- crates/core/tests/agent_window_tracking.rs | 5 +- docs/en/guide/faq.md | 4 + docs/guide/faq.md | 4 + docs/zh-tw/guide/faq.md | 4 + 11 files changed, 151 insertions(+), 30 deletions(-) diff --git a/crates/core/src/agent.rs b/crates/core/src/agent.rs index 072caaa..f847229 100644 --- a/crates/core/src/agent.rs +++ b/crates/core/src/agent.rs @@ -516,8 +516,10 @@ impl AgentState { (Response::Ok, false) } Command::AdoptWindows { hwnds } => { - let targets: Vec = - hwnds.iter().map(|&h| crate::hide::Target::bare(h, 0)).collect(); + let targets: Vec = hwnds + .iter() + .map(|&h| crate::hide::Target::bare(h, 0)) + .collect(); // 恢复工具的手动隐藏不施加副作用,仅隐藏并纳入记录。 let mut setting = self.config.setting.clone(); setting.mute_after_hide = false; @@ -1090,7 +1092,10 @@ pub fn run(options: AgentOptions) { let hwnd = match create_agent_window() { Ok(hwnd) => hwnd, Err(e) => { - log_error!("创建代理窗口失败,核心无法启动: {}", crate::util::win_err(&e)); + log_error!( + "创建代理窗口失败,核心无法启动: {}", + crate::util::win_err(&e) + ); return; } }; diff --git a/crates/core/src/effects_worker.rs b/crates/core/src/effects_worker.rs index cb37d78..df2a98a 100644 --- a/crates/core/src/effects_worker.rs +++ b/crates/core/src/effects_worker.rs @@ -113,7 +113,10 @@ mod tests { impl Effects for Recorder { fn mute(&self, pid: u32, mute: bool) { - self.calls.lock().unwrap().push(format!("mute:{pid}:{mute}")); + self.calls + .lock() + .unwrap() + .push(format!("mute:{pid}:{mute}")); } fn suspend(&self, pid: u32, _enhanced: bool) { self.calls.lock().unwrap().push(format!("suspend:{pid}")); diff --git a/crates/core/src/hide.rs b/crates/core/src/hide.rs index 7c47b5b..fd5eba3 100644 --- a/crates/core/src/hide.rs +++ b/crates/core/src/hide.rs @@ -191,7 +191,7 @@ pub fn expand_descendants(roots: &[u32], edges: &[(u32, u32)]) -> Vec { /// [`HideController::commit_hide`] 原样执行,两段之间由调用方落盘意图。 #[derive(Debug, Clone, PartialEq, Eq)] pub struct HidePlan { - /// 本次新增的隐藏目标(已剔除死句柄 / 已隐藏项 / 查不到 PID 的项)。 + /// 本次新增的隐藏目标(已剔除死句柄 / 不可见项 / 已隐藏项 / 查不到 PID 的项)。 pub fresh: Vec, /// 本次新增的静音进程。 pub mute: Vec, @@ -310,8 +310,10 @@ impl HideController { /// 与 PID 补查(仍查不到的目标剔除)。`freeze_pids` 为最终要冻结的 PID 集 /// (可能已含子进程树),仅在 `freeze_after_hide` 开启时生效。 /// - /// 隐藏是累加的,`show` 时一并恢复。已在隐藏 / 静音 / 冻结集内的目标会被 - /// 跳过——挂起是计数式的,重复施加会让解冻次数对不上。 + /// 只有「由本程序从可见变为不可见」的窗口才进入隐藏集,恢复即逆转这次 + /// 改变;本来就不可见的目标不入集。隐藏是累加的,`show` 时一并恢复。 + /// 已在隐藏 / 静音 / 冻结集内的目标会被跳过——挂起是计数式的,重复施加 + /// 会让解冻次数对不上。 pub fn plan_hide( &mut self, setting: &Setting, @@ -323,9 +325,12 @@ impl HideController { let mut fresh: Vec = Vec::new(); for t in targets { + // 只隐藏当前可见的窗口:本来就不可见的目标(如程序自行藏到托盘, + // Steam 的关闭按钮即是)不入集,恢复时也就不会被错误地弹出来。 if known.contains(&t.hwnd) || fresh.iter().any(|f| f.hwnd == t.hwnd) || !self.wm.is_window(t.hwnd) + || !self.wm.is_visible(t.hwnd) { continue; } @@ -992,7 +997,14 @@ mod tests { assert_eq!(*controller.effects.pauses.borrow(), 1, "应发送一次暂停键"); let outcome = controller.show(); - assert_eq!(outcome, ShowOutcome { shown: 1, stale: 0, refound: 0 }); + assert_eq!( + outcome, + ShowOutcome { + shown: 1, + stale: 0, + refound: 0 + } + ); assert!(!controller.is_hidden()); assert!(controller.wm.is_visible(10), "恢复后微信应可见"); assert_eq!(*controller.effects.resumes.borrow(), vec![10], "应解冻"); @@ -1212,7 +1224,14 @@ mod tests { ..Default::default() }); - assert_eq!(outcome, ShowOutcome { shown: 1, stale: 0, refound: 0 }); + assert_eq!( + outcome, + ShowOutcome { + shown: 1, + stale: 0, + refound: 0 + } + ); assert!(!controller.is_hidden(), "恢复完成后应回到未隐藏状态"); assert!(controller.wm.is_visible(10), "崩溃前隐藏的窗口应被找回"); assert_eq!(*controller.effects.resumes.borrow(), vec![10], "应解冻进程"); @@ -1280,7 +1299,11 @@ mod tests { let outcome = controller.show(); assert_eq!( outcome, - ShowOutcome { shown: 1, stale: 2, refound: 0 }, + ShowOutcome { + shown: 1, + stale: 2, + refound: 0 + }, "死句柄与被复用句柄都应跳过" ); assert!(!controller.wm.is_visible(20), "被复用的句柄不得被弹出来"); @@ -1354,11 +1377,23 @@ mod tests { }; let wm = MockWm::new(vec![win("微信", 10, "WeChat.exe", "C:\\WeChat.exe")], 0); let mut controller = HideController::new(wm, MockEffects::default()); - controller.apply_hide(&setting, &[Target::from_window(&win("微信", 10, "WeChat.exe", "C:\\WeChat.exe"))], &[]); + controller.apply_hide( + &setting, + &[Target::from_window(&win( + "微信", + 10, + "WeChat.exe", + "C:\\WeChat.exe", + ))], + &[], + ); assert!(controller.tracks_window(10)); assert!(controller.update_title(10, "微信 - 新会话")); - assert!(!controller.update_title(10, "微信 - 新会话"), "标题未变不算更新"); + assert!( + !controller.update_title(10, "微信 - 新会话"), + "标题未变不算更新" + ); assert!( !controller.update_title(10, bosskey_common::NO_TITLE), "NO_TITLE 不参与同步" @@ -1378,8 +1413,14 @@ mod tests { ]; assert!(sync_rule_titles(&mut rules, 10, "新标题")); assert_eq!(rules[0].title, "新标题"); - assert!(!sync_rule_titles(&mut rules, 10, "新标题"), "标题未变不算更新"); - assert!(!sync_rule_titles(&mut rules, 99, "别的"), "句柄不匹配不更新"); + assert!( + !sync_rule_titles(&mut rules, 10, "新标题"), + "标题未变不算更新" + ); + assert!( + !sync_rule_titles(&mut rules, 99, "别的"), + "句柄不匹配不更新" + ); assert!( !sync_rule_titles(&mut rules, 10, NO_TITLE), "NO_TITLE 不参与同步" @@ -1452,6 +1493,53 @@ mod tests { assert!(controller.wm.is_visible(99), "可见窗口不受影响"); } + #[test] + fn already_invisible_windows_are_not_recorded_or_restored() { + let setting = Setting { + hide_current: false, + mute_after_hide: true, + ..Setting::default() + }; + // Steam 用关闭按钮把自己藏了起来(窗口仍存在但不可见),记事本可见; + // 两者都被规则命中。 + let wm = MockWm::new( + vec![ + win_pid("Steam", 10, "steam.exe", 500, "C:\\steam.exe"), + win_pid("记事本", 20, "notepad.exe", 600, "C:\\notepad.exe"), + ], + 0, + ); + wm.hide(10); + let mut controller = HideController::new(wm, MockEffects::default()); + + controller.apply_hide( + &setting, + &[Target::bare(10, 500), Target::bare(20, 600)], + &[], + ); + assert_eq!(controller.hidden_count(), 1, "已不可见的窗口不应入集"); + assert_eq!( + *controller.effects.mutes.borrow(), + vec![(600, true)], + "只对真正被隐藏的窗口施加副作用" + ); + + let outcome = controller.show(); + assert_eq!( + outcome, + ShowOutcome { + shown: 1, + stale: 0, + refound: 0 + } + ); + assert!(controller.wm.is_visible(20), "记事本应恢复"); + assert!( + !controller.wm.is_visible(10), + "Steam 自己藏起来的窗口不得被恢复弹出" + ); + } + #[test] fn release_windows_frees_whole_process_and_shows_unknown_handles() { let setting = Setting { diff --git a/crates/core/src/i18n.rs b/crates/core/src/i18n.rs index 235c500..f602e41 100644 --- a/crates/core/src/i18n.rs +++ b/crates/core/src/i18n.rs @@ -85,7 +85,9 @@ impl Msg { Msg::ErrCoreExited => "核心已退出", Msg::ErrNotifyCore => "无法通知核心", Msg::ErrCoreTimeout => "核心响应超时", - Msg::ErrCoreExeMissing => "未找到核心程序 {exe}。它很可能被杀毒软件拦截或隔离了:请尝试将 Boss Key 的程序目录加入杀毒软件的白名单 / 信任区,再从隔离区恢复该文件;若无法恢复,请重新下载完整程序包。", + Msg::ErrCoreExeMissing => { + "未找到核心程序 {exe}。它很可能被杀毒软件拦截或隔离了:请尝试将 Boss Key 的程序目录加入杀毒软件的白名单 / 信任区,再从隔离区恢复该文件;若无法恢复,请重新下载完整程序包。" + } Msg::ErrFreezePartial => "{failed}/{total} 个进程冻结失败", Msg::ErrResumePartial => "{failed}/{total} 个进程解冻失败", Msg::ErrUrlSchemeNotAllowed => "只允许打开 http/https/mailto 链接", @@ -106,7 +108,9 @@ impl Msg { Msg::HiddenBody => "Windows hidden", Msg::ShownBody => "Windows restored", Msg::ConfigExeMissing => "Settings app not found", - Msg::RecoveryPersistFailedBody => "Cannot write the crash-recovery file; windows cannot be restored automatically after an abnormal exit", + Msg::RecoveryPersistFailedBody => { + "Cannot write the crash-recovery file; windows cannot be restored automatically after an abnormal exit" + } Msg::AutostartOffTitle => "Startup disabled", Msg::AutostartOffBody => "Boss Key will no longer start with Windows", Msg::AutostartOnTitle => "Startup enabled", @@ -123,7 +127,9 @@ impl Msg { Msg::ErrCoreExited => "The core has exited", Msg::ErrNotifyCore => "Cannot reach the core", Msg::ErrCoreTimeout => "The core did not respond in time", - Msg::ErrCoreExeMissing => "Core program {exe} not found. It was most likely blocked or quarantined by antivirus software: add the Boss Key program folder to your antivirus allowlist, then restore the file from quarantine; if that is not possible, download the full package again.", + Msg::ErrCoreExeMissing => { + "Core program {exe} not found. It was most likely blocked or quarantined by antivirus software: add the Boss Key program folder to your antivirus allowlist, then restore the file from quarantine; if that is not possible, download the full package again." + } Msg::ErrFreezePartial => "Failed to freeze {failed} of {total} processes", Msg::ErrResumePartial => "Failed to resume {failed} of {total} processes", Msg::ErrUrlSchemeNotAllowed => "Only http/https/mailto links may be opened", @@ -161,7 +167,9 @@ impl Msg { Msg::ErrCoreExited => "核心已結束", Msg::ErrNotifyCore => "無法通知核心", Msg::ErrCoreTimeout => "核心回應逾時", - Msg::ErrCoreExeMissing => "找不到核心程式 {exe}。它很可能被防毒軟體攔截或隔離了:請將 Boss Key 的程式資料夾加入防毒軟體的信任區/白名單,再從隔離區還原該檔案;若無法還原,請重新下載完整程式包。", + Msg::ErrCoreExeMissing => { + "找不到核心程式 {exe}。它很可能被防毒軟體攔截或隔離了:請將 Boss Key 的程式資料夾加入防毒軟體的信任區/白名單,再從隔離區還原該檔案;若無法還原,請重新下載完整程式包。" + } Msg::ErrFreezePartial => "{failed}/{total} 個程序凍結失敗", Msg::ErrResumePartial => "{failed}/{total} 個程序解除凍結失敗", Msg::ErrUrlSchemeNotAllowed => "僅允許開啟 http/https/mailto 連結", diff --git a/crates/core/src/logging.rs b/crates/core/src/logging.rs index bbcc9d9..77ec632 100644 --- a/crates/core/src/logging.rs +++ b/crates/core/src/logging.rs @@ -367,7 +367,10 @@ mod tests { let dir = temp_dir(); let logger = Logger::new(dir.clone(), 7); // 直接验证格式化结果(warn_at 走全局 logger,测试里用本地实例复刻其格式)。 - logger.log(Level::Warn, &format!("{} ({}:{})", "落盘失败", "agent.rs", 42)); + logger.log( + Level::Warn, + &format!("{} ({}:{})", "落盘失败", "agent.rs", 42), + ); let logs: Vec<_> = fs::read_dir(&dir).unwrap().flatten().collect(); let content = fs::read_to_string(logs[0].path()).unwrap(); assert!(content.contains("[WARN] 落盘失败 (agent.rs:42)")); diff --git a/crates/core/src/recovery.rs b/crates/core/src/recovery.rs index 7bf19a3..a0934b3 100644 --- a/crates/core/src/recovery.rs +++ b/crates/core/src/recovery.rs @@ -188,10 +188,7 @@ mod tests { std::fs::write(&path, "{ not valid json !!").unwrap(); assert_eq!(load(&path), None, "损坏文件应按无快照处理而非 panic"); assert!(!path.exists(), "损坏文件不应留在原名"); - assert!( - corrupt_path(&path).exists(), - "损坏文件应改名保留现场供排查" - ); + assert!(corrupt_path(&path).exists(), "损坏文件应改名保留现场供排查"); } #[test] diff --git a/crates/core/src/win_event.rs b/crates/core/src/win_event.rs index 7731e68..feaddf8 100644 --- a/crates/core/src/win_event.rs +++ b/crates/core/src/win_event.rs @@ -93,7 +93,9 @@ impl Drop for WinEventHook { #[cfg(test)] mod tests { use super::*; - use windows::Win32::UI::WindowsAndMessaging::{EVENT_OBJECT_FOCUS, EVENT_OBJECT_HIDE, OBJID_CURSOR}; + use windows::Win32::UI::WindowsAndMessaging::{ + EVENT_OBJECT_FOCUS, EVENT_OBJECT_HIDE, OBJID_CURSOR, + }; #[test] fn only_toplevel_destroy_show_namechange_are_relevant() { @@ -103,8 +105,14 @@ mod tests { assert!(relevant(EVENT_OBJECT_SHOW, obj, child)); assert!(relevant(EVENT_OBJECT_NAMECHANGE, obj, child)); assert!(!relevant(EVENT_OBJECT_HIDE, obj, child), "隐藏事件不用追踪"); - assert!(!relevant(EVENT_OBJECT_FOCUS, obj, child), "区间内的无关事件应过滤"); - assert!(!relevant(EVENT_OBJECT_DESTROY, OBJID_CURSOR.0, child), "非窗口对象应过滤"); + assert!( + !relevant(EVENT_OBJECT_FOCUS, obj, child), + "区间内的无关事件应过滤" + ); + assert!( + !relevant(EVENT_OBJECT_DESTROY, OBJID_CURSOR.0, child), + "非窗口对象应过滤" + ); assert!(!relevant(EVENT_OBJECT_DESTROY, obj, 3), "子对象事件应过滤"); } } diff --git a/crates/core/tests/agent_window_tracking.rs b/crates/core/tests/agent_window_tracking.rs index d662503..07c18ae 100644 --- a/crates/core/tests/agent_window_tracking.rs +++ b/crates/core/tests/agent_window_tracking.rs @@ -96,10 +96,7 @@ fn destroying_a_hidden_window_clears_the_record_in_real_time() { ); std::thread::sleep(Duration::from_millis(50)); } - assert!( - !recovery_path.exists(), - "记录清空后恢复文件应同步清除" - ); + assert!(!recovery_path.exists(), "记录清空后恢复文件应同步清除"); let quit = client.send(&Command::Quit).unwrap(); assert_eq!(quit, Response::Ok); diff --git a/docs/en/guide/faq.md b/docs/en/guide/faq.md index fe67efe..10c9526 100644 --- a/docs/en/guide/faq.md +++ b/docs/en/guide/faq.md @@ -17,6 +17,10 @@ Some stripped-down or older Windows builds (Windows 7 and earlier) may not inclu A settings window that will not open **does not affect the core's hiding features** — the core is a fully native program and does not depend on WebView2. You can still hide windows with your configured hotkeys. ::: +## Will restoring pop up programs I "closed" to the tray? + +No. Hiding only records windows that were **visible at the time**, and restoring only reverses Boss Key's own hiding; windows an app hid by itself (Steam's close button, for example, merely hides its window) are untouched and will not be shown on restore. + ## Antivirus flags or blocks Boss Key — what now? Boss Key listens for global hotkeys and hides windows, behaviour that antivirus software sometimes misreads. v3 uses a native single-file Rust implementation, which considerably reduces false positives. If it is still blocked: diff --git a/docs/guide/faq.md b/docs/guide/faq.md index 4a9986f..4f6aeab 100644 --- a/docs/guide/faq.md +++ b/docs/guide/faq.md @@ -17,6 +17,10 @@ v3 的配置程序(`config.exe`)使用 Tauri 编写,其运行依赖系统 配置界面无法打开**不影响核心的隐藏功能**——核心是纯原生程序,不依赖 WebView2。你仍可用已配置好的热键正常隐藏窗口。 ::: +## 恢复时会把已"关闭"到托盘的程序弹出来吗? + +不会。隐藏时只记录**当时可见**的窗口,恢复只逆转 Boss Key 自己的隐藏动作;程序自行藏到托盘的窗口(如 Steam 的关闭按钮只是隐藏窗口)不受影响,恢复时不会被弹出。 + ## 杀毒软件报毒 / 拦截怎么办? Boss Key 会监听全局热键、隐藏窗口,这类行为有时会被杀软误判。v3 已改用 Rust 原生单文件实现,显著降低了误报概率。若仍被拦截: diff --git a/docs/zh-tw/guide/faq.md b/docs/zh-tw/guide/faq.md index 0306cab..0fb1a5b 100644 --- a/docs/zh-tw/guide/faq.md +++ b/docs/zh-tw/guide/faq.md @@ -17,6 +17,10 @@ v3 的設定程式(`config.exe`)使用 Tauri 撰寫,其執行相依於系 設定介面無法開啟**不影響核心的隱藏功能**——核心是純原生程式,不相依於 WebView2。您仍可用已設定好的快速鍵正常隱藏視窗。 ::: +## 復原時會把已「關閉」到通知區域的程式彈出來嗎? + +不會。隱藏時只記錄**當時可見**的視窗,復原只逆轉 Boss Key 自己的隱藏動作;程式自行藏到通知區域的視窗(如 Steam 的關閉按鈕只是隱藏視窗)不受影響,復原時不會被彈出。 + ## 防毒軟體誤判/攔截怎麼辦? Boss Key 會監聽全域快速鍵、隱藏視窗,這類行為有時會被防毒軟體誤判。v3 已改用 Rust 原生單一檔案實作,顯著降低了誤判機率。若仍被攔截: