喂!我是 Wei

Front-End Engineer

Be a Problem Solver.

⌘K

導覽

所有文章緣起互動小功能

文章分類

目錄
概念:訊息附件(Component)建立按鈕並附在訊息裡isButton():接收按鈕事件customId 帶參數按鈕逾時完整流程總覽完整骨架總結

相關文章

Discord Bot 監控與告警:Bot 掛掉時自動發通知到頻道

2026年5月18日

Discord Bot 串接 Groq:打造高速 AI 對話助理

2026年4月1日

Discord Bot 排程任務:node-cron 做每日公告與資料重置

2026年3月31日

最新文章
全部 →
前端 CI/CD 與正式環境除錯:從 Pull Request 到事故排查
2026-06-24
即時資料怎麼選?Polling、SSE、WebSocket 比較
2026-06-23
前端系統設計:如何拆元件、資料流與大型專案架構?
2026-06-22
無障礙不是加 ARIA:語意化 HTML、鍵盤操作與焦點管理
2026-06-21
CSS 與 RWD 面試整理:Flexbox、Grid、定位與層疊脈絡
2026-06-19
← 返回文章列表

Discord Bot 按鈕互動:ActionRow、ButtonBuilder 與事件處理

2026年3月24日·約 7 分鐘閱讀·
Discord.jsBotButtonNode.js

上一篇深入拆解了 Slash Commands 設計。這篇延伸到互動元件,拆解按鈕互動的完整流程。

Slash Command 觸發後,Bot 不只能回一段文字,還可以附上按鈕讓使用者繼續操作。按鈕是 discord.js 裡最常用的互動元件,也是「對話式 UI」的核心。


概念:訊息附件(Component)

Discord 的按鈕不是直接貼在訊息上,而是透過兩層巢狀結構附加:

  • 訊息 → components 欄位,放 ActionRow 陣列(最多 5 個)
    • ActionRow → 一橫排容器(最多放 5 個 Button)
      • Button → 最終顯示的按鈕元件

所謂「兩層」就是:ActionRow 包 Button,訊息包 ActionRow。ActionRowBuilder 對應一排、ButtonBuilder 對應一顆按鈕,最後把 ActionRow 放進 components 陣列送出去。一則訊息最多 5 排 × 5 顆 = 25 個按鈕,實際上通常 1–2 個 ActionRow 就夠了。


建立按鈕並附在訊息裡

discord.js v14 用 ActionRowBuilder + ButtonBuilder 組合。先看 ButtonStyle 有哪些選項:

Style顏色說明
Primary藍色主要操作
Secondary灰色次要或取消
Success綠色確認、完成
Danger紅色刪除、危險操作
Link灰色(外部連結)不觸發互動事件,直接開連結

以「確認刪除」流程為例——紅色確認鈕 + 灰色取消鈕:

import { ActionRowBuilder, ButtonBuilder, ButtonStyle } from "discord.js";
 
const row = new ActionRowBuilder().addComponents(
  new ButtonBuilder()
    .setCustomId("confirm_delete")
    .setLabel("確認刪除")
    .setStyle(ButtonStyle.Danger),
  new ButtonBuilder()
    .setCustomId("cancel_delete")
    .setLabel("取消")
    .setStyle(ButtonStyle.Secondary)
);
 
await interaction.reply({
  content: "確定要刪除這筆記錄嗎?",
  components: [row],
  flags: 64, // ephemeral,只有本人看到
});

/delete 指令觸發後出現的確認刪除訊息

ButtonStyle 四種顏色並排對照:

Primary(藍)、Secondary(灰)、Success(綠)、Danger(紅)並排效果

Link 類型不需要 customId,改用 .setURL("https://...") 指定目標,點擊後直接開連結不觸發互動事件。


isButton():接收按鈕事件

使用者按下按鈕後,interactionCreate 事件觸發,interaction.isButton() 回傳 true:

index.js
client.on("interactionCreate", async (interaction) => {
 
  if (interaction.isButton()) {
    const { customId } = interaction;
 
    if (customId === "confirm_delete") {
      return handleConfirmDelete(interaction);
    }
    if (customId === "cancel_delete") {
      return handleCancelDelete(interaction);
    }
    return;
  }
 
  // ... 其他互動類型
});

handler 裡用 interaction.update() 替換原始訊息(含按鈕),或用 interaction.reply() 另外回覆:

async function handleConfirmDelete(interaction) {
  // 執行刪除邏輯...
  await doDelete(interaction.user.id);
 
  // 更新原本的訊息,移除按鈕
  await interaction.update({
    content: "✅ 已刪除。",
    components: [], // 清空按鈕
  });
}
 
async function handleCancelDelete(interaction) {
  await interaction.update({
    content: "已取消。",
    components: [],
  });
}

interaction.update() 會修改按鈕所在的那則訊息,是最常見的用法。如果要另外回一則新訊息,改用 interaction.reply()。

點擊確認刪除後訊息更新為「✅ 已刪除。」且按鈕消失


customId 帶參數

按鈕的 customId 是純字串,最長 100 字元。需要帶參數時,用分隔符拼接:

// 建立按鈕時帶入目標 ID
const targetId = "user_abc123";
 
const row = new ActionRowBuilder().addComponents(
  new ButtonBuilder()
    .setCustomId(`confirm_kick_${targetId}`)
    .setLabel("確認踢出")
    .setStyle(ButtonStyle.Danger)
);

在 handler 裡解析:

if (customId.startsWith("confirm_kick_")) {
  const targetId = customId.replace("confirm_kick_", "");
  return handleConfirmKick(interaction, targetId);
}

這樣不需要另外維護一個 Map 來存「哪個按鈕對應哪個目標」,把資訊直接編在 customId 裡更簡單。


按鈕逾時

Discord 的按鈕沒有內建逾時機制,按鈕會永遠留在訊息裡。如果需要限制時間(例如 30 秒內沒按就失效),要自己處理:

const row = new ActionRowBuilder().addComponents(
  new ButtonBuilder()
    .setCustomId("confirm_action")
    .setLabel("確認")
    .setStyle(ButtonStyle.Primary)
);
 
// 30 秒後停用按鈕
const reply = await interaction.reply({
  content: "請在 30 秒內確認:",
  components: [row],
  flags: 64,
});
 
setTimeout(async () => {
  const disabledRow = new ActionRowBuilder().addComponents(
    new ButtonBuilder()
      .setCustomId("confirm_action")
      .setLabel("已逾時")
      .setStyle(ButtonStyle.Secondary)
      .setDisabled(true)
  );
  await interaction.editReply({ components: [disabledRow] });
}, 30_000);

.setDisabled(true) 讓按鈕在視覺上變灰、無法點擊。

30 秒後按鈕自動變為灰色「已逾時」且無法點擊


完整流程總覽

client.once("ready")
  └── guild.commands.set(COMMANDS)   ← 把 /delete 註冊到 Discord

使用者輸入 /delete target_id:xxx
  └── interactionCreate → isChatInputCommand() → commandName === "delete"
        └── handleDelete(interaction)
              └── interaction.reply({ content, components: [row], flags: 64 })
                    └── 使用者看到訊息 + 按鈕

使用者點擊「確認刪除」
  └── interactionCreate → isButton() → customId.startsWith("confirm_delete_")
        └── handleConfirmDelete(interaction, targetId)
              └── doDelete(targetId)
              └── interaction.update({ content: "✅ 已刪除", components: [] })

完整骨架總結

把這篇所有概念組合在一起,index.js 的按鈕互動結構長這樣:

index.js(按鈕互動完整骨架)
import {
  Client, GatewayIntentBits,
  ActionRowBuilder, ButtonBuilder, ButtonStyle,
} from "discord.js";
import "dotenv/config";
 
const client = new Client({
  intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers],
});
 
// ── 1. 指令定義 ───────────────────────────────────────────────
const COMMANDS = [
  {
    name: "delete",
    description: "刪除一筆記錄",
    options: [
      {
        name: "target_id",
        type: 3,            // STRING
        description: "要刪除的記錄 ID",
        required: true,
      },
    ],
  },
];
 
// ── 2. ready:註冊指令 ────────────────────────────────────────
client.once("ready", async () => {
  const guild = await client.guilds.fetch(process.env.GUILD_ID);
  await guild.commands.set(COMMANDS);
  console.log("✅ Bot 上線,指令已更新");
});
 
// ── 3. 發送帶按鈕的訊息(在 Slash Command handler 裡呼叫)──────
async function handleDelete(interaction) {
  const targetId = interaction.options.getString("target_id"); // 假設指令有此 option
 
  const row = new ActionRowBuilder().addComponents(
    new ButtonBuilder()
      .setCustomId(`confirm_delete_${targetId}`)
      .setLabel("確認刪除")
      .setStyle(ButtonStyle.Danger),
    new ButtonBuilder()
      .setCustomId("cancel_delete")
      .setLabel("取消")
      .setStyle(ButtonStyle.Secondary)
  );
 
  await interaction.reply({
    content: `確定要刪除 \`${targetId}\` 嗎?`,
    components: [row],
    flags: 64, // ephemeral,只有本人看到
  });
 
  // 30 秒後自動停用按鈕
  setTimeout(async () => {
    const disabledRow = new ActionRowBuilder().addComponents(
      new ButtonBuilder()
        .setCustomId(`confirm_delete_${targetId}`)
        .setLabel("已逾時")
        .setStyle(ButtonStyle.Secondary)
        .setDisabled(true)
    );
    await interaction.editReply({ components: [disabledRow] });
  }, 30_000);
}
 
// ── 4. interactionCreate:按鈕事件路由 ───────────────────────
client.on("interactionCreate", async (interaction) => {
 
  // 按鈕互動
  if (interaction.isButton()) {
    const { customId } = interaction;
 
    if (customId.startsWith("confirm_delete_")) {
      const targetId = customId.replace("confirm_delete_", "");
      return handleConfirmDelete(interaction, targetId);
    }
    if (customId === "cancel_delete") {
      return handleCancelDelete(interaction);
    }
    return;
  }
 
  // Slash Command
  if (!interaction.isChatInputCommand()) return;
  if (interaction.commandName === "delete") return handleDelete(interaction);
});
 
// ── 5. handler:確認刪除 ──────────────────────────────────────
async function handleConfirmDelete(interaction, targetId) {
  // 執行實際刪除邏輯(換成你的資料層呼叫)
  // await deleteRecord(targetId);
 
  await interaction.update({
    content: `✅ \`${targetId}\` 已刪除。`,
    components: [], // 清空按鈕
  });
}
 
async function handleCancelDelete(interaction) {
  await interaction.update({
    content: "已取消。",
    components: [],
  });
}
 
client.login(process.env.BOT_TOKEN);

核心模式是:指令觸發 → reply 附帶按鈕 → 使用者點擊 → isButton() 路由 → update() 替換原訊息。customId 帶參數讓按鈕本身就能傳遞上下文,不需要額外的狀態管理。


下一篇會拆解下拉選單(StringSelectMenu)的設計:怎麼建立選項、怎麼接收 isStringSelectMenu() 事件,以及動態選項的注意事項。

分享:XLinkedIn
← 上一篇從一條指令到完整互動系統:Slash Commands 設計拆解
下一篇 →Discord Bot 下拉選單:StringSelectMenu 完整設計