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

Discord.py按钮交互任务取消问题:角色检测失效求助

Discord.py旅行任务取消问题解决方案

问题背景

我正在开发一款Discord游戏,每个频道对应游戏中的一个地点。设置了按钮菜单,用户点击按钮1时,会通过添加/移除频道关联角色实现频道切换。按钮1的执行包含asyncio.sleep()延迟以模拟旅行耗时。用户点击按钮1后会出现按钮2,按钮2的目标是在按钮1执行过程中取消其任务。

我尝试在按钮1执行结束前添加判断:检查用户是否拥有点击按钮2时添加的'cancel'角色,若有则终止执行,否则完成频道切换。

遇到的问题

通过interaction.user获取的用户对象是交互发生时的快照,不会自动更新后续的角色变化。因此即使点击按钮2添加了'cancel'角色,按钮1的判断仍无法识别,导致任务无法取消。

原始代码

import asyncio
import discord
from discord.ext import commands

# this just imports role names/ids from another file
import data.roles as ccroles


class cancel(discord.ui.View):
    def __init__(self):
        super().__init__(timeout = None)
    @discord.ui.button(label="Cancel",
                       custom_id="cancel",
                       style=discord.ButtonStyle.red)
    async def buttoncancel(self, interaction, button):
        user = interaction.user
        await interaction.response.send_message("You clicked me!",
                                                ephemeral=True,)
        await user.add_roles(user.guild.get_role(ccroles.cancel))
        print('Cancel added.')


class Menu(discord.ui.View):
    def __init__(self):
        super().__init__(timeout = None)
    @discord.ui.button(label="Location 2",
                       custom_id="location2",
                       style=discord.ButtonStyle.blurple)
    async def button1(self, interaction, button):
        user = interaction.user
        if ccroles.location_1 in [y.id for y in user.roles]:
            await interaction.response.send_message("Traveling to Location 2!",
                                                     ephemeral=True, view=cancel())
            await asyncio.sleep(10)
            print('I waited 10 seconds.')
            if ccroles.cancel in [y.id for y in user.roles]:
                print('Cancelled travel.')
                await user.remove_roles(user.guild.get_role(ccroles.cancel))
            else:
                await asyncio.sleep(1)
                print('I waited 1 second.')
                await user.edit(roles=[])
                await user.add_roles(user.guild.get_role(ccroles.location_2))

class exampleMenu(commands.Cog):
    def __init__(self, client: commands.Bot):
        self.client=client
        
    @commands.command()
    async def exampleMenu(self, ctx):
        embed = discord.Embed(title="Travel Menu",
                          description="Where would you like to travel?")
        await ctx.send(embed=embed,
                       view=Menu())

async def setup(client:commands.Bot) -> None:
    await client.add_cog(exampleMenu(client))

解决方案

方案一:刷新用户角色数据

既然interaction.user是静态快照,我们可以在需要检查角色时,重新从服务器获取用户的最新数据。修改按钮1中的角色检查逻辑:

async def button1(self, interaction, button):
    user = interaction.user
    if ccroles.location_1 in [y.id for y in user.roles]:
        await interaction.response.send_message("Traveling to Location 2!",
                                                 ephemeral=True, view=cancel())
        await asyncio.sleep(10)
        print('I waited 10 seconds.')
        # 重新拉取用户最新信息
        updated_user = await interaction.guild.fetch_member(user.id)
        if ccroles.cancel in [y.id for y in updated_user.roles]:
            print('Cancelled travel.')
            await updated_user.remove_roles(user.guild.get_role(ccroles.cancel))
        else:
            await asyncio.sleep(1)
            print('I waited 1 second.')
            await updated_user.edit(roles=[])
            await updated_user.add_roles(user.guild.get_role(ccroles.location_2))

这个方法直接解决了角色检测的问题,但需要额外的API调用,且无法提前终止sleep,必须等延迟结束才能判断是否取消。

方案二:用asyncio.Task直接管理任务(更推荐)

用角色标记取消不够高效,更好的方式是直接管理异步任务,点击取消按钮时立刻终止对应的旅行任务。这种方式不需要依赖角色,还能防止用户重复触发旅行。

修改后的完整代码:

import asyncio
import discord
from discord.ext import commands

import data.roles as ccroles


class CancelView(discord.ui.View):
    def __init__(self, user_id, cog):
        super().__init__(timeout=None)
        self.user_id = user_id
        self.cog = cog

    @discord.ui.button(label="Cancel", custom_id="cancel", style=discord.ButtonStyle.red)
    async def buttoncancel(self, interaction, button):
        if interaction.user.id != self.user_id:
            await interaction.response.send_message("你无权取消他人的旅行!", ephemeral=True)
            return
        
        # 取消并清理用户的旅行任务
        if self.user_id in self.cog.travel_tasks:
            self.cog.travel_tasks[self.user_id].cancel()
            del self.cog.travel_tasks[self.user_id]
            await interaction.response.send_message("旅行已取消!", ephemeral=True)
        else:
            await interaction.response.send_message("没有正在进行的旅行!", ephemeral=True)


class MenuView(discord.ui.View):
    def __init__(self, cog):
        super().__init__(timeout=None)
        self.cog = cog

    @discord.ui.button(label="Location 2", custom_id="location2", style=discord.ButtonStyle.blurple)
    async def button1(self, interaction, button):
        user = interaction.user
        # 检查用户当前位置
        if ccroles.location_1 not in [y.id for y in user.roles]:
            await interaction.response.send_message("你当前不在Location 1,无法前往Location 2!", ephemeral=True)
            return
        
        # 防止重复触发旅行
        if user.id in self.cog.travel_tasks:
            await interaction.response.send_message("你已经在旅行中了!", ephemeral=True)
            return
        
        await interaction.response.send_message("正在前往Location 2...", ephemeral=True, view=CancelView(user.id, self.cog))
        
        # 定义旅行任务逻辑
        async def travel_task():
            try:
                await asyncio.sleep(10)
                # 执行地点切换
                await user.edit(roles=[])
                await user.add_roles(user.guild.get_role(ccroles.location_2))
                await interaction.followup.send("已到达Location 2!", ephemeral=True)
            except asyncio.CancelledError:
                # 任务被取消时触发
                await interaction.followup.send("旅行已取消!", ephemeral=True)
            finally:
                # 无论完成还是取消,都清理任务记录
                if user.id in self.cog.travel_tasks:
                    del self.cog.travel_tasks[user.id]
        
        # 创建并存储任务
        task = asyncio.create_task(travel_task())
        self.cog.travel_tasks[user.id] = task


class exampleMenu(commands.Cog):
    def __init__(self, client: commands.Bot):
        self.client = client
        # 用字典维护每个用户的旅行任务
        self.travel_tasks = {}
        
    @commands.command()
    async def exampleMenu(self, ctx):
        embed = discord.Embed(title="Travel Menu", description="你想去哪里旅行?")
        await ctx.send(embed=embed, view=MenuView(self))

async def setup(client:commands.Bot) -> None:
    await client.add_cog(exampleMenu(client))

这个方案的优势:

  • 无需依赖角色标记,直接通过异步任务管理,响应更及时
  • 可以防止用户重复触发旅行任务
  • 取消操作会立刻终止sleep,不需要等待延迟结束

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.31 03:09:27