LINE OA Bot ระดับ production — ครบทุก feature ที่ Thai SME ต้องการ
Echo bot ใช้ลอง concept ได้ — แต่ production ต้องการอะไรมากกว่านั้น
ก่อนหน้านี้เราเขียน tutorial 5-นาทีสำหรับ LINE bot starter. บทความนี้ deep dive — ทุก feature ที่ Thai SME bot ต้องมีตอน launch จริง
Architecture pattern
LINE Platform ─────► Webhook ─────► NirvaDeploy service (FastAPI)
│
├─► Database (Postgres)
├─► Redis (rate limit + session)
├─► Job queue (Celery / BullMQ)
├─► File storage (Cloudflare R2)
└─► LLM (Claude API for smart reply)
Admin panel ──► /dashboard/[bot-id] (Next.js)
├─► Broadcast composer
├─► User list + segment
├─► Analytics dashboard
└─► Rich menu designerNirvaDeploy เหมาะเพราะ:
- Auto-deploy from GitHub push (Phase 1 ทำได้แล้ว)
- Postgres + Redis add-ons (Phase 2.x)
- ใบกำกับภาษีอัตโนมัติสำหรับ LINE Messaging API monthly fee
1. Rich Menu (เปลี่ยน UX ทันที)
Rich menu = bottom-of-chat menu (6 หรือ 12 cells). Reduce friction มหาศาล — user แทนที่จะพิมพ์ "ดูสินค้า" แค่ tap
from linebot.v3.messaging import (
MessagingApi, ApiClient, Configuration,
RichMenuRequest, RichMenuSize, RichMenuArea,
RichMenuBounds, MessageAction, URIAction,
)
config = Configuration(access_token=LINE_TOKEN)
rich_menu = RichMenuRequest(
size=RichMenuSize(width=2500, height=1686), # 2:1 ratio
selected=True,
name="Main menu Thai SME",
chat_bar_text="📱 เมนู",
areas=[
# 6 cells, 3 cols × 2 rows
RichMenuArea(
bounds=RichMenuBounds(x=0, y=0, width=833, height=843),
action=MessageAction(text="📦 ดูสินค้า"),
),
RichMenuArea(
bounds=RichMenuBounds(x=833, y=0, width=833, height=843),
action=MessageAction(text="🛒 ตะกร้า"),
),
RichMenuArea(
bounds=RichMenuBounds(x=1666, y=0, width=834, height=843),
action=URIAction(uri="https://liff.line.me/...", label="LIFF order"),
),
# ... 3 more cells
],
)
with ApiClient(config) as api_client:
api = MessagingApi(api_client)
response = api.create_rich_menu(rich_menu)
rich_menu_id = response.rich_menu_id
# Upload PNG image
with open("menu.png", "rb") as f:
api.set_rich_menu_image(rich_menu_id, "image/png", f.read())
# Apply as default for all users
api.set_default_rich_menu(rich_menu_id)2. Flex Message (ไม่ใช่ plain text)
Flex = JSON-based layout (เหมือน HTML แต่ JSON). User เห็น card สวยๆ แทน "raw text"
flex_msg = {
"type": "bubble",
"hero": {
"type": "image",
"url": "https://yourcdn.com/product.jpg",
"size": "full",
"aspectRatio": "20:13",
"aspectMode": "cover",
},
"body": {
"type": "box",
"layout": "vertical",
"contents": [
{"type": "text", "text": "เสื้อยืดผ้าฝ้าย 100%", "weight": "bold", "size": "xl"},
{
"type": "box",
"layout": "baseline",
"margin": "md",
"contents": [
{"type": "text", "text": "฿299", "size": "xl", "color": "#0EA5A4", "weight": "bold"},
{"type": "text", "text": "(ลด 40%)", "size": "sm", "color": "#aaaaaa", "margin": "md"},
],
},
],
},
"footer": {
"type": "box",
"layout": "vertical",
"spacing": "sm",
"contents": [
{
"type": "button",
"style": "primary",
"color": "#1E3A5F",
"action": {"type": "postback", "label": "เพิ่มในตะกร้า", "data": "action=add&id=42"},
},
{
"type": "button",
"style": "secondary",
"action": {"type": "uri", "label": "ดูเพิ่ม", "uri": "https://liff.line.me/.../product/42"},
},
],
},
}
api.push_message(PushMessageRequest(
to=user_id,
messages=[FlexMessage(alt_text="เสื้อยืดผ้าฝ้าย ฿299", contents=FlexContainer.from_dict(flex_msg))],
))Best practices:
- ใช้ Flex สำหรับ product cards, order confirmation, notifications
- ใช้ plain text สำหรับ FAQ replies, errors
- alt_text สำคัญ — แสดงใน notification + accessibility
3. Postback events (Button → action)
@app.post("/webhook")
async def webhook(request: Request):
body = await request.body()
signature = request.headers.get("X-Line-Signature")
# ... verify signature ...
events = parser.parse(body.decode(), signature)
for event in events:
if isinstance(event, PostbackEvent):
data = parse_qs(event.postback.data)
action = data.get("action", [""])[0]
if action == "add":
product_id = data.get("id", [""])[0]
await cart_add(event.source.user_id, product_id)
api.reply_message(...) # confirmation4. LIFF (LINE Front-end Framework)
LIFF = web app ที่ run inside LINE — got user ID + access token auto-injected. ใช้สำหรับ checkout, profile edit, complex forms.
<!DOCTYPE html>
<html>
<head>
<script src="https://static.line-scdn.net/liff/edge/2/sdk.js"></script>
</head>
<body>
<script>
liff.init({ liffId: "1234567890-AbCdEfGh" }).then(() => {
if (!liff.isLoggedIn()) liff.login();
liff.getProfile().then(profile => {
document.body.innerHTML = `<h1>สวัสดี ${profile.displayName}</h1>`;
});
});
</script>
</body>
</html>Deploy LIFF web app บน NirvaDeploy:
# Claude:
"deploy template thai-liff-starter ตั้ง LIFF_ID เป็น xxxxx"5. Broadcast + Multi-cast
- Broadcast = ส่งให้ทุกคน (ใช้ quota ทุก message × user count)
- Multi-cast = ส่งให้ list (max 500/call)
- Narrowcast = ส่งตาม audience (age, gender, region — pre-built segment)
# Broadcast (ระวัง quota!)
api.broadcast(BroadcastRequest(
messages=[TextMessage(text="🎉 Sale 11.11 เริ่มแล้ว!")],
))
# Multi-cast — ปลอดภัยกว่า เพราะระบุ user IDs ชัด
api.multicast(MulticastRequest(
to=user_ids[:500],
messages=[flex_message],
))Quota math (LINE OA pricing 2026):
- Premium plan: 45,000 messages/mo free, ฿0.04/msg ส่วนเกิน
- หาก user 10K → ส่ง 1 broadcast = 10K messages = ฿0 (within quota)
- ส่ง 5 broadcasts/เดือน @ 10K user = 50K messages = ฿200 ส่วนเกิน
Best practice:
- เก็บ
user_idทุกครั้งที่ user ทัก bot (follow event) - Segment ก่อน broadcast — ไม่ส่งให้คนที่ unfollow แล้ว
- Schedule broadcast ที่ peak hours (11am-12pm, 6pm-8pm ของไทย)
6. Webhook signature verification (security)
ห้ามข้าม — ใครก็ส่ง webhook ได้
import hmac
import hashlib
import base64
def verify_signature(body: bytes, signature: str, channel_secret: str) -> bool:
expected = base64.b64encode(
hmac.new(channel_secret.encode(), body, hashlib.sha256).digest()
).decode()
return hmac.compare_digest(expected, signature)ใส่เป็น middleware:
@app.middleware("http")
async def line_signature_middleware(request: Request, call_next):
if request.url.path == "/webhook":
body = await request.body()
sig = request.headers.get("X-Line-Signature", "")
if not verify_signature(body, sig, CHANNEL_SECRET):
return JSONResponse({"error": "invalid signature"}, status_code=403)
return await call_next(request)7. Rate limit + abuse protection
ใช้ Redis สำหรับ token bucket:
import redis
r = redis.from_url(REDIS_URL)
async def check_rate_limit(user_id: str, limit: int = 10, window: int = 60) -> bool:
key = f"rl:{user_id}"
count = await r.incr(key)
if count == 1:
await r.expire(key, window)
return count <= limit
@app.post("/webhook")
async def webhook(...):
user_id = event.source.user_id
if not await check_rate_limit(user_id, limit=30, window=60):
api.reply_message(... "⚠️ ส่งข้อความถี่เกินไป รอสักครู่")
return8. Analytics dashboard
ใช้ event log แบบนี้:
CREATE TABLE bot_events (
id BIGSERIAL PRIMARY KEY,
user_id VARCHAR(64),
event_type VARCHAR(32), -- 'message', 'postback', 'follow', 'unfollow'
payload JSONB,
created_at TIMESTAMPTZ DEFAULT now()
);
CREATE INDEX idx_events_user_time ON bot_events(user_id, created_at DESC);
CREATE INDEX idx_events_type_time ON bot_events(event_type, created_at DESC);Aggregate ใน NirvaDeploy dashboard:
- Daily Active Users (DAU)
- Most popular postback actions
- Conversion funnel (browse → add → checkout)
- Unfollow rate trend
9. Smart reply ด้วย Claude API
แทนที่จะ FAQ keyword match ใช้ Claude:
import anthropic
client = anthropic.AsyncAnthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
async def smart_reply(user_message: str, user_context: dict) -> str:
response = await client.messages.create(
model="claude-3-5-haiku-20251022", # cheap + fast
max_tokens=500,
system=(
"คุณเป็น customer service bot ของร้าน Thai SME ขายเสื้อผ้า. "
f"ลูกค้า: {user_context.get('name')}. "
"ตอบเป็นภาษาไทยสุภาพ ใส่ emoji ถ้าเหมาะสม. "
"ถ้าไม่รู้คำตอบ — แนะนำให้รอ admin ตอบ"
),
messages=[{"role": "user", "content": user_message}],
)
return response.content[0].textCost: Claude Haiku ~$0.25/1M input tokens. 1 message ≈ 500 tokens. = ~$0.000125 per reply = แสนข้อความ $12.50
ถูกกว่าจ้าง admin ตอบเอง 🤖
Deploy LINE bot ระดับ production บน NirvaDeploy
"Deploy template nirvadeploy/template-line-bot-production บน NirvaDeploy
ใช้ env:
LINE_CHANNEL_ACCESS_TOKEN=xxx
LINE_CHANNEL_SECRET=yyy
ANTHROPIC_API_KEY=zzz
ตั้ง add-ons: postgres, redis
scale เป็น 2 replicas"Claude เรียก:
nirvadeploy_deploy_template- Multiple
nirvadeploy_set_variable nirvadeploy_scale_service(replicas=2)
🎉 Production-ready LINE bot in 3 minutes
Resources
- LINE Developers docs
- LIFF SDK
- LINE Flex Message Simulator
- NirvaDeploy LINE templates (4 templates ฟรี + premium)
คำถาม? Line OA `@nirvadeploy` · email [email protected]
ชอบบทความนี้?
ลอง NirvaDeploy ฟรี
Free tier ตลอดชีพ · ไม่ต้องใส่บัตร · Sign in ด้วย Line/Google
เริ่มเลย →