From 3c8c733fec9ad870bad9913209945c383c773f2e Mon Sep 17 00:00:00 2001 From: "kevin.wu" Date: Thu, 26 Mar 2026 13:27:43 +0800 Subject: [PATCH 1/2] fix: reduce KV list() calls in LINE relay and add verify_line.sh Merge two list() calls into one to halve KV daily operation usage (free plan: 1,000/day). Add try-catch for structured error responses. Add verify_line.sh for diagnosing relay connectivity and quota issues. --- .../line-channel/relay/src/index.ts | 35 +-- scripts/verify_line.sh | 269 ++++++++++++++++++ 2 files changed, 288 insertions(+), 16 deletions(-) create mode 100644 scripts/verify_line.sh diff --git a/external_plugins/line-channel/relay/src/index.ts b/external_plugins/line-channel/relay/src/index.ts index ddab064..c3598f1 100644 --- a/external_plugins/line-channel/relay/src/index.ts +++ b/external_plugins/line-channel/relay/src/index.ts @@ -144,25 +144,28 @@ async function handleGetMessages(request: Request, env: Env): Promise const authErr = checkAuth(request, env) if (authErr) return authErr - // List both msg: and unsend: events - const msgKeys = await env.LINE_QUEUE.list({ prefix: 'msg:' }) - const unsendKeys = await env.LINE_QUEUE.list({ prefix: 'unsend:' }) - const keys = { keys: [...msgKeys.keys, ...unsendKeys.keys] } - const messages: QueuedMessage[] = [] - - for (const key of keys.keys) { - const val = await env.LINE_QUEUE.get(key.name) - if (val) { - try { - messages.push(JSON.parse(val)) - } catch {} + try { + // Single list() call — keys are prefixed with msg: or unsend:, both sort lexically + const allKeys = await env.LINE_QUEUE.list() + const messages: QueuedMessage[] = [] + + for (const key of allKeys.keys) { + const val = await env.LINE_QUEUE.get(key.name) + if (val) { + try { + messages.push(JSON.parse(val)) + } catch {} + } } - } - if (messages.length > 0) { - console.log(`[messages] returning ${messages.length} queued message(s)`) + if (messages.length > 0) { + console.log(`[messages] returning ${messages.length} queued message(s)`) + } + return json({ messages }) + } catch (e: any) { + console.error(`[messages] error: ${e?.message ?? e}`) + return json({ error: e?.message ?? 'internal error', messages: [] }, 500) } - return json({ messages }) } async function handleDeleteMessages(request: Request, env: Env): Promise { diff --git a/scripts/verify_line.sh b/scripts/verify_line.sh new file mode 100644 index 0000000..8746662 --- /dev/null +++ b/scripts/verify_line.sh @@ -0,0 +1,269 @@ +#!/usr/bin/env bash +# Verify LINE relay deployment, token configuration, and KV binding. +# +# Usage: +# ./scripts/verify_line.sh +# +# Reads from (in priority order): +# 1. Environment variables +# 2. Channel .env file (.claude/channels/line/.env) +# 3. Project .env file +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" + +# ── Load tokens ───────────────────────────────────────────── +load_env() { + local file="$1" + if [[ -f "$file" ]]; then + set -a + # shellcheck source=/dev/null + source "$file" + set +a + fi +} + +STATE_DIR="${LINE_STATE_DIR:-$PROJECT_DIR/.claude/channels/line}" +load_env "$STATE_DIR/.env" +load_env "$PROJECT_DIR/.env" + +# Helper: extract JSON field +jq_field() { + python3 -c "import sys,json; print(json.load(sys.stdin).get('$1',''))" 2>/dev/null +} + +PASS=0 +FAIL=0 +WARN=0 + +check() { + local label="$1" result="$2" + if [[ "$result" == "ok" ]]; then + echo "[PASS] $label" + PASS=$((PASS + 1)) + elif [[ "$result" == "warn" ]]; then + echo "[WARN] $label" + WARN=$((WARN + 1)) + else + echo "[FAIL] $label — $result" + FAIL=$((FAIL + 1)) + fi +} + +echo "========================================" +echo " LINE RELAY VERIFICATION" +echo "========================================" +echo "" + +# ── 1. Check required env vars ────────────────────────────── +echo "--- 1. Environment Variables ---" +MISSING=0 +for var in LINE_RELAY_URL LINE_RELAY_SECRET LINE_CHANNEL_SECRET LINE_CHANNEL_ACCESS_TOKEN; do + val="${!var:-}" + if [[ -z "$val" ]]; then + echo " MISSING: $var" + MISSING=$((MISSING + 1)) + else + echo " OK: $var (${#val} chars)" + fi +done + +if [[ $MISSING -gt 0 ]]; then + check "Environment variables" "$MISSING variable(s) missing" +else + check "Environment variables" "ok" +fi +echo "" + +# ── 2. Relay health check ─────────────────────────────────── +echo "--- 2. Relay Health Check ---" +RELAY_URL="${LINE_RELAY_URL:-}" +if [[ -z "$RELAY_URL" ]]; then + echo " Skipped (LINE_RELAY_URL not set)" + check "Relay health" "skipped" +else + HEALTH_CODE=$(curl -s -o /dev/null -w "%{http_code}" "$RELAY_URL/health" 2>/dev/null || echo "000") + if [[ "$HEALTH_CODE" == "200" ]]; then + echo " $RELAY_URL/health -> 200 OK" + check "Relay health" "ok" + else + echo " $RELAY_URL/health -> HTTP $HEALTH_CODE" + check "Relay health" "HTTP $HEALTH_CODE" + fi +fi +echo "" + +# ── 3. Broker auth (poll endpoint) ────────────────────────── +echo "--- 3. Broker Auth (GET /messages) ---" +RELAY_SECRET="${LINE_RELAY_SECRET:-}" +if [[ -z "$RELAY_URL" || -z "$RELAY_SECRET" ]]; then + echo " Skipped (LINE_RELAY_URL or LINE_RELAY_SECRET not set)" + check "Broker auth" "warn" +else + POLL_RESP=$(curl -s -w "\n%{http_code}" \ + -H "Authorization: Bearer $RELAY_SECRET" \ + "$RELAY_URL/messages" 2>/dev/null || echo -e "\n000") + POLL_BODY=$(echo "$POLL_RESP" | head -n -1) + POLL_CODE=$(echo "$POLL_RESP" | tail -n 1) + + if [[ "$POLL_CODE" == "200" ]]; then + MSG_COUNT=$(echo "$POLL_BODY" | python3 -c "import sys,json; print(len(json.load(sys.stdin).get('messages',[])))" 2>/dev/null || echo "?") + echo " GET /messages -> 200 (auth OK, $MSG_COUNT queued message(s))" + check "Broker auth" "ok" + elif [[ "$POLL_CODE" == "401" ]]; then + echo " GET /messages -> 401 (LINE_RELAY_SECRET mismatch with Worker RELAY_SECRET)" + check "Broker auth" "401 unauthorized" + elif [[ "$POLL_CODE" == "500" ]]; then + echo " GET /messages -> 500 Internal Server Error" + # Try to extract structured error from response body + ERR_MSG=$(echo "$POLL_BODY" | python3 -c "import sys,json; print(json.load(sys.stdin).get('error',''))" 2>/dev/null || echo "") + if [[ -n "$ERR_MSG" ]]; then + echo " Error: $ERR_MSG" + fi + echo "" + if echo "$POLL_BODY" | grep -qi "limit exceeded"; then + echo " → KV daily operation quota exceeded (Free plan: 1,000 list ops/day)" + echo " Resets at UTC midnight. Fix: upgrade to Workers Paid (\$5/mo) or reduce poll frequency" + else + echo " Common causes:" + echo " - KV namespace 'LINE_QUEUE' not bound (check wrangler.toml binding)" + echo " - Worker secrets not set (run: wrangler secret put LINE_CHANNEL_SECRET)" + echo " - Worker deployment failed or outdated (run: wrangler deploy)" + fi + echo "" + echo " Debug: check Worker logs with: wrangler tail --name line-relay" + check "Broker auth" "HTTP 500 — ${ERR_MSG:-unknown}" + else + echo " GET /messages -> HTTP $POLL_CODE" + echo " Response body: ${POLL_BODY:0:200}" + check "Broker auth" "HTTP $POLL_CODE" + fi +fi +echo "" + +# ── 4. Broker auth (no-token test) ────────────────────────── +echo "--- 4. Auth Rejection (no token) ---" +if [[ -z "$RELAY_URL" ]]; then + echo " Skipped (LINE_RELAY_URL not set)" + check "Auth rejection" "warn" +else + NO_AUTH_CODE=$(curl -s -o /dev/null -w "%{http_code}" "$RELAY_URL/messages" 2>/dev/null || echo "000") + if [[ "$NO_AUTH_CODE" == "401" ]]; then + echo " GET /messages (no token) -> 401 (correctly rejected)" + check "Auth rejection" "ok" + else + echo " GET /messages (no token) -> HTTP $NO_AUTH_CODE (expected 401)" + check "Auth rejection" "expected 401, got $NO_AUTH_CODE" + fi +fi +echo "" + +# ── 5. Reply endpoint (dry run) ───────────────────────────── +echo "--- 5. Reply Endpoint ---" +if [[ -z "$RELAY_URL" || -z "$RELAY_SECRET" ]]; then + echo " Skipped (LINE_RELAY_URL or LINE_RELAY_SECRET not set)" + check "Reply endpoint" "warn" +else + # Send an invalid body to test the endpoint is reachable and auth works + REPLY_RESP=$(curl -s -w "\n%{http_code}" -X POST \ + -H "Authorization: Bearer $RELAY_SECRET" \ + -H "Content-Type: application/json" \ + -d '{}' \ + "$RELAY_URL/reply" 2>/dev/null || echo -e "\n000") + REPLY_BODY=$(echo "$REPLY_RESP" | head -n -1) + REPLY_CODE=$(echo "$REPLY_RESP" | tail -n 1) + + if [[ "$REPLY_CODE" == "400" ]]; then + echo " POST /reply -> 400 (auth OK, correctly requires userId+text)" + check "Reply endpoint" "ok" + elif [[ "$REPLY_CODE" == "401" ]]; then + echo " POST /reply -> 401 (auth failed)" + check "Reply endpoint" "401 unauthorized" + elif [[ "$REPLY_CODE" == "500" ]]; then + echo " POST /reply -> 500 (Worker error)" + check "Reply endpoint" "HTTP 500" + else + echo " POST /reply -> HTTP $REPLY_CODE" + check "Reply endpoint" "HTTP $REPLY_CODE" + fi +fi +echo "" + +# ── 6. LINE Messaging API (bot info) ──────────────────────── +echo "--- 6. LINE Messaging API (bot info) ---" +LINE_TOKEN="${LINE_CHANNEL_ACCESS_TOKEN:-}" +if [[ -z "$LINE_TOKEN" ]]; then + echo " Skipped (LINE_CHANNEL_ACCESS_TOKEN not set)" + check "LINE API" "warn" +else + BOT_INFO=$(curl -s "https://api.line.me/v2/bot/info" \ + -H "Authorization: Bearer $LINE_TOKEN" 2>/dev/null || echo "{}") + BOT_NAME=$(echo "$BOT_INFO" | jq_field displayName) + BOT_ID=$(echo "$BOT_INFO" | jq_field userId) + + if [[ -n "$BOT_NAME" ]]; then + echo " Bot: $BOT_NAME" + echo " ID: $BOT_ID" + check "LINE API" "ok" + else + ERR_MSG=$(echo "$BOT_INFO" | jq_field message) + if [[ -n "$ERR_MSG" ]]; then + echo " Error: $ERR_MSG" + check "LINE API" "$ERR_MSG" + else + echo " Unexpected response: ${BOT_INFO:0:200}" + check "LINE API" "unexpected response" + fi + fi +fi +echo "" + +# ── 7. Webhook URL check ──────────────────────────────────── +echo "--- 7. Webhook Configuration ---" +if [[ -z "$LINE_TOKEN" ]]; then + echo " Skipped (LINE_CHANNEL_ACCESS_TOKEN not set)" + check "Webhook config" "warn" +else + WEBHOOK_INFO=$(curl -s "https://api.line.me/v2/bot/channel/webhook/endpoint" \ + -H "Authorization: Bearer $LINE_TOKEN" 2>/dev/null || echo "{}") + WEBHOOK_URL=$(echo "$WEBHOOK_INFO" | jq_field endpoint) + WEBHOOK_ACTIVE=$(echo "$WEBHOOK_INFO" | python3 -c "import sys,json; print(json.load(sys.stdin).get('active',''))" 2>/dev/null || echo "") + + if [[ -n "$WEBHOOK_URL" ]]; then + echo " Endpoint: $WEBHOOK_URL" + echo " Active: $WEBHOOK_ACTIVE" + if [[ -n "$RELAY_URL" && "$WEBHOOK_URL" == *"$RELAY_URL"* ]] || [[ "$WEBHOOK_URL" == "${RELAY_URL}/webhook" ]]; then + check "Webhook config" "ok" + elif [[ -n "$RELAY_URL" ]]; then + echo " WARNING: Webhook URL does not match LINE_RELAY_URL" + echo " Expected: ${RELAY_URL}/webhook" + echo " Got: $WEBHOOK_URL" + check "Webhook config" "warn" + else + check "Webhook config" "ok" + fi + else + echo " No webhook endpoint configured" + check "Webhook config" "warn" + fi +fi +echo "" + +# ── Summary ──────────────────────────────────────────────── +echo "========================================" +echo " RESULT: $PASS passed, $FAIL failed, $WARN warnings" +echo "========================================" +echo "" +echo "Troubleshooting 500 on GET /messages:" +echo " 1. Check Worker logs: wrangler tail --name line-relay" +echo " 2. Verify KV binding: wrangler kv namespace list" +echo " 3. Verify secrets: wrangler secret list --name line-relay" +echo " 4. Redeploy Worker: cd external_plugins/line-channel/relay && wrangler deploy" +echo " 5. KV quota (free plan): 1,000 list ops/day — resets at UTC midnight" +echo " With POLL_INTERVAL=5s, each poll uses 1 list() → 17,280/day (exceeds free tier)" +echo "" + +if [[ $FAIL -gt 0 ]]; then + exit 1 +fi From 962c66f4a319cdee3ea0f40b74a4f918f185a5d2 Mon Sep 17 00:00:00 2001 From: "kevin.wu" Date: Thu, 26 Mar 2026 13:30:10 +0800 Subject: [PATCH 2/2] fix: remove unused REPLY_BODY variable in verify_line.sh Fixes ShellCheck SC2034 warning. --- scripts/verify_line.sh | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/scripts/verify_line.sh b/scripts/verify_line.sh index 8746662..f74c3d3 100644 --- a/scripts/verify_line.sh +++ b/scripts/verify_line.sh @@ -166,13 +166,11 @@ if [[ -z "$RELAY_URL" || -z "$RELAY_SECRET" ]]; then check "Reply endpoint" "warn" else # Send an invalid body to test the endpoint is reachable and auth works - REPLY_RESP=$(curl -s -w "\n%{http_code}" -X POST \ + REPLY_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X POST \ -H "Authorization: Bearer $RELAY_SECRET" \ -H "Content-Type: application/json" \ -d '{}' \ - "$RELAY_URL/reply" 2>/dev/null || echo -e "\n000") - REPLY_BODY=$(echo "$REPLY_RESP" | head -n -1) - REPLY_CODE=$(echo "$REPLY_RESP" | tail -n 1) + "$RELAY_URL/reply" 2>/dev/null || echo "000") if [[ "$REPLY_CODE" == "400" ]]; then echo " POST /reply -> 400 (auth OK, correctly requires userId+text)"