fix(qqofficial): always send the state=10 closing frame in C2C streaming - #10069
Conversation
QQ's C2C streaming protocol ends a stream with a state=10 frame whose content must end with \n. AstrBot only sent it while unsent text remained in the buffer: once the throttled middle frames had flushed the full reply and the generator ended with an empty tail, no closing frame went out at all. QQ then timed the stream out and rolled the whole message back to the first packet, which is why a full 850-char reply ended up displaying only its first few characters (AstrBotDevs#10066), and why some streams hang in "generating" forever. Close every open segment: when the stream is open (a frame id exists) but the tail buffer is empty, send a minimal "\n" closing frame; when nothing was ever sent (no id), keep sending nothing. The tool_call break path closes the same way through a shared helper so an empty segment cannot orphan its stream either. Regression tests pin both paths against a fake _post_send: empty tail after a middle flush, and break arriving on an empty but open segment.
There was a problem hiding this comment.
Hey - I've found 1 issue
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="227-232" />
<code_context>
+ 否则客户端等不到结束帧,最终只显示首包几个字。
+ """
+ stream_payload["state"] = 10
+ if not self.send_buffer or not self.send_buffer.chain:
+ if stream_payload.get("id") is None:
+ # 从未发出任何分片,无流可收
+ return None
+ self.send_buffer = MessageChain(chain=[Plain(text="\n")])
+ return await self._post_send(stream=stream_payload)
+
async def send_streaming(self, generator, use_fallback: bool = False):
</code_context>
<issue_to_address>
**issue (bug_risk):** A buffer containing an empty `Plain` component is treated as non-empty because `self.send_buffer.chain` is truthy, so the helper does not install the `"\n"` fallback. `_post_send_one` then rejects the empty parsed content and no `state=10` frame is sent for an otherwise opened stream.
**Triggers:** When a streaming generator yields an empty text delta represented as `MessageChain(chain=[Plain("")])` after the stream has opened.
**Suggested fix:** Determine whether the buffer contains sendable text rather than only checking whether its component list is non-empty, and replace an effectively empty buffer with `Plain("\n")` before posting the closing frame.
</issue_to_address>Sourcery assessment
Approval pending. 1 finding to address first.
Blocking findings: astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py:232
我想知道这个 QQ C2C 流式协议的官方文档在哪里。你有完整的协议文档吗?还是经验总结? |
不出意外我猜是这个https://bot.q.qq.com/wiki/develop/api-v2/autogen/api/v2_users_user_openid_stream_messages.post.html |
…rame Review on AstrBotDevs#10069: a buffer holding only Plain("") components has a truthy chain, so the "\n" fallback was skipped, _post_send_one rejected the empty parsed content, and no state=10 frame went out for an opened stream. Check for sendable content (non-empty text or a non-Plain component) instead of chain non-emptiness. Regression test pins the empty-Plain tail after a middle flush.
|
Fixed in 1db444c, with a regression test that fails without the change. The finding was right: a buffer holding only Test: |
|
@w31r4 @piexian piexian 猜的链接是对的:官方文档在流式发送单聊消息。 文档明确的协议面: 要分清楚两条来源:state=1/10 的分片语义是文档写明的;而「 |
Fixes #10066.
问题
QQ C2C 流式协议要求以
state=10分片收尾(content 以\n结尾)。当前实现只在 buffer 还有未发内容时才发这个收尾帧:节流中间分片(state=1,增量追加)已经把全文发完、生成器以空 buffer 收尾时,收尾帧根本不会发出。QQ 侧等不到结束帧,超时后把整条流式消息回滚到首包,完整 850 字的回复最终只显示开头几个字;有时流式消息长期停在「生成中」。复现链条与 issue 的三条观察一一对应:中间分片增量发出全文(客户端先长到完整)→ 生成器结束时
self.send_buffer为空 →_post_send直接 return → 无state=10→ 超时回滚到首包 5 字。修法
所有开着的流式段都补收尾:新增
_close_stream_segment共用收尾,流已开(有分片 id)但尾 buffer 为空时发送最小"\n"收尾帧(满足 QQ 对state=10content 的\n结尾要求);从未发出任何分片(无 id)时维持原样不发。tool_call break路径走同一辅助,空 buffer 但流已开的段也不再被遗弃。测试
tests/test_qqofficial_stream_buffer_copy.py新增两条回归:(state=1, 全文)+(state=10, "\n")(修复前只有第一帧,红)state=10收尾;break 后新段未开则不多发修复前两例红、修复后 10/10 绿;
pre-commit(ruff check / ruff format / pyupgrade)通过。未做真机 QQ 端验证,覆盖边界为单测级帧序列断言。Summary by Sourcery
Ensure QQ C2C streaming segments reliably terminate with the required closing frame.
Bug Fixes:
Tests: