在昨天的文章中,我們為美食 Agent 加入了趣味滿分的美食轉盤與 LINE Quick Reply 快速按鈕,拯救了無數選擇困難的使用者。
當系統推薦出一間評分極高、看起來非常誘人的餐廳時,使用者的直覺反應往往是:
「這家看起來好讚,先存起來,下次跟朋友去吃!」
如果每次推薦完就隨風而逝,下次想再找這家店就得翻遍落落長的聊天紀錄。因此,今天 Day 19 我們要為 Agent 打造個人化專屬收藏夾:利用 LINE 的 Postback Action 機制,讓使用者只要在推薦卡片上點擊「❤️ 加入收藏」,後端 SQLite 就會將店家自動歸檔至使用者的個人私房名單,並支援隨時隨地用口語指令調閱查看!
在 LINE Flex Message 中,按鈕主要有兩種動作形式:
{
"type": "postback",
"replyToken": "nHuyWiB7yP5Zw52FIkcQobQuGDXCTA",
"postback": {
"data": "action=favorite&shop_name=恬淡日常法式甜點&cuisine=甜點/下午茶&location=板橋區&rating=4.7"
}
}
打開 app/models/memory.py,新增一個專門存放使用者最愛的 FavoriteModel 資料表:
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Float
from sqlalchemy.orm import relationship
from datetime import datetime
from app.database import Base
class FavoriteModel(Base):
__tablename__ = "favorites"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
shop_name = Column(String(100), nullable=False)
cuisine = Column(String(50), nullable=False)
location = Column(String(100), nullable=False)
rating = Column(Float, default=4.0)
saved_at = Column(DateTime, default=datetime.utcnow)
# 關聯使用者
user = relationship("UserModel", back_populates="favorites")
# 記得在 UserModel 中補上雙向關聯:
# UserModel.favorites = relationship("FavoriteModel", back_populates="user", cascade="all, delete-orphan")
在 app/routers/ 目錄下新增 favorite.py,負責收藏的「寫入」與「清單查詢」。
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from pydantic import BaseModel, Field
from typing import List
from datetime import datetime
from app.database import get_db
from app.models.memory import UserModel, FavoriteModel
router = APIRouter(
prefix="/api/v1/favorites",
tags=["Favorites"]
)
class AddFavoriteRequest(BaseModel):
user_id: str = Field(..., description="LINE 使用者 ID")
shop_name: str
cuisine: str
location: str
rating: float
class FavoriteItemResponse(BaseModel):
id: int
shop_name: str
cuisine: str
location: str
rating: float
saved_at: str
@router.post("/add")
async def add_to_favorites(payload: AddFavoriteRequest, db: Session = Depends(get_db)):
"""將餐廳加入使用者的收藏清單"""
user = db.query(UserModel).filter(UserModel.line_user_id == payload.user_id).first()
if not user:
user = UserModel(line_user_id=payload.user_id)
db.add(user)
db.commit()
db.refresh(user)
# 檢查是否已收藏過同名店家
exists = db.query(FavoriteModel).filter(
FavoriteModel.user_id == user.id,
FavoriteModel.shop_name == payload.shop_name
).first()
if exists:
return {"status": "already_exists", "message": f"【{payload.shop_name}】已經在您的私房清單中囉!"}
# 寫入收藏
new_fav = FavoriteModel(
user_id=user.id,
shop_name=payload.shop_name,
cuisine=payload.cuisine,
location=payload.location,
rating=payload.rating,
saved_at=datetime.utcnow()
)
db.add(new_fav)
db.commit()
return {"status": "success", "message": f"🎉 成功將【{payload.shop_name}】收錄到個人名單!"}
@router.get("/list/{user_id}")
async def list_favorites(user_id: str, db: Session = Depends(get_db)):
"""查詢使用者的歷史收藏清單"""
user = db.query(UserModel).filter(UserModel.line_user_id == user_id).first()
if not user:
return {"favorites": []}
items = db.query(FavoriteModel).filter(FavoriteModel.user_id == user.id).order_by(FavoriteModel.saved_at.desc()).all()
results = [
{
"id": item.id,
"shop_name": item.shop_name,
"cuisine": item.cuisine,
"location": item.location,
"rating": item.rating,
"saved_at": item.saved_at.strftime("%Y-%m-%d")
}
for item in items
]
return {"favorites": results}
記得在 app/main.py 中掛載這個新路由:
from app.routers import favorite
app.include_router(favorite.router)
回顧 Day 11 的 Flex Message 產生程式碼,我們在每張餐廳 Bubble 的 footer 中多加入一顆按鈕,類型設定為 postback:
// 在每張卡片的 footer.contents 陣列中新增第二顆按鈕:
footer: {
type: "box",
layout: "horizontal",
spacing: "sm",
contents: [
{
type: "button",
style: "primary",
color: "#06C755",
height: "sm",
action: {
type: "uri",
label: "導航",
uri: `[https://www.google.com/maps/search/?api=1&query=$](https://www.google.com/maps/search/?api=1&query=$){encodeURIComponent(shop.location + ' ' + shop.name)}`
}
},
{
type: "button",
style: "secondary",
height: "sm",
action: {
type: "postback",
label: "❤️ 收藏",
data: `action=fav&name=${encodeURIComponent(shop.name)}&cuisine=${encodeURIComponent(shop.cuisine)}&loc=${encodeURIComponent(shop.location)}&rate=${shop.rating}`,
displayText: `把【${shop.name}】加入我的收藏!` // 使用者點擊時聊天室會自動帶出的對話泡泡
}
}
]
}
將原本只收 message 事件的判斷邏輯放寬,讓它也能接納 postback 事件:
const event = $input.first().json.body.events[0];
if (!event) return [];
// 處理 postback 事件(點擊收藏按鈕)
if (event.type === 'postback') {
const params = new URLSearchParams(event.postback.data);
return {
reply_token: event.replyToken,
user_id: event.source.userId,
event_type: 'postback',
action: params.get('action'),
shop_name: decodeURIComponent(params.get('name') || ''),
cuisine: decodeURIComponent(params.get('cuisine') || ''),
location: decodeURIComponent(params.get('loc') || ''),
rating: parseFloat(params.get('rate') || '4.0')
};
}
// 處理文字與位置訊息(維持原有邏輯)
if (event.type === 'message') {
return {
reply_token: event.replyToken,
user_id: event.source.userId,
event_type: 'message',
user_message: event.message.text || '',
msg_type: event.message.type
};
}
return [];
使用 Switch 節點進行分流:
分支 1(Postback 收藏):呼叫 POST /api/v1/favorites/add,並用 LINE Reply API 回應「🎉 成功將【恬淡日常法式甜點】收錄到個人名單!」。
分支 2(口語查詢收藏):當使用者的自然語言是「查看我的收藏」、「我存了哪些店」,Dify 識別意圖為 list_favorites 時,呼叫 GET /api/v1/favorites/list/{user_id},將已存店家以條列式文字或輪播卡片優雅回傳!
1.觸發收藏:
在推薦卡片底部點擊「❤️ 收藏」,聊天室跳出 把【恬淡日常法式甜點】加入我的收藏!,小幫手隨即回覆:
🎉 成功將【恬淡日常法式甜點】收錄到個人名單!隨時對我說「查看我的收藏」就可以調閱囉!
2.調閱歷史收藏:
在聊天室打字:
使用者:「查看我的收藏清單」
美食小幫手:
📖 您的私房美食名單共有 1 間:
【恬淡日常法式甜點】(甜點/下午茶)
📍 板橋區 ⭐️ 4.7(收藏於 2026-10-15)
想去哪一間?隨時跟我說哦!
今日結語與明日預告
今天我們讓美食 Agent 擁有了更深度的客製化黏著度:
靈活運用了 LINE Postback Action 實現背景靜默操作。
透過 SQLite 關聯資料表 實作了專屬個人的私房店家收藏庫。
支援一鍵收錄與口語回顧,讓使用者真正把這隻 Bot 當成隨身的美食筆記本!
目前為止,所有互動都必須由使用者「先主動發話」。
明天 Day 20,我們將進入主動推播的全新領域:利用 n8n 的 Cron 排程觸發器與 LINE Push Message API,打造每逢中午 11:30 自動向使用者推播「今日午餐靈感」的貼心主動式助理! 敬請期待!