fix(qqofficial): finalize C2C stream to prevent replies reverting to the first fragment - #10067
Linyesantan wants to merge 2 commits into
Conversation
There was a problem hiding this comment.
Hey - I've found 2 issues
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py" line_range="266" />
<code_context>
self.message_obj.raw_message
) # 提前获取,避免 generator 为空时 NameError
+ # 累积已生成全文与已下发长度,用于断流兜底,避免回复被掐断
+ full_text = ""
+ sent_len = 0
+
</code_context>
<issue_to_address>
**issue (broader_impact):** When a `break` marker arrives after the previous throttled send has already cleared `self.send_buffer`, the `if self.send_buffer` guard skips the `state=10` frame, and this code resets `stream_payload` without finalizing the existing QQ stream. The tool-call stream therefore remains unfinished and can still revert or remain stuck.
**Triggers:** When a tool call follows a throttled C2C fragment whose buffer is already empty.
**Suggested fix:** Finalize the active stream on `break` whenever a stream id exists, using the same tail-or-newline logic as normal completion, rather than guarding finalization only on `self.send_buffer`.
</issue_to_address>
### Comment 2
<location path="astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py" line_range="302-306" />
<code_context>
+ self.send_buffer.chain.append(Plain(text=tail if tail else "\n"))
+ stream_payload["state"] = 10
+ ret = await self._post_send(stream=stream_payload)
+ elif full_text:
+ # 未拿到流式消息 id(中间分片返回无 id):降级为普通消息补发全文
+ self.send_buffer = MessageChain(use_t2i_=False, type="segment")
+ self.send_buffer.chain.append(Plain(text=full_text))
+ ret = await self._post_send()
else:
ret = await self._post_send()
</code_context>
<issue_to_address>
**issue (bug_risk):** When an intermediate `state=1` request has already delivered content but its response contains no message id, the final branch sends the entire `full_text` as an ordinary message. The user receives the already-delivered prefix in the stream and then a second ordinary message containing the complete response, duplicating the streamed content.
**Triggers:** When a `state=1` API response lacks an extractable message id after the request has been accepted.
**Suggested fix:** Do not send the full response as a separate ordinary message after a partial stream has been delivered; either recover the stream id or send only an appropriate non-duplicating fallback.
</issue_to_address>Sourcery assessment
Needs a human reviewer. 2 findings to address first, and if the accumulated-length or stream-ID handling is wrong, the finalization path could duplicate part or all of a reply, or send a fallback message after a partial stream. Reverting would stop future occurrences but could not retract messages already delivered to users.
Blocking findings: astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py:266, astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py:306
| } | ||
| last_edit_time = 0 | ||
| # 工具段结束后已下发前缀不再延续,重置全文追踪 | ||
| full_text = "" |
There was a problem hiding this comment.
issue (broader_impact): When a break marker arrives after the previous throttled send has already cleared self.send_buffer, the if self.send_buffer guard skips the state=10 frame, and this code resets stream_payload without finalizing the existing QQ stream. The tool-call stream therefore remains unfinished and can still revert or remain stuck.
Triggers: When a tool call follows a throttled C2C fragment whose buffer is already empty.
Suggested fix: Finalize the active stream on break whenever a stream id exists, using the same tail-or-newline logic as normal completion, rather than guarding finalization only on self.send_buffer.
| elif full_text: | ||
| # 未拿到流式消息 id(中间分片返回无 id):降级为普通消息补发全文 | ||
| self.send_buffer = MessageChain(use_t2i_=False, type="segment") | ||
| self.send_buffer.chain.append(Plain(text=full_text)) | ||
| ret = await self._post_send() |
There was a problem hiding this comment.
issue (bug_risk): When an intermediate state=1 request has already delivered content but its response contains no message id, the final branch sends the entire full_text as an ordinary message. The user receives the already-delivered prefix in the stream and then a second ordinary message containing the complete response, duplicating the streamed content.
Triggers: When a state=1 API response lacks an extractable message id after the request has been accepted.
Suggested fix: Do not send the full response as a separate ordinary message after a partial stream has been delivered; either recover the stream id or send only an appropriate non-duplicating fallback.
问题
QQ 官方机器人(WebSocket,C2C 私聊)开启流式输出后,回复会先正常流式增长为完整内容,随后回滚成开头的几个字;流式消息有时还会长期停在“生成中…”。
真实案例:会话数据库
data_v4.db里保存的是完整 850 字 回复,但 QQ 客户端里那条流式消息最终只剩首包 5 个字,且全程没有任何报错日志。关联 issue:#10066
根因
send_streaming每次节流发送后都会清空self.send_buffer。正常结束时执行的state=10收尾分支,如果 buffer 已被最后一次节流发送清空,_post_send会因为if not self.send_buffer: return None直接返回——结束帧从未发出,QQ 侧流式消息始终没有 finalize,超时后回滚 / 停留在未完成状态。异常、断流路径同样会丢弃尚未发送的尾段。改动
full_text/sent_len:正常结束时把未下发的尾段以state=10补齐;若尾段为空则发送"\n"收尾帧(满足 QQ API 结尾换行要求,且不会被_post_send的“空 buffer 早退”丢弃)。state=10补发,避免消息冻结在最后一个分片。tests/test_qqofficial_stream_finalize.py。与 #9875 的关系
本 PR 包含 #9875 的修复逻辑(作者 @VZService-AI,commit
bb19453),并按 #5264 的要求补上了回归测试。若 #9875 先行补测试并合并,本 PR 可关闭。Test plan
pytest tests/test_qqofficial_stream_finalize.py→ 3 passedstate=10结束帧;state=10补发。Summary by Sourcery
Ensure QQ Official C2C streaming replies are finalized reliably and preserve complete generated content across normal and interrupted delivery paths.
Bug Fixes:
Tests: