Skip to content
Open
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
50 changes: 33 additions & 17 deletions astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
from collections.abc import AsyncGenerator

from aiocqhttp import CQHttp, Event
from aiocqhttp.exceptions import ApiNotAvailable

from astrbot import logger
from astrbot.api.event import AstrMessageEvent, MessageChain
from astrbot.api.message_components import (
At,
Expand Down Expand Up @@ -103,23 +105,37 @@ async def _dispatch_send(
if isinstance(event, Event) and event.get("self_id"):
routing_params["self_id"] = event["self_id"]

if is_group and isinstance(session_id_int, int):
await bot.send_group_msg(
group_id=session_id_int,
message=messages,
**routing_params,
)
elif not is_group and isinstance(session_id_int, int):
await bot.send_private_msg(
user_id=session_id_int,
message=messages,
**routing_params,
)
elif isinstance(event, Event): # 最后兜底
await bot.send(event=event, message=messages)
else:
raise ValueError(
f"无法发送消息:缺少有效的数字 session_id({session_id}) 或 event({event})",
try:
if is_group and isinstance(session_id_int, int):
await bot.send_group_msg(
group_id=session_id_int,
message=messages,
**routing_params,
)
elif not is_group and isinstance(session_id_int, int):
await bot.send_private_msg(
user_id=session_id_int,
message=messages,
**routing_params,
)
elif isinstance(event, Event): # 最后兜底
await bot.send(event=event, message=messages, **routing_params)
else:
raise ValueError(
f"无法发送消息:缺少有效的数字 session_id({session_id}) 或 event({event})",
)
except ApiNotAvailable:

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

While handling ApiNotAvailable in _dispatch_send is a great improvement, the send_message method also calls bot.call_action directly for forward messages (when isinstance(seg, Node | Nodes)) without wrapping them in a try...except ApiNotAvailable block. If the API is unavailable when sending a forward message, it will still raise an unhandled ApiNotAvailable exception.

Consider wrapping those bot.call_action calls in send_message with a similar try...except ApiNotAvailable block to ensure consistent error handling across all message types.

if isinstance(event, Event) and isinstance(session_id_int, int):
try:
await bot.send(event=event, message=messages, **routing_params)
return
except ApiNotAvailable:
pass
logger.warning(
"aiocqhttp API unavailable, message skipped. "
"Please check that the OneBot reverse WebSocket API connection is active. "
f"self_id={routing_params.get('self_id')}, "
f"session_id={session_id}, is_group={is_group}"
)

@classmethod
Expand Down
69 changes: 69 additions & 0 deletions tests/unit/test_aiocqhttp_api_unavailable.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
from unittest.mock import AsyncMock

import pytest
from aiocqhttp import Event
from aiocqhttp.exceptions import ApiNotAvailable

import astrbot.core.message.components as Comp
from astrbot.core.message.message_event_result import MessageChain
from astrbot.core.platform.sources.aiocqhttp.aiocqhttp_message_event import (
AiocqhttpMessageEvent,
)


@pytest.mark.asyncio
async def test_aiocqhttp_send_message_falls_back_to_event_send_when_api_unavailable():
bot = AsyncMock()
bot.send_private_msg.side_effect = ApiNotAvailable
event = Event.from_payload(
{
"post_type": "message",
"message_type": "private",
"sub_type": "friend",
"self_id": 12345,
"user_id": 987654,
"message_id": 1,
"message": [],
}
)
chain = MessageChain([Comp.Plain("hello")])

await AiocqhttpMessageEvent.send_message(
bot=bot,
message_chain=chain,
event=event,
is_group=False,
session_id="987654",
)

bot.send_private_msg.assert_awaited_once_with(
user_id=987654,
message=[{"type": "text", "data": {"text": "hello"}}],
self_id=12345,
)
bot.send.assert_awaited_once_with(
event=event,
message=[{"type": "text", "data": {"text": "hello"}}],
self_id=12345,
)


@pytest.mark.asyncio
async def test_aiocqhttp_send_message_does_not_raise_when_api_unavailable_without_event():
bot = AsyncMock()
bot.send_private_msg.side_effect = ApiNotAvailable
chain = MessageChain([Comp.Plain("hello")])

await AiocqhttpMessageEvent.send_message(
bot=bot,
message_chain=chain,
event=None,
is_group=False,
session_id="987654",
)

bot.send_private_msg.assert_awaited_once_with(
user_id=987654,
message=[{"type": "text", "data": {"text": "hello"}}],
)
bot.send.assert_not_awaited()

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

To ensure full test coverage of the fallback failure path (where both the primary send and the fallback bot.send fail with ApiNotAvailable), we should add a test case verifying that the exception is caught and does not propagate.

    bot.send.assert_not_awaited()


@pytest.mark.asyncio
async def test_aiocqhttp_send_message_both_apis_unavailable():
    bot = AsyncMock()
    bot.send_private_msg.side_effect = ApiNotAvailable
    bot.send.side_effect = ApiNotAvailable
    event = Event.from_payload(
        {
            "post_type": "message",
            "message_type": "private",
            "sub_type": "friend",
            "self_id": 12345,
            "user_id": 987654,
            "message_id": 1,
            "message": [],
        }
    )
    chain = MessageChain([Comp.Plain("hello")])

    await AiocqhttpMessageEvent.send_message(
        bot=bot,
        message_chain=chain,
        event=event,
        is_group=False,
        session_id="987654",
    )

    bot.send_private_msg.assert_awaited_once()
    bot.send.assert_awaited_once()

Loading