使用DSharpPlus时SlashCommand与按钮编辑消息触发NotFound错误的修复
问题描述
基于DSharpPlus开发的Discord机器人,功能为:用户选择分类并输入查询词,机器人通过GraphQL客户端搜索后发送含5条结果的消息;用户点击1-5号按钮选择对象,机器人编辑消息。首次使用命令与按钮时功能正常,但第二次执行完整流程时,控制台抛出NotFound错误,错误日志如下:
[2023-12-25 02:56:21 +01:00] [104 /EventHandler] [Error] Event handler exception for event COMPONENT_INTERACTED thrown from System.Threading.Tasks.Task <SearchCommand>b__0(DSharpPlus.DiscordClient, DSharpPlus.EventArgs.ComponentInteractionCreateEventArgs) (defined in AnimeSharpBot.SearchModule+<>c__DisplayClass0_0) DSharpPlus.Exceptions.NotFoundException: Not found: 404 at DSharpPlus.Net.DiscordApiClient.CreateInteractionResponseAsync(UInt64 interaction_id, String interaction_token, InteractionResponseType type, DiscordInteractionResponseBuilder builder) at AnimeSharpBot.SearchModule.ButtonClick(DiscordClient sender, ComponentInteractionCreateEventArgs e, IEnumerable`1 animeDetails) in C:\Users\mrady\RiderProjects\AnimeSharpBot\SearchModule.cs:line 110 at AnimeSharpBot.SearchModule.<>c__DisplayClass0_0.<<SearchCommand>b__0>d.MoveNext() in C:\Users\mrady\RiderProjects\AnimeSharpBot\SearchModule.cs:line 44 --- End of stack trace from previous location --- at DSharpPlus.AsyncEvents.AsyncEvent`2.<>c__DisplayClass7_0.<<InvokeAsync>b__0>d.MoveNext()
功能仍能正常运行,但该错误持续出现。机器人核心代码如下:
using System.Text; using DSharpPlus; using DSharpPlus.Entities; using DSharpPlus.SlashCommands; using AniListNet; using AniListNet.Objects; using AniListNet.Parameters; using DSharpPlus.EventArgs; namespace AnimeSharpBot; public class SearchModule : ApplicationCommandModule { [SlashCommand("search", "Search for anime, manga, staff, character, or studio")] public async Task SearchCommand(InteractionContext ctx, [Option("category", "Category to search for")] [Choice("Anime", "anime")] [Choice("Manga", "manga")] [Choice("Staff", "staff")] [Choice("Character", "character")] [Choice("Studio", "studio")] string category, [Option("query", "Search query")] string query) { var (resultMessage, animeDetails) = await SearchAsync(category, query); var row = new DiscordComponent[] { new DiscordButtonComponent(ButtonStyle.Primary, "option_1", "1"), new DiscordButtonComponent(ButtonStyle.Primary, "option_2", "2"), new DiscordButtonComponent(ButtonStyle.Primary, "option_3", "3"), new DiscordButtonComponent(ButtonStyle.Primary, "option_4", "4"), new DiscordButtonComponent(ButtonStyle.Primary, "option_5", "5"), }; var message = new DiscordMessageBuilder() .WithContent(resultMessage) .AddComponents(row) .SendAsync(ctx.Channel); var client = ctx.Client; client.ComponentInteractionCreated += async (sender, e) => await ButtonClick(sender, e, animeDetails); } private async Task<(string ResultMessage, IEnumerable<Media> AnimeDetails)> SearchAsync(string category, string query) { var resultMessage = new StringBuilder(); var client = new AniClient(); IEnumerable<Media> animeDetails = null; if (category.Equals("anime", StringComparison.OrdinalIgnoreCase)) { var results = await client.SearchMediaAsync(new SearchMediaFilter { Query = query, Type = MediaType.Anime, Sort = MediaSort.Popularity, Format = new Dictionary<MediaFormat, bool> { { MediaFormat.TV, true }, { MediaFormat.Movie, true }, { MediaFormat.TVShort, true } } }); for (int i = 0; i < Math.Min(results.Data.Length, 5); i++) { var animeTitle = results.Data[i].Title.EnglishTitle; resultMessage.AppendLine($"Result {i + 1}: {animeTitle}"); } animeDetails = results.Data; } else if (category.Equals("manga", StringComparison.OrdinalIgnoreCase)) { } else if (category.Equals("staff", StringComparison.OrdinalIgnoreCase)) { } else if (category.Equals("character", StringComparison.OrdinalIgnoreCase)) { } else if (category.Equals("studio", StringComparison.OrdinalIgnoreCase)) { } else { return ("Please choose a valid category (anime, manga, staff, character, studio).", null); } return (resultMessage.ToString(), animeDetails); } private async Task ButtonClick(DiscordClient sender, ComponentInteractionCreateEventArgs e, IEnumerable<Media> animeDetails) { int selectedOption = int.Parse(e.Id.Substring("option_".Length)); var selectedAnime = animeDetails.ElementAtOrDefault(selectedOption - 1); if (selectedAnime != null) { await e.Interaction.CreateResponseAsync(InteractionResponseType.UpdateMessage, new DiscordInteractionResponseBuilder().WithContent($"You selected option {selectedOption}! Here are the details:\n{selectedAnime.Title.EnglishTitle} (ID: {selectedAnime.Id})\n{selectedAnime.Description}")); } } }
错误原因与修复方案
错误原因
每次执行SearchCommand时,都会给ComponentInteractionCreated全局事件添加一个新的处理器。当第二次点击按钮时,所有已注册的处理器都会触发:第一个处理器会正常响应交互,第二个处理器再尝试响应时,Discord会返回404错误,因为该交互已经被处理过,无法重复响应。
修复方案
使用DSharpPlus提供的WaitForComponentAsync方法替代全局事件注册,该方法会为当前命令实例创建一个临时的组件等待器,仅处理本次命令发送的按钮点击,避免全局事件冲突。修改后的核心代码如下:
[SlashCommand("search", "Search for anime, manga, staff, character, or studio")] public async Task SearchCommand(InteractionContext ctx, [Option("category", "Category to search for")] [Choice("Anime", "anime")] [Choice("Manga", "manga")] [Choice("Staff", "staff")] [Choice("Character", "character")] [Choice("Studio", "studio")] string category, [Option("query", "Search query")] string query) { var (resultMessage, animeDetails) = await SearchAsync(category, query); if (animeDetails == null) { await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().WithContent(resultMessage)); return; } var row = new DiscordComponent[] { new DiscordButtonComponent(ButtonStyle.Primary, "option_1", "1"), new DiscordButtonComponent(ButtonStyle.Primary, "option_2", "2"), new DiscordButtonComponent(ButtonStyle.Primary, "option_3", "3"), new DiscordButtonComponent(ButtonStyle.Primary, "option_4", "4"), new DiscordButtonComponent(ButtonStyle.Primary, "option_5", "5"), }; // 先确认命令交互,避免Discord超时 await ctx.DeferAsync(); var message = await new DiscordMessageBuilder() .WithContent(resultMessage) .AddComponents(row) .SendAsync(ctx.Channel); // 仅监听当前消息的按钮点击 var componentResult = await message.WaitForComponentAsync(e => e.Id.StartsWith("option_")); await HandleButtonClick(componentResult, animeDetails); } // 替换原ButtonClick方法,适配临时组件等待器的返回结果 private async Task HandleButtonClick(ComponentInteractionResult result, IEnumerable<Media> animeDetails) { int selectedOption = int.Parse(result.Interaction.Id.Substring("option_".Length)); var selectedAnime = animeDetails.ElementAtOrDefault(selectedOption - 1); if (selectedAnime != null) { // 清理Description中的HTML标签,处理空标题 var cleanDescription = System.Text.RegularExpressions.Regex.Replace(selectedAnime.Description ?? "", "<.*?>", string.Empty); var title = selectedAnime.Title.EnglishTitle ?? selectedAnime.Title.RomajiTitle ?? selectedAnime.Title.NativeTitle; await result.Interaction.CreateResponseAsync(InteractionResponseType.UpdateMessage, new DiscordInteractionResponseBuilder().WithContent( $"You selected option {selectedOption}! Here are the details:\n{title} (ID: {selectedAnime.Id})\n{cleanDescription}")); } else { await result.Interaction.CreateResponseAsync(InteractionResponseType.UpdateMessage, new DiscordInteractionResponseBuilder().WithContent("Invalid selection!")); } }
关键修改点:
- 使用
ctx.DeferAsync()先确认命令交互,避免Discord判定超时 - 用
message.WaitForComponentAsync()替代全局事件注册,仅监听当前消息的按钮 - 移除全局事件绑定,彻底避免重复处理器冲突
代码优化建议
- 复用AniClient实例:每次
SearchAsync创建新的AniClient会增加开销,建议将其作为模块私有字段初始化,复用同一个实例:
private readonly AniClient _aniClient = new AniClient(); private async Task<(string ResultMessage, IEnumerable<Media> AnimeDetails)> SearchAsync(string category, string query) { // 使用_aniClient替代new AniClient() }
- 优化分类判断逻辑:用
switch语句替代多个if-else,代码更简洁易维护:
switch (category.ToLower()) { case "anime": // 处理动漫搜索逻辑 break; case "manga": // 处理漫画搜索逻辑 break; case "staff": // 处理staff搜索逻辑 break; case "character": // 处理角色搜索逻辑 break; case "studio": // 处理工作室搜索逻辑 break; default: return ("Please choose a valid category (anime, manga, staff, character, studio).", null); }
- 添加按钮唯一标识:为避免不同搜索请求的按钮互相干扰,可给按钮ID添加短UUID前缀:
var searchId = Guid.NewGuid().ToString("N")[..8]; var row = new DiscordComponent[] { new DiscordButtonComponent(ButtonStyle.Primary, $"{searchId}_option_1", "1"), new DiscordButtonComponent(ButtonStyle.Primary, $"{searchId}_option_2", "2"), // 其他按钮同理 }; // 等待时匹配对应前缀 var componentResult = await message.WaitForComponentAsync(e => e.Id.StartsWith($"{searchId}_option_"));
完善空值处理:对
Media对象的所有可能为null的字段(如标题、描述)添加降级逻辑,避免空引用异常。异步操作全await:确保所有异步方法调用都添加
await,避免异步操作未完成就执行后续逻辑。
内容的提问来源于stack exchange,提问作者ziomek64
相关产品推荐
相关产品推荐

