语音浮窗生命周期(macOS)

面向 macOS 应用贡献者。目标:当唤醒词和按键说话(push-to-talk)重叠时,保持语音浮窗行为可预测。

当前设计意图

  • 如果浮窗因唤醒词已经显示,此时用户按下快捷键,快捷键会话会_接管_已有文本而非重置。浮窗在按键期间保持显示。松开时:有有效文本就发送,否则关闭。
  • 唤醒词模式仍然在静默后自动发送;按键说话松开后立即发送。

已实现(2025年12月9日)

  • 浮窗会话现在每次捕获都带有一个 token(唤醒词或按键说话)。partial/final/send/dismiss/level 更新在 token 不匹配时被丢弃,避免旧回调干扰。
  • 按键说话会接管浮窗中已有文本作为前缀(所以在唤醒浮窗显示时按下快捷键会保留文本并追加新语音)。松开后最多等待 1.5 秒以获取最终转录,否则回退到当前文本。
  • 音效/浮窗日志以 info 级别记录在 voicewake.overlayvoicewake.pttvoicewake.chime 类别下(会话开始、partial、final、发送、关闭、音效原因)。

后续规划

  1. VoiceSessionCoordinator(actor)
    • 同一时间只持有一个 VoiceSession
    • API(基于 token):beginWakeCapturebeginPushToTalkupdatePartialendCapturecancelapplyCooldown
    • 丢弃携带过期 token 的回调(防止旧识别器重新打开浮窗)。
  2. VoiceSession(model)
    • 字段:tokensource(wakeWord|pushToTalk)、committed/volatile 文本、音效标记、定时器(自动发送、空闲超时)、overlayMode(display|editing|sending)、冷却截止时间。
  3. 浮窗绑定
    • VoiceSessionPublisherObservableObject)将活跃会话映射到 SwiftUI。
    • VoiceWakeOverlayView 仅通过 publisher 渲染;不直接修改全局单例。
    • 浮窗用户操作(sendNowdismissedit)通过会话 token 回调 coordinator。
  4. 统一发送路径
    • endCapture 时:去空白后文本为空 → 关闭;否则 performSend(session:)(播放一次发送音效,转发,关闭)。
    • 按键说话:无延迟;唤醒词:可选自动发送延迟。
    • 按键说话结束后对唤醒运行时施加短暂冷却期,防止唤醒词立即重新触发。
  5. 日志
    • Coordinator 在 subsystem ai.openclaw 下以 .info 级别记录日志,类别为 voicewake.overlayvoicewake.chime
    • 关键事件:session_startedadopted_by_push_to_talkpartialfinalizedsenddismisscancelcooldown

调试清单

  • 重现浮窗卡住时流式查看日志:

    sudo log stream --predicate 'subsystem == "ai.openclaw" AND category CONTAINS "voicewake"' --level info --style compact
  • 确认只有一个活跃会话 token;过期回调应被 coordinator 丢弃。

  • 确保按键说话松开时一定用活跃 token 调用 endCapture;文本为空时预期为无音效无发送的 dismiss

迁移步骤(建议)

  1. 添加 VoiceSessionCoordinatorVoiceSessionVoiceSessionPublisher
  2. 重构 VoiceWakeRuntime:创建/更新/结束会话,而非直接操作 VoiceWakeOverlayController
  3. 重构 VoicePushToTalk:接管现有会话,松开时调用 endCapture;施加运行时冷却。
  4. VoiceWakeOverlayController 接入 publisher;移除运行时/PTT 的直接调用。
  5. 添加会话接管、冷却和空文本关闭的集成测试。