Files
openless/openless-all/scripts
93c6f6f448 fix(hotkey): 修饰键热键被当组合键用时不再误唤起听写(150ms 仲裁 + 即时撤销,胶囊不再赖着) (#858)
* fix(hotkey): 修饰键热键被当组合键用时不再唤起听写

modifier-only 触发键(如 Option)按住期间只要按下普通键,就说明用户在打
Option+任意字母/数字键这类组合键、不是想说话。此前 tap/hook 只看修饰键自身的边
沿:按下即 Pressed → 开录音,Auto 模式下快速松手还会被判成「短按锁存」,录音一直
开着停不下来。

- hotkey.rs:macOS CGEventTap / Windows WH_KEYBOARD_LL 在触发键按住期间收到普通
  键 KEY_DOWN 时发一次新的 TriggerCombined 边沿(每次按住只发一次;修饰键叠加不
  算,Shift 翻译修饰键行为不变),之后仍照常发 Released。
- coordinator:TriggerCombined 清掉按住态(随后的 Released 被 was_held 吞掉,不
  再走 Hold 松手 / Auto 短按锁存),并只取消「这一次按下开出来的」会话 ——
  toggle 停止 / 被冷却拦下 / 路由给 QA 的按下什么都不动,绝不误杀正在转写的上一
  条。同时清掉冷却与防抖时间戳,紧接着那次真想说话的按下不会被静默吞掉。
- Less Computer 的 modifier 触发键走同一条撤销路径(顺带熄灭整屏描边)。

* feat(hotkey): 修饰键热键加 150ms 组合键仲裁窗口——组合键不再闪一下胶囊

上一提交是「事后撤销」:Option+任意字母/数字键仍会真的开麦、弹胶囊、烧一次 ASR
建连,然后被取消——功能对了,观感上闪一下。

改为按下后先等 150ms 再开会话,期间监听器若已报告叠加了普通键,这次按下整条作废:
麦克风不开、胶囊不弹。等待只加在 modifier-only 触发键的「开录」分支上:

- 自定义组合键(Cmd+Shift+D)本身没有歧义,不等;
- toggle 停止 / QA 路由 / 防抖与冷却判定都在窗口之前,时序不变(尤其不挤占
  is_queued_chain_press 的 120ms 排队接力窗口);
- 窗口没盖住的慢速组合键(按住 Option 半秒再按 Tab)仍由 TriggerCombined 事后撤销
  兜底,两条路径互补。

150ms 覆盖绝大多数组合键的「修饰键→普通键」间隔,又低于人从按键到开口的反应时间,
不吃首字。

* fix(hotkey): 补上移动端 hotkey stub 的 TriggerCombined / trigger_combined_since_press

Android CI(cargo check --target android)红:`mobile_stubs/hotkey.rs` 是移动端的
HotkeyEvent / HotkeyMonitor 替身,桌面端新增的枚举变体与方法必须同步补上,否则
coordinator 里那两条 match 臂和仲裁窗口的调用在移动端找不到符号。

移动端没有键盘监听器,`trigger_combined_since_press` 恒为 false(仲裁窗口等价于
直接放行)。

* fix(hotkey): 组合键撤销改走独立通道——按下第二个键的那一帧胶囊就消失

150ms 仲裁窗口只盖住了「修饰键→普通键」间隔够快的组合键;慢一点的(实测日志里
press→combined 中位数 428ms)仍然走事后撤销兜底,本该只是「闪一下」,实际却晚了
几百毫秒才收——观感上像录音真的起来了。

原因不是手速,是排队:TriggerCombined 和 Pressed/Released 挤同一条 channel,而
bridge 为修 #468/#475 的 latch 竞态改成了串行 block_on —— 这次按下自己的
begin_session(开麦 + ASR 握手)正卡在 bridge 线程上,撤销只能在队列里等它跑完。
日志里那个 428ms 因此是上界,真实键间隔被 begin_session 的耗时撑大了。

修法同 #853(Esc 取消):把撤销从 HotkeyEvent 枚举拆出,tap/hook 回调改发独立的
Sender<()>,由专用 combo-abort-bridge 线程消费。macOS CGEventTap 与 Windows
WH_KEYBOARD_LL 同步修改;Less Computer 的修饰键 monitor 走同一条通道,只是 handler
换成 cancel_less_computer_press。

并发面两处:
- 撤销与 Released 之间不再有先后保证。改为只看 hotkey_press_began_session
  (每个 Pressed 边沿都会重置)判断要不要撤销,不再看 hotkey_trigger_held ——
  否则 Released 抢先跑完清了按住态,撤销会认不出这条会话是自己开的、放着不管,
  Auto 模式下就是一条停不下来的录音。清 hotkey_trigger_held 只为吞掉后面的
  Released,与撤销与否无关。加了回归测试覆盖这个倒序。
- 撤销落在 begin_session 还在 await 的中途,由既有的
  startup_race_status_for_starting / CancelRaced 检查点接住(audit HIGH #1),
  与 Esc 取消同一条路径。

至此两条路径分工:仲裁窗口内的快组合键胶囊根本不出现;窗口没盖住的慢组合键胶囊
冒个头,按下第二个键即刻消失。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(hotkey): 取消先收胶囊再拆麦克风 + watchdog 碎觉——撤销不再拖着胶囊一秒

上一提交把撤销决策做到了「按下第二个键的那一帧」(tap → handle_trigger_combined
实测 0.14ms),但真机日志显示胶囊仍然晚 0.8~1 秒才消失。11 次组合键测试分成干净的
两组:这次按下已经把 recorder 开起来的 5 次是 769/900/931/965/986ms,还没开起来的
6 次是 0ms。

链路:cancel_session 先 stop_recorder_for_session 才 emit_capsule。
Recorder::stop() 要 join 音频线程,音频线程退出前要 join liveness watchdog,
watchdog 睡在 1000ms 的检查间隔里 —— 所以停采要等它当前那一觉睡完,胶囊排在后面。
按 Esc 取消走同一个函数,同样拖一秒。

两处一起改(缺一不可,见下):

- coordinator/dictation.rs:cancel_session 把 UI 收尾(finish_cancel_session_state
  → emit_capsule → schedule_capsule_idle → 熄描边)整块提到三个拆资源调用之前。
  emit_capsule 仍排在 finish_cancel_session_state 之后 —— 它要读 state.voice_agent /
  phase 拼 payload,再提前会发出「还在进行中」的那一帧。拆资源都按 session_id 取,
  不依赖 phase,挪到后面语义不变。
- recorder.rs:watchdog 的一个检查间隔切成 50ms 的碎觉来睡,每觉醒来重看 stop_flag。
  停采的等待从最坏 1000ms 降到 50ms。判据(回调静默 3s / 首帧 5s)用的是真实时间差
  而非醒来次数,灵敏度不变;代价是录音期间线程多醒几次,醒来只读一个时间戳。

为什么必须成对:UI 先收之后,胶囊消失到麦克风真正释放之间有一段窗口,其间旧
recorder 还占着设备。`Recorder` 没有 Drop 停采,recorder 槽被下一条会话覆盖后旧音频
线程会继续跑、抓着麦克风不放。只做第一处会把这段窗口留在 ~1 秒,紧接着那次真想说话
的按下(最快也要 250ms 防抖 + 150ms 仲裁)就可能撞上;watchdog 切碎后窗口降到几十
毫秒,够不着。

副作用:正常说完话松手结束(end_session 也走 Recorder::stop())同样少等将近一秒。
可观察的代价:胶囊消失后系统菜单栏的录音小圆点会多亮几十毫秒。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(hotkey): resolve modifier combo abort after beta merge

* test(hotkey): initialize mac companion press id

* fix(hotkey): clone Linux fcitx combo channel

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Chris233 <h-chris233@outlook.com>
2026-08-01 18:01:11 +08:00
..