跳到內容

搞定 macOS 語音懸浮窗:Wake-word 與 Push-to-talk 的完美協作

你有遇過開發語音功能時,懸浮窗狀態亂跳的情況嗎?特別是當 wake-word 觸發到一半,使用者又突然按住 push-to-talk 快捷鍵,處理不好的話文字就會遺失、重複發送,甚至讓 UI 進入無法預測的狀態。

要讓語音懸浮窗在 macOS 上保持穩定,你需要一套清晰的生命週期管理機制,確保不同觸發來源能無縫接軌,而不是互相打架。

  • macOS App 專案程式碼
  • SwiftUI 框架(用於 UI 綁定)
  • bot.molt 子系統的日誌存取權限

為了確保語音懸浮窗行為可預測,我們採用了「Token 驗證」與「Session 繼承」的機制。當 push-to-talk (PTT) 在 wake-word 懸浮窗顯示時觸發,PTT 會直接繼承現有文字,而不是重置它。

  • Session 繼承:如果懸浮窗已由 wake-word 開啟,使用者按下快捷鍵時,PTT 會接管該 session 並保留文字。
  • 發送規則:Wake-word 在偵測到靜音時自動發送;PTT 則是在放開快捷鍵時立即發送。
  • Token 驗證:每個 capture session 都帶有一個唯一的 token,所有非當前 token 的 callback 都會被捨棄,避免過期的回傳干擾 UI。

這是管理語音狀態的核心 Actor,確保同一時間只有一個 active session:

  • 提供 beginWakeCapture 與 beginPushToTalk API。
  • 透過 token 過濾 updatePartial、endCapture 等過期呼叫。
  • 在 PTT 結束後套用短暫的 cooldown,防止 wake-word 立即重新觸發。

不要讓 View 直接修改全域狀態,而是透過 VoiceSessionPublisher 進行單向綁定:

  • VoiceWakeOverlayView 只根據 publisher 的內容進行渲染。
  • 使用者的 UI 操作(如 sendNow、dismiss)必須帶著當前的 session token 回傳給 coordinator。

在 endCapture 時,系統會判斷:

  • 如果修剪後的文字為空,則直接 dismiss。
  • 如果有文字,則執行 performSend,播放發送音效並關閉懸浮窗。

如果你發現懸浮窗卡住或行為異常,可以使用以下命令即時監控日誌:

Terminal window
sudo log stream --predicate 'subsystem == "bot.molt" AND category CONTAINS "voicewake"' --level info --style compact

常見問題排查:

  • 懸浮窗沒關閉:檢查 PTT 放開時是否正確呼叫 endCapture 並帶上正確的 token。
  • 文字被覆蓋:確認 PTT 觸發時是否有檢查現有的 VoiceSession 並將其文字作為 prefix 繼承。
  • 過期回傳:如果看到 partial 更新發生在 session 結束後,代表 coordinator 的 token 過濾機制失效。

想要更進一步自動化你的開發流程?試試 AI Setup Assistant。

  • VoiceWakeRuntime 重構:將原本直接操作 UI 的邏輯改為呼叫 VoiceSessionCoordinator。
  • 整合測試:針對 session 繼承、cooldown 機制與空文字關閉進行測試。
OpenClaw

OpenClaw Expert

還是卡住了?

如果這篇文件沒有解決你的情境,直接問 OpenClaw Expert,拿到可執行步驟。