😶‍🌫️

サーバーレスで無料!DifyとCloudflare Workersで作るDiscord AIボット

に公開

はじめに

Dify x Discord x Cloudflare

ちょっと前に、私のX(旧Twitter)でこんなポストをしたんですよ。

「Discordコミュニティをもっと活発にしたい?🎉 DifyとReplitを使えば、オリジナルのAI搭載のDiscordボットが簡単に作れる!」って。

あの頃はReplitでボットを作ってみて、確かにブラウザだけでサクッと開発できて便利だったんですけど、無料プランだとプロジェクトが非アクティブになると自動的にスリープモードになって処理が止まっちゃうんですよね。

例えば5〜10分操作がないだけでボットがオフラインになっちゃって、常時稼働が必要なDiscordボットにはちょっと厳しい制限があるんです。

そんな経験から、「自分のDiscordサーバーに、独自の知識を学んだAIボットがいたら、どんなに便利だろう…」って改めて思いました🤔

あの夢を、無料で、しかもサーバーの管理なんて一切不要のサーバーレスで、もっと安定して実現できる方法を見つけたんですよ!

今回は、ノーコードでAIアプリを作れるDify、サーバーレス環境のCloudflare Workers、そしてDiscordを組み合わせ、スラッシュコマンドでAIとチャットできるボットを作ってみましょう。

初心者さんでもわかりやすいように、手順を丁寧に解説しますね😊

この記事で作るもの

Discord

  • Discordで /ask というスラッシュコマンドを打つと、Difyで作ったAIが答えてくれるボットです。
  • すべての仕組みはCloudflare Workersで動くので、サーバー管理はゼロ!
  • Difyのワークフロー機能を使って、AIの指示や流れを自由にカスタマイズできますよ。簡単につながるだけで、すごいボットができちゃいます!

対象読者

  • オリジナルのDiscordボットを作ってみたい方
  • サーバーレスに興味がある方
  • DifyでAIを他のサービスと連携させたい方
  • JavaScriptの基本的な知識がある方(ちょっとしたプログラミングだけですよ)

必要なもの

これらが揃ったら、さっそく始めましょう! ワクワクしますね。

1. 全体の構成

まずは、どんな仕組みで動くのか、全体像をサクッと把握しましょうね。

  1. Discord が、ユーザーのスラッシュコマンドを受け取って、Cloudflare Workerに情報を送ります。
  2. Cloudflare Worker は、DiscordからのリクエストをDifyに渡しやすい形に変換して、Difyに送っちゃいます。
  3. Dify は、受け取った質問を基にAIを動かして、結果をWorkerに戻します。
  4. Cloudflare Worker は、その結果をDiscordにメッセージとして投稿するんです。

2. DifyでAIワークフローを作成する

最初に、ボットの脳みそ部分をDifyで作っていきましょう。大丈夫です、簡単に作ることができます👍

ステップ1: Difyでアプリを作成

  1. Difyにログインして、「アプリを作成」から「チャットボット」か「ワークフロー」を選びます。今回は柔軟にカスタムできるワークフローを選ぶと良いですね。
  2. アプリ名(例: My Discord Bot)を入れて、作成ボタンをポチッと押しましょう。

ステップ2: ワークフローを設計する

編集画面(スタジオ)で、AIの動きを決めていきます。一番シンプルな形にしますね。

  1. 「開始」ノード
    ここが外からの入力を受け取る入り口です。inputs に変数を2つ定義しましょう。

変数名 タイプ 説明
command 選択 コマンド名で、今回はaskという固定値が入ります。
prompt 長文 ユーザーの質問本文ですよ。
  1. 「IF/ELSE」ノード
    ここは、将来コマンドを増やしたくなった時のために置いておきます(今は特に意味ないけど、拡張しやすいんです)。

  2. 「LLM」ノード
    ここでAIが実際に考えます。「開始」ノードの prompt を、このノードのプロンプト入力につなぐだけでOK。ユーザーの質問がそのままAIに伝わりますね!

  3. 「終了」ノード
    AIの答えを外に出す部分です。「LLM」の出力をここにつなげて、出力変数名を textanswer にするとわかりやすいですよ。

これで、質問を受け取って答えを返すシンプルなAIができちゃいました。Difyって、ノードをつなぐだけで本格的なものが作れるんですよ😆

ステップ3: APIキーを取得する

  1. アプリの左メニューから「API」を選んでください。
  2. 「APIキー」セクションのキーをコピーして、安全に保管しましょう。

3. Discordでボットアプリを作成する

次は、ユーザーと話す窓口のDiscordボットを作りましょう。Developer Portalを使いますよ。

ステップ1: Discord Developer Portalでアプリを作成


Discord Developer PortalのApplications画面

  1. Discord Developer Portal にアクセスして、「New Application」をクリック。
  2. アプリ名を決めて作成します。
  3. 「Bot」ページでMessage Content Intentをオンに。こうすると、ボットがメッセージを読めるようになります。


Discord Developer PortalのBot画面

ステップ2: 必要な情報を取得する


Discord Developer PortalのGeneral Information画面

あとで使うので、3つの情報をメモしましょう。

  1. Application ID
    「General Information」ページの APPLICATION ID をコピー。
  2. Public Key
    同じページの PUBLIC KEY をコピー。
  3. Bot Token

    Discord Developer PortalのBot画面
    「Bot」ページで「Reset Token」を押して、表示されたトークンをコピー。

ステップ3: ボットをサーバーに追加する

  1. 「OAuth2」>「URL Generator」へ。

  2. SCOPESbotapplications.commands にチェック。


    Discord Developer PortalのOAuth2画面

  3. BOT PERMISSIONS でこれらにチェック(拡張を考えた多め設定です)。

    • View Channels
    • Send Messages
    • Embed Links
    • Attach Files
    • Read Message History


    Discord Developer PortalのOAuth2画面

  4. 生成されたURLをブラウザで開いて、サーバーを選んで認証。


    Discord Developer PortalのOAuth2画面

  5. 認証画面で「認証」をクリック。完了したら、ボットがサーバーに入りますよ!


    Bot招待リンク画面

4. Cloudflare Workerをセットアップする

ここで、DiscordとDifyをつなぐWorkerを作ります。コードを入れるだけですよ!🙌


Cloudflare worker作成画面

ステップ1: Workerを作成する

  1. Cloudflareダッシュボードから「Workers & Pages」へ。
  2. 「Create application」>「Create Worker」を選んで、「Hello World」テンプレート。
  3. Worker名(例: discord-dify-bot)を決めて、「Deploy」。

ステップ2: Workerのコードを編集する


Cloudflare worker コード編集画面

「Edit code」を押して、このコードを貼り付けましょう。全部の処理が入ってますよ。コピペオッケーです👌

worker.js
const InteractionType = {
  PING: 1,
  APPLICATION_COMMAND: 2,
};

const InteractionResponseType = {
  PONG: 1,
  CHANNEL_MESSAGE_WITH_SOURCE: 4,
  DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE: 5,
};

export default {
  async fetch(request, env, ctx) {
    if (request.method !== 'POST') {
      return new Response('Method Not Allowed', { status: 405 });
    }

    const isValidRequest = await verifySignature(request, env);
    if (!isValidRequest) {
      console.error('Invalid signature');
      return new Response('Bad request signature.', { status: 401 });
    }

    const interaction = await request.json();

    if (interaction.type === InteractionType.PING) {
      return createJsonResponse({ type: InteractionResponseType.PONG });
    }

    if (interaction.type === InteractionType.APPLICATION_COMMAND && interaction.data.name === 'ask') {
      const prompt = interaction.data.options?.find(opt => opt.name === 'prompt')?.value || '';
      const userId = `discord:${interaction.member?.user?.id || interaction.user?.id}`;

      if (!prompt) {
        return createJsonResponse({
          type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
          data: { content: 'プロンプトを入力してください。', flags: 64 },
        });
      }
      
      ctx.waitUntil(handleDifyRequest(interaction.token, prompt, userId, env));
      return createJsonResponse({ type: InteractionResponseType.DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE });
    }

    console.warn('Received an unhandled interaction type:', interaction.type);
    return createJsonResponse({
      type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
      data: { content: '不明なコマンドです。', flags: 64 },
    });
  },
};

async function handleDifyRequest(interactionToken, prompt, userId, env) {
  try {
    const aiResponse = await callDifyWorkflow(prompt, userId, env);
    await sendFollowupMessage(env.DISCORD_APPLICATION_ID, interactionToken, aiResponse);
  } catch (error) {
    console.error('Error in handleDifyRequest:', error);
    await sendFollowupMessage(env.DISCORD_APPLICATION_ID, interactionToken, 'AIとの通信中にエラーが発生しました。');
  }
}

async function callDifyWorkflow(prompt, userId, env) {
  const difyApiUrl = `${env.DIFY_API_BASE_URL || 'https://api.dify.ai'}/v1/workflows/run`;
  const response = await fetch(difyApiUrl, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${env.DIFY_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      inputs: { command: "ask", prompt },
      response_mode: 'blocking',
      user: userId,
    }),
  });

  if (!response.ok) {
    const errorText = await response.text();
    throw new Error(`Dify API error: ${response.status} - ${errorText}`);
  }

  const result = await response.json();
  return result.data?.outputs?.text || '応答が空でした。';
}

async function sendFollowupMessage(applicationId, token, content) {
  const webhookUrl = `https://discord.com/api/v10/webhooks/${applicationId}/${token}`;
  await fetch(webhookUrl, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ content: String(content).substring(0, 2000) }),
  });
}

async function verifySignature(request, env) {
  const signature = request.headers.get('x-signature-ed25519');
  const timestamp = request.headers.get('x-signature-timestamp');
  const body = await request.clone().text();

  if (!signature || !timestamp || !body || !env.DISCORD_PUBLIC_KEY) return false;

  try {
    const signatureBytes = hexToUint8Array(signature);
    const publicKeyBytes = hexToUint8Array(env.DISCORD_PUBLIC_KEY);
    const messageBytes = new TextEncoder().encode(timestamp + body);
    const cryptoKey = await crypto.subtle.importKey('raw', publicKeyBytes, { name: 'Ed25519' }, false, ['verify']);
    return await crypto.subtle.verify('Ed25519', cryptoKey, signatureBytes, messageBytes);
  } catch (error) {
    console.error('Error verifying signature:', error);
    return false;
  }
}

function createJsonResponse(data) {
  return new Response(JSON.stringify(data), { headers: { 'Content-Type': 'application/json' } });
}

function hexToUint8Array(hex) {
  return new Uint8Array(hex.match(/.{1,2}/g).map(byte => parseInt(byte, 16)));
}

貼り付けたら、「Save and deploy」を押して完了です。コードはもう準備済みなので、あと少しですよ❗

ステップ3: 環境変数を設定する


Cloudflare 設定画面

  1. Workerの「Settings」>「Variables」へ。
  2. 「Environment Variables」にこれらを追加。値は暗号化(Encrypt)で安全に!
変数名
DIFY_API_KEY DifyのAPIキー
DISCORD_APPLICATION_ID DiscordのApplication ID
DISCORD_PUBLIC_KEY DiscordのPublic Key
DIFY_API_BASE_URL (任意) DifyのURL(公式なら不要)

5. 仕上げ:全てを繋ぎこむ

最後に、DiscordにWorkerのURLを教えて、コマンドを登録しましょう。これで完成です!👏👏👏

ステップ1: Interactions Endpoint URLを設定する


Discord Developer PortalのGeneral Information画面

  1. CloudflareでWorkerのURL(xxx.yyy.workers.dev)をコピー。
  2. Discord Developer Portalの「General Information」で INTERACTIONS ENDPOINT URL に貼り付けて保存。
    • エラーが出なければOK! 鍵の設定が正しければ通りますよ。

ステップ2: スラッシュコマンドを登録する

ターミナルで一度だけ実行。curlコマンドをあなたの情報に置き換えてくださいね。クラウドだけでもDifyのHTTPノードを使って実行できますよ🙌

  • YOUR_DISCORD_BOT_TOKEN: Bot Token
  • YOUR_DISCORD_APPLICATION_ID: Application ID
curl -X PUT \
  -H "Authorization: Bot YOUR_DISCORD_BOT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '[
    {
      "name": "ask",
      "description": "AIに質問する",
      "options": [
        {
          "name": "prompt",
          "description": "質問内容",
          "type": 3,
          "required": true
        }
      ]
    }
  ]' \
  "https://discord.com/api/v10/applications/YOUR_DISCORD_APPLICATION_ID/commands"

JSONが返ってきたら成功です!⭕

6. 動作確認

お疲れ様でした! Discordサーバーで /ask を試してみてください。質問を入れて実行すると、AIが答えてくれます!🎉🎉

まとめ

いやー、こんなに簡単にDify、Cloudflare Workers、Discordを連携させてサーバーレスAIボットが作れちゃうなんて、すごいですよね!😁

DifyでAIアプリをサクッと準備して、Cloudflare Workersでお手軽にサーバー管理をゼロにしつつ、Discordのチャットから気軽に質問できるボットが完成です。

Xで投稿した動画で実際に動きを確認できます👇

拡張も自由自在ですよー、知識ベースを追加したり他のAPIとつなげたりして、自分だけのボットをどんどん進化させてみてくださいね。実際に作ってみて、わからないところがあったら遠慮なく聞いてください!

それと、XでDifyのTipsを毎日ポストしてるので、フォローしてもらえるとめっちゃ嬉しいです😊

Discussion