Skip to content

Commit 30a7502

Browse files
committed
fix(live): 审查即修——全局快捷键注册失败死局与打开漂移
P1:register 失败仅日志——锁定态浮窗不可点+主窗隐藏时全局键是唯一解锁入口,注册失败=死局。修复:register 幂等(is_registered 先查)+ 前置注册,失败中止打开(浮窗不显示/主窗不隐藏/错误可见可重试)。P2:set_always_on_top 失败在 show 之后报错——留下窗口已显示但 ui.open 未置位漂移态;改为失败容忍(仅影响置顶层级)。P3:字幕条锁定态双锁图标冗余合并。文档同步(v0.12.6/ADR-025 风险描述)。
1 parent aa3b7fd commit 30a7502

4 files changed

Lines changed: 33 additions & 20 deletions

File tree

‎app/src-tauri/src/commands_window.rs‎

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -133,21 +133,35 @@ fn show_main_core(app: &tauri::AppHandle) {
133133
}
134134
}
135135

136-
/// 全局快捷键注册(浮窗打开期间有效——进程级热键冲突面最小化)。
137-
fn register_float_shortcut(app: &tauri::AppHandle) {
138-
if let Err(e) = app.global_shortcut().register(FLOAT_SHORTCUT) {
139-
eprintln!("[capture-float] 全局快捷键注册失败(不影响浮窗使用): {}", e);
136+
/// 全局快捷键注册(幂等 + 可失败——打开浮窗的前置条件)。
137+
///
138+
/// @ai-context: 审查修复(2026-08-23 新增代码审查):原实现失败仅日志——
139+
/// 但锁定态浮窗不可点、主窗已隐藏,全局快捷键是**唯一**解锁入口,
140+
/// 注册失败 = 用户被锁死在无可见窗口(只能任务管理器杀进程)。
141+
/// 改为:先查 is_registered(幂等——show 失败后的重试路径自愈),
142+
/// 失败则返回 Err 中止打开(错误可见、主窗保持可见,可重试)。
143+
fn register_float_shortcut(app: &tauri::AppHandle) -> Result<(), String> {
144+
let global = app.global_shortcut();
145+
if global.is_registered(FLOAT_SHORTCUT) {
146+
return Ok(()); // 已注册(此前 show 失败重试等场景)——幂等放行
140147
}
148+
global
149+
.register(FLOAT_SHORTCUT)
150+
.map_err(|e| format!("全局快捷键 Ctrl+Shift+F 注册失败(可能被系统占用): {}", e))
141151
}
142152

143153
/// 全局快捷键注销(幂等——未注册/重复调用均无副作用)。
144154
fn unregister_float_shortcut(app: &tauri::AppHandle) {
145155
let _ = app.global_shortcut().unregister(FLOAT_SHORTCUT);
146156
}
147157

148-
/// 浮窗打开核心:显示浮窗 + 主窗隐藏(用户要求:浮窗出现后主页面隐藏)。
158+
/// 浮窗打开核心:注册全局快捷键 → 显示浮窗 + 主窗隐藏(用户要求:浮窗出现后主页面隐藏)。
149159
fn float_open_core(app: &tauri::AppHandle) -> Result<(), String> {
150-
// State 绑定到局部变量——app.state() 临时值在语句末释放会导致锁守卫借用失效
160+
// 审查修复(2026-08-23 新增代码审查):① 注册前置——失败时中止打开
161+
// (浮窗不显示、主窗不隐藏、错误可见),杜绝"打开后锁定但无法解锁"死局;
162+
// ② 置顶设置失败降级容忍(仅影响置顶层级,不阻断打开——原实现
163+
// window.show() 成功后才报错,留下"窗口已显示但 ui.open 未置位"漂移态)
164+
register_float_shortcut(app)?;
151165
let state = app.state::<AppState>();
152166
let topmost = state
153167
.float_ui
@@ -159,9 +173,7 @@ fn float_open_core(app: &tauri::AppHandle) -> Result<(), String> {
159173
None => build_float_window(app, false)?,
160174
};
161175
window.show().map_err(|e| format!("显示采集浮窗失败: {}", e))?;
162-
window
163-
.set_always_on_top(topmost)
164-
.map_err(|e| format!("设置浮窗置顶失败: {}", e))?;
176+
let _ = window.set_always_on_top(topmost);
165177
// 浮窗出现 → 主页面隐藏(顺序:先显示浮窗再隐藏主窗,避免无窗口空窗)
166178
if let Some(main) = app.get_webview_window(MAIN_WINDOW_LABEL) {
167179
let _ = main.hide();
@@ -175,8 +187,6 @@ fn float_open_core(app: &tauri::AppHandle) -> Result<(), String> {
175187
ui.open = true;
176188
emit_float_state(app, &ui);
177189
}
178-
// 锁定态浮窗不可点且主窗已隐藏——全局快捷键是唯一解锁/切换入口(ADR-025)
179-
register_float_shortcut(app);
180190
Ok(())
181191
}
182192

‎app/src/components/CaptureFloatPanel.tsx‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -115,14 +115,13 @@ export default function CaptureFloatPanel() {
115115
{lastLine}
116116
</span>
117117
<span style={{ color: "#6b7280", fontVariantNumeric: "tabular-nums", flexShrink: 0 }}>⏱ {fmtTime(elapsedMs)}</span>
118-
{snapshot.locked && (
118+
{snapshot.locked ? (
119119
<span style={{ color: "#b45309", fontWeight: 600, flexShrink: 0 }} title="点击穿透已锁定——按 Ctrl+Shift+F 解锁">
120-
🔒
120+
🔒 已锁定
121121
</span>
122+
) : (
123+
<button style={iconBtn} title="点击穿透锁定" onClick={toggleLocked}>🔓</button>
122124
)}
123-
<button style={iconBtn} title={snapshot.locked ? "已锁定(Ctrl+Shift+F 解锁)" : "点击穿透锁定"} onClick={toggleLocked}>
124-
{snapshot.locked ? "🔒" : "🔓"}
125-
</button>
126125
<button style={iconBtn} title="展开为面板(Esc)" onClick={expandToPanel}>⤢</button>
127126
</div>
128127
);

‎docs/adr/ADR-025-float-global-shortcut.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,9 @@ tauri 2.11.5 / wry 0.55.1 无 `set_cursor_hit_test`(分区点击穿透,源
6666
- 收起/停止后主窗回显是行为变化(主窗不再保持隐藏直至用户自己回显)。
6767

6868
### 风险
69-
- 全局快捷键与宿主系统注册冲突(同键已被占用时注册失败)——注册失败仅日志 + 降级
70-
(浮窗仍可用,锁定后经主窗按钮解锁路径保留:主窗可经 ⌂ 回显后点"解锁浮窗")。
69+
- 全局快捷键与宿主系统注册冲突(同键已被占用时注册失败)——注册失败即**打开浮窗前置
70+
失败**(2026-08-23 审查即修:浮窗不显示、主窗不隐藏、错误可见可重试;杜绝"浮窗已开但
71+
锁定后无法解锁"的死局),未做冲突提示文案(记需求池)。
7172
- 主窗隐藏的行为(浮窗打开即隐藏)需真机验收(含 ⌂ 主窗 → 主窗回显、浮窗保留语义)。
7273

7374
## 合规性验证

‎docs/versions/v0.12.6.md‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,15 @@ ADR-025);浮窗**窗口级 CSS 重置**(body 8px margin + 100vh 溢出 →
7676

7777
## 已知问题与后续
7878

79-
- **全局快捷键冲突**:Ctrl+Shift+F 已被宿主系统占用时注册失败——仅日志降级(浮窗可用,
80-
锁定后经 ⌂ 主窗 → 按钮解锁路径保留);未做热键冲突提示(记需求池)。
79+
- **全局快捷键冲突**:Ctrl+Shift+F 被系统/其他程序占用时注册失败——浮窗打开中止并展示错误
80+
(主窗保持可见、可重试;绝不让用户陷入"浮窗已开但无法解锁"死局——2026-08-23 审查即修)。
81+
冲突提示文案未做(记需求池)。
8182
- **快捷键占用全程**:浮窗打开期间 Ctrl+Shift+F 为进程级热键(不区分焦点窗口)——语义即
8283
"浮窗切换键",界面已明示;若用户反馈冲突面大,可改为仅采集中注册(记需求池)。
8384
- **主窗隐藏无独立指示**:主窗随浮窗打开而隐藏——浮窗内已有 ⌂ 主窗出口;若用户误关浮窗
8485
期望"回到主窗"则主窗已回显(close 必回显,无死角)。
86+
- **多通道并发观察**:浮窗 toggle/按钮快速连按理论可交错(无动作级互斥锁)——各操作幂等 +
87+
事件回流,最终状态一致(登记观察项,按反馈门控)。
8588

8689
## 关联
8790

0 commit comments

Comments
 (0)