Skip to content

feat(discord): thread interaction, slash commands, and mention-free replies#26

Open
YoYuTW wants to merge 10 commits into
geminixiang:mainfrom
YoYuTW:main
Open

feat(discord): thread interaction, slash commands, and mention-free replies#26
YoYuTW wants to merge 10 commits into
geminixiang:mainfrom
YoYuTW:main

Conversation

@YoYuTW

@YoYuTW YoYuTW commented Apr 23, 2026

Copy link
Copy Markdown

Summary

  • Add thread creation with embeds, button interactions, and stop mechanism
  • Dynamic thread naming, execution header/footer with step numbers
  • Allow mention-free replies in bot-created threads
  • Persist ownThreads to disk so mention-free replies survive restarts
  • Reuse existing thread for CoT/tool logs in follow-up requests
  • Fix: use postMessage for event-triggered responses instead of postReply
  • Add slash command support via skill slash-command.json + slash-handler.sh

🤖 Generated with Claude Code

YoYuTW and others added 10 commits April 23, 2026 13:53
…umbers

Thread names now show user and prompt summary (🤖 user · prompt) instead
of hardcoded "🤖 Response". Thread body includes a header embed with
execution context, sequential step numbers on thinking/tool messages,
and an enhanced summary footer with steps breakdown and duration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Track thread IDs created by the bot in ownThreads set. Messages in
these threads skip the @mention check. Register thread alias via
handler.registerThreadAlias so replies resolve to the original session.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…oubleshooting

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ive restarts

Saves thread IDs to own-threads.json in workingDir. Loads on startup.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…plies

threadChannelId was null for in-thread follow-ups, causing respondInThread()
to buffer messages indefinitely since nested threads can't be created.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When an event triggers the agent, there is no real Discord message to reply
to. Using postReply with a synthetic ts (channelId) fails silently because
no such message exists. Instead, use postMessage to post directly to the
channel when isEvent is true.
Resolve the Discord context merge conflict against main.
Carry forward the thread alias/session routing helpers required by the PR.
Fix the usage summary footer newline regression that broke lint/build.
Comment on lines +417 to +418
const commandDefPath = join(skillDir, "slash-command.json");
const handlerPath = join(skillDir, "slash-handler.sh");

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

這兩個是什麼新玩意兒,我原本以為是去掃描 SKILL.md

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

記得是繞過 agent 直接註冊 slash command 的功能來著,就不會花 api credit 還啥的
媽呀 pr 開著就忘了

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

有沒有現成的檔案,我想看看這兩個裡面寫了什麼

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#!/bin/bash
# /order slash command handler
# Args: $1 = subcommand
# Env: OPTION_ID, OPTION_STATUS, OPTION_SHIP_DATE, DISCORD_USER_ID, etc.

SUBCOMMAND="$1"

# Helper: call admin status update API
update_status() {
  local id="$1"
  local status="$2"
  local ship_date="$3"

  if [ -n "$ship_date" ]; then
    BODY="{\"status\":\"$status\",\"expected_ship_date\":\"$ship_date\"}"
  else
    BODY="{\"status\":\"$status\"}"
  fi

  RESPONSE=$(curl -s -w "\n%{http_code}" -X PATCH \
    "$WORKER_URL/admin/order/$id/status" \
    -H "Authorization: Bearer $ADMIN_TOKEN" \
    -H "Content-Type: application/json" \
    -d "$BODY")

  HTTP_CODE=$(echo "$RESPONSE" | tail -1)
  BODY_RESP=$(echo "$RESPONSE" | head -1)

  if [ "$HTTP_CODE" = "200" ]; then
    echo "訂單 #$id 已更新為 $status"
  else
    echo "錯誤 ($HTTP_CODE): $BODY_RESP"
    exit 1
  fi
}

# Helper: format order row
format_order() {
  local json="$1"
  echo "$json" | python3 -c "
import json, sys
o = json.load(sys.stdin)
status_label = {
  'pending_payment': '待付款',
  'payment_submitted': '已付款待確認',
  'confirmed': '已確認待出貨',
  'shipped': '已出貨',
  'completed': '已完成',
  'payment_issue': '付款異常',
  'cancelled': '已取消',
}
def fmt(o):
  s = status_label.get(o.get('status',''), o.get('status',''))
  items = json.loads(o.get('items','[]')) if isinstance(o.get('items'), str) else o.get('items',[])
  item_str = ', '.join(f\"{i['spec']} x{i['quantity']}\" for i in items)
  line = f\"#{o['id']} {o['name']} [{s}] {item_str}\"
  if o.get('note'):
    line += f\" 備註:{o['note']}\"
  return line

data = json.load(open('/dev/stdin')) if False else o
if isinstance(data, list):
  for row in data:
    print(fmt(row))
else:
  o2 = data
  s = status_label.get(o2.get('status',''), o2.get('status',''))
  items = json.loads(o2.get('items','[]')) if isinstance(o2.get('items'), str) else o2.get('items',[])
  item_str = '\n'.join(f\"  {i['spec']} x{i['quantity']}\" for i in items)
  print(f\"訂單 #{o2['id']}\")
  print(f\"姓名:{o2.get('name','')}\")
  print(f\"電話:{o2.get('phone','')}\")
  print(f\"地址:{o2.get('address','')}\")
  print(f\"品項:\n{item_str}\")
  print(f\"狀態:{s}\")
  if o2.get('transfer_last5'): print(f\"匯款末五碼:{o2['transfer_last5']}\")
  if o2.get('expected_ship_date'): print(f\"預計出貨:{o2['expected_ship_date']}\")
  if o2.get('shipped_at'): print(f\"出貨時間:{o2['shipped_at']}\")
  if o2.get('note'): print(f\"備註:{o2['note']}\")
  print(f\"下單時間:{o2.get('created_at','')}\")
"
}

case "$SUBCOMMAND" in
  list)
    if [ -n "$OPTION_STATUS" ]; then
      WHERE="WHERE status='$OPTION_STATUS'"
    else
      WHERE=""
    fi
    RESULT=$(wrangler d1 execute x --remote \
      --command="SELECT id, name, items, status, note, created_at FROM orders $WHERE ORDER BY created_at DESC" \
      --json 2>/dev/null | python3 -c "
import json, sys
data = json.load(sys.stdin)
rows = data[0].get('results', []) if data else []
if not rows:
    print('沒有訂單')
    sys.exit(0)
status_label = {
  'pending_payment': '待付款',
  'payment_submitted': '已付款待確認',
  'confirmed': '已確認待出貨',
  'shipped': '已出貨',
  'completed': '已完成',
  'payment_issue': '付款異常',
  'cancelled': '已取消',
}
lines = []
for o in rows:
    s = status_label.get(o.get('status',''), o.get('status',''))
    try:
        items = json.loads(o.get('items','[]'))
        item_str = ', '.join(f\"{i['spec']} x{i['quantity']}\" for i in items)
    except:
        item_str = o.get('items','')
    line = f\"#{o['id']} {o['name']} [{s}] {item_str}\"
    if o.get('note'):
        line += f\" 備:{o['note']}\"
    lines.append(line)
print('\n'.join(lines))
")
    echo "$RESULT"
    ;;

  view)
    ID="${OPTION_ID}"
    if [ -z "$ID" ]; then echo "缺少 id"; exit 1; fi
    wrangler d1 execute x --remote \
      --command="SELECT * FROM orders WHERE id=$ID" \
      --json 2>/dev/null | python3 -c "
import json, sys
data = json.load(sys.stdin)
rows = data[0].get('results', []) if data else []
if not rows:
    print('找不到訂單')
    sys.exit(0)
o = rows[0]
status_label = {
  'pending_payment': '待付款',
  'payment_submitted': '已付款待確認',
  'confirmed': '已確認待出貨',
  'shipped': '已出貨',
  'completed': '已完成',
  'payment_issue': '付款異常',
  'cancelled': '已取消',
}
s = status_label.get(o.get('status',''), o.get('status',''))
try:
    items = json.loads(o.get('items','[]'))
    item_str = '\n'.join(f\"  {i['spec']} x{i['quantity']}\" for i in items)
except:
    item_str = o.get('items','')
print(f\"訂單 #{o['id']}\")
print(f\"姓名:{o.get('name','')}\")
print(f\"電話:{o.get('phone','')}\")
print(f\"Email:{o.get('email','')}\")
print(f\"地址:{o.get('address','')}\")
print(f\"品項:\n{item_str}\")
print(f\"狀態:{s}\")
if o.get('transfer_last5'): print(f\"匯款末五碼:{o['transfer_last5']}\")
if o.get('expected_ship_date'): print(f\"預計出貨:{o['expected_ship_date']}\")
if o.get('shipped_at'): print(f\"出貨時間:{o['shipped_at']}\")
if o.get('note'): print(f\"備註:{o['note']}\")
print(f\"下單時間:{o.get('created_at','')}\")
"
    ;;

  confirm)
    update_status "${OPTION_ID}" "confirmed" "${OPTION_SHIP_DATE:-}"
    ;;

  ship)
    update_status "${OPTION_ID}" "shipped"
    ;;

  done)
    update_status "${OPTION_ID}" "completed"
    ;;

  issue)
    update_status "${OPTION_ID}" "payment_issue"
    ;;

  cancel)
    update_status "${OPTION_ID}" "cancelled"
    ;;

  *)
    echo "未知的 subcommand: $SUBCOMMAND"
    exit 1
    ;;
esac
{
  "name": "order",
  "description": "訂單管理",
  "options": [
    {
      "type": 1,
      "name": "list",
      "description": "列出訂單",
      "options": [
        {
          "type": 3,
          "name": "status",
          "description": "篩選狀態",
          "required": false,
          "choices": [
            { "name": "待付款", "value": "pending_payment" },
            { "name": "已提交付款", "value": "payment_submitted" },
            { "name": "已確認待出貨", "value": "confirmed" },
            { "name": "已出貨", "value": "shipped" },
            { "name": "已完成", "value": "completed" },
            { "name": "付款異常", "value": "payment_issue" },
            { "name": "已取消", "value": "cancelled" }
          ]
        }
      ]
    },
    {
      "type": 1,
      "name": "view",
      "description": "查看單筆訂單",
      "options": [
        {
          "type": 4,
          "name": "id",
          "description": "訂單 ID",
          "required": true
        }
      ]
    },
    {
      "type": 1,
      "name": "confirm",
      "description": "確認付款",
      "options": [
        {
          "type": 4,
          "name": "id",
          "description": "訂單 ID",
          "required": true
        },
        {
          "type": 3,
          "name": "ship_date",
          "description": "預計出貨日 (YYYY-MM-DD)",
          "required": false
        }
      ]
    },
    {
      "type": 1,
      "name": "ship",
      "description": "標記出貨",
      "options": [
        {
          "type": 4,
          "name": "id",
          "description": "訂單 ID",
          "required": true
        }
      ]
    },
    {
      "type": 1,
      "name": "done",
      "description": "標記完成",
      "options": [
        {
          "type": 4,
          "name": "id",
          "description": "訂單 ID",
          "required": true
        }
      ]
    },
    {
      "type": 1,
      "name": "issue",
      "description": "標記付款異常",
      "options": [
        {
          "type": 4,
          "name": "id",
          "description": "訂單 ID",
          "required": true
        }
      ]
    },
    {
      "type": 1,
      "name": "cancel",
      "description": "取消訂單",
      "options": [
        {
          "type": 4,
          "name": "id",
          "description": "訂單 ID",
          "required": true
        }
      ]
    }
  ]
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants