Discord.js 14井字棋按钮交互报错DiscordAPIError[40060]求助
问题根源分析
你的问题核心是交互确认逻辑不规范,以及游戏状态管理缺失:
DiscordAPIError[40060]:同一个按钮交互被多次调用reply()/update()/deferUpdate(),或者先defer后又重复确认交互。- 应用无响应:按钮点击后未在3秒内调用任何交互确认方法,Discord判定超时。
- 偶尔正常:仅当代码逻辑刚好在3秒内完成且无重复确认时,才能正常运行。
修复方案
- 强制及时确认交互:所有按钮点击事件第一时间调用
deferUpdate()(更新现有消息场景用该方法,而非deferReply),给后续逻辑争取处理时间。 - 避免重复确认:确保每个交互只被确认一次,逻辑分支中不要重复调用
update/reply类方法。 - 维护游戏状态:用对象存储每个对局的状态(当前玩家、棋盘格子状态),避免重复操作或逻辑冲突。
- 按钮ID携带状态:按钮ID包含格子坐标,方便快速定位操作位置。
优化后的完整代码示例
const { Client, GatewayIntentBits, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js'); const client = new Client({ intents: [GatewayIntentBits.Guilds] }); // 存储活跃对局:key为频道ID+用户ID,value为游戏状态 const activeGames = new Map(); client.on('ready', () => { console.log(`Logged in as ${client.user.tag}!`); }); // 触发游戏的命令(示例:/tic-tac-toe) client.on('interactionCreate', async interaction => { if (!interaction.isChatInputCommand() || interaction.commandName !== 'tic-tac-toe') return; const gameKey = `${interaction.channelId}-${interaction.user.id}`; // 初始化游戏状态 activeGames.set(gameKey, { currentPlayer: 'X', // X先手 board: Array(9).fill(null), // 9个格子初始为空 }); // 构建井字棋按钮行 const rows = []; for (let i = 0; i < 3; i++) { const row = new ActionRowBuilder(); for (let j = 0; j < 3; j++) { const index = i * 3 + j; row.addComponents( new ButtonBuilder() .setCustomId(`ttt-${index}`) .setStyle(ButtonStyle.Secondary) .setLabel(' ') ); } rows.push(row); } await interaction.reply({ content: '井字棋游戏开始!X先下', components: rows }); }); // 处理按钮点击逻辑 client.on('interactionCreate', async interaction => { if (!interaction.isButton() || !interaction.customId.startsWith('ttt-')) return; // 第一时间确认交互,避免超时 await interaction.deferUpdate(); const gameKey = `${interaction.channelId}-${interaction.user.id}`; const game = activeGames.get(gameKey); if (!game) { await interaction.editReply({ content: '该游戏已结束或不存在,请重新发起!', components: [] }); return; } // 解析按钮对应的格子索引 const index = parseInt(interaction.customId.split('-')[1]); if (game.board[index] !== null) { await interaction.editReply({ content: `该格子已被${game.board[index]}占用,请选择其他格子!`, components: interaction.message.components }); return; } // 更新棋盘状态并切换玩家 game.board[index] = game.currentPlayer; game.currentPlayer = game.currentPlayer === 'X' ? 'O' : 'X'; // 更新按钮样式与文本 const updatedRows = interaction.message.components.map(row => { return ActionRowBuilder.from(row).setComponents( row.components.map(btn => { const btnIndex = parseInt(btn.customId.split('-')[1]); if (btnIndex === index) { return ButtonBuilder.from(btn) .setStyle(game.board[index] === 'X' ? ButtonStyle.Primary : ButtonStyle.Danger) .setLabel(game.board[index]) .setDisabled(true); // 点击后禁用按钮,防止重复触发 } return btn; }) ); }); // 检查胜负或平局 const winner = checkWinner(game.board); let content = `轮到${game.currentPlayer}下棋!`; if (winner) { content = `🎉 ${winner}获胜!游戏结束!`; // 禁用所有按钮 updatedRows.forEach(row => row.components.forEach(btn => btn.setDisabled(true))); activeGames.delete(gameKey); // 清理游戏状态 } else if (game.board.every(cell => cell !== null)) { content = '🤝 平局!游戏结束!'; activeGames.delete(gameKey); } // 更新消息内容与组件 await interaction.editReply({ content, components: updatedRows }); }); // 胜负判断辅助函数 function checkWinner(board) { const winPatterns = [ [0,1,2], [3,4,5], [6,7,8], // 横向 [0,3,6], [1,4,7], [2,5,8], // 纵向 [0,4,8], [2,4,6] // 对角线 ]; for (const pattern of winPatterns) { const [a, b, c] = pattern; if (board[a] && board[a] === board[b] && board[a] === board[c]) { return board[a]; } } return null; } client.login('你的机器人令牌');
关键修复点说明
- 及时确认交互:按钮点击后立即调用
deferUpdate(),确保Discord在3秒内收到确认,避免超时提示。 - 单一交互确认:后续仅调用一次
editReply(),杜绝重复确认导致的40060错误。 - 状态管理:用
activeGames存储对局状态,防止无状态导致的逻辑混乱。 - 按钮禁用:点击过的格子按钮直接禁用,避免重复触发交互。
- 胜负检测:每次操作后自动检查胜负,及时结束游戏并清理状态。
内容的提问来源于stack exchange,提问作者Wutachi
相关产品推荐
相关产品推荐

