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)程式碼。

#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(回覆或推播)────────┘
- 使用者傳送訊息、點按鈕,或把你的帳號加為好友。
- LINE 平台以 HTTPS POST 把一個或多個事件送到你的 Webhook URL。
- 伺服器驗證簽章,回傳
200,再處理事件。 - 用事件的回覆權杖呼叫回覆訊息(reply),或之後用使用者 ID 傳送推播訊息(push)。
回覆訊息不計入方案的每月訊息則數;推播、多人推播(multicast)、群發(broadcast)與分眾群發(narrowcast)則會計入,每位收件人算 1 則。
#你要做哪一種 Bot?
| 類型 | 運作方式 | 適合 | 要注意 |
|---|---|---|---|
| 關鍵字型 | 訊息含特定字詞就送出預先準備的回答 | 常見問題、營業時間、價格 | 沒列到的說法就接不住 |
| 選單 / Postback 型 | 按鈕、快速回覆與圖文選單把 postback 資料送到伺服器 | 預約、點餐、分步驟填寫 | 前期設計比較花工夫 |
| AI 輔助型 | 由語言模型回答自由輸入的問題 | 問法多變、多語言 | 必須限定在你提供的資料,並保留真人接手 |
| 混合型 | 主要流程用按鈕,其他交給 AI,例外由真人處理 | 大多數實際的店家 | 要測試的路徑比較多 |
以下程式碼是一間美髮店的混合型 Bot:預約用按鈕進行,AI 只根據店家的 FAQ 回答,「找真人客服」按鈕則把對話交給店員。
#事前準備與 Channel 設定
#需要準備
- 已啟用 Messaging API 的 LINE 官方帳號(申請免費)
- Node.js 22 以上(
@line/bot-sdkv11 的要求)或 Python 3.10 以上(line-bot-sdkv3) - 可透過 HTTPS 連線的伺服器;在本機測試時可用 ngrok 或 Cloudflare Tunnel 等通道工具
#步驟 1:建立 LINE 官方帳號並啟用 Messaging API
自 2024 年 9 月 4 日起,已無法在 LINE Developers Console 直接建立 Messaging API Channel,請改用以下流程:
- 建立 LINE 官方帳號:註冊 Business ID 並填寫申請表單。可參考LINE 官方帳號申請教學。
- 在 LINE Official Account Manager 開啟設定 → Messaging API,啟用 Messaging API。
- 選擇 Provider。請謹慎選擇:Provider 之後無法更改,而且同一位使用者在不同 Provider 底下會有不同的使用者 ID,所以同一個品牌的 Channel 請放在同一個 Provider。
完成後,新的 Messaging API Channel 就會出現在 LINE Developers Console。
#步驟 2:取得憑證
| 憑證 | 位置 | 用途 |
|---|---|---|
| Channel secret | Console 的 Basic settings 分頁 | 驗證 Webhook 簽章 |
| Channel access token | Console 的 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):
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:
{
"scripts": {
"dev": "tsx watch --env-file=.env src/index.ts",
"build": "tsc",
"start": "node dist/index.js"
}
}
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"outDir": "dist",
"skipLibCheck": true
},
"include": ["src"]
}
Python:
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 匯出環境變數,或使用主機平台的密鑰管理功能。
LINE_CHANNEL_SECRET=your_channel_secret
LINE_CHANNEL_ACCESS_TOKEN=your_channel_access_token
PORT=3000
#Webhook 實作:接收並回覆
#Node.js(TypeScript、@line/bot-sdk v11)
// 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)
# 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 則:
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 與日期選擇器完成三步驟預約,並讓使用者切換到真人客服:
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 加入:
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 為例)
語言模型可以處理按鈕涵蓋不到的問題(「可以幫小朋友剪頭髮嗎?」「附近有停車場嗎?」),但它也可能捏造價格或承諾。以下三個原則能讓它安全上線:
- 只給它你自己的資料,並要求資料裡沒有答案時回覆
NO_ANSWER。 - 不要讓它猜,改交給真人。
- 在回覆權杖的時限內完成。 LINE 說明回覆權杖要在 1 分鐘內使用,所以模型呼叫要設上限(下例為 15 秒)。需要更久的工作,請先回「請稍候」,之後再用推播訊息送出結果,推播會計入每月訊息則數。
這個範例使用 Google 的 Gen AI SDK(npm install @google/genai)。模型版本會更新,所以模型名稱放在環境變數,請到 Google 的文件選擇目前的模型。
// 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 文字分支的最後一行換成:
// 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 映像檔
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 指向正式網址
- 在 LINE Developers Console 的 Messaging API 分頁,把 Webhook URL 設為
https://your-domain/webhook。 - 點 Verify。LINE 會送出空的事件陣列,伺服器必須回 200。
- 確認 Use webhook 已開啟,再用手機實際測試。
#重複事件、重送與日誌
- Webhook 重送預設為關閉。 在 Console 開啟後,沒有收到 2xx 回應的 Webhook 會由 LINE 重送;次數與間隔不公開。
- 即使不開重送,也可能收到重複事件。 已處理過的
webhookEventId請直接略過;重送的事件會帶有deliveryContext.isRedelivery: true。 - 日誌用一行一筆的 JSON,並記下事件 ID,方便追蹤單一對話。
- 了解各項上限。 Messaging API 多數端點每個 Channel 每秒 2,000 次請求,multicast 每秒 200 次,群發與分眾群發每小時 60 次。
429代表觸及速率限制或每月訊息上限。
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,回覆會送到整個群組,而且群組無法使用載入動畫。本教學只處理一對一聊天。
下一步:
- 深入了解簽章驗證與
replyMessage:@line/bot-sdk Express 中介軟體教學 - 加入 LINE Login、LIFF 與直接呼叫 HTTP API:LINE API 整合教學
- 幫 Bot 加上常駐選單:LINE 圖文選單怎麼做
- 想交給專業團隊開發:LINE 開發服務
- 不想寫程式:試試 LINE 聊天機器人的關鍵字 Bot
關於 LineBot.pro
LineBot.pro 是專為 LINE 官方帳號打造的 AI 工具,可製作圖文選單、圖文訊息與橫幅,群發行銷活動,並依關鍵字自動回覆。這些指南由我們的團隊根據 LINE Messaging API 的實務經驗撰寫。
聯絡我們相關文章

@line/bot-sdk Express 中介軟體實作範例(v11)
@line/bot-sdk v11 的 Express + TypeScript Webhook 完整範例:middleware() 該放哪裡、validateSignature 在 Node.js、Python、PHP 的用法、搭配 Flex Message 的 replyMessage、sentMessages 與引用 token,以及實際會遇到的錯誤。
閱讀更多
LINE API 串接教學:Messaging API、Login 與 LIFF
給開發者的 2026 年 LINE API 全貌,以及不用 SDK 直接呼叫的方法:各 API 能做什麼、Channel 設定、用 fetch 回覆與推播、Webhook 簽章驗證、搭配 PKCE 的 LINE Login、LIFF 的 ID Token、速率限制與錯誤處理。
閱讀更多
LINE圖文訊息、多頁訊息與Flex Message:尺寸與製作教學
圖文訊息、多頁訊息、進階影片訊息與Flex Message差在哪?一篇整理官方圖片尺寸、版型、字數限制、後台建立步驟,以及群發則數怎麼算。
閱讀更多