Spaces:
Runtime error
Runtime error
| import os | |
| import sqlite3 | |
| import requests | |
| from google import genai | |
| from flask import Flask, request, abort | |
| from linebot import LineBotApi, WebhookHandler | |
| from linebot.exceptions import InvalidSignatureError | |
| import io | |
| import json | |
| from PIL import Image | |
| from linebot.models import ( | |
| MessageEvent, TextMessage, TextSendMessage, QuickReply, | |
| QuickReplyButton, MessageAction, ImageMessage, FlexSendMessage, | |
| LocationMessage, LocationAction | |
| ) | |
| import urllib.parse | |
| import math | |
| from datetime import datetime, timezone, timedelta | |
| app = Flask(__name__) | |
| # ================= 1. 從環境變數讀取金鑰(安全防護) ================= | |
| line_bot_api = LineBotApi(os.environ.get('LINE_CHANNEL_ACCESS_TOKEN')) | |
| handler = WebhookHandler(os.environ.get('LINE_CHANNEL_SECRET')) | |
| gemini_client = genai.Client(api_key=os.environ.get('GEMINI_API_KEY')) | |
| DB_PATH = os.path.join(os.path.dirname(__file__), "products.db") | |
| STORE_LOGOS = { | |
| "屈臣氏": "https://openhouse.osa.nycu.edu.tw/media/data/company_logos/3._%E5%B1%88%E8%87%A3%E6%B0%8Flogo.png", | |
| "康是美": "https://img.skmbuy.com.tw/App_Images/727/Brand/2022/1215/4b8e1829-8f30-46a9-b044-834c11fc884a.jpg", | |
| "寶雅": "https://color-matching.s3.ap-northeast-1.amazonaws.com/color-image/%E5%AF%B6%E9%9B%85%EF%BC%8C%E6%A8%99%E8%AA%8C.jpg", | |
| } | |
| def query_product(name): | |
| """從 SQLite 查詢商品,回傳各店資料的 list;查無則回傳 None。""" | |
| try: | |
| conn = sqlite3.connect(DB_PATH) | |
| conn.row_factory = sqlite3.Row | |
| rows = conn.execute( | |
| "SELECT * FROM products WHERE name = ? ORDER BY store", (name,) | |
| ).fetchall() | |
| conn.close() | |
| if rows: | |
| return [dict(r) for r in rows] | |
| except Exception: | |
| pass | |
| return None | |
| # ================= 2. 建立狀態儲存區 ================= | |
| user_states = {} | |
| def health(): | |
| return "OK" | |
| def callback(): | |
| signature = request.headers['X-Line-Signature'] | |
| body = request.get_data(as_text=True) | |
| try: | |
| handler.handle(body, signature) | |
| except InvalidSignatureError: | |
| abort(400) | |
| return 'OK' | |
| # ================= 3. 處理使用者傳送照片的邏輯 (拍照精靈) ================= | |
| def handle_image(event): | |
| user_id = event.source.user_id | |
| state = user_states.get(user_id) | |
| if state and state.get("step") == "ASK_PHOTO_ITEM": | |
| line_bot_api.reply_message( | |
| event.reply_token, | |
| TextSendMessage(text="✨ 收到照片!精靈正在用魔法辨識這款產品,請稍候...") | |
| ) | |
| try: | |
| message_content = line_bot_api.get_message_content(event.message.id) | |
| image_bytes = io.BytesIO(message_content.content) | |
| pil_image = Image.open(image_bytes) | |
| vision_prompt = """ | |
| 請辨識這張圖片中的保養品。 | |
| 如果是 CeraVe (適樂膚) 的產品,請直接回答:CeraVe適樂膚 長效潤澤修護霜 340g | |
| 如果是 Avene (雅漾) 活泉水,請直接回答:Avene 雅漾舒護活泉水150ml | |
| 如果都不是,請發揮你的專業,判斷它是什麼真實的品牌與品名,並「直接回答品牌與品名」。 | |
| 注意:非常重要!無論如何,你只能輸出「商品名稱」,絕對不要輸出任何其他廢話、標點符號、價格 or 商家資訊。 | |
| """ | |
| response = gemini_client.models.generate_content( | |
| model='gemini-2.5-flash', | |
| contents=[vision_prompt, pil_image] | |
| ) | |
| product_name = response.text.strip() | |
| data_list = query_product(product_name) | |
| if not data_list: | |
| estimate_prompt = f""" | |
| 請估算「{product_name}」這款產品在台灣屈臣氏、康是美、寶雅的合理售價。 | |
| 請務必「只回傳」一個標準的 JSON 陣列,不要有任何額外的說明文字。格式嚴格如下: | |
| [ | |
| {{"store": "屈臣氏", "name": "{product_name}", "price": "估計數字"}}, | |
| {{"store": "康是美", "name": "{product_name}", "price": "估計數字"}}, | |
| {{"store": "寶雅", "name": "{product_name}", "price": "估計數字"}} | |
| ] | |
| """ | |
| est_response = gemini_client.models.generate_content( | |
| model='gemini-2.5-flash', | |
| contents=estimate_prompt | |
| ) | |
| clean_text = est_response.text.replace("```json", "").replace("```", "").strip() | |
| data_list = json.loads(clean_text) | |
| bubbles = [] | |
| for item in data_list: | |
| safe_store = item.get("store", "推薦藥妝店") | |
| safe_name = item.get("name", "未知名稱商品") | |
| safe_price = item.get("price", "請洽門市") | |
| safe_logo = item.get("logo", STORE_LOGOS.get(safe_store, "https://via.placeholder.com/600x300/eeeeee/999999?text=Store+Logo")) | |
| safe_discount = item.get("discount", "") | |
| search_query = urllib.parse.quote(f"{safe_store} {safe_name}") | |
| safe_link = item.get("link", f"https://www.google.com/search?q={search_query}") | |
| specs_box = { | |
| "type": "box", | |
| "layout": "vertical", | |
| "spacing": "sm", | |
| "margin": "lg", | |
| "contents": [ | |
| { | |
| "type": "box", | |
| "layout": "horizontal", | |
| "contents": [ | |
| {"type": "text", "text": "販售商家", "size": "md", "color": "#8c8c8c", "flex": 1}, | |
| {"type": "text", "text": safe_store, "size": "md", "color": "#111111", "weight": "bold", "flex": 2} | |
| ] | |
| }, | |
| { | |
| "type": "box", | |
| "layout": "horizontal", | |
| "contents": [ | |
| {"type": "text", "text": "商品售價", "size": "md", "color": "#8c8c8c", "flex": 1}, | |
| {"type": "text", "text": f"${safe_price}", "size": "md", "color": "#111111", "weight": "bold", "flex": 2} | |
| ] | |
| } | |
| ] | |
| } | |
| if safe_discount: | |
| specs_box["contents"].append( | |
| { | |
| "type": "box", | |
| "layout": "horizontal", | |
| "contents": [ | |
| {"type": "text", "text": "獨家優惠", "size": "md", "color": "#8c8c8c", "flex": 1}, | |
| {"type": "text", "text": safe_discount, "size": "md", "color": "#ff3b30", "weight": "bold", "flex": 2} | |
| ] | |
| } | |
| ) | |
| card = { | |
| "type": "bubble", | |
| "size": "mega", | |
| "hero": { | |
| "type": "image", | |
| "url": safe_logo, | |
| "size": "full", | |
| "aspectRatio": "2:1", | |
| "aspectMode": "fit", | |
| "backgroundColor": "#FFFFFF" | |
| }, | |
| "body": { | |
| "type": "box", | |
| "layout": "vertical", | |
| "contents": [ | |
| {"type": "text", "text": safe_name, "weight": "bold", "size": "xl", "wrap": True}, | |
| specs_box | |
| ] | |
| }, | |
| "footer": { | |
| "type": "box", | |
| "layout": "vertical", | |
| "contents": [ | |
| { | |
| "type": "button", | |
| "style": "primary", | |
| "color": "#000000", | |
| "height": "md", | |
| "action": { | |
| "type": "uri", | |
| "label": "前往商家購買", | |
| "uri": safe_link | |
| } | |
| } | |
| ] | |
| } | |
| } | |
| bubbles.append(card) | |
| flex_message = FlexSendMessage.new_from_json_dict({ | |
| "type": "flex", | |
| "alt_text": "🔍 藥妝精靈比價報告來囉!", | |
| "contents": { | |
| "type": "carousel", | |
| "contents": bubbles | |
| } | |
| }) | |
| line_bot_api.push_message(user_id, flex_message) | |
| if user_id in user_states: del user_states[user_id] | |
| except Exception as e: | |
| line_bot_api.push_message( | |
| user_id, | |
| TextSendMessage(text=f"❌ 哎呀!精靈遇到了一點問題。錯誤原因:\n{e}") | |
| ) | |
| # ================= 4. 處理使用者傳送位置的邏輯 (附近店家地圖) ================= | |
| def handle_location(event): | |
| user_id = event.source.user_id | |
| user_lat = event.message.latitude | |
| user_lon = event.message.longitude | |
| state = user_states.get(user_id, {}) | |
| brand_preference = state.get("search_brand", "全都要") | |
| # 使用與 Hugging Face Secrets 完美對應的正確環境變數 | |
| google_api_key = os.environ.get('GOOGLE_MAPS_API_KEY') | |
| if not google_api_key: | |
| line_bot_api.reply_message(event.reply_token, TextSendMessage(text="❌ 系統尚未設定 Google Maps API 金鑰。")) | |
| return | |
| # 計算距離的數學公式 | |
| def calculate_distance(lat1, lon1, lat2, lon2): | |
| R = 6371.0 | |
| lat1_rad, lon1_rad = math.radians(lat1), math.radians(lon1) | |
| lat2_rad, lon2_rad = math.radians(lat2), math.radians(lon2) | |
| dlon = lon2_rad - lon1_rad | |
| dlat = lat2_rad - lat1_rad | |
| a = math.sin(dlat / 2)**2 + math.cos(lat1_rad) * math.cos(lat2_rad) * math.sin(dlon / 2)**2 | |
| c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a)) | |
| return int(R * c * 1000) | |
| # 查詢「今天確切營業時間」的 Place Details 函式 | |
| def get_exact_hours(place_id, api_key): | |
| url = f"https://maps.googleapis.com/maps/api/place/details/json?place_id={place_id}&fields=opening_hours&language=zh-TW&key={api_key}" | |
| try: | |
| res = requests.get(url).json() | |
| if 'result' in res and 'opening_hours' in res['result']: | |
| weekday_text = res['result']['opening_hours'].get('weekday_text', []) | |
| if weekday_text: | |
| tw_tz = timezone(timedelta(hours=8)) | |
| today_idx = datetime.now(tw_tz).weekday() | |
| today_str = weekday_text[today_idx] | |
| if ": " in today_str: | |
| return today_str.split(": ", 1)[1] | |
| except Exception: | |
| pass | |
| return "詳情請洽門市" | |
| brands_to_search = ["屈臣氏", "康是美", "寶雅"] if brand_preference == "全都要" else [brand_preference] | |
| search_radius = 3000 | |
| try: | |
| line_bot_api.reply_message(event.reply_token, TextSendMessage(text="🧚♀️ 精靈正在為您精準定位各品牌門市,請稍候...")) | |
| nearest_stores = [] | |
| for brand in brands_to_search: | |
| url = f"https://maps.googleapis.com/maps/api/place/textsearch/json?query={brand}&location={user_lat},{user_lon}&radius={search_radius}&language=zh-TW&key={google_api_key}" | |
| response = requests.get(url).json() | |
| if response['status'] == 'OK' and response.get('results'): | |
| brand_stores = [] | |
| for place in response['results']: | |
| store_lat = place['geometry']['location']['lat'] | |
| store_lon = place['geometry']['location']['lng'] | |
| distance = calculate_distance(user_lat, user_lon, store_lat, store_lon) | |
| photo_ref = "" | |
| if 'photos' in place and len(place['photos']) > 0: | |
| photo_ref = place['photos'][0]['photo_reference'] | |
| open_now = place.get('opening_hours', {}).get('open_now') | |
| if open_now is True: | |
| status_light = "🟢 營業中" | |
| elif open_now is False: | |
| status_light = "🔴 休息中" | |
| else: | |
| status_light = "🕒 營業狀態" | |
| brand_stores.append({ | |
| "place_id": place.get('place_id'), | |
| "name": place.get('name', '未知名稱'), | |
| "rating": place.get('rating', '無評分'), | |
| "lat": store_lat, | |
| "lon": store_lon, | |
| "distance": distance, | |
| "address": place.get('formatted_address', ''), | |
| "status_light": status_light, | |
| "photo_ref": photo_ref | |
| }) | |
| sorted_brand_stores = sorted(brand_stores, key=lambda x: x['distance']) | |
| if brand_preference == "全都要": | |
| if sorted_brand_stores: | |
| nearest_stores.append(sorted_brand_stores[0]) | |
| else: | |
| nearest_stores.extend(sorted_brand_stores[:3]) | |
| if not nearest_stores: | |
| line_bot_api.push_message(user_id, TextSendMessage(text="🥺 哎呀,精靈在您附近 3 公里內都找不到藥妝店呢...")) | |
| if user_id in user_states: del user_states[user_id] | |
| return | |
| nearest_stores = sorted(nearest_stores, key=lambda x: x['distance']) | |
| bubbles = [] | |
| for store in nearest_stores: | |
| exact_hours = get_exact_hours(store['place_id'], google_api_key) | |
| display_hours = f"{store['status_light']} ({exact_hours})" | |
| map_query = urllib.parse.quote(f"{store['name']} {store['address']}") | |
| map_url = f"https://www.google.com/maps/search/?api=1&query={map_query}" | |
| if store['photo_ref']: | |
| image_url = f"https://maps.googleapis.com/maps/api/place/photo?maxwidth=600&photo_reference={store['photo_ref']}&key={google_api_key}" | |
| else: | |
| image_url = "https://images.unsplash.com/photo-1576426863848-c21f53c60b19?q=80&w=600&auto=format&fit=crop" | |
| card = { | |
| "type": "bubble", | |
| "size": "mega", | |
| "hero": { | |
| "type": "image", | |
| "url": image_url, | |
| "size": "full", | |
| "aspectRatio": "16:9", | |
| "aspectMode": "cover" | |
| }, | |
| "body": { | |
| "type": "box", | |
| "layout": "vertical", | |
| "contents": [ | |
| {"type": "text", "text": store['name'], "weight": "bold", "size": "xl", "wrap": True, "color": "#111111"}, | |
| { | |
| "type": "box", | |
| "layout": "vertical", | |
| "spacing": "sm", | |
| "margin": "lg", | |
| "contents": [ | |
| { | |
| "type": "box", | |
| "layout": "baseline", | |
| "spacing": "sm", | |
| "contents": [ | |
| {"type": "text", "text": "目前距離", "size": "sm", "color": "#8c8c8c", "flex": 3}, | |
| {"type": "text", "text": f"{store['distance']} 公尺", "size": "sm", "color": "#111111", "weight": "bold", "flex": 5} | |
| ] | |
| }, | |
| { | |
| "type": "box", | |
| "layout": "baseline", | |
| "spacing": "sm", | |
| "contents": [ | |
| {"type": "text", "text": "營業時間", "size": "sm", "color": "#8c8c8c", "flex": 3}, | |
| {"type": "text", "text": display_hours, "size": "sm", "color": "#111111", "weight": "bold", "flex": 5, "wrap": True} | |
| ] | |
| }, | |
| { | |
| "type": "box", | |
| "layout": "baseline", | |
| "spacing": "sm", | |
| "contents": [ | |
| {"type": "text", "text": "店家評分", "size": "sm", "color": "#8c8c8c", "flex": 3}, | |
| {"type": "text", "text": f"⭐ {store['rating']}", "size": "sm", "color": "#fbbc04", "weight": "bold", "flex": 5} | |
| ] | |
| } | |
| ] | |
| } | |
| ] | |
| }, | |
| "footer": { | |
| "type": "box", | |
| "layout": "vertical", | |
| "contents": [ | |
| { | |
| "type": "button", | |
| "style": "primary", | |
| "color": "#000000", | |
| "height": "md", | |
| "action": { | |
| "type": "uri", | |
| "label": "開啟地圖導航", | |
| "uri": map_url | |
| } | |
| } | |
| ] | |
| } | |
| } | |
| bubbles.append(card) | |
| flex_message = FlexSendMessage.new_from_json_dict({ | |
| "type": "flex", | |
| "alt_text": "🔍 附近的藥妝店地圖出爐囉!", | |
| "contents": { | |
| "type": "carousel", | |
| "contents": bubbles | |
| } | |
| }) | |
| line_bot_api.push_message(user_id, flex_message) | |
| if user_id in user_states: del user_states[user_id] | |
| except Exception as e: | |
| line_bot_api.push_message(user_id, TextSendMessage(text=f"❌ 搜尋過程發生錯誤:{e}")) | |
| if user_id in user_states: del user_states[user_id] | |
| # ================= 5. 多輪對話核心邏輯 ================= | |
| def handle_message(event): | |
| user_id = event.source.user_id | |
| user_msg = event.message.text.strip() | |
| # 附近店家 - 步驟 1:選擇品牌 | |
| if user_msg == "附近店家": | |
| user_states[user_id] = {"step": "ASK_BRAND"} | |
| brand_buttons = QuickReply(items=[ | |
| QuickReplyButton(action=MessageAction(label="屈臣氏", text="屈臣氏")), | |
| QuickReplyButton(action=MessageAction(label="康是美", text="康是美")), | |
| QuickReplyButton(action=MessageAction(label="寶雅", text="寶雅")), | |
| QuickReplyButton(action=MessageAction(label="全都要", text="全都要")) | |
| ]) | |
| line_bot_api.reply_message( | |
| event.reply_token, | |
| TextSendMessage(text="請問你想尋找哪一間藥妝店呢?", quick_reply=brand_buttons) | |
| ) | |
| return | |
| # 附近店家 - 步驟 2:要求定位 | |
| state = user_states.get(user_id) | |
| if state and state.get("step") == "ASK_BRAND": | |
| if user_msg in ["屈臣氏", "康是美", "寶雅", "全都要"]: | |
| state["search_brand"] = user_msg | |
| state["step"] = "ASK_LOCATION" | |
| location_button = QuickReply(items=[ | |
| QuickReplyButton(action=LocationAction(label="📍 點我分享位置")) | |
| ]) | |
| line_bot_api.reply_message( | |
| event.reply_token, | |
| TextSendMessage(text=f"沒問題!請分享您的位置,精靈來幫您找最近的【{user_msg}】!", quick_reply=location_button) | |
| ) | |
| else: | |
| line_bot_api.reply_message(event.reply_token, TextSendMessage(text="請點擊下方的按鈕選擇品牌喔!")) | |
| return | |
| # 任何時候輸入「精靈顧問」或「重新開始」,皆重置該用戶的狀態 | |
| if user_msg == "精靈顧問" or user_msg == "重新開始": | |
| user_states[user_id] = {"step": "ASK_SKIN"} | |
| skin_buttons = QuickReply(items=[ | |
| QuickReplyButton(action=MessageAction(label="乾性肌", text="乾性肌")), | |
| QuickReplyButton(action=MessageAction(label="油性肌", text="油性肌")), | |
| QuickReplyButton(action=MessageAction(label="混合肌", text="混合肌")), | |
| QuickReplyButton(action=MessageAction(label="敏感肌", text="敏感肌")) | |
| ]) | |
| line_bot_api.reply_message( | |
| event.reply_token, | |
| TextSendMessage(text="🧚♀️ 嗨!我是你的專屬精靈顧問。為了給你最精準的保養建議,先告訴我,你覺得自己偏向哪一種膚質呢?", quick_reply=skin_buttons) | |
| ) | |
| return | |
| if user_msg == "拍照精靈": | |
| user_states[user_id] = {"step": "ASK_PHOTO_ITEM"} | |
| line_bot_api.reply_message( | |
| event.reply_token, | |
| TextSendMessage(text="📷 歡迎開啟拍照精靈!\n您可以直接拍下商品照片傳給我,或是輸入「文字」來查詢商品售價喔!") | |
| ) | |
| return | |
| state = user_states.get(user_id) | |
| if not state: | |
| line_bot_api.reply_message( | |
| event.reply_token, | |
| TextSendMessage(text="請輸入「精靈顧問」來開啟膚況諮詢服務喔!") | |
| ) | |
| return | |
| current_step = state.get("step") | |
| # 【步驟一】處理膚質選擇 | |
| if current_step == "ASK_SKIN": | |
| if user_msg in ["乾性肌", "油性肌", "混合肌", "敏感肌"]: | |
| state["skin_type"] = user_msg | |
| state["step"] = "ASK_ISSUE" | |
| issue_buttons = QuickReply(items=[ | |
| QuickReplyButton(action=MessageAction(label="嚴重脫皮", text="嚴重脫皮")), | |
| QuickReplyButton(action=MessageAction(label="暗沉無光", text="暗沉無光")), | |
| QuickReplyButton(action=MessageAction(label="痘痘粉刺", text="痘痘粉刺")), | |
| QuickReplyButton(action=MessageAction(label="泛紅乾癢", text="泛紅乾癢")) | |
| ]) | |
| line_bot_api.reply_message( | |
| event.reply_token, | |
| TextSendMessage(text=f"記錄下來了!你是 {user_msg}。那最近最讓你困擾的肌膚問題是什麼呢?", quick_reply=issue_buttons) | |
| ) | |
| else: | |
| line_bot_api.reply_message(event.reply_token, TextSendMessage(text="請選擇下方的膚質選項,或是輸入「重新開始」喔!")) | |
| # 【步驟二】處理困擾選擇 | |
| elif current_step == "ASK_ISSUE": | |
| if user_msg in ["嚴重脫皮", "暗沉無光", "痘痘粉刺", "泛紅乾癢"]: | |
| state["issue"] = user_msg | |
| state["step"] = "ASK_PREFERENCE" | |
| pref_buttons = QuickReply(items=[ | |
| QuickReplyButton(action=MessageAction(label="偏好清爽凝露", text="偏好清爽凝露")), | |
| QuickReplyButton(action=MessageAction(label="偏好滋潤乳霜", text="偏好滋潤乳霜")), | |
| QuickReplyButton(action=MessageAction(label="都可以", text="都可以")) | |
| ]) | |
| line_bot_api.reply_message( | |
| event.reply_token, | |
| TextSendMessage(text=f"收到!針對 {user_msg} 的問題,你在挑選保養品時有特別偏好哪種質地嗎?", quick_reply=pref_buttons) | |
| ) | |
| else: | |
| line_bot_api.reply_message(event.reply_token, TextSendMessage(text="請選擇下方的肌膚問題選項,或是輸入「重新開始」喔!")) | |
| # 【步驟三】處理偏好選擇 -> 產生建議 | |
| elif current_step == "ASK_PREFERENCE": | |
| if user_msg in ["偏好清爽凝露", "偏好滋潤乳霜", "都可以"]: | |
| skin_type = state["skin_type"] | |
| issue = state["issue"] | |
| preference = user_msg | |
| line_bot_api.reply_message( | |
| event.reply_token, | |
| TextSendMessage(text="🧚♀️ 精靈正在為您翻閱保養圖鑑,請稍候幾秒鐘...") | |
| ) | |
| prompt = f""" | |
| 你現在是一位「台灣藥妝店皮膚科精靈顧問🧚♀️」,擁有10年以上保養諮詢經驗。 | |
| 使用者資料: | |
| * 膚質:{skin_type} | |
| * 困擾:{issue} | |
| * 偏好:{preference} | |
| 請根據以上資訊提供個人化保養建議。 | |
| 格式要求固定包括:精靈診斷、保養建議、推薦產品。 | |
| """ | |
| try: | |
| response = gemini_client.models.generate_content( | |
| model='gemini-2.5-flash', | |
| contents=prompt | |
| ) | |
| ai_reply = response.text | |
| line_bot_api.push_message(user_id, TextSendMessage(text=ai_reply)) | |
| except Exception as e: | |
| line_bot_api.push_message(user_id, TextSendMessage(text=f"❌ 精靈魔法發動失敗!錯誤原因:\n{e}")) | |
| del user_states[user_id] | |
| else: | |
| line_bot_api.reply_message(event.reply_token, TextSendMessage(text="請選擇下方的質地偏好選項,或是輸入「重新開始」喔!")) | |
| # 處理純文字商品查詢 | |
| elif current_step == "ASK_PHOTO_ITEM": | |
| text_prompt = f"請幫我查詢「{user_msg}」這款產品在台灣藥妝店的合理售價範圍與推薦購買商家。請簡單溫柔地回答,並條列出品名、價格、商家。" | |
| try: | |
| line_bot_api.reply_message(event.reply_token, TextSendMessage(text="✨ 收到文字!精靈正在幫您查詢這款產品,請稍候...")) | |
| response = gemini_client.models.generate_content( | |
| model='gemini-2.5-flash', | |
| contents=text_prompt | |
| ) | |
| line_bot_api.push_message(user_id, TextSendMessage(text=f"🧚♀️ 查詢成功!\n\n{response.text}")) | |
| except Exception as e: | |
| line_bot_api.push_message(user_id, TextSendMessage(text=f"❌ 查詢失敗,錯誤原因:\n{e}")) | |
| del user_states[user_id] | |
| if __name__ == "__main__": | |
| app.run(host='0.0.0.0', port=7860) |