diff --git a/astrbot/core/platform/sources/discord/discord_platform_adapter.py b/astrbot/core/platform/sources/discord/discord_platform_adapter.py index 4c0b8eaf9..e5e023d3e 100644 --- a/astrbot/core/platform/sources/discord/discord_platform_adapter.py +++ b/astrbot/core/platform/sources/discord/discord_platform_adapter.py @@ -427,8 +427,22 @@ class DiscordPlatformAdapter(Platform): # 使用 Pycord 的方法同步指令 # 注意:这可能需要一些时间,并且有频率限制 - await self.client.sync_commands() - logger.info("[Discord] Command synchronization completed.") + try: + await self.client.sync_commands() + logger.info("[Discord] Command synchronization completed.") + except discord.HTTPException as e: + if self._is_daily_command_quota_error(e): + logger.warning( + "[Discord] Daily application command create quota reached " + "(30034); command sync skipped. Existing commands should " + "continue to work until the quota resets.", + ) + return + logger.warning(f"[Discord] Sync commands failed: {e}") + + @staticmethod + def _is_daily_command_quota_error(error: discord.HTTPException) -> bool: + return getattr(error, "code", None) == 30034 def _create_dynamic_callback(self, cmd_name: str): """为每个指令动态创建一个异步回调函数""" diff --git a/tests/test_discord_command_sync.py b/tests/test_discord_command_sync.py new file mode 100644 index 000000000..dee2db89d --- /dev/null +++ b/tests/test_discord_command_sync.py @@ -0,0 +1,71 @@ +import asyncio +from unittest.mock import Mock + +import pytest + +from tests.fixtures.mocks.discord import ( + MockDiscordBuilder, + mock_discord_modules, # noqa: F401 +) + + +class DiscordSyncError(Exception): + def __init__(self, message: str, code: int | None = None) -> None: + super().__init__(message) + self.code = code + + +def _build_adapter(monkeypatch: pytest.MonkeyPatch): + from astrbot.core.platform.sources.discord import discord_platform_adapter + from astrbot.core.platform.sources.discord.discord_platform_adapter import ( + DiscordPlatformAdapter, + ) + + monkeypatch.setattr(discord_platform_adapter, "star_handlers_registry", []) + monkeypatch.setattr( + discord_platform_adapter.discord, + "HTTPException", + DiscordSyncError, + raising=False, + ) + + adapter = DiscordPlatformAdapter( + {"discord_command_register": True}, + {}, + asyncio.Queue(), + ) + adapter.client = MockDiscordBuilder.create_client() + return adapter + + +@pytest.mark.asyncio +async def test_discord_command_sync_ignores_daily_quota(monkeypatch): + from astrbot.core.platform.sources.discord import discord_platform_adapter + + adapter = _build_adapter(monkeypatch) + warning = Mock() + monkeypatch.setattr(discord_platform_adapter.logger, "warning", warning) + adapter.client.sync_commands.side_effect = DiscordSyncError( + "Max number of daily application command creates reached", + code=30034, + ) + + await adapter._collect_and_register_commands() + + adapter.client.sync_commands.assert_awaited_once() + warning.assert_called_once() + assert "30034" in warning.call_args.args[0] + + +@pytest.mark.asyncio +async def test_discord_command_sync_keeps_other_errors_fatal(monkeypatch): + adapter = _build_adapter(monkeypatch) + adapter.client.sync_commands.side_effect = DiscordSyncError( + "Missing Access", + code=50001, + ) + + with pytest.raises(DiscordSyncError): + await adapter._collect_and_register_commands() + + adapter.client.sync_commands.assert_awaited_once()