教學

LINE Bot 開發教學 2026:Node.js 與 Python 實作

用 Messaging API 一步步開發 LINE Bot:啟用 Channel、驗證 Webhook 簽章、用 Flex Message 與快速回覆回應、以 postback 完成預約流程、加入只根據 FAQ 回答的 AI,最後部署上線。附 Node.js(@line/bot-sdk v11)與 Python(line-bot-sdk v3)程式碼。

LineBot.pro Team· 更新 19 分鐘閱讀
LINE Bot 開發教學 2026:Node.js 與 Python 實作

#LINE Bot 怎麼運作

LINE Bot 是一個網路服務:接收 LINE 平台送來的 Webhook 事件,再透過 Messaging API 回覆。2026 年要開發一個 LINE Bot,需要三樣東西:已啟用 Messaging API 的 LINE 官方帳號、會驗證 x-line-signature 標頭的 HTTPS 端點,以及用事件中的回覆權杖(reply token)回覆的程式碼。本教學用 Node.js(TypeScript、@line/bot-sdk v11)與 Python(line-bot-sdk v3)一步步完成:回聲回覆、按鈕、簡單的預約流程、只根據你自己 FAQ 回答的 AI,以及上線前的部署檢查。

圖表
使用者 ── 訊息 ──▶ LINE 平台 ── Webhook(POST + x-line-signature)──▶ 你的伺服器
                                                                       │
使用者 ◀── 回覆 ── LINE 平台 ◀──── Messaging API(回覆或推播)────────┘
  1. 使用者傳送訊息、點按鈕,或把你的帳號加為好友。
  2. LINE 平台以 HTTPS POST 把一個或多個事件送到你的 Webhook URL。
  3. 伺服器驗證簽章,回傳 200,再處理事件。
  4. 用事件的回覆權杖呼叫回覆訊息(reply),或之後用使用者 ID 傳送推播訊息(push)。

回覆訊息不計入方案的每月訊息則數;推播、多人推播(multicast)、群發(broadcast)與分眾群發(narrowcast)則會計入,每位收件人算 1 則。

#你要做哪一種 Bot?

類型運作方式適合要注意
關鍵字型訊息含特定字詞就送出預先準備的回答常見問題、營業時間、價格沒列到的說法就接不住
選單 / Postback 型按鈕、快速回覆與圖文選單把 postback 資料送到伺服器預約、點餐、分步驟填寫前期設計比較花工夫
AI 輔助型由語言模型回答自由輸入的問題問法多變、多語言必須限定在你提供的資料,並保留真人接手
混合型主要流程用按鈕,其他交給 AI,例外由真人處理大多數實際的店家要測試的路徑比較多

以下程式碼是一間美髮店的混合型 Bot:預約用按鈕進行,AI 只根據店家的 FAQ 回答,「找真人客服」按鈕則把對話交給店員。

#事前準備與 Channel 設定

#需要準備

  • 已啟用 Messaging API 的 LINE 官方帳號(申請免費)
  • Node.js 22 以上(@line/bot-sdk v11 的要求)或 Python 3.10 以上(line-bot-sdk v3)
  • 可透過 HTTPS 連線的伺服器;在本機測試時可用 ngrok 或 Cloudflare Tunnel 等通道工具

#步驟 1:建立 LINE 官方帳號並啟用 Messaging API

自 2024 年 9 月 4 日起,已無法在 LINE Developers Console 直接建立 Messaging API Channel,請改用以下流程:

  1. 建立 LINE 官方帳號:註冊 Business ID 並填寫申請表單。可參考LINE 官方帳號申請教學。
  2. 在 LINE Official Account Manager 開啟設定 → Messaging API,啟用 Messaging API。
  3. 選擇 Provider。請謹慎選擇:Provider 之後無法更改,而且同一位使用者在不同 Provider 底下會有不同的使用者 ID,所以同一個品牌的 Channel 請放在同一個 Provider。

完成後,新的 Messaging API Channel 就會出現在 LINE Developers Console。

#步驟 2:取得憑證

憑證位置用途
Channel secretConsole 的 Basic settings 分頁驗證 Webhook 簽章
Channel access tokenConsole 的 Messaging API 分頁,或透過 API 發行呼叫 Messaging API

Channel access token 共有四種。要開始測試,最快的是在 Console 發行的長期權杖(long-lived,沒有期限,每個 Channel 1 組)。正式環境 LINE 建議使用可自訂期限的 Channel access token v2.1(最長 30 天),以你自己金鑰簽署的 JSON Web Token 申請。另外還有短期(30 天)與無狀態(stateless,15 分鐘)權杖。

#步驟 3:設定 Channel

  • Webhook URL:你的端點,例如 https://bot.example.com/webhook。必須是 HTTPS,且憑證要由一般瀏覽器信任的憑證機構簽發,不接受自簽憑證。每個 Channel 只能設定一個 Webhook URL。
  • 點 Verify,再開啟 Use webhook。
  • 若由程式負責回覆,請在 LINE Official Account Manager 的回應設定關閉加入好友的歡迎訊息與自動回應訊息。兩者預設都是開啟,會和你的 Bot 重複回覆。若希望店員能手動回覆,請保持聊天開啟。

#步驟 4:建立專案

Node.js(TypeScript):

bash
mkdir line-bot && cd line-bot
npm init -y && npm pkg set type=module
npm install @line/bot-sdk express
npm install -D typescript tsx @types/express @types/node

在 package.json 加入指令,並建立 tsconfig.json:

json
{
  "scripts": {
    "dev": "tsx watch --env-file=.env src/index.ts",
    "build": "tsc",
    "start": "node dist/index.js"
  }
}
json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "outDir": "dist",
    "skipLibCheck": true
  },
  "include": ["src"]
}

Python:

bash
mkdir line-bot && cd line-bot
python -m venv .venv
source .venv/bin/activate   # Windows: .venv\Scripts\activate
pip install line-bot-sdk flask

把憑證寫在 .env 檔,不要提交到程式碼儲存庫。Node 用 --env-file 讀取;Python 請在 shell 匯出環境變數,或使用主機平台的密鑰管理功能。

bash
LINE_CHANNEL_SECRET=your_channel_secret
LINE_CHANNEL_ACCESS_TOKEN=your_channel_access_token
PORT=3000

#Webhook 實作:接收並回覆

#Node.js(TypeScript、@line/bot-sdk v11)

ts
// src/index.ts
import express from "express"
import {
  JSONParseError,
  LineBotClient,
  SignatureValidationFailed,
  middleware,
  webhook,
} from "@line/bot-sdk"

const channelSecret = process.env.LINE_CHANNEL_SECRET
const channelAccessToken = process.env.LINE_CHANNEL_ACCESS_TOKEN
if (!channelSecret || !channelAccessToken) {
  throw new Error("請設定 LINE_CHANNEL_SECRET 與 LINE_CHANNEL_ACCESS_TOKEN")
}

const client = LineBotClient.fromChannelAccessToken({ channelAccessToken })
const app = express()

// LINE 中介軟體要用原始請求內容驗證 x-line-signature,
// 所以必須放在任何 JSON 解析器之前。
app.post("/webhook", middleware({ channelSecret }), (req, res) => {
  res.sendStatus(200) // 先回應 LINE,再處理事件
  const { events } = req.body as webhook.CallbackRequest
  for (const event of events) {
    handleEvent(event).catch((err) => console.error("事件處理失敗", err))
  }
})

app.use(express.json()) // 其他路由照常解析 JSON

async function handleEvent(event: webhook.Event) {
  if (event.type !== "message" || event.message.type !== "text" || !event.replyToken) return

  await client.replyMessage({
    replyToken: event.replyToken,
    messages: [{ type: "text", text: `你說:${event.message.text}` }],
  })
}

// 中介軟體只會把錯誤交給 next();沒有這個處理器,Express 會回 500。
app.use((err: unknown, _req: express.Request, res: express.Response, next: express.NextFunction) => {
  if (err instanceof SignatureValidationFailed) {
    res.status(401).send("Invalid signature")
    return
  }
  if (err instanceof JSONParseError) {
    res.status(400).send("Invalid JSON")
    return
  }
  next(err)
})

const port = Number(process.env.PORT ?? 3000)
app.listen(port, () => console.log(`LINE Bot 在連接埠 ${port} 等待中`))

執行 npm run dev,用通道工具公開 3000 連接埠,把 HTTPS 網址加上 /webhook 貼到 Console,點 Verify。接著用手機傳訊息給你的帳號,就會收到回聲回覆。

常見的卡關點:

  • 中介軟體要放最前面。 middleware() 需要原始內容來重新計算 HMAC-SHA256 簽章;如果 express.json() 先執行,驗證就會失敗。細節可參考 @line/bot-sdk Express 中介軟體教學。
  • 中介軟體不會自己回 401。 它只把 SignatureValidationFailed 或 JSONParseError 交給 next(),沒有錯誤處理器時 Express 會回 500。
  • 每次呼叫只傳一個請求物件。 SDK v11 的方法都像 replyMessage({ replyToken, messages }) 這樣傳一個物件。舊的 Client 類別與 client.replyMessage(token, message) 已在 v11.0.0(2026 年 4 月)移除。
  • 先回 200,再處理。 LINE 建議以非同步方式處理事件。常駐的伺服器可以先回應再處理;若是回應送出後就會停止函式的無伺服器平台,請先處理完再回應,或交給佇列。
  • 回覆權杖只能用一次,LINE 的說明是要在收到 Webhook 後 1 分鐘內使用。
  • 空的事件陣列。 LINE 可能為了確認連線送來 {"destination": "...", "events": []}(Verify 按鈕就是這樣),請回 200。

#Python(line-bot-sdk v3、Flask)

python
# app.py
import os

from flask import Flask, abort, request
from linebot.v3 import WebhookHandler
from linebot.v3.exceptions import InvalidSignatureError
from linebot.v3.messaging import (
    ApiClient,
    Configuration,
    MessagingApi,
    ReplyMessageRequest,
    TextMessage,
)
from linebot.v3.webhooks import MessageEvent, TextMessageContent

app = Flask(__name__)
configuration = Configuration(access_token=os.environ["LINE_CHANNEL_ACCESS_TOKEN"])
handler = WebhookHandler(os.environ["LINE_CHANNEL_SECRET"])


@app.post("/webhook")
def webhook():
    signature = request.headers.get("X-Line-Signature", "")
    body = request.get_data(as_text=True)  # 解析 JSON 之前的原始內容
    try:
        handler.handle(body, signature)
    except InvalidSignatureError:
        abort(400)
    return "OK"


@handler.add(MessageEvent, message=TextMessageContent)
def handle_text(event):
    with ApiClient(configuration) as api_client:
        MessagingApi(api_client).reply_message(
            ReplyMessageRequest(
                reply_token=event.reply_token,
                messages=[TextMessage(text=f"你說:{event.message.text}")],
            )
        )


if __name__ == "__main__":
    app.run(port=int(os.environ.get("PORT", 3000)))

匯出兩個環境變數後,用 python app.py 啟動。WebhookHandler.handle() 會以原始內容驗證簽章,不符時拋出 InvalidSignatureError。請從 linebot.v3 匯入:2.x 版的 LineBotApi 已不建議使用,官方只持續維護 linebot.v3 模組。

#訊息類型與按鈕

以下都是可以放進 messages 的訊息物件,每次回覆或推播最多 5 則:

ts
import { messagingApi } from "@line/bot-sdk"

const text: messagingApi.TextMessage = {
  type: "text",
  text: "您好!請問今天需要什麼服務?", // 最多 5,000 個字元
}

const image: messagingApi.ImageMessage = {
  type: "image",
  originalContentUrl: "https://example.com/price-list.jpg", // HTTPS,JPEG 或 PNG,最大 10 MB
  previewImageUrl: "https://example.com/price-list-preview.jpg", // 最大 1 MB
}

const card: messagingApi.FlexMessage = {
  type: "flex",
  altText: "剪髮 NT$600", // 顯示在通知與聊天列表
  contents: {
    type: "bubble",
    hero: {
      type: "image",
      url: "https://example.com/haircut.jpg",
      size: "full",
      aspectRatio: "20:13",
      aspectMode: "cover",
    },
    body: {
      type: "box",
      layout: "vertical",
      contents: [
        { type: "text", text: "剪髮", weight: "bold", size: "xl" },
        { type: "text", text: "NT$600・45 分鐘", color: "#06C755" },
      ],
    },
    footer: {
      type: "box",
      layout: "vertical",
      contents: [
        {
          type: "button",
          style: "primary",
          color: "#06C755",
          action: { type: "postback", label: "立即預約", data: "action=service&id=cut", displayText: "我想預約剪髮" },
        },
      ],
    },
  },
}

const menu: messagingApi.TextMessage = {
  type: "text",
  text: "請選擇您需要的服務:",
  quickReply: {
    items: [
      // 最多 13 個按鈕
      { type: "action", action: { type: "postback", label: "預約", data: "action=start_booking", displayText: "預約" } },
      { type: "action", action: { type: "message", label: "營業時間", text: "營業時間" } },
      { type: "action", action: { type: "location", label: "傳送我的位置" } },
    ],
  },
}
  • 文字訊息最多 5,000 個字元。
  • 圖片網址必須是 HTTPS;原圖最大 10 MB(JPEG 或 PNG),預覽圖最大 1 MB。
  • Flex Message 是用 JSON 組成的版面(單一 bubble 或多個 bubble 組成的 carousel),altText 是通知裡顯示的文字。可以用 LINE 的 Flex Message Simulator 預覽版面。
  • 快速回覆在鍵盤上方顯示最多 13 個按鈕。相機、相簿與位置資訊動作只能用在快速回覆。
  • Postback 動作不需要使用者打字,就會把最多 300 個字元的 data 以 postback 事件送到伺服器;displayText 是以使用者名義顯示在聊天室的文字。

手寫 Flex JSON 很花時間。如果想從做好的卡片開始,可以用 LineBot.pro 的 AI 圖文訊息產生器產生 Flex bubble(主圖、文字與一個按鈕),複製 JSON 後用上面的程式碼傳送。聊天室下方的常駐選單,請參考LINE 圖文選單怎麼做。

#對話流程:Postback、狀態管理與轉真人

預約要分好幾步,所以 Bot 必須記住每位使用者進行到哪裡。下面的 handleEvent 用來取代 src/index.ts 裡的回聲版本(請在 @line/bot-sdk 的匯入加上 messagingApi)。它會歡迎新好友、用 postback 與日期選擇器完成三步驟預約,並讓使用者切換到真人客服:

ts
type Session =
  | { step: "idle" }
  | { step: "pick_date"; service: string }
  | { step: "confirm"; service: string; date: string }
  | { step: "human" } // 店員已接手這段對話

// 教學用的記憶體儲存:重新啟動就會消失,也無法在多個執行個體間共用。
// 正式環境請改用 Redis 或資料庫。
const sessions = new Map<string, Session>()

const SERVICES: Record<string, string> = { cut: "剪髮", color: "染髮" }

async function handleEvent(event: webhook.Event) {
  // 本教學只處理一對一聊天
  if (event.source?.type !== "user" || !event.source.userId) return
  const userId = event.source.userId

  if (event.type === "follow") {
    await reply(event.replyToken, [menuMessage("歡迎!謝謝您加入好友。")])
    return
  }

  const session = sessions.get(userId) ?? { step: "idle" }
  if (session.step === "human") return // 由店員在 LINE Official Account Manager 回覆

  if (event.type === "postback" && event.replyToken) {
    await onPostback(userId, session, event.postback, event.replyToken)
    return
  }

  if (event.type === "message" && event.replyToken) {
    if (event.message.type !== "text") {
      await reply(event.replyToken, [menuMessage("謝謝!目前只能讀取文字訊息。")])
      return
    }
    const text = event.message.text.trim()
    if (["staff", "真人客服"].includes(text.toLowerCase())) {
      sessions.set(userId, { step: "human" })
      await reply(event.replyToken, [
        { type: "text", text: "好的!店員會在營業時間(10:00–20:00)於這個聊天室回覆您。" },
      ])
      return
    }
    await reply(event.replyToken, [menuMessage("請從下方選擇服務。")]) // AI 章節會取代這一行
  }
}

async function onPostback(userId: string, session: Session, postback: webhook.PostbackContent, replyToken: string) {
  const data = new URLSearchParams(postback.data)
  const action = data.get("action")

  if (action === "start_booking") {
    await reply(replyToken, [{
      type: "text",
      text: "請問要預約哪一項服務?",
      quickReply: {
        items: Object.entries(SERVICES).map(([id, label]) => ({
          type: "action" as const,
          action: { type: "postback" as const, label, data: `action=service&id=${id}`, displayText: label },
        })),
      },
    }])
    return
  }

  if (action === "service") {
    const service = SERVICES[data.get("id") ?? ""]
    if (!service) return
    sessions.set(userId, { step: "pick_date", service })
    await reply(replyToken, [{
      type: "text",
      text: `${service}:請選擇方便的日期。`,
      quickReply: {
        items: [{ type: "action", action: { type: "datetimepicker", label: "選擇日期", data: "action=date", mode: "date" } }],
      },
    }])
    return
  }

  if (action === "date" && session.step === "pick_date" && postback.params?.date) {
    const next: Session = { step: "confirm", service: session.service, date: postback.params.date }
    sessions.set(userId, next)
    await reply(replyToken, [{
      type: "text",
      text: `${next.date} ${next.service},要送出預約申請嗎?`,
      quickReply: {
        items: [
          { type: "action", action: { type: "postback", label: "確認送出", data: "action=confirm", displayText: "確認送出" } },
          { type: "action", action: { type: "postback", label: "取消", data: "action=cancel", displayText: "取消" } },
        ],
      },
    }])
    return
  }

  if (action === "confirm" && session.step === "confirm") {
    sessions.delete(userId)
    // 在這裡寫入預約系統或通知店員
    await reply(replyToken, [
      { type: "text", text: `已收到申請:${session.date} ${session.service}。確切時間會在這個聊天室跟您確認。` },
    ])
    return
  }

  if (action === "cancel") {
    sessions.delete(userId)
    await reply(replyToken, [menuMessage("已取消。還有什麼需要幫忙的嗎?")])
  }
}

function menuMessage(text: string): messagingApi.TextMessage {
  return {
    type: "text",
    text,
    quickReply: {
      items: [
        { type: "action", action: { type: "postback", label: "預約", data: "action=start_booking", displayText: "預約" } },
        { type: "action", action: { type: "message", label: "找真人客服", text: "真人客服" } },
      ],
    },
  }
}

async function reply(replyToken: string, messages: messagingApi.Message[]) {
  await client.replyMessage({ replyToken, messages })
}

重點說明:

  • Postback 資料是你自訂的協定。 用 URLSearchParams 解析 action=service&id=cut。請控制在 300 個字元內,也不要照單全收,要檢查目前步驟與數值是否合理。
  • 日期選擇器是 datetimepicker 動作,選好的日期會放在 postback.params.date(date 模式為 YYYY-MM-DD)。
  • 每種事件都要有回應。 使用者傳貼圖、照片或位置時,回到選單,而不是沒反應或程式出錯。群組聊天的來源類型不同,這裡先略過。
  • 正式環境請把對話狀態存在記憶體以外的地方,以使用者 ID 為鍵,並替中途放棄的預約設定期限。
  • 轉真人客服。 使用者按下「找真人客服」後,Bot 不再回覆這位使用者,由店員在 LINE Official Account Manager 的聊天功能回覆(回應設定中的聊天要保持開啟)。也請準備把對話交還給 Bot 的方式,例如管理指令、後台按鈕或逾時自動恢復。
  • 把主要功能放在圖文選單,讓使用者幾乎不必打字。

用 Python(line-bot-sdk v3)處理同樣的 postback,可在 app.py 加入:

python
from urllib.parse import parse_qsl

from linebot.v3.webhooks import PostbackEvent


@handler.add(PostbackEvent)
def handle_postback(event):
    data = dict(parse_qsl(event.postback.data))  # "action=service&id=cut"
    if data.get("action") == "date" and event.postback.params:
        text = f"選擇的日期:{event.postback.params['date']}"
    else:
        text = f"動作:{data.get('action')}"
    with ApiClient(configuration) as api_client:
        MessagingApi(api_client).reply_message(
            ReplyMessageRequest(reply_token=event.reply_token, messages=[TextMessage(text=text)])
        )

#加入 AI 回答(以 Gemini 為例)

語言模型可以處理按鈕涵蓋不到的問題(「可以幫小朋友剪頭髮嗎?」「附近有停車場嗎?」),但它也可能捏造價格或承諾。以下三個原則能讓它安全上線:

  1. 只給它你自己的資料,並要求資料裡沒有答案時回覆 NO_ANSWER。
  2. 不要讓它猜,改交給真人。
  3. 在回覆權杖的時限內完成。 LINE 說明回覆權杖要在 1 分鐘內使用,所以模型呼叫要設上限(下例為 15 秒)。需要更久的工作,請先回「請稍候」,之後再用推播訊息送出結果,推播會計入每月訊息則數。

這個範例使用 Google 的 Gen AI SDK(npm install @google/genai)。模型版本會更新,所以模型名稱放在環境變數,請到 Google 的文件選擇目前的模型。

ts
// src/ai.ts
import { GoogleGenAI } from "@google/genai"

const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY })
const model = process.env.GEMINI_MODEL // Google 文件中目前提供的模型名稱
if (!model) throw new Error("請設定 GEMINI_MODEL")

// 只放你願意讓 Bot 原封不動說出的資訊
const FAQ = `
營業時間:10:00–20:00,每週三公休。
地址:台北市大安區忠孝東路四段 100 號(捷運忠孝敦化站步行 3 分鐘)。
剪髮:NT$600,約 45 分鐘。染髮:NT$2,000 起,約 2 小時。
預約:點選單中的「預約」。最晚可在 3 小時前更改。
`

export async function answerFromFaq(question: string): Promise<string | null> {
  const response = await ai.models.generateContent({
    model: model!,
    contents: question,
    config: {
      systemInstruction:
        "你是美髮店的客服助理,只能根據下方 FAQ 回答顧客問題。" +
        "如果 FAQ 裡沒有答案,只回覆 NO_ANSWER。" +
        "用顧客使用的語言回答,最多 3 句,簡短清楚。不可以自行編造價格、時間或優惠。\n\n" +
        `FAQ:\n${FAQ}`,
      temperature: 0.2,
      maxOutputTokens: 300,
      abortSignal: AbortSignal.timeout(15_000), // 確保在回覆權杖時限內完成
    },
  })
  const text = response.text?.trim()
  if (!text || text.includes("NO_ANSWER")) return null
  return text.slice(0, 5000) // LINE 文字訊息最多 5,000 個字元
}

接著把 handleEvent 文字分支的最後一行換成:

ts
// src/index.ts 開頭(ES 模組需要加上 .js 副檔名)
import { answerFromFaq } from "./ai.js"

// 在 handleEvent 中,不再對每則自由輸入都回選單:
await client.showLoadingAnimation({ chatId: userId, loadingSeconds: 20 })
const answer = await answerFromFaq(text).catch(() => null)
await reply(event.replyToken, [
  answer
    ? { type: "text", text: answer }
    : menuMessage("這個問題我無法確定,請點「找真人客服」由店員為您說明。"),
])

載入動畫只在一對一聊天顯示,時間為 5 到 60 秒(以 5 秒為單位),你的回覆一送達就會消失。

隱私:交給 AI 服務商的訊息會離開 LINE。請在隱私權政策中說明,不要轉傳模型用不到的個人資料,並確認服務商的資料使用條款。

#部署與監控

#Docker 映像檔

dockerfile
FROM node:22-slim AS build
WORKDIR /app
COPY package*.json tsconfig.json ./
RUN npm ci
COPY src ./src
RUN npm run build

FROM node:22-slim
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=build /app/dist ./dist
EXPOSE 3000
CMD ["node", "dist/index.js"]

任何能在 HTTPS 後方執行容器或 Node.js 程序的主機都可以。Channel secret、存取權杖與 AI 金鑰請用環境變數或密鑰功能傳入,不要放進映像檔。

#把 LINE 指向正式網址

  1. 在 LINE Developers Console 的 Messaging API 分頁,把 Webhook URL 設為 https://your-domain/webhook。
  2. 點 Verify。LINE 會送出空的事件陣列,伺服器必須回 200。
  3. 確認 Use webhook 已開啟,再用手機實際測試。

#重複事件、重送與日誌

  • Webhook 重送預設為關閉。 在 Console 開啟後,沒有收到 2xx 回應的 Webhook 會由 LINE 重送;次數與間隔不公開。
  • 即使不開重送,也可能收到重複事件。 已處理過的 webhookEventId 請直接略過;重送的事件會帶有 deliveryContext.isRedelivery: true。
  • 日誌用一行一筆的 JSON,並記下事件 ID,方便追蹤單一對話。
  • 了解各項上限。 Messaging API 多數端點每個 Channel 每秒 2,000 次請求,multicast 每秒 200 次,群發與分眾群發每小時 60 次。429 代表觸及速率限制或每月訊息上限。
ts
import { HTTPFetchError } from "@line/bot-sdk"

const seen = new Map<string, number>() // webhookEventId -> 收到時間;多個執行個體請改用 Redis(SET NX + TTL)

function firstDelivery(event: webhook.Event): boolean {
  const now = Date.now()
  for (const [id, at] of seen) if (now - at > 30 * 60_000) seen.delete(id)
  if (seen.has(event.webhookEventId)) return false
  seen.set(event.webhookEventId, now)
  return true
}

function log(level: "info" | "error", msg: string, data: Record<string, unknown> = {}) {
  console.log(JSON.stringify({ time: new Date().toISOString(), level, msg, ...data }))
}

app.post("/webhook", middleware({ channelSecret }), (req, res) => {
  res.sendStatus(200)
  const { events } = req.body as webhook.CallbackRequest
  for (const event of events) {
    if (!firstDelivery(event)) continue // 同一事件收到兩次
    handleEvent(event).catch((err) => {
      if (err instanceof HTTPFetchError) {
        // 429:速率限制或每月上限;400「Invalid reply token」:已使用或逾時
        log("error", "LINE API error", { status: err.status, body: err.body, eventId: event.webhookEventId })
      } else {
        log("error", "event failed", { error: String(err), eventId: event.webhookEventId })
      }
    })
  }
})

#上線前檢查清單

  • x-line-signature 錯誤的請求會被拒絕
  • 貼圖、照片與位置訊息都有合適的回應
  • 轉真人客服正常運作,店員也能把對話交還給 Bot
  • LINE Official Account Manager 的歡迎訊息與自動回應訊息不會重複回覆
  • 密鑰放在環境變數,不在程式碼儲存庫裡
  • Messaging API 連續回傳 4xx 或 5xx 時會發出警示

#LineBot.pro:不寫程式的做法

如果你的目標是「有一個 Bot 可用」,而不是「自己寫一個」,還有更短的路。LineBot.pro 不是 SDK、API 或主機服務,而是一套給 LINE 官方帳號用的 AI 工具。和本教學重疊的部分,是自動化裡的關鍵字 Bot:

  • 1–8 個意圖,每個意圖 1–15 個關鍵字,各自以你核准過的圖文訊息卡片回覆
  • 關鍵字回覆免費;選用的 AI 意圖分流會判斷沒有命中任何關鍵字的訊息(每判斷 1 則扣 1 點),而且只能從你核准的卡片中挑選,AI 不會自己撰寫回答
  • 轉真人關鍵字可以讓 Bot 對該位顧客暫停
  • 只支援一對一聊天,上線前可以用免費的模擬器測試

串接方式是把 LineBot.pro 的 Webhook URL 貼到你的 Channel。每個 Channel 只有一個 Webhook URL,所以無法和你自己的 Webhook 伺服器同時用在同一個 Channel。

開發者也可以只用產生器:圖文訊息每次 8 點、圖文選單 10 點,下載圖片與 JSON 後再用自己的程式傳送或上傳。新註冊送 50 點,之後採一次性購買點數包,沒有月費,點數不會過期(價格)。關鍵字 Bot 的介紹請見 LINE 聊天機器人頁面。

需要和預約系統、會員資料或 LIFF 應用串接的 Bot?那屬於客製開發,請參考 LINE 開發服務。

#常見問題

Q: @line/bot-sdk 的 new Client() 還能用嗎? 不能。舊的 Client 在 v10.8.0 標示為不建議使用,並在 2026 年 4 月 3 日發布的 v11.0.0 移除。請改用 LineBotClient.fromChannelAccessToken() 或 messagingApi.MessagingApiClient,方法都只傳一個請求物件,例如 replyMessage({ replyToken, messages })。v11 需要 Node.js 22 以上。

Q: 回覆訊息會計入 LINE 的每月訊息則數嗎? 不會。用回覆權杖送出的訊息不計入;推播、multicast、群發與分眾群發則會計入,每位收件人算 1 則。方案與價格依國家不同,請參考 LINE 官方帳號費用指南。

Q: 回覆權杖的有效時間多長? 只能使用一次,LINE 說明要在收到 Webhook 後 1 分鐘內使用;超過時間不保證有效,而且時限可能調整。比較耗時的工作,可以先顯示載入動畫,或立即回覆,再用推播訊息送出結果。

Q: 為什麼 LINE Developers Console 的 Verify 會失敗? 常見原因有:網址不是使用受信任憑證的 HTTPS、路徑錯誤、簽章驗證前就先解析了內容、Channel secret 屬於其他 Channel 或已重新發行,或伺服器沒有對空的事件陣列回 200。

Q: LINE Bot 一定要用 AI 嗎? 不一定。很多好用的 Bot 只靠按鈕、圖文選單和關鍵字回覆。AI 適合處理自由輸入的問題,請限定在你自己的資料,並保留真人接手。

Q: Bot 可以在群組聊天中使用嗎? 可以,但要先在 Messaging API 分頁開啟 Allow bot to join group chats(預設關閉)。之後事件會帶有 groupId,回覆會送到整個群組,而且群組無法使用載入動畫。本教學只處理一對一聊天。


下一步:

關於 LineBot.pro

LineBot.pro 是專為 LINE 官方帳號打造的 AI 工具,可製作圖文選單、圖文訊息與橫幅,群發行銷活動,並依關鍵字自動回覆。這些指南由我們的團隊根據 LINE Messaging API 的實務經驗撰寫。

聯絡我們
LineBot.pro

準備好讓您的 LINE 官方帳號自動運作了嗎?

立即使用 LineBot.pro,開始自動化您的 LINE 訊息溝通。