# 语音输入 > 点悬浮球上的麦克风说一句话,识别结果实时上屏,可编辑后一键发送到当前会话。 > App 退到后台也能发出去。 ## 1. 入口与形态 - **入口**:无障碍服务持有的悬浮球(状态球)上的麦克风按钮——不依赖主界面存活。 - **面板**:屏幕下方滑出可编辑输入框(`TYPE_ACCESSIBILITY_OVERLAY` 窗口,由无障碍服务持有)。 - **交互**:说话时识别结果实时写入输入框;可点输入框自己改字;点「发送」提交、点「重说」重新聆听、 点「关闭」收起。 ## 2. 识别通道(优先级从高到低) | 优先级 | 通道 | 说明 | |---|---|---| | ① | **系统识别** | 设备自带 `SpeechRecognizer`(零配置、通常离线、质量好)。优先使用 | | ② | **云端识别** | 系统识别不可用时,使用设置里配置的 OpenAI 兼容 API(准流式:按停顿切段、逐段上传、边录边追加) | 两条都不可用时,面板会明确提示去配置云端 API,而不是「点了没反应」。 ## 3. 发送链路(为什么不会重复、不会丢) 发送不是直接调引擎 API,而是**走 WebUI 自己的输入框**(复用前端的附件、@引用等既有逻辑): 1. 用原生 setter 写入输入框,并派发 `input` 事件,让 React 受控组件感知变化; 2. 若编辑器对合成 paste 有兼容问题,回读校验后走 `execCommand` 修补; 3. 通过发送键提交; 4. 多层去重守卫: - 面板按钮的 `consumed` 守卫(一次触摸只触发一次); - 文本内容 + 1.5 秒时间窗的去重(最终防线); - 「识别结果恰好是同一句重复两遍」的折叠(IME 迟到提交的兜底)。 **为什么后台也能发**:整条链路由 Android 侧时序驱动,不依赖 `requestAnimationFrame` 或页面 JS 定时器; 同时主界面 WebView 在后台保持 `resumeTimers`,保证前端能继续渲染流式回复。 ## 4. 面板生命周期与「停靠」 面板是独立的无障碍浮窗,增删窗口在部分机型上会带来 1~2 帧的合成残影。因此: - **关闭/重说**:播「下滑 + 淡出」动画后移除窗口; - **发送**:窗口**不销毁**,改为「停靠」—— `alpha=0` + 窗口 flags 改 `NOT_TOUCHABLE | NOT_FOCUSABLE`,下次语音直接复用同一个窗口(`unpark` 恢复)。 这样避免了发送瞬间的窗口增删,也避免了半透明面板压在正在滚动的对话页上。 > ⚠️ 已知问题:部分机型在发送瞬间仍可能出现 1~2 帧残影。这是系统合成层的行为, > 应用层已经试过「立即移除 / 延迟移除 / alpha 归零 / 窗口停靠」四种时序,影响已降到最低。 ## 5. 状态与悬浮球 - 聆听中:状态点变红、状态行显示「正在聆听」; - 识别结束但有文字:**保持聆听态**直到你发送/关闭(避免文字出现那一瞬悬浮窗突然收起); - 面板隐藏(任意路径):统一停识别、释放麦克风、状态回空闲; - 看门狗:识别期间每 1.5 秒检查面板是否还在,面板被系统移除会立即停止录音并释放麦克风。 ## 6. 权限与依赖 | 权限 | 用途 | |---|---| | `RECORD_AUDIO` | 麦克风(首次使用会请求) | | 无障碍服务 | 承载浮窗与面板;需在系统设置中手动开启 | | 通知权限(Android 13+) | 可选,用于通知类功能 | 离线识别可选装开源插件 [dsh-asr-service](https://github.com/Soodok/dsh-asr-service); 云端识别在设置里配置 OpenAI 兼容端点与 key。 ## 7. 故障排查 | 现象 | 排查 | |---|---| | 点麦克风没反应 | 麦克风权限是否给了;手机是否有可用的识别服务或已配置云端 API | | 识别文字进不了输入框 | 主界面是否还活着(App 被回收时只会在悬浮窗提示「请先打开 App」) | | 同一句发了两遍 | 已有多层去重;若仍出现请提供日志(面板会记录 `send tapped` 与去重结果) | | 发完麦克风图标不消失 | 面板隐藏会统一释放;若持续占用请重开一次语音或重启引擎 |