Skip to content

fix(qqofficial): render @ mentions in group messages#9285

Open
IQ-Director wants to merge 3 commits into
AstrBotDevs:masterfrom
IQ-Director:master
Open

fix(qqofficial): render @ mentions in group messages#9285
IQ-Director wants to merge 3 commits into
AstrBotDevs:masterfrom
IQ-Director:master

Conversation

@IQ-Director

@IQ-Director IQ-Director commented Jul 14, 2026

Copy link
Copy Markdown
  • serialize At components as <@openid> markup
  • send mention-bearing replies and proactive messages as Markdown
  • preserve payload compatibility for media and guild channel messages
  • support legacy and current incoming mention formats
  • add regression tests for QQ Official @ mentions

QQ Official outgoing messages currently discard At components in _parse_to_qqofficial, causing user mentions to disappear from replies.

In addition, messages sent through send_by_session, including proactive and scheduled group messages, are sent through the plain content field. QQ does not correctly resolve <@openid> mentions through this path, so the mention may be displayed as plain text instead of an actual @ mention.

This change preserves At components and ensures that messages containing user mentions are sent as Markdown, allowing QQ clients to render them correctly.

Modifications / 改动点

  • Updated qqofficial_message_event.py to serialize At components as <@openid>.
  • Skipped At(qq="all"), because QQ group chats do not support bot @all mentions.
  • Ensured event replies containing At use Markdown even when Markdown was explicitly disabled on the message chain.
  • Updated send_by_session so proactive, scheduled, and directly sent group messages containing At use msg_type=2 with a Markdown payload.
  • Kept ordinary messages without mentions on the existing plain-text sending path to avoid changing unrelated behavior.
  • Preserved compatibility with media messages by removing the Markdown payload when switching to msg_type=7.
  • Removed the incompatible msg_type field when sending messages to guild text channels.
  • Added support for cleaning legacy and current incoming mention formats:
    • <@bot_id>
    • <@!bot_id>
    • <qqbot-at-user id="bot_id" />
  • Added regression tests for outgoing mention serialization, incoming mention cleanup, and proactive group messages containing At.

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

Test command:
uv run --isolated --no-project --with pytest --with pytest-asyncio --with-editable . pytest tests/test_qqofficial_group_message_create.py -q

Test result:
16 passed, 4 warnings in 17.91s

Ruff command:
uv run --isolated --no-project --with ruff ruff check astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py

Ruff result:
All checks passed!

image

Summary by Sourcery

Ensure QQ Official group messages preserve and correctly render @ mentions by using Markdown for mention-bearing sends and adjusting parsing and serialization of mention markup.

New Features:

  • Support serialization of At components in outgoing QQ Official messages using <@openid> mention markup.
  • Add handling for legacy and current QQ Official mention formats when parsing incoming group and direct messages.

Bug Fixes:

  • Prevent At components from being dropped in QQ Official group replies and proactive messages so user mentions are not lost.
  • Avoid sending mention-bearing group messages through plain-text content fields that QQ does not resolve as mentions.
  • Ensure media and guild channel messages remain compatible by stripping unsupported msg_type and Markdown payloads where required.

Enhancements:

  • Automatically force Markdown sending for message chains that contain user mentions, even when Markdown was disabled on the chain.
  • Adjust QQ Official platform adapter payload construction to select msg_type based on presence of mentions and media content.

Tests:

  • Add parametrized regression tests covering multiple incoming mention markup variants for bot mentions in group messages.
  • Add tests verifying At component order and serialization in QQ Official message parsing.
  • Add tests confirming send_by_session uses Markdown for group messages containing At while preserving behavior for non-mention messages.

- serialize At components as <@openid> markup
- send mention-bearing replies and proactive messages as Markdown
- preserve payload compatibility for media and guild channel messages
- support legacy and current incoming mention formats
- add regression tests for QQ Official @ mentions
@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 14, 2026

@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, and left some high level feedback:

  • The logic for detecting mentions and deciding when to force Markdown (has_mention / use_markdown_) is duplicated in both QQOfficialMessageEvent._post_send_one and _send_by_session_common; consider centralizing this in a shared helper to avoid divergence over time.
  • There are multiple repeated blocks that switch msg_type to 7 and then pop('markdown', None) and reset content; extracting a small helper to normalize the payload when adding media would make the send path easier to maintain.
  • When stripping legacy/current mention formats from incoming content, the chained replace calls are scattered in two places with slightly different keys; a single function that normalizes all known mention syntaxes in one place could reduce the chance of missing a format or introducing inconsistent behavior.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The logic for detecting mentions and deciding when to force Markdown (`has_mention` / `use_markdown_`) is duplicated in both `QQOfficialMessageEvent._post_send_one` and `_send_by_session_common`; consider centralizing this in a shared helper to avoid divergence over time.
- There are multiple repeated blocks that switch `msg_type` to 7 and then `pop('markdown', None)` and reset `content`; extracting a small helper to normalize the payload when adding media would make the send path easier to maintain.
- When stripping legacy/current mention formats from incoming content, the chained `replace` calls are scattered in two places with slightly different keys; a single function that normalizes all known mention syntaxes in one place could reduce the chance of missing a format or introducing inconsistent behavior.

## Individual Comments

### Comment 1
<location path="astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py" line_range="411-412" />
<code_context>
                     )
                     payload["media"] = media
                     payload["msg_type"] = 7
+                    payload.pop("markdown", None)
+                    payload["content"] = plain_text or None
</code_context>
<issue_to_address>
**suggestion:** Avoid repeating markdown cleanup and content reset logic in each media branch.

`payload.pop("markdown", None)` and `payload["content"] = plain_text or None` are repeated across all media branches (`image_path`, `record_file_path`, `video_file_source`, `file_source` in both group and C2C). Centralizing this (e.g., `normalize_text_payload_for_media(payload, plain_text)` or a single pre-branch normalization step) will reduce duplication and keep future changes consistent across branches.

Suggested implementation:

```python
            payload: dict[str, Any] = {
                "markdown": botpy.types.message.MarkdownPayload(content=plain_text),
                "msg_type": 2,
            }

            def normalize_text_payload_for_media(
                payload: dict[str, Any],
                plain_text: str | None,
            ) -> None:
                # When sending media messages, QQ Official API expects plain text in `content`
                # and does not support markdown payloads. Normalize accordingly.
                payload.pop("markdown", None)
                payload["content"] = plain_text or None

        else:
            payload = {"content": plain_text, "msg_type": 0}

```

```python
                    )
                    payload["media"] = media
                    payload["msg_type"] = 7
                    normalize_text_payload_for_media(payload, plain_text)
                if record_file_path:
                    media = await QQOfficialMessageEvent.upload_group_and_c2c_media(
                        send_helper,  # type: ignore
                    if media:
                        payload["media"] = media
                        payload["msg_type"] = 7
                        normalize_text_payload_for_media(payload, plain_text)
                if video_file_source:
                    media = await QQOfficialMessageEvent.upload_group_and_c2c_media(

```

1. Apply the same replacement (`payload.pop("markdown", None)` / `payload["content"] = plain_text or None``normalize_text_payload_for_media(payload, plain_text)`) in all other media branches in this function/file, including:
   - `image_path` handling
   - `file_source` handling in both group and C2C contexts
   - Any other branch that sets `payload["media"]` and `payload["msg_type"] = 7`.
2. Ensure `plain_text` is available in the scope where `normalize_text_payload_for_media` is called; if any media branch is in a different function, define a module-level helper instead of a nested function and import/use it appropriately.
</issue_to_address>

### Comment 2
<location path="tests/test_qqofficial_group_message_create.py" line_range="172-173" />
<code_context>
+)
 @pytest.mark.asyncio
-async def test_parse_group_message_create_bot_mention_cleans_plain_text():
+async def test_parse_group_message_create_bot_mention_cleans_plain_text(
+    mention_markup: str,
+):
     _, message = _dispatch_group_message(
</code_context>
<issue_to_address>
**suggestion (testing):** Assert the exact cleaned content to make the regression test more explicit.

The current assertion (`abm.raw_content.startswith("hello there")`) is too loose and could still pass if some mention markup remains (e.g., leading spaces or partial tags). Please assert the fully cleaned content instead (e.g., `assert abm.raw_content == "hello there"`, or a small set of exact variants if whitespace is intentionally preserved) to ensure all mention formats are fully stripped.

Suggested implementation:

```python
@pytest.mark.asyncio
async def test_parse_group_message_create_bot_mention_cleans_plain_text(
    mention_markup: str,
):
    abm, _ = _dispatch_group_message(
        _make_group_payload(
            content=f"{mention_markup} hello there",
            mentions=[{"id": "bot-123", "is_you": True}],
        )
    )

    assert abm.group_id == "group-1"
    assert abm.raw_content == "hello there"

```

Depending on the actual return signature of `_dispatch_group_message`, you may need to adjust `abm, _` to match the correct ordering (e.g., `message, abm` or similar). If mention stripping can legitimately leave specific whitespace variants, replace the strict equality assertion with a small set of allowed exact strings, e.g.:
`assert abm.raw_content in ("hello there", "hello there ")`.
</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_platform_adapter.py Outdated
Comment thread tests/test_qqofficial_group_message_create.py

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request adds support for handling and resolving At (mention) components in the QQ Official platform adapter, including automatic Markdown mode activation for messages containing mentions and cleaning up bot mentions from incoming messages. The review feedback highlights potential bugs where At components with None or empty qq attributes could be incorrectly processed or serialized as <@None>, and suggests refactoring duplicated payload cleanup logic across multiple media upload blocks to improve code maintainability.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py Outdated
Comment thread astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py Outdated
Comment thread astrbot/core/platform/sources/qqofficial/qqofficial_platform_adapter.py Outdated
- validate mention IDs before serialization
- centralize mention detection and media payload normalization
- consolidate incoming mention markup cleanup
- add regression tests for invalid mention IDs
@dosubot dosubot Bot added size:L This PR changes 100-499 lines, ignoring generated files. and removed size:M This PR changes 30-99 lines, ignoring generated files. labels Jul 14, 2026
@IQ-Director

Copy link
Copy Markdown
Author

Addressed the review feedback in the latest commit:

  • validated mention IDs before serialization
  • centralized mention detection and media payload normalization
  • consolidated incoming mention markup cleanup
  • added regression tests for null and empty mention IDs

Tests: 18 passed.
Ruff: all checks passed.

@Soulter

Soulter commented Jul 15, 2026

Copy link
Copy Markdown
Member

Hi, thanks for your contribution. Please fix the code format check and then I'll merge this PR.

@IQ-Director

Copy link
Copy Markdown
Author

Fixed the Ruff format check in f642a60.

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:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants