Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions astrbot/core/pipeline/preprocess_stage/stage.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,8 @@ async def process(
return
message_chain = event.get_messages()
for idx, component in enumerate(message_chain):
if isinstance(component, Record) and component.url:
path = component.url.removeprefix("file://")
if isinstance(component, Record):
path = await component.convert_to_file_path()
retry = 5
for i in range(retry):
try:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import os
import random
import time
import uuid
from pathlib import Path
from types import SimpleNamespace
from typing import Any, cast

Expand All @@ -24,6 +26,8 @@
)
from astrbot.core.message.components import BaseMessageComponent
from astrbot.core.platform.astr_message_event import MessageSesion
from astrbot.core.utils.astrbot_path import get_astrbot_temp_path
from astrbot.core.utils.io import download_file

from ...register import register_platform_adapter
from .qqofficial_message_event import QQOfficialMessageEvent
Expand All @@ -42,7 +46,7 @@ def set_platform(self, platform: QQOfficialPlatformAdapter) -> None:
async def on_group_at_message_create(
self, message: botpy.message.GroupMessage
) -> None:
abm = QQOfficialPlatformAdapter._parse_from_qqofficial(
abm = await QQOfficialPlatformAdapter._parse_from_qqofficial(
message,
MessageType.GROUP_MESSAGE,
)
Expand All @@ -53,7 +57,7 @@ async def on_group_at_message_create(

# 收到频道消息
async def on_at_message_create(self, message: botpy.message.Message) -> None:
abm = QQOfficialPlatformAdapter._parse_from_qqofficial(
abm = await QQOfficialPlatformAdapter._parse_from_qqofficial(
message,
MessageType.GROUP_MESSAGE,
)
Expand All @@ -66,7 +70,7 @@ async def on_at_message_create(self, message: botpy.message.Message) -> None:
async def on_direct_message_create(
self, message: botpy.message.DirectMessage
) -> None:
abm = QQOfficialPlatformAdapter._parse_from_qqofficial(
abm = await QQOfficialPlatformAdapter._parse_from_qqofficial(
message,
MessageType.FRIEND_MESSAGE,
)
Expand All @@ -76,7 +80,7 @@ async def on_direct_message_create(

# 收到 C2C 消息
async def on_c2c_message_create(self, message: botpy.message.C2CMessage) -> None:
abm = QQOfficialPlatformAdapter._parse_from_qqofficial(
abm = await QQOfficialPlatformAdapter._parse_from_qqofficial(
message,
MessageType.FRIEND_MESSAGE,
)
Expand Down Expand Up @@ -336,7 +340,22 @@ def _normalize_attachment_url(url: str | None) -> str:
return f"https://{url}"

@staticmethod
def _append_attachments(
async def _prepare_audio_attachment(
url: str,
filename: str,
) -> Record:
temp_dir = Path(get_astrbot_temp_path())
temp_dir.mkdir(parents=True, exist_ok=True)

ext = Path(filename).suffix.lower()
source_ext = ext or ".audio"
source_path = temp_dir / f"qqofficial_{uuid.uuid4().hex}{source_ext}"
Comment on lines +343 to +352
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.

suggestion (bug_risk): Persisting every audio attachment to a temp file without cleanup may cause temp directory growth over time.

_prepare_audio_attachment writes each audio file to get_astrbot_temp_path() and returns a Record with that path, but nothing deletes or reuses these files. For a long-running bot this can steadily grow the temp directory. If no external process is cleaning these up, add a lifecycle strategy (e.g., per-session subdirs plus cleanup, pruning old files, or an explicit delete after processing).

Suggested implementation:

    async def _prepare_audio_attachment(
        url: str,
        filename: str,
    ) -> Record:
        temp_dir = Path(get_astrbot_temp_path())
        temp_dir.mkdir(parents=True, exist_ok=True)

        # Prune old qqofficial audio temp files to avoid unbounded temp dir growth
        cutoff = datetime.now() - timedelta(hours=24)
        for path in temp_dir.glob("qqofficial_*"):
            try:
                if path.is_file() and datetime.fromtimestamp(path.stat().st_mtime) < cutoff:
                    path.unlink(missing_ok=True)
            except OSError:
                # Best-effort cleanup; ignore failures so we don't break message handling
                pass

        ext = Path(filename).suffix.lower()
        source_ext = ext or ".audio"

To make this compile, ensure the following imports exist at the top of qqofficial_platform_adapter.py (or in the appropriate imports section):

  1. from datetime import datetime, timedelta

If these imports are already present (perhaps used elsewhere in the file), no further changes are needed.

await download_file(url, str(source_path))

return Record(file=str(source_path), url=str(source_path))
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

For consistency with how Record components are created from file paths elsewhere in the codebase, and to ensure the semantic correctness of the url attribute, it would be better to use Record.fromFileSystem(). This will create a Record object with a proper file:// URI and is more idiomatic.

Suggested change
return Record(file=str(source_path), url=str(source_path))
return Record.fromFileSystem(str(source_path))


@staticmethod
async def _append_attachments(
msg: list[BaseMessageComponent],
attachments: list | None,
) -> None:
Expand All @@ -363,7 +382,7 @@ def _append_attachments(
or getattr(attachment, "name", None)
or "attachment",
)
ext = os.path.splitext(filename)[1].lower()
ext = Path(filename).suffix.lower()
image_exts = {".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp"}
audio_exts = {
".mp3",
Expand All @@ -381,8 +400,21 @@ def _append_attachments(
".webm",
}

if content_type.startswith("audio") or ext in audio_exts:
msg.append(Record.fromURL(url))
if content_type.startswith("voice") or ext in audio_exts:
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.

issue (bug_risk): Switching the content-type check from 'audio' to 'voice' may miss standard audio/* payloads.

This used to match content_type.startswith("audio"), but now only startswith("voice") triggers this branch. Unless QQ always uses voice/* for audio, audio/* types like audio/mpeg or audio/ogg will no longer be handled here and will fall through to other branches. If you want to support both, consider checking for audio/* and voice/* (e.g. content_type.startswith("audio") or content_type.startswith("voice")).

try:
msg.append(
await QQOfficialPlatformAdapter._prepare_audio_attachment(
url,
filename,
)
)
except Exception as e:
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

Catching a broad Exception can hide unrelated issues (like TypeError or KeyboardInterrupt) and make debugging more difficult. It's better to catch more specific exceptions related to network or file operations that you expect to happen here. For example, you could catch (aiohttp.ClientError, IOError) after importing aiohttp.

logger.warning(
"[QQOfficial] Failed to prepare audio attachment %s: %s",
url,
e,
)
msg.append(Record.fromURL(url))
elif content_type.startswith("video") or ext in video_exts:
msg.append(Video.fromURL(url))
elif content_type.startswith("image") or ext in image_exts:
Expand Down Expand Up @@ -432,13 +464,13 @@ def replace_face(match):
return re.sub(r"<faceType=\d+[^>]*>", replace_face, content)

@staticmethod
def _parse_from_qqofficial(
async def _parse_from_qqofficial(
message: botpy.message.Message
| botpy.message.GroupMessage
| botpy.message.DirectMessage
| botpy.message.C2CMessage,
message_type: MessageType,
):
) -> AstrBotMessage:
abm = AstrBotMessage()
abm.type = message_type
abm.timestamp = int(time.time())
Expand All @@ -463,7 +495,9 @@ def _parse_from_qqofficial(
abm.self_id = "unknown_selfid"
msg.append(At(qq="qq_official"))
msg.append(Plain(abm.message_str))
QQOfficialPlatformAdapter._append_attachments(msg, message.attachments)
await QQOfficialPlatformAdapter._append_attachments(
msg, message.attachments
)
abm.message = msg

elif isinstance(message, botpy.message.Message) or isinstance(
Expand All @@ -482,7 +516,9 @@ def _parse_from_qqofficial(
).strip()
)

QQOfficialPlatformAdapter._append_attachments(msg, message.attachments)
await QQOfficialPlatformAdapter._append_attachments(
msg, message.attachments
)
abm.message = msg
abm.message_str = plain_content
abm.sender = MessageMember(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def set_platform(self, platform: "QQOfficialWebhookPlatformAdapter") -> None:
async def on_group_at_message_create(
self, message: botpy.message.GroupMessage
) -> None:
abm = QQOfficialPlatformAdapter._parse_from_qqofficial(
abm = await QQOfficialPlatformAdapter._parse_from_qqofficial(
message,
MessageType.GROUP_MESSAGE,
)
Expand All @@ -42,7 +42,7 @@ async def on_group_at_message_create(

# 收到频道消息
async def on_at_message_create(self, message: botpy.message.Message) -> None:
abm = QQOfficialPlatformAdapter._parse_from_qqofficial(
abm = await QQOfficialPlatformAdapter._parse_from_qqofficial(
message,
MessageType.GROUP_MESSAGE,
)
Expand All @@ -55,7 +55,7 @@ async def on_at_message_create(self, message: botpy.message.Message) -> None:
async def on_direct_message_create(
self, message: botpy.message.DirectMessage
) -> None:
abm = QQOfficialPlatformAdapter._parse_from_qqofficial(
abm = await QQOfficialPlatformAdapter._parse_from_qqofficial(
message,
MessageType.FRIEND_MESSAGE,
)
Expand All @@ -65,7 +65,7 @@ async def on_direct_message_create(

# 收到 C2C 消息
async def on_c2c_message_create(self, message: botpy.message.C2CMessage) -> None:
abm = QQOfficialPlatformAdapter._parse_from_qqofficial(
abm = await QQOfficialPlatformAdapter._parse_from_qqofficial(
message,
MessageType.FRIEND_MESSAGE,
)
Expand Down
Loading