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

Discord.py 2.0:并行使用同一命令时交互处理失败求助

Discord机器人多用户同时调用游戏命令时交互失败问题排查与修复

问题描述

我正在开发一款搭载多款小游戏的Discord机器人(一个命令对应一款游戏),游戏采用交互按钮操作。当前遇到异步处理问题:当多个用户同时使用同一命令(比如两人同时玩带方向按钮的迷宫游戏),会出现交互失败,进而影响每个命令实例的正常运行。

迷宫命令的代码结构如下:

@bot.command(brief="...",help="...")
@commands.cooldown(1, 60*60, commands.BucketType.member)
async def maze(ctx):
    #PostreSQL check
    cur = conn.cursor()
    req = "..."
    cur.execute(req)
    rows = cur.fetchall()
    if len(rows)>0 :    
        m = Maze(13,13) #Generate a 13x13 maze board
        brd = m.saveEmojiBoard(True) #str representation of the board (unicode emojis)
        class myButton(ui.Button):
            def __init__(self,label,style):
                super().__init__(label=label,style=style)
                    
            async def callback(self,interaction):
                nonlocal rows
                if interaction.user.id==ctx.author.id  :
                    moved = False                  
                    if  str(self.label)=="\U000025C0":
                        moved = m.left()
                    elif  str(self.label)=="\U000025B2":
                        moved = m.up()
                    elif  str(self.label)=="\U000025BC":
                        moved = m.down()
                    elif  str(self.label)=="\U000025B6":
                        moved = m.right()  
                    if moved: 
                        brd = m.saveEmojiBoard(True) #Update board if position changed
                        await self.view.msg.edit(content=brd)   
                        await interaction.response.defer()     
                        if m.goalreached() : 
                            #Game ends when exit found, then update database
                            req = "..."
                            cur.execute(req)
                            conn.commit()
                            self.view.stop()
                            await ctx.send("...") 
                    else : await interaction.response.defer()                    
                else : await interaction.response.defer()
                        
        class myView(ui.View):   
            def __init__(self):
                super().__init__(timeout=120)   
                self.add_item(myButton("\U000025C0",style=discord.ButtonStyle.primary))
                self.add_item(myButton("\U000025B2",style=discord.ButtonStyle.primary))
                self.add_item(myButton("\U000025BC",style=discord.ButtonStyle.primary))
                self.add_item(myButton("\U000025B6",style=discord.ButtonStyle.primary))
                              
            async def on_timeout(self):
                await ctx.send("...")
        
            async def on_error(self,error, item, interaction) :
                print("...")  
                traceback.print_exc()
                
        v = myView()    
        await ctx.send(view=v) 
                           
    else : await ctx.send("...")  

迷宫游戏截图

问题根源

  1. 嵌套类的上下文污染:在maze命令函数内部定义myButton和myView类,每次调用命令都会重新定义类,但Python闭包特性会导致不同实例意外共享外部作用域变量(比如m、ctx),多用户操作时上下文直接混乱。
  2. 全局数据库游标共享:用全局的conn.cursor(),多用户并发操作时游标冲突,数据库操作直接异常。
  3. View未绑定消息实例:代码里self.view.msg.edit但msg属性没在myView中初始化,会触发属性错误,直接导致交互失败。
  4. 非线程安全的游戏实例:Maze实例在命令函数内创建,但按钮回调直接操作这个实例,多实例共享上下文时状态会乱。

修复方案

1. 把View和Button类移到命令函数外,通过初始化传递上下文

抽离嵌套类,用构造函数传入游戏实例、用户ID、消息对象等,彻底避免闭包共享问题:

class MazeButton(ui.Button):
    def __init__(self, label, style, maze_instance, author_id, view):
        super().__init__(label=label, style=style)
        self.maze = maze_instance
        self.author_id = author_id
        self.parent_view = view

    async def callback(self, interaction):
        if interaction.user.id != self.author_id:
            await interaction.response.defer()
            return

        moved = False
        if self.label == "\U000025C0":
            moved = self.maze.left()
        elif self.label == "\U000025B2":
            moved = self.maze.up()
        elif self.label == "\U000025BC":
            moved = self.maze.down()
        elif self.label == "\U000025B6":
            moved = self.maze.right()

        if moved:
            new_board = self.maze.saveEmojiBoard(True)
            await self.parent_view.msg.edit(content=new_board)
            await interaction.response.defer()

            if self.maze.goalreached():
                # 数据库操作改用局部游标
                async with conn.cursor() as cur:
                    req = "..."
                    await cur.execute(req)
                    await conn.commit()
                self.parent_view.stop()
                await interaction.channel.send("恭喜你走出迷宫!")
        else:
            await interaction.response.defer()

class MazeView(ui.View):
    def __init__(self, maze_instance, author_id, ctx):
        super().__init__(timeout=120)
        self.maze = maze_instance
        self.author_id = author_id
        self.ctx = ctx
        self.msg = None  # 后续绑定发送的消息

        self.add_item(MazeButton("\U000025C0", discord.ButtonStyle.primary, maze_instance, author_id, self))
        self.add_item(MazeButton("\U000025B2", discord.ButtonStyle.primary, maze_instance, author_id, self))
        self.add_item(MazeButton("\U000025BC", discord.ButtonStyle.primary, maze_instance, author_id, self))
        self.add_item(MazeButton("\U000025B6", discord.ButtonStyle.primary, maze_instance, author_id, self))

    async def on_timeout(self):
        await self.ctx.send("游戏超时结束!")
        self.stop()

    async def on_error(self, error, item, interaction):
        print(f"交互出错: {error}")
        traceback.print_exc()
        await interaction.response.defer()

2. 修改命令函数,用局部数据库游标并绑定View的消息实例

@bot.command(brief="迷宫游戏", help="游玩13x13的迷宫,用方向按钮移动")
@commands.cooldown(1, 3600, commands.BucketType.member)
async def maze(ctx):
    # 用局部游标,避免全局共享冲突
    async with conn.cursor() as cur:
        req = "..."
        await cur.execute(req)
        rows = await cur.fetchall()

    if len(rows) > 0:
        m = Maze(13, 13)
        brd = m.saveEmojiBoard(True)
        v = MazeView(m, ctx.author.id, ctx)
        # 发送消息并绑定到View的msg属性
        v.msg = await ctx.send(content=brd, view=v)
    else:
        await ctx.send("你暂时无法游玩这个游戏!")

3. 确保数据库连接异步安全

如果用asyncpg这类异步PostgreSQL库,确保每个操作使用独立游标或连接;如果是同步库,要用线程池包装,避免阻塞事件循环。

4. 严谨验证用户交互权限

在按钮回调里,除了检查用户ID,还可以检查交互消息是否属于当前View实例,进一步避免跨实例干扰。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.20 19:15:44