Skip to content

fix: sanitize qqofficial command tags#8756

Open
tangtaizong666 wants to merge 3 commits into
AstrBotDevs:masterfrom
tangtaizong666:fix/8755-qqofficial-command-tags-pr
Open

fix: sanitize qqofficial command tags#8756
tangtaizong666 wants to merge 3 commits into
AstrBotDevs:masterfrom
tangtaizong666:fix/8755-qqofficial-command-tags-pr

Conversation

@tangtaizong666

@tangtaizong666 tangtaizong666 commented Jun 13, 2026

Copy link
Copy Markdown

Summary

  • sanitize QQ Official command interaction tags in incoming messages
  • preserve command intent by extracting the tag text attribute when present
  • add focused tests for the sanitizer and incoming parser output

Fixes #8755

Root Cause

QQ Official can include platform-specific command interaction tags in incoming
message content. AstrBot parsed that content directly into message_str and the
Plain component, so the raw tag could reach LLM context or cross-platform
display.

Tests

  • UV_PROJECT_ENVIRONMENT=/tmp/astrbot-venv-8755 uv run pytest -q tests/test_qqofficial_platform_adapter.py
  • UV_PROJECT_ENVIRONMENT=/tmp/astrbot-venv-8755 uv run ruff format --check .
  • UV_PROJECT_ENVIRONMENT=/tmp/astrbot-venv-8755 uv run ruff check .
  • UV_PROJECT_ENVIRONMENT=/tmp/astrbot-venv-8755 uv run pytest -q tests/test_neo_skill_sync.py tests/test_neo_skill_tools.py tests/test_computer_skill_sync.py tests/test_skill_manager_sandbox_cache.py tests/test_dashboard.py::test_neo_skills_routes

Note: the exact make pr-test-neo target was not available in this Windows
PowerShell environment, so I ran its lint and Neo pytest portions under WSL with
a Linux uv environment.

Summary by Sourcery

Sanitize QQ Official incoming message content by stripping or replacing platform-specific command interaction tags while preserving user-visible text, and add tests to cover the sanitizer and QQ Official message parsing.

Bug Fixes:

  • Prevent QQ Official command interaction tags from leaking raw platform-specific markup into message_str and Plain components by replacing them with their user-facing text or removing them when no text is present.

Tests:

  • Add focused unit tests for QQ Official command interaction tag sanitization and for _parse_from_qqofficial handling of tagged content and None message content in both group and guild messages.

@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 introduces a sanitization method, _sanitize_command_interaction_tags, to extract text from QQ Official command interaction tags within incoming messages, along with corresponding unit tests. The review feedback highlights potential AttributeError crashes if message.content is None (such as in attachment-only messages) when calling .strip() or .replace(). It is recommended to add defensive checks to handle None content gracefully and to include a test case covering this scenario.

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 on lines 575 to 581
abm.message_str = (
QQOfficialPlatformAdapter._sanitize_command_interaction_tags(
QQOfficialPlatformAdapter._parse_face_message(
message.content.strip()
)
)
)

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.

high

If message.content is None (which can happen if the message contains only attachments/images and no text), calling message.content.strip() will raise an AttributeError and crash the message handler. We should add a defensive check to handle None content gracefully.

            raw_content = message.content.strip() if message.content else ""
            abm.message_str = (
                QQOfficialPlatformAdapter._sanitize_command_interaction_tags(
                    QQOfficialPlatformAdapter._parse_face_message(raw_content)
                )
            )

Comment on lines 599 to 608
plain_content = (
QQOfficialPlatformAdapter._sanitize_command_interaction_tags(
QQOfficialPlatformAdapter._parse_face_message(
message.content.replace(
"<@!" + str(abm.self_id) + ">",
"",
).strip()
)
)
)

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.

high

Similarly, if message.content is None, calling message.content.replace(...) will raise an AttributeError. We should safely default to an empty string if message.content is None.

Suggested change
plain_content = (
QQOfficialPlatformAdapter._sanitize_command_interaction_tags(
QQOfficialPlatformAdapter._parse_face_message(
message.content.replace(
"<@!" + str(abm.self_id) + ">",
"",
).strip()
)
)
)
raw_content = message.content or ""
plain_content = (
QQOfficialPlatformAdapter._sanitize_command_interaction_tags(
QQOfficialPlatformAdapter._parse_face_message(
raw_content.replace(
"<@!" + str(abm.self_id) + ">",
"",
).strip()
)
)
)

Comment on lines +82 to +101
@pytest.mark.asyncio
async def test_parse_from_qqofficial_sanitizes_message_str_and_plain_component(
monkeypatch,
) -> None:
monkeypatch.setattr(
qqofficial_platform_adapter.botpy.message,
"GroupMessage",
FakeGroupMessage,
)
raw_message = FakeGroupMessage(
'hello <qqbot-cmd-input text="/quick-map" show="quick map" />'
)

message = await QQOfficialPlatformAdapter._parse_from_qqofficial(
raw_message,
MessageType.GROUP_MESSAGE,
)

assert message.message_str == "hello /quick-map"
assert _plain_texts(message.message) == ["hello /quick-map"]

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

It would be beneficial to add a test case to verify that the parser handles None content gracefully without crashing, especially since we added defensive checks for it.

@pytest.mark.asyncio
async def test_parse_from_qqofficial_sanitizes_message_str_and_plain_component(
    monkeypatch,
) -> None:
    monkeypatch.setattr(
        qqofficial_platform_adapter.botpy.message,
        "GroupMessage",
        FakeGroupMessage,
    )
    raw_message = FakeGroupMessage(
        'hello <qqbot-cmd-input text="/quick-map" show="quick map" />'
    )

    message = await QQOfficialPlatformAdapter._parse_from_qqofficial(
        raw_message,
        MessageType.GROUP_MESSAGE,
    )

    assert message.message_str == "hello /quick-map"
    assert _plain_texts(message.message) == ["hello /quick-map"]


@pytest.mark.asyncio
async def test_parse_from_qqofficial_handles_none_content(
    monkeypatch,
) -> None:
    monkeypatch.setattr(
        qqofficial_platform_adapter.botpy.message,
        "GroupMessage",
        FakeGroupMessage,
    )
    raw_message = FakeGroupMessage(None)

    message = await QQOfficialPlatformAdapter._parse_from_qqofficial(
        raw_message,
        MessageType.GROUP_MESSAGE,
    )

    assert message.message_str == ""
    assert _plain_texts(message.message) == [""]
References
  1. New functionality, such as handling attachments, should be accompanied by corresponding unit tests.

@tangtaizong666 tangtaizong666 marked this pull request as ready for review June 13, 2026 10:17
@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 Jun 13, 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 left some high level feedback:

  • In test_parse_from_qqofficial_handles_none_guild_content you pass MessageType.GROUP_MESSAGE while monkeypatching botpy.message.Message to FakeGuildMessage, which may mean this test never actually exercises the guild-specific code path; consider using the appropriate message type to align the test with its intent.
  • The logic that strips message.content, sanitizes command interaction tags, and parses face messages is duplicated between the group and guild branches in _parse_from_qqofficial; consider extracting this into a small helper to keep the normalization path consistent and reduce maintenance overhead.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `test_parse_from_qqofficial_handles_none_guild_content` you pass `MessageType.GROUP_MESSAGE` while monkeypatching `botpy.message.Message` to `FakeGuildMessage`, which may mean this test never actually exercises the guild-specific code path; consider using the appropriate message type to align the test with its intent.
- The logic that strips `message.content`, sanitizes command interaction tags, and parses face messages is duplicated between the group and guild branches in `_parse_from_qqofficial`; consider extracting this into a small helper to keep the normalization path consistent and reduce maintenance overhead.

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.

@Zzzzzzouhang

Copy link
Copy Markdown

问题2 跨平台显示异常:如果通过 AstrBot 将消息路由、转发或同步到其他非 QQ 平台(如微信、Telegram、KOOK 或 WebUI),这些标签会作为纯文本原样显示,造成严重的阅读干扰和排版混乱。这个并没有解决哎

@tangtaizong666

Copy link
Copy Markdown
Author

@Zzzzzzouhang 感谢反馈。关于「问题2 跨平台显示异常」,想先同步一下当前实现的覆盖范围,再确认是否还有遗漏的场景。

最新的提交(9f4e6617)里,清洗不只作用于 message_str(喂给 LLM 的文本),也作用于消息链里的 Plain 组件——也就是消息被路由 / 转发 / 同步到其他平台时实际使用的内容。两个入站分支(群消息 GroupMessage/C2CMessage 与频道消息 Message/DirectMessage)都统一走 _normalize_message_content

abm.message_str = ..._normalize_message_content(message.content)
msg.append(Plain(abm.message_str))   # 跨平台转发用的就是这个 Plain

所以 <qqbot-cmd-input .../> 这类标签在入站标准化阶段就会被剥离(有 text 属性时提取为命令文本,否则移除),理论上不会再以原文形式出现在转发到微信 / Telegram / KOOK / WebUI 的消息链里。

如果你在实测中仍看到标签泄漏,能否补充一下具体路径?尤其是:转发是通过哪个插件 / 机制做的?因为有些同步插件会直接读取 raw_message(原始 botpy 对象,而非标准化后的 message_str / Plain),这种情况适配器层的清洗无法覆盖,需要单独处理。

另外 issue 里提到的「框架层全局清洗配置」(可自定义需剥离的标签正则列表)是个更通用的特性,影响面比这个 qqofficial 适配器修复大不少。按贡献规范,这类新特性更适合单独开一个 PR 并先和维护者对齐设计,避免把这个聚焦的 bugfix 撑得过大。你觉得呢?

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.

[Feature] 支持自动剥离 QQ 官方平台特有交互标签(如 <qqbot-cmd-input>,<qqbot-cmd-enter>)以优化多平台与 LLM 上下文

2 participants