fix: include Discord reply text in model requests (#10452)

This commit is contained in:
Wei Chengqian
2026-10-11 11:46:44 +08:00
committed by GitHub
parent a06ea2e4b6
commit a457e59923
2 changed files with 145 additions and 3 deletions
@@ -3,13 +3,14 @@ import re
import sys
from typing import Any, cast
import aiohttp
import discord
from discord.abc import GuildChannel, Messageable, PrivateChannel
from discord.channel import DMChannel
from astrbot import logger
from astrbot.api.event import MessageChain
from astrbot.api.message_components import File, Image, Plain, Record
from astrbot.api.message_components import File, Image, Plain, Record, Reply
from astrbot.api.platform import (
AstrBotMessage,
MessageMember,
@@ -292,6 +293,38 @@ class DiscordPlatformAdapter(Platform):
"""将平台消息转换成 AstrBotMessage"""
# 由于 on_interaction 已被禁用,我们只处理普通消息
abm = self._convert_message_to_abm(data)
message = data["message"]
reference = getattr(message, "reference", None)
if reference and message.type == discord.MessageType.reply:
quoted = reference.resolved
if quoted is None:
quoted = reference.cached_message
if quoted is None and reference.message_id is not None:
try:
quoted = await message.channel.fetch_message(reference.message_id)
except (
discord.HTTPException,
aiohttp.ClientError,
asyncio.TimeoutError,
OSError,
) as e:
logger.warning(
"[Discord] Could not fetch replied-to message %s: %s",
reference.message_id,
e,
)
# Deleted references are sentinels without message content.
if isinstance(quoted, discord.Message):
abm.message.insert(
0,
Reply(
id=str(quoted.id),
chain=[Plain(text=quoted.content)] if quoted.content else [],
message_str=quoted.content,
sender_id=str(quoted.author.id),
sender_nickname=quoted.author.display_name,
),
)
for component in abm.message:
if isinstance(component, Record):
audio_ref = component.url or component.file
+111 -2
View File
@@ -1,11 +1,14 @@
import asyncio
import base64
from io import BufferedReader, BytesIO
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock
from unittest.mock import AsyncMock, MagicMock, Mock
import aiohttp
import discord
import pytest
from astrbot.api.message_components import File, Image, Plain, Record, Video
from astrbot.api.message_components import File, Image, Plain, Record, Reply, Video
from astrbot.api.platform import Group, MessageType
from astrbot.core.message.message_event_result import MessageChain
from astrbot.core.platform.sources.discord import (
@@ -27,6 +30,112 @@ _WAV_BYTES = b"RIFF\x24\x00\x00\x00WAVEfmt " + b"\x00" * 16
_WAV_PATH = "/tmp/discord_voice.wav"
@pytest.mark.asyncio
@pytest.mark.parametrize("source", ["resolved", "cached", "fetched"])
async def test_discord_reply_body_reaches_model_request(source):
"""Verify that Discord reply text reaches the model request.
Args:
source: Where the referenced Discord message is available.
"""
from astrbot.core.astr_main_agent import _process_quote_message
from astrbot.core.provider.entities import ProviderRequest
adapter = DiscordPlatformAdapter.__new__(DiscordPlatformAdapter)
adapter.bot_self_id = "1"
adapter.client = SimpleNamespace(user=SimpleNamespace(id=1))
quoted = Mock(
spec=discord.Message,
id=41,
content="<@1> TEST_CODE 1008",
author=SimpleNamespace(id=3, display_name="original sender"),
)
channel = SimpleNamespace(
id=123, guild=None, fetch_message=AsyncMock(return_value=quoted)
)
message = SimpleNamespace(
id=42,
type=discord.MessageType.reply,
content="<@1> What is the quoted code?",
channel=channel,
author=SimpleNamespace(id=2, display_name="tester"),
attachments=[],
reference=SimpleNamespace(
message_id=41,
resolved=quoted if source == "resolved" else None,
cached_message=quoted if source == "cached" else None,
),
)
abm = await adapter.convert_message({"message": message})
req = ProviderRequest(prompt=abm.message_str)
await _process_quote_message(SimpleNamespace(message_obj=abm), req, "", Mock())
assert req.prompt == "What is the quoted code?"
quoted_text = "\n".join(part.text for part in req.extra_user_content_parts)
assert quoted.content in quoted_text
assert quoted.author.display_name in quoted_text
reply = next(comp for comp in abm.message if isinstance(comp, Reply))
assert str(reply.sender_id) == "3"
if source == "fetched":
channel.fetch_message.assert_awaited_once_with(41)
else:
channel.fetch_message.assert_not_awaited()
@pytest.mark.asyncio
@pytest.mark.parametrize(
"case",
["deleted", "non_reply", "http_error", "timeout", "disconnect", "cancelled"],
)
async def test_discord_unavailable_reply_preserves_current_message(case):
"""Preserve the current message on quote failure, but propagate cancellation.
Args:
case: Unavailable reference or interrupted lookup scenario.
"""
adapter = DiscordPlatformAdapter.__new__(DiscordPlatformAdapter)
adapter.bot_self_id = "1"
adapter.client = SimpleNamespace(user=SimpleNamespace(id=1))
reference = SimpleNamespace(message_id=41, resolved=None, cached_message=None)
errors = {
"http_error": discord.Forbidden(
SimpleNamespace(status=403, reason="Forbidden"), "Cannot read message"
),
"timeout": asyncio.TimeoutError("Lookup timed out"),
"disconnect": aiohttp.ServerDisconnectedError("Connection lost"),
"cancelled": asyncio.CancelledError(),
}
channel = SimpleNamespace(
id=123, guild=None, fetch_message=AsyncMock(side_effect=errors.get(case))
)
if case == "deleted":
reference.resolved = discord.DeletedReferencedMessage(reference)
message = SimpleNamespace(
id=42,
type=discord.MessageType.default
if case == "non_reply"
else discord.MessageType.reply,
content="<@1> Current question",
channel=channel,
author=SimpleNamespace(id=2, display_name="tester"),
attachments=[],
reference=reference,
)
if case == "cancelled":
with pytest.raises(asyncio.CancelledError):
await adapter.convert_message({"message": message})
else:
abm = await adapter.convert_message({"message": message})
assert abm.message_str == "Current question"
assert abm.message == [Plain(text="Current question")]
if case in errors:
channel.fetch_message.assert_awaited_once_with(41)
else:
channel.fetch_message.assert_not_awaited()
@pytest.mark.asyncio
@pytest.mark.parametrize("allow_bot_messages", [False, True])
@pytest.mark.parametrize(("author_id", "is_bot"), [(1, True), (2, True), (3, False)])