---
read_when:
  - 调整语音浮层行为
summary: 唤醒词与按键说话重叠时的语音浮层生命周期
title: 语音浮层
x-i18n:
  generated_at: "2026-02-01T21:33:26Z"
  model: claude-opus-4-6
  provider: pi
  source_hash: 3be1a60aa7940b2368ff62cd49f04b2b8422876030e8ea206b467f66a5a6bd4d
  source_path: platforms/mac/voice-overlay.md
  workflow: 15
---

# 语音浮层生命周期（macOS）

受众：macOS 应用贡献者。目标：在唤醒词与按键说话重叠时保持语音浮层行为可预测。

### 当前意图

- 如果浮层已因唤醒词显示，此时用户按下热键，热键会话会*接管*现有文本而非重置。浮层在热键按住期间保持显示。用户松开时：如果有去除空白后的文本则发送，否则关闭。
- 单独使用唤醒词时仍在静音后自动发送；按键说话在松开时立即发送。

### 已实现（2025 年 12 月 9 日）

- 浮层会话现在为每次捕获（唤醒词或按键说话）携带一个令牌。当令牌不匹配时，部分/最终/发送/关闭/音量更新会被丢弃，避免过时回调。
- 按键说话会接管任何可见的浮层文本作为前缀（因此在唤醒浮层显示时按下热键会保留文本并追加新语音）。它最多等待 1.5 秒获取最终转录结果，然后回退到当前文本。
- 提示音/浮层日志以 `info` 级别输出，分类为 `voicewake.overlay`、`voicewake.ptt` 和 `voicewake.chime`（会话开始、部分、最终、发送、关闭、提示音原因）。

### 后续步骤

1. **VoiceSessionCoordinator（actor）**
   - 同一时间只拥有一个 `VoiceSession`。
   - API（基于令牌）：`beginWakeCapture`、`beginPushToTalk`、`updatePartial`、`endCapture`、`cancel`、`applyCooldown`。
   - 丢弃携带过时令牌的回调（防止旧识别器重新打开浮层）。
2. **VoiceSession（模型）**
   - 字段：`token`、`source`（wakeWord|pushToTalk）、已提交/临时文本、提示音标志、计时器（自动发送、空闲）、`overlayMode`（display|editing|sending）、冷却截止时间。
3. **浮层绑定**
   - `VoiceSessionPublisher`（`ObservableObject`）将活跃会话镜像到 SwiftUI。
   - `VoiceWakeOverlayView` 仅通过 publisher 渲染；绝不直接修改全局单例。
   - 浮层用户操作（`sendNow`、`dismiss`、`edit`）携带会话令牌回调到 coordinator。
4. **统一发送路径**
   - `endCapture` 时：如果去除空白后文本为空 → 关闭；否则 `performSend(session:)`（播放一次发送提示音、转发、关闭）。
   - 按键说话：无延迟；唤醒词：可选自动发送延迟。
   - 按键说话结束后对唤醒运行时施加短暂冷却，防止唤醒词立即重新触发。
5. **日志**
   - Coordinator 在子系统 `bot.molt`、分类 `voicewake.overlay` 和 `voicewake.chime` 下输出 `.info` 级别日志。
   - 关键事件：`session_started`、`adopted_by_push_to_talk`、`partial`、`finalized`、`send`、`dismiss`、`cancel`、`cooldown`。

### 调试清单

- 复现浮层粘滞问题时流式查看日志：

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

- 验证只有一个活跃会话令牌；过时回调应被 coordinator 丢弃。
- 确保按键说话松开时始终使用活跃令牌调用 `endCapture`；如果文本为空，预期 `dismiss` 且不播放提示音或发送。

### 迁移步骤（建议）

1. 添加 `VoiceSessionCoordinator`、`VoiceSession` 和 `VoiceSessionPublisher`。
2. 重构 `VoiceWakeRuntime`，使其创建/更新/结束会话，而非直接操作 `VoiceWakeOverlayController`。
3. 重构 `VoicePushToTalk`，使其接管现有会话并在松开时调用 `endCapture`；施加运行时冷却。
4. 将 `VoiceWakeOverlayController` 连接到 publisher；移除来自 runtime/PTT 的直接调用。
5. 添加会话接管、冷却和空文本关闭的集成测试。
