社内アプリの不具合報告に「対応したら本人の Discord へ返信」を付ける(Next.js + Discord Bot)
報告者を名簿から選ばせて Discord ユーザーIDを確定し、対応完了時に Bot が本人へ DM を返す。二重送信防止・DM不可の可視化・特権インテントの有効化と本番検証手順まで。
約4.1万トークンの節約 (API料金換算で約62円分)。 要件定義・技術調査・試行錯誤ぶんのトークンがまるごと不要になります。※ 出品者申告とレビューに基づく推定値。モデル・タスク内容により変動します。
この巻物について
「社内アプリの不具合報告に「対応したら本人の Discord へ返信」を付ける(Next.js + Discord Bot)」は、Web開発カテゴリのAI指示書(MDファイル)です。報告者を名簿から選ばせて Discord ユーザーIDを確定し、対応完了時に Bot が本人へ DM を返す。二重送信防止・DM不可の可視化・特権インテントの有効化と本番検証手順まで。この巻物をAIに読み込ませると、ゼロから設計・調査する場合に比べて 約4.1万トークン(API料金換算で約62円)・85%のトークンを節約できます。
- カテゴリ
- Web開発
- 対応AI
- claude-code、cursor、codex-cli
- ライセンス
- 商用利用可 (再販不可)
- 価格
- 無料
- ゼロから開発時
- 約4.8万トークン
- この巻物使用時
- 約7,000トークン
- 節約量
- 約4.1万トークン (約62円)
- 更新日
- 2026-10-12
使い方 (AIに渡す3つの方法)
いちばん簡単なのはワンライナー。Claude Code のターミナルに貼るだけです。
claude "https://makimono-md.vercel.app/api/v1/files/discord-next-js-discord-bot/raw を読み込んで、この指示書どおりに実装して"
中身
社内アプリの不具合・要望フォームで「対応したら報告者本人の Discord に返信」を実装する(Next.js App Router + Discord Bot)
社内アプリに不具合報告フォームを付けると、報告した人は「あれどうなった?」を自分からは追えない。 報告が増えるほど「対応済みを伝える」手作業が増え、やがて誰も報告しなくなる。
この指示書は、報告者を特定し、対応完了時にその人の Discord へ Bot が DM を返すまでを、 Next.js App Router + 任意の DB(ここでは PostgREST 互換を例示)+ Discord Bot で作る手順をまとめる。 AI エージェントにそのまま読ませて実装させられる粒度で書いてある。
前提
- Next.js App Router(
app/ディレクトリ) - Discord Bot を1つ作成済み(
DISCORD_BOT_TOKENがある)。Bot は対象サーバーに参加済み - 投稿を保存するテーブルが1つある(列は後述)
- 管理画面が Cookie などで認証されている
設計の要点(ここを外すと動かない)
- 報告者の Discord ユーザーIDを投稿時に確定する。 名前の文字列だけでは DM を送れない。 フォームで「お名前」をサーバーのメンバー一覧から選ばせて、裏で ID を hidden に積む。
- 名簿が引けない環境でも壊れない。 メンバー一覧 API が失敗したら自由入力に落とし、 ID が無い投稿は「DM不可」と管理画面に表示する。沈黙しない。
- 二重送信を防ぐ列を持つ。 状態を何度 done にしても DM は1回だけ。
- 通知の失敗で本処理を失敗にしない。 状態更新は成功させ、DM 失敗は管理者へ別途知らせる。
1. テーブルに3列足す(冪等)
alter table public.app_feedback add column if not exists submitter_discord_id text;
alter table public.app_feedback add column if not exists reply_message text;
alter table public.app_feedback add column if not exists replied_at timestamptz;
既存列は id / kind / title / body / page_path / submitter / status / priority / admin_note / created_at / updated_at を想定。
status は new / triaged / in_progress / done / rejected。
2. Discord 送信ヘルパー lib/discord-dm.ts
const API = "https://discord.com/api/v10";
const H = () => ({ Authorization: `Bot ${process.env.DISCORD_BOT_TOKEN}` });
export function isDiscordUserId(v: string): boolean {
return /^\d{17,20}$/.test(v);
}
export async function sendDirectMessage(userId: string, content: string): Promise<boolean> {
if (!process.env.DISCORD_BOT_TOKEN || !isDiscordUserId(userId)) return false;
try {
// 1) 相手との DM チャンネルを開く(既にあれば同じ ID が返る)
const ch = await fetch(`${API}/users/@me/channels`, {
method: "POST",
headers: { ...H(), "Content-Type": "application/json" },
body: JSON.stringify({ recipient_id: userId }),
});
if (!ch.ok) return false;
const { id } = await ch.json();
// 2) そこへ送る。allowed_mentions を空にして意図しないメンションを防ぐ
const msg = await fetch(`${API}/channels/${id}/messages`, {
method: "POST",
headers: { ...H(), "Content-Type": "application/json" },
body: JSON.stringify({ content, allowed_mentions: { parse: [] } }),
});
return msg.ok;
} catch {
return false; // 通知の失敗を外へ投げない
}
}
let cache: { at: number; rows: { id: string; name: string }[] } | null = null;
export async function fetchGuildMembers(): Promise<{ id: string; name: string }[]> {
if (cache && Date.now() - cache.at < 60 * 60 * 1000) return cache.rows;
try {
const g = await fetch(`${API}/users/@me/guilds`, { headers: H() });
if (!g.ok) return [];
const map = new Map<string, string>();
for (const guild of await g.json()) {
const m = await fetch(`${API}/guilds/${guild.id}/members?limit=1000`, { headers: H() });
if (!m.ok) continue; // 権限が無いサーバーは黙って飛ばす
for (const row of await m.json()) {
if (row.user?.bot) continue;
map.set(row.user.id, row.nick || row.user.global_name || row.user.username);
}
}
const rows = [...map].map(([id, name]) => ({ id, name }))
.sort((a, b) => a.name.localeCompare(b.name, "ja"));
cache = { at: Date.now(), rows };
return rows;
} catch {
return [];
}
}
メンバー一覧には特権インテントが要る。 無効のままだと 403 {"code":50001,"message":"Missing Access"} になる。
開発者ポータルの Bot ページで SERVER MEMBERS INTENT をオンにするか、API で有効化する(後述)。
3. 名簿 API app/api/feedback/members/route.ts
import { NextResponse } from "next/server";
import { fetchGuildMembers } from "@/lib/discord-dm";
export const runtime = "nodejs";
export const dynamic = "force-dynamic";
export async function GET() {
const members = await fetchGuildMembers();
// 取れなくても 200 + 空配列。フォーム側は自由入力に落ちる
return NextResponse.json({ ok: true, members }, { headers: { "Cache-Control": "no-store" } });
}
4. フォーム側(クライアント)
- モーダルを開いた最初の1回だけ
/api/feedback/membersを取得する(開くたびに叩かない) - 1件以上あれば
<select>、末尾に「一覧にない(自分で入力)」を置く。0件ならテキスト入力だけ - 選んだ
{name, id}をlocalStorageに保存し、次回の初期選択にする。読み書きは必ず try/catch (プライベートウィンドウで例外になる)。保存値が今の名簿に無ければ無視する - 送信する FormData に
submitter(表示名)とsubmitter_discord_id(選択時のみ)を入れる
5. 投稿 API(抜粋)
const submitterId = String(form.get("submitter_discord_id") || "");
const discordId = isDiscordUserId(submitterId) ? submitterId : null;
// ... DB へ insert(discordId も保存)
// 管理者への通知は DM を第1経路にし、失敗時だけ従来のチャンネル通知へ落とす
let via = "dm";
let ok = await sendDirectMessage(process.env.ADMIN_DISCORD_USER_ID!, content);
if (!ok) { ok = await notifyChannel(content); via = "bot"; }
return NextResponse.json({ ok: true, id, sinks: { db: true, discord: ok, via } });
sinks.via を返すと、本番で1回叩くだけでどの経路を通ったか分かる。検証がとても楽になる。
6. 状態更新 API(ここが本題)
// 1) 先に現在の行を読む(旧 status と replied_at が要る)
const before = await getRow(id);
// 2) 更新する(reply_message も保存)
await patchRow(id, { status, priority, reply_message: replyMessage || null, updated_at: now });
// 3) 条件を全部満たす時だけ DM
const closing = status === "done" || status === "rejected";
if (closing && before.status !== status && !before.replied_at
&& isDiscordUserId(before.submitter_discord_id || "")) {
const body = [
`✅ ${APP_NAME} — ご報告の対応が完了しました`,
`件名: ${before.title}`,
status === "rejected" ? "今回は対応を見送らせていただきました。" : "",
"",
replyMessage || "対応しました。ご報告ありがとうございました。",
"",
"── ご報告いただいた内容",
String(before.body).slice(0, 200),
].filter(Boolean).join("\n");
if (await sendDirectMessage(before.submitter_discord_id, body)) {
await patchRow(id, { replied_at: new Date().toISOString() }); // 二重送信を防ぐ印
} else {
await sendDirectMessage(ADMIN_ID, `⚠️ ${APP_NAME} — 完了報告を報告者へ返せませんでした\n件名: ${before.title}`);
}
}
// 4) 通知の成否に関わらず一覧へリダイレクト
管理画面側には 返信メッセージ の textarea と、行ごとの DM可 / DM不可(名前が一覧から選ばれていません) /
返信済み <日時> を出す。DM不可が見えないと、届いていないことに誰も気づかない。
7. 特権インテントを API で有効にする
開発者ポータルを開かずに済ませたい場合、アプリ所有者の Bot トークンで次を1回叩く。
curl -X PATCH "https://discord.com/api/v10/applications/@me" \
-H "Authorization: Bot $DISCORD_BOT_TOKEN" \
-H "Content-Type: application/json" \
--data-binary '{"flags":32768}'
32768 = 1 << 15(GATEWAY_GUILD_MEMBERS_LIMITED)。
注意: flags は置き換えなので、元から立っていたビットが消える。 先に現在値を読んでから OR する。
curl -s -H "Authorization: Bot $DISCORD_BOT_TOKEN" \
"https://discord.com/api/v10/applications/@me" | grep -o '"flags":[0-9]*'
成功判定は「設定が変わったこと」ではなく 実際に名簿が引けることで行う。
curl -s -o /dev/null -w "%{http_code}\n" -H "Authorization: Bot $DISCORD_BOT_TOKEN" \
"https://discord.com/api/v10/guilds/<GUILD_ID>/members?limit=5" # 200 なら成功
8. 本番での検証手順(これを通すまで「完成」と呼ばない)
# ① 名簿が返るか(0件ならインテント未設定)
curl -s "https://<app>/api/feedback/members?cb=$(date +%s)"
# ② 投稿 → 管理者へ DM。via が dm なら第1経路を通っている
curl -s -X POST -F "kind=bug" -F "title=動作確認" -F "body=テストです" \
-F "submitter=動作確認" -F "submitter_discord_id=<自分のID>" \
"https://<app>/api/feedback"
# ③ 完了にする(管理者 Cookie が要る)
curl -s -X POST -H "Cookie: <認証Cookie>" \
-F "id=<①で返った id>" -F "status=done" -F "priority=normal" \
-F "reply_message=動作確認です" "https://<app>/api/feedback/update"
# ④ replied_at が入っていれば DM 送信成功(成功時だけ記録される設計なので、これが証拠になる)
最後にテスト行を削除する。実在のチャンネルや他人へ DM を飛ばすテストはしない。 宛先は自分か、もともとその通知を受け取っている管理者だけにする。
つまずきやすい点
| 症状 | 原因と対処 |
|---|---|
| 名簿が常に空 | SERVER MEMBERS INTENT 未設定。403 code 50001 が出ているはず |
| 設定変更 API が拒否される | 複数フラグをまとめて送っていないか。変更したい1つだけに絞ると通ることがある |
| DM が届かない | 相手が DM を拒否している/共通サーバーに居ない。sendDirectMessage が false を返すので管理者へ通知して気づけるようにする |
| 完了にするたび DM が飛ぶ | replied_at を見ていない。旧 status との比較だけでは再発する |
| 日本語が DB で化ける | Windows の curl -F が CP932 で送っている。検証は Node の FormData + fetch で行う |
| PostgREST が 401 を返す | PowerShell の Invoke-RestMethod は新形式キーで 401 になることがある。curl.exe なら 200 |
この設計を選ぶ理由
報告者の特定を「名前の自由入力」にすると、対応完了時に誰へ返すか決められず、結局人が仲介する。 逆に「全員に Discord ID を調べて入力させる」のは初回の手間が大きい。 名簿から1回選ぶだけにして ID を裏で確定させるのが、報告側の手間と確実性のつり合いが最も良い。
よくある質問
+「社内アプリの不具合報告に「対応したら本人の Discord へ返信」を付ける(Next.js + Discord Bot)」とは何ですか?
報告者を名簿から選ばせて Discord ユーザーIDを確定し、対応完了時に Bot が本人へ DM を返す。二重送信防止・DM不可の可視化・特権インテントの有効化と本番検証手順まで。
+どれくらいトークン(費用)を節約できますか?
ゼロから開発すると約4.8万トークンかかりますが、この巻物を使えば約7,000トークンで済みます。差し引き約4.1万トークン(API料金換算で約62円)・85%の節約です。
+どうやって使いますか?
無料です。MDファイルを Claude Code などのAIに読み込ませるだけ。ワンライナーをターミナルに貼れば実装が始まります。要件定義や技術調査を省いて実装だけにトークンを使えます。
+どのAIツールに対応していますか?
claude-code、cursor、codex-cli に対応しています。
+商用利用できますか?
ライセンスは「商用利用可 (再販不可)」です。
🤝 自分でAIを動かすのは、まだ不安…という方へ
この巻物の内容を、AIを使うプロに丸ごと任せることもできます。姉妹サービスAI代行堂なら「LINEで頼むだけで、仕事が完成」。
関連する巻物
Next.js + Supabase + Vercel 立ち上げ完全自動化MD
新規Webサービスの立ち上げ (GCP/GitHub/Vercel/Supabase のプロジェクト作成〜環境変数〜本番デプロイ) を AI に一気通貫でやらせる指示書。人間の作業はログイン1回だけ。
投稿の審査キューを「信頼済みだけ自動公開」で捌く設計(なりすまし穴つき)
審査キューに投稿が溜まったまま埋もれる問題を、信頼済み投稿だけ即公開する形で潰す指示書。無検証のキー発行を信頼判定に使うと第三者が自社メールを騙れる穴と、サーバレスで静的公開棚に実行時公開を足す方法、検証10項目まで含む。
DB型サイトを「一覧だけ会員限定・個別ページは残す」に切り替える指示書(Next.js App Router)
自社DBで集客していたサイトが競合のリスト抜き取りに気づいた時の改修手順。名前が並ぶバルクな一覧だけを会員限定にし、個別ページはtitle/H1とCTAを残す。ItemList JSON-LDやsitemapの漏れ、force-dynamic化のコスト副作用、Layer1(HTML)+Layer2(Playwright実描画)の受け入れテストまで含む。
この巻物、誰かのトークンも救えます
𝕏 で節約レシートをシェア