目標

將 Claude Code(小可)接上 Discord,讓用戶可以透過 Discord 頻道或私訊(DM)直接與 Claude Code 互動。


使用的工具

  • Repo~/claude-code-discord
  • Runtime:Deno
  • Bot Framework:discord.js 14
  • AI:Anthropic Claude Agent SDK(@anthropic-ai/claude-agent-sdk

遇到的問題與解法

1. Nested Claude Code Protection(Exit Code 1)

症狀:Bot 啟動後呼叫 Claude 時直接 exit code 1。

原因:Claude Code 有保護機制,偵測到 CLAUDECODE 環境變數就拒絕在 nested 環境下啟動。Bot 本身是在 Claude Code TUI 裡跑的,所以繼承了這個 env var。

修復:在 claude/client.tsenvVars 裡明確清掉:

CLAUDECODE: '',

2. Cannot read properties of null (substring)

症狀:MessageCreate handler 中呼叫假的 ctx 時出現 null 錯誤。

原因:假的 InteractionContext 缺少 getChannelId()getMemberRoleIds()getUserId() 等方法,並且 editReply / deferReply 的機制與真正的 Discord Interaction 不同。

修復:補齊所有 ctx 方法,並改用 statusMsg.edit() + channel.send() 取代 message.reply()


3. Invalid Form Body embeds[0].timestamp

症狀:Discord API 回傳 DATE_TIME_TYPE_PARSE 錯誤。

原因:全域 embed 裡有 timestamp: true,但 Discord 要求 ISO8601 字串,不接受 boolean。

修復:全域搜尋替換 14 個檔案,將 timestamp: true 改為 timestamp: new Date().toISOString()


4. ⚙️ System: init 每次對話都顯示

症狀:每次發訊息都會看到 “System: init” 的系統 embed。

原因:Claude Agent SDK 每次 session 啟動都會發送一個 system init 事件,透過 sendContent(followUp/reply)傳出來。

修復:在 sendContent 裡過濾 title 含 'System: init''Startup Complete' 的 embed:

if (title.includes('System: init') || title.includes('Startup Complete')) return;

5. DM 完全沒有回應

症狀:DM 傳訊息後,Discord DM 視窗沒有任何回應(但 session 有在跑)。

根本原因sendClaudeMessages 在 bot 啟動時就被綁定到 guild channel 的 sender,DM 走的是同一個 handlers.get('claude').execute(ctx),Claude 的輸出全跑去 guild channel 的 thread,DM 視窗看不到。

架構分析

guild channel 路徑:
  MessageCreate → handlers.get('claude') → createThreadSender → guild thread
  
DM 路徑(錯誤):
  MessageCreate → handlers.get('claude') → sendClaudeMessages → guild channel ← 問題在這
  
DM 路徑(正確):
  MessageCreate → sendToClaudeCode 直接呼叫 → DM channel sender → DM 視窗

修復:為 DM 建立獨立路徑 runClaudeForDM(),直接呼叫 sendToClaudeCode,輸出用 DiscordSender wrapping DM channel:

const dmDiscordSender: DiscordSender = {
  async sendMessage(content) {
    await channel.send(convertMessageContent(content));
  }
};
const dmSender = createClaudeSender(dmDiscordSender);
 
const result = await sendToClaudeCode(
  workDir, prompt, controller, sessionId,
  undefined,
  (jsonData) => {
    const msgs = convertToClaudeMessages(jsonData);
    if (msgs.length > 0) dmSender(msgs).catch(() => {});
  },
  false,
);

6. Typing Indicator 不停

症狀:Claude 回應後,DM 視窗仍持續顯示 typing indicator。

原因:Typing loop 每 8 秒才檢查一次 done flag,加上 Discord 本身 typing 動畫會持續 10 秒,所以完成後最長出現 18 秒延遲。

修復:改為每 1 秒檢查一次:

for (let i = 0; i < 8; i++) {
  if (done) return;
  await new Promise(r => setTimeout(r, 1000));
}

7. DM 安全性(任何人都能觸發)

解法:從 .env 讀取 USER_ID,DM handler 只允許該 user 觸發:

if (allowedUserId && message.author.id !== allowedUserId) return;

8. DM Channel Partial

症狀:DM channel 啟用 Partials.Channel 後,channel.send() 可能失敗。

修復:在 DM handler 裡先 fetch partial channel:

if ((message.channel as any).partial) {
  try { await message.channel.fetch(); } catch { return; }
}

最終設定

.env

DISCORD_TOKEN=...
APPLICATION_ID=...
WORK_DIR=/Users/clawdbot520
USER_ID=YOUR_USER_ID            ← 限制 DM 存取
CATEGORY_NAME=claude-code
CHANNEL_ID=YOUR_CHANNEL_ID     ← 固定 guild channel

啟動方式

cd ~/claude-code-discord
nohup deno run --allow-all index.ts > /tmp/discord-bot.log 2>&1 &

查看 log

tail -f /tmp/discord-bot.log

架構圖(最終)

Discord Guild Channel
  用戶輸入 → MessageCreate → runClaudeOnChannel
                               → handlers.get('claude').execute(ctx)
                               → createThreadSender → guild thread(輸出在這)

Discord DM
  用戶輸入 → MessageCreate → USER_ID 驗證 → runClaudeForDM
                               → sendToClaudeCode 直接呼叫
                               → DM channel sender(輸出直接在 DM)

相關 Backlog

  • TASK-00012:Discord Bot 速度優化(移除 subprocess 冷啟動)
    • 方向:Warm subprocess pool / 真正 streaming / Typing indicator 已完成