はじめに
ホームサーバーで自前の AI 環境を整えていく中で、Slack から手軽に呼び出せる Bot が欲しくなりました。 既存の記事では OpenAI や Claude の API を直接叩くものが多いですが、今回は Dify のバックエンド API を経由する構成にしました。 Dify を挟むことで、自前で構築したナレッジベースの検索や MCP ツール連携をそのまま Bot から利用できます。
本記事では、Slack アプリの作成からソケットモードの設定、Dify API との連携、ホームサーバーでの起動まで、一通りの手順を紹介します。
ソケットモードとは
Slack Bot を実装する方法には大きく 2 種類あります。
| 方式 | 概要 | 外部公開 URL |
|---|---|---|
| HTTP (Webhook) | Slack からのイベントを HTTP エンドポイントで受け取る | 必要 |
| ソケットモード | Bot 側から WebSocket で Slack に接続しイベントを受け取る | 不要 |
ソケットモードの最大のメリットは、外部から到達可能な URL を用意しなくてよい点です。 ホームサーバーや社内のオンプレサーバーで動かす場合、通常はポート開放やリバースプロキシの設定が必要になりますが、ソケットモードならその手間が一切かかりません。 Bot 側からアウトバウンド接続するだけなので、ファイアウォールの内側にいてもそのまま動作します。
使用環境
- Node.js 22
- TypeScript 5.x
- @slack/bolt 4.x
- tsx 4.x
- Dify(セルフホスト)
- ホームサーバー(Ubuntu 24.04 on Proxmox)
Slack アプリを作成する
1. アプリを新規作成する
https://api.slack.com/apps にアクセスし、Create New App をクリックします。 From scratch を選択し、アプリ名とインストール先のワークスペースを入力して Create App をクリックします。
2. ソケットモードを有効にする
左サイドバーの Settings → Socket Mode を開き、Enable Socket Mode をオンにします。
トークン名の入力を求められます(例: my-bot-app-token)。 任意の名前を入力して Generate をクリックすると、xapp- から始まる App-Level Token が発行されます。 このトークンは後で使うので控えておきます。
3. イベントの購読を設定する
左サイドバーの Features → Event Subscriptions を開きます。 Enable Events をオンにします(ソケットモードが有効な場合、Request URL の入力は不要です)。
Subscribe to bot events のセクションで Add Bot User Event をクリックし、以下のイベントを追加します。
message.channels— パブリックチャンネルのメッセージmessage.im— ダイレクトメッセージ
ページ下部の Save Changes をクリックします。
4. ボットのスコープを設定する
左サイドバーの Features → OAuth & Permissions を開きます。 Scopes → Bot Token Scopes のセクションで Add an OAuth Scope をクリックし、以下のスコープを追加します。
chat:write— メッセージの送信channels:history— チャンネルのメッセージ履歴を読むim:history— DM の履歴を読むapp_mentions:read— Bot へのメンションを受け取る
5. ワークスペースにインストールする
OAuth & Permissions ページの上部にある Install to Workspace ボタンをクリックします。 権限の確認画面で 許可する をクリックするとインストールが完了します。
インストール後に xoxb- から始まる Bot User OAuth Token が表示されます。こちらも控えておきます。
6. トークンの確認
ここまでで 2 種類のトークンが手元にそろっているはずです。
| トークン | プレフィックス | 用途 |
|---|---|---|
| App-Level Token | xapp- |
ソケットモードの WebSocket 接続 |
| Bot User OAuth Token | xoxb- |
API の呼び出し(メッセージ送信など) |
Dify 連携の準備
1. Dify でアプリを作成する
Dify のダッシュボードにアクセスし、スタジオ → アプリを作成する からチャットボット型のアプリを作成します。 ナレッジベースや MCP ツールをあらかじめ設定しておくと、Bot 経由でそのまま利用できます。 今回は Ollama で gemma3 を利用します。

Ollama を Dify で使用する設定はこちらの記事を参照してください。
2. API キーを取得する
作成したアプリの左側のメニューを開き、APIキー を発行します。


app- から始まるキーが発行されるので控えておきます。
エンドポイントの URL(例: https://your-dify-host/v1)も確認しておきます。 セルフホストの場合はホームサーバー上の Dify のアドレスになります。
Slack Bot を実装する
ディレクトリ構成
slack-dify-bot/
├── src/
│ └── index.ts
├── package.json
├── tsconfig.json
├── docker-compose.yml
└── .env依存パッケージのインストール
npm install @slack/bolt
npm install -D typescript tsx @types/node環境変数の設定
.env ファイルを作成し、取得したトークンを設定します。
SLACK_BOT_TOKEN=xoxb-xxxxxxxxxxxx
SLACK_APP_TOKEN=xapp-xxxxxxxxxxxx
DIFY_API_KEY=app-xxxxxxxxxxxx
DIFY_BASE_URL=https://your-dify-host/v1tsconfig.json の作成
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src"]
}ボットの実装
src/index.ts を作成します。
import { App } from "@slack/bolt";
const app = new App({
token: process.env.SLACK_BOT_TOKEN!,
appToken: process.env.SLACK_APP_TOKEN!,
socketMode: true, // ソケットモードを有効にする
});
interface DifyRetrieverResource {
position: number;
dataset_id: string;
dataset_name: string;
document_id: string;
document_name: string;
segment_id: string;
score: number;
content: string;
}
interface DifyUsage {
prompt_tokens: number;
completion_tokens: number;
total_tokens: number;
total_price: string;
currency: string;
latency: number;
}
interface DifyResponse {
event: string;
task_id: string;
id: string;
message_id: string;
conversation_id: string;
mode: string;
answer: string;
metadata: {
usage: DifyUsage;
retriever_resources?: DifyRetrieverResource[];
};
created_at: number;
}
async function callDify(
userMessage: string,
conversationId = ""
): Promise<{ answer: string; conversationId: string }> {
console.log("Calling Dify with message:", userMessage);
const resp = await fetch(`${process.env.DIFY_BASE_URL}/chat-messages`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.DIFY_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
inputs: {},
query: userMessage,
response_mode: "blocking", // 同期レスポンス
conversation_id: conversationId,
user: "slack-bot",
}),
signal: AbortSignal.timeout(60_000), // 60秒でタイムアウト
});
console.log("Dify response status:", resp.status, resp.headers.get("content-type"));
if (!resp.ok) {
const body = await resp.text();
throw new Error(`Dify API error: ${resp.status} ${resp.statusText} - ${body}`);
}
const data = (await resp.json()) as DifyResponse;
console.log("Dify API response:", JSON.stringify(data, null, 2));
// レスポンスと会話IDを返す(次のターンで会話を継続するため)
return { answer: data.answer, conversationId: data.conversation_id ?? "" };
}
// チャンネルへのメッセージを受け取る
app.message(async ({ message, say }) => {
// Bot 自身の投稿は無視する
if ("bot_id" in message) return;
if (!("text" in message) || !message.text) return;
try {
const { answer } = await callDify(message.text);
console.log("Posting answer to channel:", answer);
await say(answer);
} catch (err) {
console.error("Error in message handler:", err);
}
});
// Bot へのメンションを受け取る
app.event("app_mention", async ({ event, say }) => {
// メンション部分(<@UXXXXXXXX>)を除去する
const userText = event.text.split(">").slice(1).join(">").trim();
if (!userText) return;
try {
const { answer } = await callDify(userText);
console.log("Posting answer to mention:", answer);
await say({ text: answer, thread_ts: event.ts });
} catch (err) {
console.error("Error in app_mention handler:", err);
}
});
(async () => {
await app.start();
console.log("⚡️ Bolt app is running!");
})();起動する
開発時は tsx で直接実行できます。--env-file オプションで .env を読み込むため、dotenv は不要です。
npx tsx --env-file .env src/index.ts本番運用は後述の Docker Compose で実行します。
起動に成功すると以下のようなログが表示されます。
⚡️ Bolt app is running!ソケットモードで Slack に接続が確立され、メッセージを受け取れる状態になります。 サーバーを再起動しても自動で起動するよう、Docker Compose でコンテナ化しておくと便利です。
実行例は以下のようになります。

Docker Compose での永続化
プロジェクトルートに Dockerfile と docker-compose.yml を作成します。
# Dockerfile
FROM node:22-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev && npm install tsx
COPY src ./src
CMD ["npx", "tsx", "src/index.ts"]# docker-compose.yml
services:
slack-dify-bot:
build: .
env_file:
- .env
restart: always以下のコマンドでバックグラウンド起動できます。
docker compose up -dホストの再起動後も restart: always によって自動的に起動します。 ログの確認は docker compose logs -f でリアルタイムに追えます。 実運用する場合はindex.tsをビルドして node dist/index.js で起動するようにするのがよいかと思います。
ソケットモードで動かすメリット
冒頭でも触れたようにソケットモードを使うと、ネットワーク的に同じセグメントにいる他のオンプレサービスとシームレスに連携できます。
たとえば以下のような構成が、追加のポート開放なしに実現できます。
- Dify(セルフホスト):外部公開せずローカルネットワーク内でのみ通信
- GitLab:コードリポジトリの情報を Dify のナレッジに取り込む
- Grafana:メトリクスをクエリして Slack に通知する
Dify の MCP 連携を使えば、これらのサービスを Bot から自然言語で操作することも可能です。 外部公開なしに強力な AI アシスタントを社内・自宅環境に閉じて運用できるのはソケットモードならではのメリットです。
トラブルシュート
Dify 1.13.0 で blocking モードが機能しない
Dify 1.13.0 には、response_mode: "blocking" を指定しても text/event-stream が返ってくるバグがあります。
具体的には以下のような挙動になります。
- レスポンスヘッダーが
Content-Type: text/event-stream; charset=utf-8になる - レスポンスが数十バイトで途切れ、
resp.json()がパースエラーになる curlではtransfer closed with outstanding read data remainingのようなエラーが出る
この問題は GitHub Issue #32351 として報告されており、Dify 1.13.1 で修正済みです。 セルフホストで 1.13.0 を使っている場合は 1.13.1 以降へのアップデートで解消されます。
デバッグ時は console.log でステータスコードと Content-Type ヘッダーを確認しておくと原因を特定しやすいです。
console.log("Dify response status:", resp.status, resp.headers.get("content-type"));Content-Type が text/event-stream になっていれば、このバグを踏んでいる可能性が高いです。
まとめ
ソケットモードを使うと外部公開 URL が不要になるため、ホームサーバーや社内オンプレ環境でも簡単に Slack Bot を動かせます。 Dify のバックエンド API を経由することで、自前のナレッジベースや MCP ツールを Bot からそのまま利用できます。 ホームサーバー上で完結する構成なので、他のオンプレサービスとの連携もネットワーク的に容易です。ぜひ試してみてください。
続きはSlackのスレッドでDifyとの会話を継続するをご覧ください。


コメント