Hanzora 的语音打断有多个入口。用户说一句「停止」,浏览器端关键词检测可能先命中,流式语音识别又把同一句话识别成打断意图。两个结果都合理,但如果各自关闭一遍当前轮次,取消请求、监听恢复和结束事件也会执行两遍。
项目的变更记录明确列过这个问题:并发打断被双重处理。修复落在 InterruptOrchestrator,关键是把多个入口共享的副作用收进同一条流程。
让后到的调用等待正在执行的流程
KWS 与 streaming 两个入口先检查 inflight。关闭流程正在执行时,后到的调用等待已有任务;没有任务时,才创建新的关闭流程。任务结束后清空守卫,让下一次打断可以正常开始。
共享流程负责标记当前轮次已被打断、发起聊天取消、关闭流式监听并恢复唤醒词模式,最后发出关闭事件。
这解决的是同一关闭流程执行期间的并发重入。它没有把所有未来的「停止」永久去重,也没有假定取消请求一定成功。边界足够小,后续轮次不会被上一轮的守卫挡住。
顺序比多一把锁更重要
markCycleInterrupted() 必须先于 interruptChat() 调用。
服务端收到取消请求后,可能立即推送取消消息;这条消息到达客户端时,HTTP 请求的 Promise 还没有结束。如果本地标记落在请求之后,回声保护模块就可能把这次结束当成正常播报完成,再次打开连续监听。
因此,代码先声明「这一轮已被用户终止」,再发网络请求。聊天取消的错误进入诊断,本地监听收尾可以继续。请求的完成时刻与本地关闭流程的完成时刻分别管理,避免网络响应拖住本地恢复。
五种来源不等于五次取消
编排器区分 kws、streaming、implicit、api、server 五种来源。
其中 KWS 和 streaming 执行共享关闭流程;隐式打断、宿主 API 和服务端取消入口只发出来源标识,实际取消由各自上游负责。来源标签用于解释一次关闭从哪里来,不重复承担已经发生的副作用。
用调用次数验证修复
本次在源码快照上重跑 InterruptOrchestrator.test.ts,24 项用例通过。其中两个并发案例直接约束结果:
- 两次 KWS 打断并发到达时,轮次标记、聊天取消、监听关闭、唤醒模式恢复各执行 1 次,关闭事件只有 1 条。
- KWS 与 streaming 跨来源并发到达时,仍只执行一次轮次标记、发出一条关闭事件。
另有顺序测试检查取消标记先于聊天请求,错误路径检查请求失败仍完成本地收尾。这些断言验证的是副作用次数和执行顺序;真实设备上的打断响应时间需要另外测量。
本文依据 Hanzora SDK 的 InterruptOrchestrator.ts、对应测试与变更记录整理。源码核对与测试日期为 2026-10-10;项目见 Hanzora。