喂!我是 Wei

Front-End Engineer

Be a Problem Solver.

⌘K

導覽

所有文章緣起互動小功能

文章分類

目錄
Slash Commands 的定義方式靜態指令 vs 動態指令權限控制:PermissionFlagsBitsinteractionCreate:三層分流ephemeral 回覆與 deferReply完整骨架總結

相關文章

Discord Bot Autocomplete:Slash Command 即時搜尋實戰

2026年3月30日

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

2026年5月18日

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

2026年4月1日

最新文章
全部 →
前端 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
← 返回文章列表

從一條指令到完整互動系統:Slash Commands 設計拆解

2026年3月23日·約 10 分鐘閱讀·
Discord.jsBotSlash CommandsNode.js

上一篇完成了 Bot 的基本設定與第一個 /ping 指令。這篇深入拆解 Slash Commands 的完整設計:帶參數的指令怎麼定義、選項怎麼設計、所有互動事件怎麼分流。

discord.js v14 把「指令」和「互動」分得很清楚。指令是使用者在 Discord 輸入框看到的選項,互動是使用者按下按鈕、選擇選單、或輸入指令後觸發的事件。兩件事要分開設計。


Slash Commands 的定義方式

discord.js v14 的指令定義是一個純物件陣列,在 client.once("ready") 時呼叫 guild.commands.set() 一次性註冊到 Discord:

index.js
const COMMANDS = [
  {
    name: "ping",
    description: "測試 Bot 是否正常運作",
  },
  {
    name: "info",
    description: "查詢指定成員資訊",
    options: [
      {
        name: "user",
        type: 6,           // USER 類型
        description: "要查詢的成員(不填則查詢自己)",
        required: false,
      },
    ],
  },
  {
    name: "remind",
    description: "設定提醒",
    options: [
      {
        name: "message",
        type: 3,           // STRING 類型
        description: "提醒內容",
        required: true,
      },
      {
        name: "minutes",
        type: 4,           // INTEGER 類型
        description: "幾分鐘後提醒",
        required: true,
        min_value: 1,
        max_value: 1440,
      },
    ],
  },
];
 
// 和上一篇一樣,在 ready 事件裡呼叫 guild.commands.set()
// 差別只是把物件陣列抽成變數,方便後續動態新增指令
client.once("ready", async () => {
  const guild = await client.guilds.fetch(process.env.GUILD_ID);
  await guild.commands.set(COMMANDS);
  console.log("📝 指令已更新!");
});

type 是數字代碼,常用的有:

type說明
3STRING(文字輸入或靜態 choices)
4INTEGER(整數,可設 min_value / max_value)
5BOOLEAN
6USER(選擇伺服器成員)
11ATTACHMENT(上傳檔案)

指令定義完畢、Bot 上線後,在 Discord 輸入框輸入 / 就能看到已註冊的指令出現:

指令在 Discord 輸入框出現的樣子

選擇一個帶 options 的指令,Discord 會自動展開參數欄位:

帶 options 的指令展開後的參數欄位

在 interactionCreate 裡用對應的 getter 取得使用者傳入的值:

if (commandName === "remind") {
  const message = interaction.options.getString("message");
  const minutes = interaction.options.getInteger("minutes");
  await interaction.reply(`✅ 會在 ${minutes} 分鐘後提醒你:${message}`);
}

靜態指令 vs 動態指令

大多數指令的 options 是固定的,但有時候選項內容需要在啟動時從外部讀取。例如分類選項定義在試算表裡,資料會隨時更新。

作法是在 ready 事件裡先讀取資料,組好選項後再 push 進 COMMANDS 陣列,最後才呼叫 guild.commands.set():

index.js
async function prepareCommands() {
  // 從 Sheets 讀取分類選項
  const rows = await getCategoryOptions(); // 回傳 [["A 類"], ["B 類"], ...]
 
  const choices = rows
    .map((row) => row[0]?.trim())
    .filter(Boolean)
    .map((name) => ({ name, value: name }));
 
  COMMANDS.push({
    name: "search",
    description: "依分類搜尋",
    options: [
      {
        name: "category",
        type: 3,
        description: "選擇分類",
        required: true,
        choices, // 動態組合的 choices
      },
    ],
  });
}

有 choices 的選項在 Discord 會呈現為下拉選單,使用者只能從預設選項中挑選:

choices 呈現為下拉選單的樣子

這個設計有一個限制:Discord 的 choices 最多 25 個。超過的話要改用 Autocomplete 模式(在使用者輸入時即時回傳候選選項,不受 25 個限制)。


權限控制:PermissionFlagsBits

有些指令只有特定人可以用。default_member_permissions 讓 Discord 在前端就攔截不符合條件的使用者,不符合權限的人根本看不到這個指令:

index.js
import { PermissionFlagsBits } from "discord.js";
 
COMMANDS.push({
  name: "announce",
  description: "發送公告(僅限管理者)",
  default_member_permissions: PermissionFlagsBits.Administrator.toString(),
  options: [
    {
      name: "message",
      type: 3,
      description: "公告內容",
      required: true,
    },
  ],
});

另一種是在程式碼層做身份組檢查,用於不能用 Discord 權限系統表達的條件(例如「有某個特定身分組才能用」):

if (commandName === "announce") {
  const ALLOWED_ROLE_ID = "123456789012345678"; // 改成你的身分組 ID
 
  if (!interaction.member.roles.cache.has(ALLOWED_ROLE_ID)) {
    return interaction.reply({
      content: "你沒有權限使用此指令。",
      flags: 64, // ephemeral —— 只有本人看得到
    });
  }
 
  const message = interaction.options.getString("message");
  await interaction.reply(`📢 公告:${message}`);
}

兩種方式可以混用:default_member_permissions 做第一層(前端)過濾,程式碼做第二層精細控制。


interactionCreate:三層分流

所有互動都透過同一個 interactionCreate 事件觸發,用類型判斷分流:

index.js
client.on("interactionCreate", async (interaction) => {
 
  // 第一層:按鈕互動
  if (interaction.isButton()) {
    if (interaction.customId === "confirm_action") {
      return handleConfirm(interaction);
    }
    if (interaction.customId === "cancel_action") {
      return handleCancel(interaction);
    }
    return;
  }
 
  // 第二層:下拉選單互動
  if (interaction.isStringSelectMenu()) {
    if (interaction.customId === "select_category") {
      return handleCategorySelect(interaction);
    }
    return;
  }
 
  // 第三層:Slash Command 互動
  if (!interaction.isChatInputCommand()) return;
  const { commandName } = interaction;
 
  if (commandName === "ping")     return handlePing(interaction);
  if (commandName === "info")     return handleInfo(interaction);
  if (commandName === "remind")   return handleRemind(interaction);
  if (commandName === "daily")    return handleDaily(interaction);
  if (commandName === "announce") return handleAnnounce(interaction);
});

isButton() 和 isStringSelectMenu() 要放在前面,因為它們在 isChatInputCommand() 回傳 false 之前就需要被攔截處理。按鈕與選單的詳細設計會在後續篇章拆解。


ephemeral 回覆與 deferReply

flags: 64 讓回覆「只有本人看得到」,用於錯誤訊息、權限拒絕、操作確認等場景:

await interaction.reply({
  content: "你沒有權限使用此指令。",
  flags: 64,
});

flags: 64 的回覆只有指令發送者看得到,訊息框右下角會有一個鎖頭圖示:

ephemeral 回覆的樣子

Discord Bot 有 3 秒的響應時限,如果處理邏輯超過 3 秒(例如要打 Sheets API),要先 deferReply 告訴 Discord「我還在處理」:

await interaction.deferReply({ flags: 64 }); // 可加 flags: 64 讓轉圈只有本人看到
 
// 執行耗時操作…
const data = await sheets.spreadsheets.values.get({ ... });
 
// 再用 editReply 回傳最終結果
await interaction.editReply(`處理完成:${data}`);

deferReply 呼叫後,Discord 會先顯示一個轉圈的「思考中」狀態,等 editReply 回傳後才變成最終內容:

deferReply 的轉圈狀態

呼叫 deferReply 之後就不能再用 reply,必須改用 editReply。


完整骨架總結

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

index.js(完整骨架)
import { Client, GatewayIntentBits, PermissionFlagsBits } from "discord.js";
import "dotenv/config";
import { getLastCheckIn, setCheckIn } from "./googleSheets.js"; // 上一篇建立的函式
 
const client = new Client({
  intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers],
});
 
// ── 1. 靜態指令定義 ──────────────────────────────────────────
const COMMANDS = [
  { name: "ping", description: "測試 Bot 是否正常運作" },
  {
    name: "info",
    description: "查詢指定成員資訊",
    options: [{ name: "user", type: 6, description: "要查詢的成員", required: false }],
  },
  {
    name: "remind",
    description: "設定提醒",
    options: [
      { name: "message", type: 3, description: "提醒內容", required: true },
      { name: "minutes", type: 4, description: "幾分鐘後提醒", required: true, min_value: 1, max_value: 1440 },
    ],
  },
];
 
// ── 2. 動態指令(從外部來源讀取,在 ready 前 push 進去)────────
// 這裡先用 mock 資料示範概念;實際場景換成從 Google Sheets 或資料庫讀取
// 例如上一篇 googleSheets.js 的 getLastCheckIn 改寫成讀取選項欄位即可
async function getCategoryOptions() {
  // mock:回傳與 Sheets 相同格式 [["A 類"], ["B 類"], ["C 類"]]
  return [["A 類"], ["B 類"], ["C 類"]];
}
 
async function prepareCommands() {
  const rows = await getCategoryOptions();
  const choices = rows.map((r) => r[0]?.trim()).filter(Boolean).map((n) => ({ name: n, value: n }));
  COMMANDS.push({
    name: "search",
    description: "依分類搜尋",
    options: [{ name: "category", type: 3, description: "選擇分類", required: true, choices }],
  });
}
 
// ── 3. 需要權限的指令 ─────────────────────────────────────────
COMMANDS.push({
  name: "announce",
  description: "發送公告(僅限管理者)",
  default_member_permissions: PermissionFlagsBits.Administrator.toString(),
  options: [{ name: "message", type: 3, description: "公告內容", required: true }],
});
 
// ── 4. ready:組完所有指令後統一註冊 ─────────────────────────
client.once("ready", async () => {
  await prepareCommands();                              // 先組動態指令
  const guild = await client.guilds.fetch(process.env.GUILD_ID);
  await guild.commands.set(COMMANDS);                  // 再一次性送給 Discord
  console.log("✅ Bot 上線,指令已更新");
});
 
// ── 5. interactionCreate:三層分流 ───────────────────────────
client.on("interactionCreate", async (interaction) => {
 
  // 第一層:按鈕
  if (interaction.isButton()) {
    if (interaction.customId === "confirm_action") return handleConfirm(interaction);
    if (interaction.customId === "cancel_action")  return handleCancel(interaction);
    return;
  }
 
  // 第二層:下拉選單
  if (interaction.isStringSelectMenu()) {
    if (interaction.customId === "select_category") return handleCategorySelect(interaction);
    return;
  }
 
  // 第三層:Slash Command
  if (!interaction.isChatInputCommand()) return;
  const { commandName } = interaction;
 
  if (commandName === "ping")     return handlePing(interaction);
  if (commandName === "info")     return handleInfo(interaction);
  if (commandName === "remind")   return handleRemind(interaction);
  if (commandName === "announce") return handleAnnounce(interaction);
});
 
// ── 6. handler 範例:需要耗時操作時,先 deferReply ───────────
async function handleRemind(interaction) {
  const message = interaction.options.getString("message");
  const minutes = interaction.options.getInteger("minutes");
 
  // 快速回覆,不需要 deferReply
  await interaction.reply(`✅ 會在 ${minutes} 分鐘後提醒你:${message}`);
}
 
async function handleAnnounce(interaction) {
  // 權限檢查(程式碼層第二道防線)
  const ALLOWED_ROLE_ID = "123456789012345678";
  if (!interaction.member.roles.cache.has(ALLOWED_ROLE_ID)) {
    return interaction.reply({ content: "你沒有權限使用此指令。", flags: 64 });
  }
 
  // 耗時操作先 deferReply,之後改用 editReply
  await interaction.deferReply();
  const message = interaction.options.getString("message");
  // 耗時操作(例如呼叫 API、寫入 Sheets)
  // 此處可呼叫上一篇定義的 setCheckIn,或換成你自己的寫入邏輯
  const today = new Date().toISOString().slice(0, 10);
  await setCheckIn(interaction.user.id, today);
  await interaction.editReply(`📢 公告:${message}`);
}
 
client.login(process.env.BOT_TOKEN);

一個完整的 Bot 就是這六個區塊:定義指令 → 動態組指令 → ready 時註冊 → interactionCreate 分流 → 各 handler 處理。


下一篇會拆解按鈕互動的完整流程:ActionRow、ButtonBuilder 的組合方式,以及 isButton() 事件怎麼路由到對應 handler。

分享:XLinkedIn
← 上一篇Discord Bot 怎麼從零開始:申請、設定、第一個 Slash Command
下一篇 →Discord Bot 按鈕互動:ActionRow、ButtonBuilder 與事件處理