Skip to content

fix(assistant): 按住说话点掉回复后,上一轮回复会重现并挡住语音条 - #364

Merged
yyy-router merged 1 commit into
1024XEngineer:mainfrom
LUPENGHAN:worktree-other-bugfix
Aug 24, 2026
Merged

fix(assistant): 按住说话点掉回复后,上一轮回复会重现并挡住语音条#364
yyy-router merged 1 commit into
1024XEngineer:mainfrom
LUPENGHAN:worktree-other-bugfix

Conversation

@LUPENGHAN

@LUPENGHAN LUPENGHAN commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

变更说明

  • AssistantConversationService(前端)在 _startTurn() 里给每一轮按住说话生成
    一个自增的 request_idptt-turn-N),随 voice.stream.start 一起发出。后端
    _request_id_of() 拿到后会存进 StreamContextdeliver_reply_text() 再把它
    回显到该轮的 voice.dialogue.reply 上——这两处都是既有实现,后端不用改。
  • handleMessage()voice.dialogue.reply 分支据此认轮次:request_id 跟当前
    这一轮对不上的直接丢弃,不再写 replyText。上一轮被 interrupt 后迟到的回复
    不会再把已经点掉的气泡弹回来,也就不会再用那层全屏 Pressable 挡住语音条、逼用户
    多点一次才能说话。

两个刻意的取舍:

  • 只 gate voice.dialogue.reply 这一条消息,其余(传输错误信封、
    voice.stream.startedvoice.command.resultvoice.tts.*)全部照原路走。
    错误信封是 _startTurn()await started 唯一的解套途径(后端
    "A stream is already active" / "Audio frame is empty" 这类错误会带上旧流
    request_id),voice.stream.startedstreamId 的唯一来源,
    voice.command.result 代表服务端已提交的写入——按轮次丢弃它们的代价都远大于
    显示了一条过期气泡。
  • request_idnullundefined 时一律放行。后端 model_dump() 把缺省的
    request_id 序列化成 JSON null 而不是省略字段,只判 undefined 会把这类回复
    全部误吞。

turnRequestId 的赋值紧挨着 connection.send(),没有放在 _startTurn() 开头:
connect()(含 2s 定位 race)和 requestPermission()(权限弹框)都可能停留数秒,
提前换 id 会让这段窗口里仍在跑的上一轮流被当成"别人的"而误丢自己的回复。

测试计划

  • npm run check(前端,exit 0;lint + prettier + tsc + vitest + jest,68 suites / 647 tests 全绿)
  • 新增 3 条回归测试,并验证过测试本身能抓出问题:
    • 上一轮迟到的 voice.dialogue.reply 不再写 replyText(核心场景);
    • request_idnull 的回复仍然正常显示(防"修过头"把正常回复吞掉);
    • request_id 的传输错误信封仍能让 startTurn() 解套并进入 error 态
      (护栏:防止以后有人把这道判断提到 handleMessage() 开头,那样会让按住说话永久卡死)。
    • 验证方式:把源码修复分别临时还原回旧逻辑(去掉 gate / 只判 undefined),重新跑
      这几条新测试,均按预期失败;改回修复后全部转绿——证明测试确实在验证这次改动。
  • 真机人工验证:按住说话 → 点掉气泡 → 再按住说话,确认旧回复不再重现、语音条不被挡

影响面

改动只落在 AssistantConversationService 一个类里,后端和消息契约都没动,连续对话
AssistantContinuousConversationService)不受影响——它本来就用 request_id 按轮
路由,TTS 侧另有 audio_id 守卫。

每一轮按住说话生成自增的 request_id 随 voice.stream.start 发出,服务端会把它
回显到该轮的 voice.dialogue.reply 上;handleMessage 据此认轮次,对不上的直接
丢弃。上一轮被 interrupt 后迟到的回复不会再把已经点掉的气泡弹回来——气泡是一层
全屏 Pressable,会挡住语音条,用户得多点一次才能说话。

只 gate voice.dialogue.reply 这一条:错误信封是 startTurn() 唯一的解套途径,
voice.stream.started 是 streamId 的唯一来源,voice.command.result 代表服务端
已提交的写入,按轮次丢弃它们的代价都远大于显示一条过期气泡。request_id 为
null/undefined 时一律放行——后端 model_dump() 把缺省值序列化成 null 而非省略
字段,只判 undefined 会把正常回复误吞。

Co-Authored-By: Claude <noreply@anthropic.com>
@codecov

codecov Bot commented Aug 24, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@yyy-router yyy-router left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

@yyy-router
yyy-router merged commit 26a7fd7 into 1024XEngineer:main Aug 24, 2026
5 checks passed

@fennoai fennoai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

本次改动中,voice.stream.start 的逐轮 request_id 与后端 StreamContext/voice.dialogue.reply 回显链路保持一致;过滤仅作用于文字回复,未影响启动确认、错误解套、命令落库与 TTS 状态路径。未发现需要阻塞合并的正确性或回归问题。

已验证:目标 Jest 套件(21 tests)、TypeScript 类型检查、改动文件 ESLint/Prettier,以及固定 SHA 范围的 git diff --check 均通过。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants