fix(qqofficial): render @ mentions in group messages#9285
Conversation
- 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
There was a problem hiding this comment.
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 bothQQOfficialMessageEvent._post_send_oneand_send_by_session_common; consider centralizing this in a shared helper to avoid divergence over time. - There are multiple repeated blocks that switch
msg_typeto 7 and thenpop('markdown', None)and resetcontent; 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
replacecalls 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>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
There was a problem hiding this comment.
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.
- validate mention IDs before serialization - centralize mention detection and media payload normalization - consolidate incoming mention markup cleanup - add regression tests for invalid mention IDs
|
Addressed the review feedback in the latest commit:
Tests: 18 passed. |
|
Hi, thanks for your contribution. Please fix the code format check and then I'll merge this PR. |
|
Fixed the Ruff format check in f642a60. |
QQ Official outgoing messages currently discard
Atcomponents 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 plaincontentfield. 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
Atcomponents and ensures that messages containing user mentions are sent as Markdown, allowing QQ clients to render them correctly.Modifications / 改动点
qqofficial_message_event.pyto serializeAtcomponents as<@openid>.At(qq="all"), because QQ group chats do not support bot@allmentions.Atuse Markdown even when Markdown was explicitly disabled on the message chain.send_by_sessionso proactive, scheduled, and directly sent group messages containingAtusemsg_type=2with a Markdown payload.msg_type=7.msg_typefield when sending messages to guild text channels.<@bot_id><@!bot_id><qqbot-at-user id="bot_id" />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!
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:
Bug Fixes:
Enhancements:
Tests: