Use Cases (更新: 2026/6/3)

用 Claude Code 构建 Discord Bot:discord.js 斜杠命令实战指南

用 Claude Code 和 discord.js 做一个可上线的 Discord 支持 Bot,覆盖权限、环境变量和部署检查。

用 Claude Code 构建 Discord Bot:discord.js 斜杠命令实战指南

Discord Bot 应该是支持流程,不只是聊天玩具

Discord Bot 是加入 Discord 服务器并响应用户操作的应用。换成更直白的话,它就是社区里的自动接待员。它可以收集支持请求,返回 FAQ,通知版主,把交接信息整理到内部频道,或者在严重问题扩散前把事件送到正确的人面前。

这比“会聊天的机器人”更实用。真实社区里经常出现这些问题:“我卡住了”“安装文档在哪里”“谁负责这个问题”“这个要不要升级处理”。如果这些内容都发在普通聊天频道里,几分钟后就会被讨论淹没。改成 application commands 之后,用户输入、处理路径、责任人和下一步都会更清楚。

Claude Code 在这里的价值不是替你写一段 client.login(),而是帮你把容易漏掉的生产细节一起补齐:命令注册、环境变量、权限边界、interaction 回复、错误处理、token 安全、部署前检查清单。如果只让它“做一个 Discord Bot”,你可能得到一个演示能跑、上线就脆的项目。

本文用 discord.js 构建一个支持接待 Bot,实现 /support/faq/handoff 三个命令。示例避免使用 message content intent,使用斜杠命令,默认把命令注册到测试服务器,并处理 mention 滥用。Node.js 版本按当前 discord.js 文档要求使用 22.12.0 或更新版本。

flowchart LR
  A["User runs /support"] --> B["Discord interaction"]
  B --> C["discord.js bot"]
  C --> D["Ephemeral user reply"]
  C --> E["Support channel message"]
  E --> F["Moderator handoff"]

这个架构故意保持简单:先把支持入口做稳定,再考虑数据库、队列、LLM 摘要、CRM 同步或付费状态检查。Claude Code 可以继续扩展这些能力,但第一版应该先让命令、权限和失败路径可靠。

用白话理解 application commands 和 interactions

Discord application commands 是 Discord 客户端中原生显示的命令。最常见的是 /support 这样的 slash command。它比旧式 !help 前缀命令更适合支持场景,因为用户在提交前就能看到名称、说明、选项、选择值和权限提示,输入错误会少很多。

Interactions 是用户执行命令、点击按钮、使用选择菜单或提交 modal 时,Discord 发送给应用的事件。使用 discord.js 和 Gateway 时,一般通过 Events.InteractionCreate 处理。Discord 也支持 HTTP endpoint 接收 interactions,但小团队和本地开发阶段,用 Gateway Bot 更容易启动、打日志和排查问题。

规范必须以官方文档为准。命令类型、命名规则、guild command 与 global command 的差异在 Discord Application Commands。初次响应、followup、interaction token 的行为在 Receiving and Responding to Interactions。discord.js 的安装要求和 API 参考看 discord.js documentationdiscord.js guide

权限、环境变量和最小生产架构

在 Developer Portal 创建 Discord application 后,需要添加 Bot 用户,并生成包含 botapplications.commands scopes 的邀请链接。不要一开始就给 administrator 权限。这个 Bot 只需要看见支持频道并发送消息。/handoff 则应该只允许有版主权限的人使用,例如具备 Manage Messages 权限的成员。

项目生产注意点
Node.js22.12.0 或更新当前 discord.js 文档要求
OAuth2 scopesbot, applications.commandsBot 与斜杠命令都需要
Bot permissionsView Channels, Send Messages从最小权限开始
DISCORD_TOKENBot token不提交、不截图、不写日志
DISCORD_CLIENT_IDApplication ID注册命令时使用
DISCORD_GUILD_ID测试服务器 ID开发时使用 guild command
SUPPORT_CHANNEL_ID内部支持频道确认 Bot 能发送消息

给 Claude Code 的提示词要写到这个程度:“创建 Node.js 22 的 discord.js 支持 Bot,包含 /support/faq/handoff。使用 .env,开发阶段注册 guild commands,最小权限,用户回复使用 ephemeral,并生成部署检查清单。” 这样它更可能生成接近真实项目的结构,而不是泛泛的聊天机器人。

相关内部阅读可以接 环境变量管理错误处理模式代码审查清单。Bot 虽然小,但这些习惯与更大的 Claude Code 项目完全一致。

可直接运行的 discord.js 示例

下面的示例使用 JavaScript ES modules,不要求先配置 TypeScript。DISCORD_GUILD_ID 存在时会注册到测试服务器,移除后才会注册 global commands。开发时可以保留 DEPLOY_COMMANDS=true,生产常规重启时建议关闭,避免每次启动都覆盖命令。

mkdir discord-support-bot
cd discord-support-bot
npm init -y
npm install discord.js dotenv
mkdir src

package.json 中加入 typestart

{
  "type": "module",
  "scripts": {
    "start": "node src/bot.js"
  },
  "dependencies": {
    "discord.js": "latest",
    "dotenv": "latest"
  }
}

创建 .env

DISCORD_TOKEN=replace_with_bot_token
DISCORD_CLIENT_ID=replace_with_application_id
DISCORD_GUILD_ID=replace_with_test_guild_id
SUPPORT_CHANNEL_ID=replace_with_support_channel_id
DEPLOY_COMMANDS=true

创建 src/bot.js

import "dotenv/config";
import {
  Client,
  Events,
  GatewayIntentBits,
  MessageFlags,
  PermissionFlagsBits,
  REST,
  Routes,
  SlashCommandBuilder,
} from "discord.js";

const token = process.env.DISCORD_TOKEN;
const clientId = process.env.DISCORD_CLIENT_ID;
const guildId = process.env.DISCORD_GUILD_ID;
const supportChannelId = process.env.SUPPORT_CHANNEL_ID;

for (const [name, value] of Object.entries({ token, clientId, supportChannelId })) {
  if (!value) throw new Error(`${name} is required.`);
}

const commands = [
  new SlashCommandBuilder()
    .setName("support")
    .setDescription("Send a support request to the team")
    .addStringOption((option) =>
      option
        .setName("summary")
        .setDescription("What happened?")
        .setMaxLength(900)
        .setRequired(true),
    )
    .addStringOption((option) =>
      option
        .setName("severity")
        .setDescription("How urgent is it?")
        .setRequired(true)
        .addChoices(
          { name: "low", value: "low" },
          { name: "normal", value: "normal" },
          { name: "high", value: "high" },
        ),
    )
    .addStringOption((option) =>
      option
        .setName("context")
        .setDescription("Steps, links, or error messages")
        .setMaxLength(1500),
    ),
  new SlashCommandBuilder()
    .setName("faq")
    .setDescription("Show a short answer for a common topic")
    .addStringOption((option) =>
      option
        .setName("topic")
        .setDescription("FAQ topic")
        .setRequired(true)
        .addChoices(
          { name: "setup", value: "setup" },
          { name: "permissions", value: "permissions" },
          { name: "rollout", value: "rollout" },
        ),
    ),
  new SlashCommandBuilder()
    .setName("handoff")
    .setDescription("Create a moderator handoff note")
    .setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages)
    .addUserOption((option) =>
      option.setName("target").setDescription("User to hand off").setRequired(true),
    )
    .addStringOption((option) =>
      option
        .setName("note")
        .setDescription("What should the next moderator know?")
        .setMaxLength(1500)
        .setRequired(true),
    ),
].map((command) => command.toJSON());

const client = new Client({ intents: [GatewayIntentBits.Guilds] });

client.once(Events.ClientReady, (readyClient) => {
  console.log(`Logged in as ${readyClient.user.tag}`);
});

client.on(Events.InteractionCreate, async (interaction) => {
  if (!interaction.isChatInputCommand()) return;

  try {
    if (!interaction.inGuild()) {
      await interaction.reply({
        content: "Please use this command inside the server.",
        flags: MessageFlags.Ephemeral,
      });
      return;
    }

    if (interaction.commandName === "support") await handleSupport(interaction);
    else if (interaction.commandName === "faq") await handleFaq(interaction);
    else if (interaction.commandName === "handoff") await handleHandoff(interaction);
    else await safeReply(interaction, "Unknown command.");
  } catch (error) {
    console.error("Interaction failed:", error);
    await safeReply(interaction, "Something went wrong. Please contact a moderator.");
  }
});

async function handleSupport(interaction) {
  const summary = interaction.options.getString("summary", true);
  const severity = interaction.options.getString("severity", true);
  const context = interaction.options.getString("context") ?? "No extra context.";
  const channel = await fetchSupportChannel();

  await channel.send({
    content: [
      "**New support request**",
      `Reporter: ${interaction.user.tag} (${interaction.user.id})`,
      `Severity: ${severity}`,
      `Channel: <#${interaction.channelId}>`,
      `Summary: ${neutralizeMentions(summary)}`,
      `Context: ${neutralizeMentions(context)}`,
    ].join("\n"),
    allowedMentions: { parse: [] },
  });

  await interaction.reply({
    content: "Thanks. Your request was sent to the support team.",
    flags: MessageFlags.Ephemeral,
  });
}

async function handleFaq(interaction) {
  const topic = interaction.options.getString("topic", true);
  const answers = {
    setup: "Install Node.js 22.12+, invite the bot with bot and applications.commands scopes, then run npm start.",
    permissions: "Start with View Channels and Send Messages. Reserve Manage Messages for moderator-only commands.",
    rollout: "Use guild commands for testing. Promote to global commands only after rollback and logging are checked.",
  };

  await interaction.reply({
    content: answers[topic],
    flags: MessageFlags.Ephemeral,
  });
}

async function handleHandoff(interaction) {
  if (!interaction.memberPermissions?.has(PermissionFlagsBits.ManageMessages)) {
    await interaction.reply({
      content: "You need Manage Messages permission to use this command.",
      flags: MessageFlags.Ephemeral,
    });
    return;
  }

  const target = interaction.options.getUser("target", true);
  const note = interaction.options.getString("note", true);
  const channel = await fetchSupportChannel();

  await channel.send({
    content: [
      "**Moderator handoff**",
      `Target: ${target.tag} (${target.id})`,
      `From: ${interaction.user.tag} (${interaction.user.id})`,
      `Note: ${neutralizeMentions(note)}`,
    ].join("\n"),
    allowedMentions: { parse: [] },
  });

  await interaction.reply({
    content: "Handoff note created.",
    flags: MessageFlags.Ephemeral,
  });
}

async function fetchSupportChannel() {
  const channel = await client.channels.fetch(supportChannelId);
  if (!channel || !channel.isTextBased() || typeof channel.send !== "function") {
    throw new Error("SUPPORT_CHANNEL_ID must be a text channel the bot can send to.");
  }
  return channel;
}

function neutralizeMentions(value) {
  return value
    .replaceAll("@everyone", "@ everyone")
    .replaceAll("@here", "@ here")
    .replace(/<@!?(\d+)>/g, "user:$1")
    .replace(/<@&(\d+)>/g, "role:$1");
}

async function safeReply(interaction, content) {
  const payload = { content, flags: MessageFlags.Ephemeral };
  if (interaction.replied || interaction.deferred) await interaction.followUp(payload);
  else await interaction.reply(payload);
}

async function deployCommands() {
  const rest = new REST({ version: "10" }).setToken(token);
  const route = guildId
    ? Routes.applicationGuildCommands(clientId, guildId)
    : Routes.applicationCommands(clientId);

  await rest.put(route, { body: commands });
  console.log(guildId ? "Guild commands deployed." : "Global commands deployed.");
}

if (process.env.DEPLOY_COMMANDS === "true") {
  await deployCommands();
}

await client.login(token);

本地先运行 node --version,确认版本不低于 22.12.0,再执行 npm start。先在测试服务器运行 /support,确认用户只看到 ephemeral 回复,而内部支持频道收到结构化消息。然后测试 /faq。最后分别用版主账号和普通账号测试 /handoff,确认权限不是摆设。

Use case: 三个真正值得保留的场景

第一个 Use case 是支持接待。好的接待命令只问 summary、severity、context 三项,就能完成基本 triage。triage 可以理解为“判断下一步该忽略、回答、升级、复现还是分配”。在小型测试服务器中,这种结构比自由发言更容易留下错误信息、紧急程度和复现线索,版主第一轮回复不再只是“请补充环境”。

第二个 Use case 是 FAQ 分流。不要让 Bot 在聊天里贴一整篇文档。更好的做法是给短答案,再给正确链接。安装问题可以指向 Claude Code 入门指南,CLI 问题可以接 CLI 工具开发,团队规则可以接 CLAUDE.md 模板。这样 Bot 是站点导航的一部分,而不是第二套失控文档。

第三个 Use case 是版主交接。付费社区、训练营、游戏服务器和产品支持都会遇到轮班。如果当前负责人只在频道里写“下一个人看一下”,接手的人很难知道目标用户、上下文和下一步。/handoff 让这些信息固定成一条内部消息,减少误会。

第四个场景是培训支持。Claude Code 训练营里,很多学员会遇到相同的 Node 版本、环境变量或命令错误。Bot 先收集版本、执行命令和最后几行错误,讲师就能从诊断开始,而不是反复问基础信息。要把这套流程放进团队服务器,可以从 培训与咨询 开始。

Pitfall: 上线前必须排掉的坑

第一个 Pitfall 是泄露 Bot token。token 出现在 Git 历史、截图、CI 日志或文章里,都要当作已经泄露处理。立刻在 Developer Portal 轮换 token,并删除旧值。提示 Claude Code 时要明确写:只创建 .env.example 占位文件,不打印真实 secret,不把 token 写入文档。

第二个坑是在开发阶段频繁注册 global commands。guild commands 只影响测试服务器,反馈快,风险低。global commands 是面向生产的入口,应该在命令名、描述、权限和回滚步骤都确认后再发布。不要让生产环境每次重启都自动覆盖命令,除非这是有审查的部署步骤。

第三个坑是某些 interaction 分支没有回复。用户会以为命令坏了。成功、失败、未知命令、异常捕获都必须 reply、defer 或 follow up。如果以后加入外部 API 或 LLM 摘要,先 defer,再补结果。本文代码工作量很短,所以可以直接回复。

第四个坑是 mention 滥用。用户输入如果原样转发到内部频道,@everyone@here、用户 mention 和角色 mention 都可能造成通知事故。代码同时使用 allowedMentions: { parse: [] } 和字符串清理,这是公开服务器应该保留的防线。

第五个坑是为了省事给 Bot administrator 权限。它会掩盖权限设计问题,也会在 token 被盗时扩大损失。先用 View Channels 和 Send Messages,只有在代码审查中能解释清楚时才增加权限。

安全与部署检查清单

  • Developer Portal 中有人负责轮换 Bot token
  • .env 不进 Git,.env.example 只写占位值
  • 邀请链接从 botapplications.commands scopes 开始
  • Bot permissions 以 View Channels 和 Send Messages 为基础
  • /handoff 只允许版主级成员使用
  • 先测试 guild commands,再发布 global commands
  • 所有 interaction 分支都有 reply、defer 或 follow up
  • 用户输入已防止 mention 滥用
  • 日志不包含 token、支付信息、邮箱或私人支持内容
  • 部署目标使用 Node.js 22.12.0 或更新
  • 上线前写好重启和回滚步骤

小型 Bot 可以部署到 Railway、Render、Fly.io、VPS 或内部服务器。选择平台前先确认三件事:secret 能和代码分离,日志能查看,重启和回滚步骤能写下来。让 Claude Code 生成 README 时,也要明确要求包含这些部分。

文章不应该只停在代码。个人开发者需要可复用模板时,可以看 ClaudeCodeLab 产品。团队要把 Discord 支持、Claude Code 权限、代码审查和训练流程做成制度,可以看 培训与咨询。如果要把 Bot 当作生产功能维护,也建议阅读 代码审查自动化

实际测试后的结果

把这个 starter 放到小型测试服务器后,最大的收益不是 Bot 变得更聪明,而是请求形状变清楚了。自由发言经常只有“运行不了”,而 /support 会强制留下 summary、severity 和 context,版主第一次回复更接近诊断。真正容易出事故的是 token、global command、mention 和权限。Claude Code 最有用的用法,是让它在写代码的同时生成 .env.example、权限表、失败回复和部署前检查清单。

#Claude Code #Discord Bot #discord.js #chatbot #社区
免费

免费 PDF: Claude Code 速查表

输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。

我们会妥善保护你的信息,不发送垃圾邮件。

把 Claude Code 变成真正能带来结果的工作流

先领取中文说明的免费 PDF,再进入英文商品页选择合适的教材。如果你需要团队落地、流程设计或内容变现支持,也可以直接咨询。

Masa

关于作者

Masa

专注 Claude Code 实务流程、团队导入和内容转化的工程师。