Skip to content

fix(qqofficial): prevent leading char loss in streaming buffer - #9444

Open
OWWZO wants to merge 2 commits into
AstrBotDevs:masterfrom
OWWZO:master
Open

fix(qqofficial): prevent leading char loss in streaming buffer#9444
OWWZO wants to merge 2 commits into
AstrBotDevs:masterfrom
OWWZO:master

Conversation

@OWWZO

@OWWZO OWWZO commented Jul 29, 2026

Copy link
Copy Markdown

Copy stream deltas into an owned buffer instead of holding references to yielded MessageChain/Plain objects. Upstream reuse/mutation dropped the first character(s) on group (and C2C) streaming accumulation.

Add regression tests covering reference-mutation, independent deltas, and C2C path.

Fixes QQ Official streaming dropping the first character(s) when accumulating deltas (especially group chat; C2C accumulation uses the same helper).

Root cause / 根因:
send_streaming used send_buffer = chain (reference). Upstream can reuse/mutate the same MessageChain/Plain between yields, so the first char was lost on the next delta.

bug日志截图:
2db614b0d0cf786237a0c5261b865afb
431d41890de36df0763ca17ec8df821d
f8723a6b59f8dca80a152a98cf3c7a6b

Related: #9440

Modifications / 改动点

  • astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py

  • Add _append_stream_delta(): copy Plain text into an owned buffer

  • Group (non-C2C) and C2C accumulation both use this helper (no more bare reference assign)

  • tests/test_qqofficial_stream_buffer_copy.py

  • Regression tests: reference-mutation bug repro, independent deltas, multi-char/\n\n, C2C path, single final group send

  • This is NOT a breaking change. / 这不是一个破坏性变更。

Screenshots or Test Results / 运行截图或测试结果

修复后的验证截图:
d5f1201ac42de6df04e11497a21daf1b
233f6df560a36bd6e08eb4c1b94ec2d7
首字不再被吞掉

Checklist / 检查清单

  • [ x] 😊 If there are new features added in the PR, I have discussed it with the authors through issues/emails, etc.
    / 如果 PR 中有新加入的功能,已经通过 Issue / 邮件等方式和作者讨论过。

  • [x ] 👀 My changes have been well-tested, and "Verification Steps" and "Screenshots" have been provided above.
    / 我的更改经过了良好的测试,并已在上方提供了“验证步骤”和“运行截图”

  • [x ] 🤓 I have ensured that no new dependencies are introduced, OR if new dependencies are introduced, they have been added to the appropriate locations in requirements.txt and pyproject.toml.
    / 我确保没有引入新依赖库,或者引入了新依赖库的同时将其添加到 requirements.txtpyproject.toml 文件相应位置。

  • [ x] 😮 My changes do not introduce malicious code.
    / 我的更改没有引入恶意代码。

Summary by Sourcery

Ensure QQ Official streaming accumulates text deltas in an owned buffer to avoid leading-character loss when upstream reuses MessageChain objects.

Bug Fixes:

  • Fix QQ Official group and C2C streaming where the first character(s) of accumulated deltas could be dropped due to holding mutable MessageChain references.

Enhancements:

  • Introduce a helper to append streaming deltas into a dedicated MessageChain, copying Plain text while preserving non-text components.

Tests:

  • Add regression tests covering buffer behavior under upstream MessageChain mutation, independent delta chains, multi-character and newline deltas, C2C streaming path, and single final group send semantics.

Copy stream deltas into an owned buffer instead of holding references
to yielded MessageChain/Plain objects. Upstream reuse/mutation dropped
the first character(s) on group (and C2C) streaming accumulation.

Add regression tests covering reference-mutation, independent deltas,
and C2C path.
@dosubot dosubot Bot added size:M This PR changes 30-99 lines, ignoring generated files. area:platform The bug / feature is about IM platform adapter, such as QQ, Lark, Telegram, WebChat and so on. labels Jul 29, 2026
@dosubot

dosubot Bot commented Jul 29, 2026

Copy link
Copy Markdown

📄 Knowledge review

Dosu skipped reviewing this PR because your organization has used its 200 included credits for the month. Your usage will reset on 2026-08-01. To have Dosu review this PR before then, ask your organization admin to upgrade to a pro account.


Leave Feedback Ask Dosu about AstrBot Add Dosu to your team

@sourcery-ai sourcery-ai 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.

Hey - I've found 1 issue, and left some high level feedback:

  • In _append_stream_delta, non-Plain components are still appended by reference, so if upstream reuses/mutates those components across deltas you could see similar buffer corruption; consider cloning or otherwise guarding non-Plain components as well if that scenario is possible for QQ streaming.
  • _append_stream_deltanormalizesPlain.textto""when falsy, which changes behavior forNone` or other falsy values; if those are semantically meaningful in your MessageChain usage, it may be safer to preserve the original value rather than coercing to an empty string.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `_append_stream_delta`, non-Plain components are still appended by reference, so if upstream reuses/mutates those components across deltas you could see similar buffer corruption; consider cloning or otherwise guarding non-Plain components as well if that scenario is possible for QQ streaming.
- _append_stream_delta` normalizes `Plain.text` to `""` when falsy, which changes behavior for `None` or other falsy values; if those are semantically meaningful in your MessageChain usage, it may be safer to preserve the original value rather than coercing to an empty string.

## Individual Comments

### Comment 1
<location path="astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py" line_range="195-196" />
<code_context>
+                type=chain.type,
+            )
+        for comp in chain.chain:
+            if isinstance(comp, Plain):
+                self.send_buffer.chain.append(Plain(text=comp.text or ""))
+            else:
+                self.send_buffer.chain.append(comp)
</code_context>
<issue_to_address>
**question:** Using `comp.text or ""` may change behavior for falsy-but-meaningful values compared to the previous implementation.

Previously, the buffer appended `chain.chain` directly, preserving whatever `Plain.text` contained, including `None` or other falsy-but-intentional values. With `Plain(text=comp.text or "")`, all falsy values are coerced to an empty string, which may change semantics if `Plain.text` can legitimately be `None` or other falsy values. If `None` should always be treated as `""`, this is fine; otherwise, prefer `Plain(text=comp.text)` and rely on the constructor to perform any necessary normalization.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py Outdated
- deepcopy non-Plain components to avoid shared-reference mutation
- preserve Plain.text as-is instead of coercing falsy values with or "
@OWWZO

OWWZO commented Jul 29, 2026

Copy link
Copy Markdown
Author

Hey - I've found 1 issue, and left some high level feedback:Hey - I've found 1 issue, and left some high level feedback:嘿——我发现了1个问题,还留下了一些高层次的反馈:

  • In _append_stream_delta, non-Plain components are still appended by reference, so if upstream reuses/mutates those components across deltas you could see similar buffer corruption; consider cloning or otherwise guarding non-Plain components as well if that scenario is possible for QQ streaming.In _append_stream_delta, non-Plain components are still appended by reference, so if upstream reuses/mutates those components across deltas you could see similar buffer corruption; consider cloning or otherwise guarding non-Plain components as well if that scenario is possible for QQ streaming.在 _append_stream_delta 中,非 Plain 组件仍会按引用追加,因此如果上游在不同增量间复用/修改这些组件,可能会出现类似的缓冲区损坏问题;如果 QQ 流式处理存在这种情况,也请考虑对非 Plain 组件进行克隆或采取其他保护措施。
  • _append_stream_deltanormalizesPlain.textto""when falsy, which changes behavior forNone or other falsy values; if those are semantically meaningful in your MessageChain usage, it may be safer to preserve the original value rather than coercing to an empty string._append_stream_deltanormalizesPlain.textto""when falsy, which changes behavior forNone or other falsy values; if those are semantically meaningful in your MessageChain usage, it may be safer to preserve the original value rather than coercing to an empty string._append_stream_deltanormalizes普通文本to空字符串when falsy, which changes behavior forNone`或其他假值的行为;如果这些值在你的消息链使用中具有语义意义,那么保留原始值而非强制转换为空字符串可能更安全。

Prompt for AI Agents 给 AI 智能体的提示

Please address the comments from this code review:

## Overall Comments
- In `_append_stream_delta`, non-Plain components are still appended by reference, so if upstream reuses/mutates those components across deltas you could see similar buffer corruption; consider cloning or otherwise guarding non-Plain components as well if that scenario is possible for QQ streaming.
- _append_stream_delta` normalizes `Plain.text` to `""` when falsy, which changes behavior for `None` or other falsy values; if those are semantically meaningful in your MessageChain usage, it may be safer to preserve the original value rather than coercing to an empty string.

## Individual Comments

### Comment 1
<location path="astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py" line_range="195-196" />
<code_context>
+                type=chain.type,
+            )
+        for comp in chain.chain:
+            if isinstance(comp, Plain):
+                self.send_buffer.chain.append(Plain(text=comp.text or ""))
+            else:
+                self.send_buffer.chain.append(comp)
</code_context>
<issue_to_address>
**question:** Using `comp.text or ""` may change behavior for falsy-but-meaningful values compared to the previous implementation.

Previously, the buffer appended `chain.chain` directly, preserving whatever `Plain.text` contained, including `None` or other falsy-but-intentional values. With `Plain(text=comp.text or "")`, all falsy values are coerced to an empty string, which may change semantics if `Plain.text` can legitimately be `None` or other falsy values. If `None` should always be treated as `""`, this is fine; otherwise, prefer `Plain(text=comp.text)` and rely on the constructor to perform any necessary normalization.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Sourcery 对开源项目免费——如果你喜欢我们的代码评审,欢迎分享 ✨

Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
帮我变得更有用吧!请在每条评论上点击👍或👎,我会利用这些反馈来改进你的评价。

addressed in f6e250b:

  1. Non-Plain components are now copy.deepcopy'd into the owned buffer.
  2. Plain uses Plain(text=comp.text) (no or "" coercion).

Tests still pass: uv run pytest tests/test_qqofficial_stream_buffer_copy.py -v

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

Labels

area:platform The bug / feature is about IM platform adapter, such as QQ, Lark, Telegram, WebChat and so on. size:M This PR changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant