You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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()替代全局事件注册,仅监听当前消息的按钮
  • 移除全局事件绑定,彻底避免重复处理器冲突
代码优化建议
  1. 复用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()
}
  1. 优化分类判断逻辑:用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);
}
  1. 添加按钮唯一标识:为避免不同搜索请求的按钮互相干扰,可给按钮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_"));
  1. 完善空值处理:对Media对象的所有可能为null的字段(如标题、描述)添加降级逻辑,避免空引用异常。

  2. 异步操作全await:确保所有异步方法调用都添加await,避免异步操作未完成就执行后续逻辑。

内容的提问来源于stack exchange,提问作者ziomek64

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.03 15:35:55