Skip to content

fix(qqofficial): fill message_reference when sending Reply components - #10401

Open
he-yufeng wants to merge 2 commits into
AstrBotDevs:masterfrom
he-yufeng:fix/qqofficial-outbound-message-reference
Open

he-yufeng wants to merge 2 commits into
AstrBotDevs:masterfrom
he-yufeng:fix/qqofficial-outbound-message-reference

Conversation

@he-yufeng

@he-yufeng he-yufeng commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #10391.

Inbound quotes already work: the adapter reads message_reference off incoming messages and builds a Reply component from it. The outbound half was never wired up. _parse_to_qqofficial() dropped Reply components in its fallback branch, and although post_c2c_message() declares a message_reference parameter, no call site ever passed one, so a plugin quoting a message with Comp.Reply(id=...) produced plain text with no quote bubble.

Changes:

  • _parse_to_qqofficial() now also returns the first Reply component's message id. Empty or missing ids are ignored, so quote-free chains behave exactly as before.
  • _post_send_one() and _send_by_session_common() attach message_reference: {"message_id": ...} to the payload when such an id is present. All four send APIs used here (v2 groups, v2 C2C, guild channel, guild DM) accept that field.
  • The active-send fallback in _send_with_markdown_fallback() drops the reference together with msg_id, so a stale or rejected quote id degrades to plain text instead of blocking the whole message.

Tests:

  • Parse extraction: id present, absent, empty string, and first-Reply-wins.
  • Proactive payloads: group send carries message_reference, C2C send carries it through the post_c2c_message wrapper into the request JSON, and quote-free group sends do not gain the key.
  • Existing unpack sites and the monkeypatched parse in the media test updated for the extra return value.

Verified with pytest tests/test_qqofficial_group_message_create.py tests/test_platform_audio_media_resolver.py (44 + 5 passed) and ruff format / ruff check clean on the touched files. I could not exercise the real QQ API from here; the payload shape follows the message_reference contract linked in the issue and botpy's Reference TypedDict.

Summary by Sourcery

Enable QQ Official outbound messages to carry message references from Reply components while gracefully degrading when a reference is unavailable or rejected.

New Features:

  • Support sending QQ Official messages with Reply components as quoted replies across group, C2C, guild channel, and guild DM APIs.

Bug Fixes:

  • Preserve message references during active-send fallback and retry without the reference when the quoted message cannot be accepted, allowing the message text to still be delivered.

Tests:

  • Add coverage for reply-reference extraction, outbound payloads, quote-free sends, and fallback behavior.

@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 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="643" />
<code_context>
             if payload.get("msg_id"):
                 fallback_payload = payload.copy()
                 fallback_payload.pop("msg_id", None)
+                # 主动发送兜底同样去掉引用:引用 id 失效不应阻断正文送达
+                fallback_payload.pop("message_reference", None)
                 try:
</code_context>
<issue_to_address>
**Valid quote context is lost**

When a quoted reply fails initially for an unrelated caught error and the retry succeeds, `_send_with_markdown_fallback` removes `message_reference` before retrying, so the recipient gets the message body without its valid quote context.

Remove `message_reference` and retry only when the initial error indicates that the reference was rejected.

Also at `astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py:644`.
</issue_to_address>

### Comment 2
<location path="tests/test_qqofficial_group_message_create.py" line_range="257" />
<code_context>
+@pytest.mark.asyncio
+async def test_parse_to_qqofficial_extracts_reply_reference():
+    parsed = await QQOfficialMessageEvent._parse_to_qqofficial(
+        MessageChain(chain=[Reply(id="quoted-1"), Plain("hello")])
+    )
+
</code_context>
<issue_to_address>
**Replies only tested at chain start**

When a `Reply` component follows another component in the chain, a parser that checks only the first chain component for `Reply` passes these cases because each test puts `Reply` first; it still misses replies after preceding text, so the tests do not catch that broken extraction behavior.

Add a parser case with `Plain` before `Reply` and assert the reference is extracted.

Also at `tests/test_qqofficial_group_message_create.py:280`.
</issue_to_address>

Sourcery assessment

Needs a human reviewer. 2 findings to address first, and if the extracted or attached reference is wrong, sent messages could contain an incorrect reply target or fail through the QQ API when the reference is rejected. Reverting stops future references but cannot remove metadata from messages already delivered, though the impact is bounded to those messages and can be corrected by sending another message.

Blocking findings: astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py:643, tests/test_qqofficial_group_message_create.py:257


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

if payload.get("msg_id"):
fallback_payload = payload.copy()
fallback_payload.pop("msg_id", None)
# 主动发送兜底同样去掉引用:引用 id 失效不应阻断正文送达

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.

🟡 Medium · Valid quote context is lost

When a quoted reply fails initially for an unrelated caught error and the retry succeeds, _send_with_markdown_fallback removes message_reference before retrying, so the recipient gets the message body without its valid quote context.

Remove message_reference and retry only when the initial error indicates that the reference was rejected.

Also at astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py:644.

Prompt for AI agents
In `astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py` at line 643:

**Valid quote context is lost**

When a quoted reply fails initially for an unrelated caught error and the retry succeeds, `_send_with_markdown_fallback` removes `message_reference` before retrying, so the recipient gets the message body without its valid quote context.

Remove `message_reference` and retry only when the initial error indicates that the reference was rejected.

Also at `astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py:644`.

Comment thread tests/test_qqofficial_group_message_create.py
Inbound quotes already work: the adapter reads message_reference off
incoming messages and builds a Reply component from it. The outbound
half was never wired up, so a plugin quoting a message with
Comp.Reply(id=...) produced plain text with no quote bubble.

_parse_to_qqofficial() now returns the first Reply component's message
id alongside the media fields, and both send paths (_post_send_one and
_send_by_session_common) attach it as message_reference. All four send
APIs used here (v2 groups, v2 C2C, guild channel, guild DM) accept that
field. The active-send fallback drops the reference together with
msg_id, so a stale quote id degrades to plain text instead of blocking
the message.
@he-yufeng
he-yufeng force-pushed the fix/qqofficial-outbound-message-reference branch from 2284ebf to 0a89b87 Compare October 7, 2026 10:15
…nd fallback

Address review findings:

- The passive-to-active fallback stripped message_reference together with
  msg_id on any failure, so an unrelated send error (markdown rejection,
  transient server error) lost a perfectly valid quote context. The active
  retry now keeps the reference first and only drops it when the retry
  with the reference also fails, since a dead reference id must not block
  the message body either.
- The Reply extraction tests only covered Reply at the chain start. Added
  a Plain-before-Reply parser case and two fallback-behavior cases.
@he-yufeng

Copy link
Copy Markdown
Contributor Author

Both findings addressed in 117240d:

  • Quote context loss: the passive-to-active fallback now keeps message_reference on the first active retry and only drops it if that retry also fails. An unrelated send error (markdown rejection, transient server error) no longer costs a valid quote, while a dead reference id still cannot block the message body: one extra retry happens only in the failure path with a reference attached.
  • Test coverage: added a parser case with Plain before Reply (extraction iterates the whole chain; the gap was test-only), plus two fallback cases pinning the new order: first active retry carries the reference, reference-stripped retry happens only after the first one fails.

tests/test_qqofficial_group_message_create.py: 47 passed.

This branch has not been deployed

No deployments
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.

[Bug] qqofficial 出站不发送引用回复:Reply 组件被丢弃,message_reference 从未填充

1 participant