mirror of
https://github.com/AstrBotDevs/AstrBot.git
synced 2026-10-13 16:37:03 +08:00
fix: include Discord reply text in model requests (#10452)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)])
|
||||
|
||||
Reference in New Issue
Block a user