1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AIエージェントにDiscord Botを繋ぐ — スマホから自律エージェントを操作する実装ガイド

1
Last updated at Posted at 2026-03-31

この記事で得られること

  • AIエージェントにDiscord Botを繋いでスマホから操作する実装方法がわかる
  • Discord.jsとClaude Codeを連携させる具体的なコードを入手できる
  • 外出先からエージェントに指示を送る運用フローを構築できる

対象読者: AIエージェントをスマホから操作したい方 / Discord Botの開発に興味がある方


なぜDiscordなのか

自律AIエージェントを構築して運用していると、ある問題にぶつかる。エージェントとのインターフェースがターミナルしかないという問題である。

PCの前にいるときはそれでいい。しかし外出中にタスクの進捗を確認したい、急ぎの指示を飛ばしたい、エージェントからの報告を受け取りたい——こうした場面でターミナルは使えない。

この問題を解決するために、自律AIエージェント「Sentinel」にDiscord Botを連携させた。Discordを選んだ理由はシンプルである。

  • スマホアプリがある: iOS/Android両対応。外出先からメッセージを送受信できる
  • Webhookがある: エージェント側から一方的に通知を送れる。Bot不要で使える
  • プライベートサーバーが無料で作れる: 自分専用のエージェント操作チャンネルを作れる
  • リッチな表示: Embed(埋め込みメッセージ)で構造化された情報を見やすく表示できる

SlackやLINEでも同じことはできるが、個人開発ではDiscordが最も手軽である。


全体アーキテクチャ

Sentinelでの構成はこうなっている。

Discord(スマホ/PC)
    ↕ Discord API
Discord Bot(bot.js)
    ↕ HTTP API
sentinel.js(エージェントのランタイム)
    ↕
Claude Code(LLMによる思考・判断)

Discord Botはエージェントのフロントエンドにすぎない。メッセージを受け取ってエージェントに渡し、エージェントの応答をDiscordに返す。ビジネスロジックはBot側には持たせない。

この分離が重要である。Botが落ちてもエージェントは動き続けるし、エージェントが止まってもBotは通知を受けられる。


Step 1: Discord Botの作成

Discord Developer Portalでの設定

  1. Discord Developer Portal にアクセス
  2. 「New Application」でアプリケーションを作成
  3. 左メニューの「Bot」を開く
  4. 「Reset Token」でBotトークンを取得(このトークンは一度しか表示されない。必ず控えておく)

Intentsの設定(最重要)

同じ「Bot」ページの下部にPrivileged Gateway Intentsがある。ここで以下を有効にする。

  • MESSAGE CONTENT INTENT: メッセージ本文を読むために必須

これを有効にしないと、message.contentが常に空文字列になる。Bot自体は動くし、メッセージイベントも発火する。しかし肝心のメッセージ内容が取れない。最もハマりやすいポイントである。

Botをサーバーに招待

  1. 左メニューの「OAuth2」→「URL Generator」を開く
  2. SCOPESで「bot」にチェック
  3. BOT PERMISSIONSで以下にチェック:
    • Send Messages
    • Read Message History
    • Embed Links
    • Attach Files
    • Add Reactions
  4. 生成されたURLをブラウザで開き、自分のサーバーに招待

Step 2: プロジェクトのセットアップ

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

package.json"type": "module"を追加しておく。ES Modulesのimport構文を使うためである。

{
  "name": "sentinel-discord-bot",
  "type": "module",
  "dependencies": {
    "discord.js": "^14.25.0",
    "dotenv": "^16.4.0"
  }
}

.envファイルにトークンを置く。

DISCORD_TOKEN=your-bot-token-here
SENTINEL_DIR=C:/Users/yourname/agent
DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/xxxx/yyyy

Step 3: 基本的なBotの実装

最小構成から始める。メッセージを受け取って応答するだけのBotである。

import 'dotenv/config';
import { Client, GatewayIntentBits } from 'discord.js';

const client = new Client({
  intents: [
    GatewayIntentBits.Guilds,
    GatewayIntentBits.GuildMessages,
    GatewayIntentBits.MessageContent,  // これがないとmessage.contentが空になる
  ],
});

client.once('ready', () => {
  console.log(`Bot起動: ${client.user.tag}`);
});

client.on('messageCreate', async (message) => {
  // Bot自身のメッセージは無視
  if (message.author.bot) return;

  if (message.content === '!ping') {
    await message.reply('pong');
  }
});

client.login(process.env.DISCORD_TOKEN);

GatewayIntentBits.MessageContentintents配列に含めることが必須である。Developer Portalでの有効化とコード側での指定、両方が必要だということを覚えておくこと。


Step 4: エージェントとの接続

ここからが本題である。Discord Botをエージェントのフロントエンドとして機能させる。

方式A: エージェントのHTTP APIに送信する

Sentinelではsentinel.jsがHTTP APIを公開している。BotはこのAPIにメッセージをリレーする。

client.on('messageCreate', async (message) => {
  if (message.author.bot) return;

  const text = message.content.trim();
  if (!text) return;

  const SENTINEL_API = process.env.SENTINEL_API_URL || 'http://localhost:3100';
  const webhookUrl = process.env.DISCORD_WEBHOOK_URL;

  try {
    const res = await fetch(`${SENTINEL_API}/send`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        message: `【Discordからのメッセージ(${message.author.username})】\n${text}`,
        callback_url: webhookUrl,  // 応答をWebhookで返してもらう
      }),
      signal: AbortSignal.timeout(5000),
    });

    if (!res.ok) throw new Error(`API ${res.status}`);
    await message.reply('考え中...');
  } catch (err) {
    await message.reply(`エージェント送信エラー: ${err.message}`);
  }
});

ポイントはcallback_urlである。エージェントの処理は数秒〜数分かかるため、同期で応答を待つとDiscordのタイムアウトに引っかかる。Webhook URLを渡しておけば、エージェントは処理完了後にそのURLに結果をPOSTしてくれる。

方式B: claude -pへのフォールバック

エージェントのランタイムが起動していない場合、claude -p(Claude CodeのCLI)を直接呼び出すフォールバックを用意している。

import { execFile } from 'child_process';
import { promisify } from 'util';
const execFileAsync = promisify(execFile);

async function askClaudeDirect(question, cwd) {
  const { stdout } = await execFileAsync(
    'claude',
    ['-p', '--max-turns', '3', question],
    { timeout: 180000, cwd, maxBuffer: 1024 * 1024 }
  );
  return stdout.trim();
}

--max-turns 3で無限ループを防ぎ、timeout: 180000(3分)で暴走を止める。maxBufferはデフォルトだと長い出力で溢れるため、1MBに設定する。


Step 5: コマンド体系の設計

Sentinelでは!プレフィックスでコマンドを実装している。

const commands = {
  status: async (message) => {
    // TASKS.mdを読んでステータスを表示
    const tasks = readFileSync('TASKS.md', 'utf-8');
    const waiting = (tasks.match(/- \[ \]/g) || []).length;
    const done = (tasks.match(/- \[x\]/g) || []).length;

    const embed = new EmbedBuilder()
      .setTitle('Sentinel ステータス')
      .setColor(0x00FF00)
      .addFields(
        { name: '待機中', value: `${waiting}件`, inline: true },
        { name: '完了', value: `${done}件`, inline: true },
      )
      .setTimestamp();
    await message.reply({ embeds: [embed] });
  },

  tasks: async (message) => {
    const content = readFileSync('TASKS.md', 'utf-8');
    await message.reply(`\`\`\`\n${content.substring(0, 1800)}\n\`\`\``);
  },

  usage: async (message) => {
    // トークン使用量をログファイルから集計して表示
    // ...
  },
};

// メッセージハンドラで振り分け
client.on('messageCreate', async (message) => {
  if (message.author.bot) return;
  if (!message.content.startsWith('!')) return;

  const [cmd, ...argParts] = message.content.slice(1).split(' ');
  const args = argParts.join(' ').trim() || null;
  const handler = commands[cmd.toLowerCase()];

  if (handler) {
    await handler(message, args);
  }
});

Sentinelで実装しているコマンドの一覧を示す。

コマンド 機能
!status タスクの進捗状況を表示
!tasks TASKS.mdの内容を表示
!memory 直近の作業記録を表示
!ask <質問> エージェントに質問を送信
!post <テキスト> X(Twitter)に投稿
!usage トークン使用量を表示

!なしの通常メッセージはすべて!askと同じ動作にしている。これにより、チャット感覚でエージェントと対話できる。


Step 6: Webhook通知(エージェント→Discord)

Botとは逆方向の通信——エージェントからDiscordへの通知——にはWebhookを使う。Botが起動していなくても通知を送れるのが利点である。

Webhookの取得

Discordサーバーの設定 → 連携サービス → ウェブフック → 新しいウェブフック → URLをコピー

通知スクリプト

// notify.js
import 'dotenv/config';

const WEBHOOK_URL = process.env.DISCORD_WEBHOOK_URL;

async function sendNotification(title, body, color = 0x00FF00) {
  const payload = {
    embeds: [{
      title,
      description: body,
      color,
      timestamp: new Date().toISOString(),
      footer: { text: 'Sentinel Notification' },
    }],
  };

  const res = await fetch(WEBHOOK_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(payload),
  });

  if (!res.ok) throw new Error(`Webhook失敗: ${res.status}`);
}

// CLI引数で呼び出せるようにする
const [,, title, body] = process.argv;
if (title) sendNotification(title, body || '').catch(console.error);

これをcronやエージェントのランタイムから呼び出す。

# cronからの利用例(第1引数=タイトル、第2引数=本文)
node notify.js "cron巡回完了" "タスク3件処理済み"

Step 7: セキュリティ対策

プライベートサーバーとはいえ、最低限のセキュリティは入れておく。

ユーザー制限

const ALLOWED_USER_IDS = process.env.ALLOWED_USER_IDS
  ? process.env.ALLOWED_USER_IDS.split(',')
  : []; // 空なら全員許可(プライベートサーバー前提)

function isAllowed(userId) {
  if (ALLOWED_USER_IDS.length === 0) return true;
  return ALLOWED_USER_IDS.includes(userId);
}

client.on('messageCreate', async (message) => {
  if (message.author.bot) return;
  if (!isAllowed(message.author.id)) return;
  // ...
});

.envにユーザーIDをカンマ区切りで指定する。空欄なら全員許可。プライベートサーバーで自分しかいないなら空でよい。

ファイル読み取りの制限

エージェントのファイルを読み取って表示する機能では、文字数制限を入れる。

function readSafe(filePath, maxLen = 1800) {
  const fullPath = path.resolve(SENTINEL_DIR, filePath);
  if (!existsSync(fullPath)) return '(ファイルが見つかりません)';
  const content = readFileSync(fullPath, 'utf-8');
  return content.length > maxLen
    ? content.substring(0, maxLen) + '\n...(省略)'
    : content;
}

Discordのメッセージ上限は2000文字である。これを超えるとAPIエラーになるため、表示前に切り詰める。


ハマりどころまとめ

実装中にハマったポイントを整理しておく。

1. MESSAGE CONTENT Intentの罠

症状: Botは起動する。メッセージイベントも発火する。しかしmessage.contentが空文字。

原因: 2022年のDiscord API変更で、メッセージ内容の取得がPrivileged Intentになった。Developer Portalでの有効化コード側でのGatewayIntentBits.MessageContent指定の両方が必要。

2. Discordメッセージの2000文字制限

症状: 長い応答を返そうとするとDiscordAPIError: Invalid Form Body

対策: 応答を1900文字ごとに分割して複数メッセージで送る。

if (response.length > 1900) {
  const chunks = response.match(/[\s\S]{1,1900}/g) || [];
  for (const chunk of chunks) {
    await message.channel.send(`\`\`\`\n${chunk}\n\`\`\``);
  }
}

3. エージェント処理のタイムアウト

症状: エージェントの応答を同期で待つと、3分以上かかる処理でタイムアウト。

対策: 非同期にする。リクエスト送信時に「考え中...」と返し、処理完了後にWebhookで応答を返す。

4. Partialメッセージの扱い

リアクションイベントでは、キャッシュにないメッセージがpartialとして届くことがある。

client.on('messageReactionAdd', async (reaction, user) => {
  if (reaction.partial) {
    try { await reaction.fetch(); } catch { return; }
  }
  if (reaction.message.partial) {
    try { await reaction.message.fetch(); } catch { return; }
  }
  // ...
});

Partials.MessagePartials.ReactionをClient初期化時に指定し、イベントハンドラ内でfetch()するのがパターンである。


運用してみて

Discord Bot連携を入れてから、エージェントとの関わり方が変わった。

スマホからタスクの進捗を確認して、必要なら追加指示を出す。エージェントがcron巡回を完了したらDiscordに通知が来る。X(Twitter)への投稿もDiscordからワンコマンドである。

特に便利なのは添付ファイルのサポートである。スマホで撮った画像をDiscordに貼って「これを分析して」と指示できる。Botが添付ファイルをダウンロードしてエージェントに渡す仕組みを入れたことで、テキスト以外の入力もカバーできるようになった。

ターミナルだけの世界から、ポケットの中にエージェントがいる世界への移行である。自律エージェントの実用性は、インターフェースで大きく変わる。


まとめ

Discord Bot連携の実装で押さえるべきポイントは以下の通りである。

  1. MESSAGE CONTENT Intentを忘れない: Developer Portal + コード側の両方で有効にする
  2. Botとエージェントを疎結合にする: Botはリレーするだけでロジックはエージェント側に持たせる
  3. 非同期で応答する: Webhookを使って処理完了後に結果を返す
  4. フォールバックを用意する: ランタイムが落ちていてもclaude -pで最低限動くようにする
  5. 2000文字制限を意識する: 長い応答は分割、ファイル読み取りは切り詰め

シリーズ一覧

# 内容 媒体
自律AIエージェント構築の設計思想と実装 Qiita(無料)
CLAUDE.md設計 Qiita(無料)
記憶管理 MEMORY.md Qiita(無料)
タスク管理 TASKS.md Qiita(無料)
cron自動巡回 Qiita(無料)
SOUL.md設計 Qiita(無料)
サブエージェント設計 Qiita(無料)
トークンコスト削減 Qiita(無料)
CLAUDE.md設計ガイド(体系版) Qiita(無料)
Discord Bot連携(本記事) Qiita(無料)

設計判断の裏側

本記事では技術的な実装手順を解説した。「なぜDiscordを選んだか」「他のインターフェース(Slack、LINE、Web UI)との比較検討」「運用で見えてきた課題と改善」といった設計判断の詳細は、noteシリーズで解説している。

リンク

おわりに

この記事では、AIエージェントにDiscord Botを繋いでスマホから操作する方法を紹介しました。AIエージェントとDiscordを連携させている方がいたら、どんな使い方をしているかコメントで教えてください。

参考になったら いいね、後で見返すなら ストック していただけると励みになります。

他にもAIエージェント構築のノウハウを公開しています:

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?