Nai-Yun commited on
Commit
29c1325
·
verified ·
1 Parent(s): 24b8ac8

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +407 -141
app.py CHANGED
@@ -7,14 +7,10 @@ from linebot.exceptions import InvalidSignatureError
7
  import io
8
  import json
9
  from PIL import Image
10
- from linebot.models import (
11
- MessageEvent, TextMessage, TextSendMessage, QuickReply,
12
- QuickReplyButton, MessageAction, ImageMessage, FlexSendMessage,
13
- LocationMessage, LocationAction
14
- )
15
  import urllib.parse
16
  import math
17
- from datetime import datetime, timezone, timedelta
18
 
19
  app = Flask(__name__)
20
 
@@ -24,7 +20,7 @@ handler = WebhookHandler(os.environ.get('LINE_CHANNEL_SECRET'))
24
  genai.configure(api_key=os.environ.get('GEMINI_API_KEY'))
25
  model = genai.GenerativeModel('gemini-2.5-flash')
26
 
27
- # 建立虛擬藥妝資料庫 (Mock Database)
28
  fake_database = {
29
  "CeraVe適樂膚 長效潤澤修護霜 340g": [
30
  {"store": "屈臣氏", "name": "CeraVe適樂膚 長效潤澤修護霜 340g", "price": "619","link": "https://www.watsons.com.tw/cerave-cerave%E9%81%A9%E6%A8%82%E8%86%9A%E9%95%B7%E6%95%88%E6%BD%A4%E6%BE%A4%E4%BF%AE%E8%AD%B7%E9%9C%9C-340g/p/BP_153671","logo": "https://openhouse.osa.nycu.edu.tw/media/data/company_logos/3._%E5%B1%88%E8%87%A3%E6%B0%8Flogo.png"},
@@ -38,7 +34,8 @@ fake_database = {
38
  ]
39
  }
40
 
41
- # ================= 2. 建立狀態儲存區 =================
 
42
  user_states = {}
43
 
44
  @app.route("/callback", methods=['POST'])
@@ -51,38 +48,48 @@ def callback():
51
  abort(400)
52
  return 'OK'
53
 
54
- # ================= 3. 處理使用者傳送照片的邏輯 (拍照精靈) =================
55
  @handler.add(MessageEvent, message=ImageMessage)
56
  def handle_image(event):
57
  user_id = event.source.user_id
58
  state = user_states.get(user_id)
59
 
 
60
  if state and state.get("step") == "ASK_PHOTO_ITEM":
 
61
  line_bot_api.reply_message(
62
  event.reply_token,
63
  TextSendMessage(text="✨ 收到照片!精靈正在用魔法辨識這款產品,請稍候...")
64
  )
65
 
66
  try:
 
67
  message_content = line_bot_api.get_message_content(event.message.id)
68
  image_bytes = io.BytesIO(message_content.content)
69
  pil_image = Image.open(image_bytes)
70
 
 
71
  vision_prompt = """
72
  請辨識這張圖片中的保養品。
73
  如果是 CeraVe (適樂膚) 的產品,請直接回答:CeraVe適樂膚 長效潤澤修護霜 340g
74
  如果是 Avene (雅漾) 活泉水,請直接回答:Avene 雅漾舒護活泉水150ml
75
  如果都不是,請發揮你的專業,判斷它是什麼真實的品牌與品名,並「直接回答品牌與品名」。
76
 
77
- 注意:非常重要!無論如何,你只能輸出「商品名稱」,絕對不要輸出任何其他廢話、標點符號、價格 or 商家資訊。
78
  """
79
 
 
80
  response = model.generate_content([vision_prompt, pil_image])
81
  product_name = response.text.strip()
82
 
 
83
  if product_name in fake_database:
 
84
  data_list = fake_database[product_name]
 
85
  else:
 
 
86
  estimate_prompt = f"""
87
  請估算「{product_name}」這款產品在台灣屈臣氏、康是美、寶雅的合理售價。
88
  請務必「只回傳」一個標準的 JSON 陣列,不要有任何額外的說明文字。格式嚴格如下:
@@ -95,118 +102,227 @@ def handle_image(event):
95
  est_response = model.generate_content(estimate_prompt)
96
  clean_text = est_response.text.replace("```json", "").replace("```", "").strip()
97
  data_list = json.loads(clean_text)
 
98
 
99
- bubbles = []
100
- for item in data_list:
101
- default_logos = {
102
- "屈臣氏": "https://openhouse.osa.nycu.edu.tw/media/data/company_logos/3._%E5%B1%88%E8%87%A3%E6%B0%8Flogo.png",
103
- "康是美": "https://img.skmbuy.com.tw/App_Images/727/Brand/2022/1215/4b8e1829-8f30-46a9-b044-834c11fc884a.jpg",
104
- "寶雅": "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"
105
- }
106
-
107
- safe_store = item.get("store", "推薦藥妝店")
108
- safe_name = item.get("name", "未知名稱商品")
109
- safe_price = item.get("price", "請洽門市")
110
- safe_logo = item.get("logo", default_logos.get(safe_store, "https://via.placeholder.com/600x300/eeeeee/999999?text=Store+Logo"))
111
- safe_discount = item.get("discount", "")
112
-
113
- search_query = urllib.parse.quote(f"{safe_store} {safe_name}")
114
- safe_link = item.get("link", f"https://www.google.com/search?q={search_query}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
 
116
- specs_box = {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  "type": "box",
118
  "layout": "vertical",
119
- "spacing": "sm",
120
- "margin": "lg",
121
  "contents": [
 
122
  {
123
  "type": "box",
124
- "layout": "horizontal",
125
- "contents": [
126
- {"type": "text", "text": "販售商家", "size": "md", "color": "#8c8c8c", "flex": 1},
127
- {"type": "text", "text": safe_store, "size": "md", "color": "#111111", "weight": "bold", "flex": 2}
128
- ]
129
- },
130
- {
131
- "type": "box",
132
- "layout": "horizontal",
133
- "contents": [
134
- {"type": "text", "text": "商品售價", "size": "md", "color": "#8c8c8c", "flex": 1},
135
- {"type": "text", "text": f"${safe_price}", "size": "md", "color": "#111111", "weight": "bold", "flex": 2}
136
- ]
137
- }
138
- ]
139
- }
140
-
141
- if safe_discount:
142
- specs_box["contents"].append(
143
- {
144
- "type": "box",
145
- "layout": "horizontal",
146
  "contents": [
147
- {"type": "text", "text": "獨家優惠", "size": "md", "color": "#8c8c8c", "flex": 1},
148
- {"type": "text", "text": safe_discount, "size": "md", "color": "#ff3b30", "weight": "bold", "flex": 2}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  ]
150
  }
151
- )
152
-
153
- card = {
154
- "type": "bubble",
155
- "size": "mega",
156
- "hero": {
157
- "type": "image",
158
- "url": safe_logo,
159
- "size": "full",
160
- "aspectRatio": "2:1",
161
- "aspectMode": "fit",
162
- "backgroundColor": "#FFFFFF"
163
- },
164
- "body": {
165
- "type": "box",
166
- "layout": "vertical",
167
- "contents": [
168
- {"type": "text", "text": safe_name, "weight": "bold", "size": "xl", "wrap": True},
169
- specs_box
170
  ]
171
- },
172
- "footer": {
173
  "type": "box",
174
  "layout": "vertical",
175
  "contents": [
176
- {
177
- "type": "button",
178
- "style": "primary",
179
- "color": "#000000",
180
- "height": "md",
181
- "action": {
182
- "type": "uri",
183
- "label": "前往商家購買",
184
- "uri": safe_link
 
185
  }
186
- }
187
  ]
188
- }
189
  }
190
- bubbles.append(card)
191
-
192
- flex_message = FlexSendMessage.new_from_json_dict({
193
- "type": "flex",
194
- "alt_text": "🔍 藥妝精靈比價報告來囉!",
195
- "contents": {
196
- "type": "carousel",
197
- "contents": bubbles
198
- }
199
- })
200
- line_bot_api.push_message(user_id, flex_message)
201
- if user_id in user_states: del user_states[user_id]
202
-
203
- except Exception as e:
204
- line_bot_api.push_message(
205
- user_id,
206
- TextSendMessage(text=f"❌ 哎呀!精靈遇到了一點問題。錯誤原因\n{e}")
207
- )
208
 
209
- # ================= 4. 處理使用者傳送位置的邏輯 (附近店家地圖) =================
 
 
 
 
210
  @handler.add(MessageEvent, message=LocationMessage)
211
  def handle_location(event):
212
  user_id = event.source.user_id
@@ -216,14 +332,14 @@ def handle_location(event):
216
  state = user_states.get(user_id, {})
217
  brand_preference = state.get("search_brand", "全都要")
218
 
219
- # 使用與 Hugging Face Secrets 完美對應的正確環境變數
220
- google_api_key = os.environ.get('GOOGLE_MAPS_API_KEY')
221
  if not google_api_key:
222
  line_bot_api.reply_message(event.reply_token, TextSendMessage(text="❌ 系統尚未設定 Google Maps API 金鑰。"))
223
  return
224
 
225
- # 計算距離的數學公式
226
  def calculate_distance(lat1, lon1, lat2, lon2):
 
227
  R = 6371.0
228
  lat1_rad, lon1_rad = math.radians(lat1), math.radians(lon1)
229
  lat2_rad, lon2_rad = math.radians(lat2), math.radians(lon2)
@@ -233,7 +349,7 @@ def handle_location(event):
233
  c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
234
  return int(R * c * 1000)
235
 
236
- # 查詢「今天確切營業時間」的 Place Details 函式
237
  def get_exact_hours(place_id, api_key):
238
  url = f"https://maps.googleapis.com/maps/api/place/details/json?place_id={place_id}&fields=opening_hours&language=zh-TW&key={api_key}"
239
  try:
@@ -241,6 +357,7 @@ def handle_location(event):
241
  if 'result' in res and 'opening_hours' in res['result']:
242
  weekday_text = res['result']['opening_hours'].get('weekday_text', [])
243
  if weekday_text:
 
244
  tw_tz = timezone(timedelta(hours=8))
245
  today_idx = datetime.now(tw_tz).weekday()
246
  today_str = weekday_text[today_idx]
@@ -250,14 +367,16 @@ def handle_location(event):
250
  pass
251
  return "詳情請洽門市"
252
 
 
253
  brands_to_search = ["屈臣氏", "康是美", "寶雅"] if brand_preference == "全都要" else [brand_preference]
254
- search_radius = 3000
255
 
256
  try:
257
  line_bot_api.reply_message(event.reply_token, TextSendMessage(text="🧚‍♀️ 精靈正在為您精準定位各品牌門市,請稍候..."))
258
 
259
  nearest_stores = []
260
 
 
261
  for brand in brands_to_search:
262
  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}"
263
  response = requests.get(url).json()
@@ -293,12 +412,14 @@ def handle_location(event):
293
  "photo_ref": photo_ref
294
  })
295
 
 
296
  sorted_brand_stores = sorted(brand_stores, key=lambda x: x['distance'])
297
 
298
  if brand_preference == "全都要":
299
- if sorted_brand_stores:
300
- nearest_stores.append(sorted_brand_stores[0])
301
  else:
 
302
  nearest_stores.extend(sorted_brand_stores[:3])
303
 
304
  if not nearest_stores:
@@ -306,8 +427,10 @@ def handle_location(event):
306
  if user_id in user_states: del user_states[user_id]
307
  return
308
 
 
309
  nearest_stores = sorted(nearest_stores, key=lambda x: x['distance'])
310
 
 
311
  bubbles = []
312
  for store in nearest_stores:
313
  exact_hours = get_exact_hours(store['place_id'], google_api_key)
@@ -321,6 +444,7 @@ def handle_location(event):
321
  else:
322
  image_url = "https://images.unsplash.com/photo-1576426863848-c21f53c60b19?q=80&w=600&auto=format&fit=crop"
323
 
 
324
  card = {
325
  "type": "bubble",
326
  "size": "mega",
@@ -401,23 +525,16 @@ def handle_location(event):
401
  "contents": bubbles
402
  }
403
  })
404
-
405
- line_bot_api.push_message(user_id, flex_message)
406
- if user_id in user_states: del user_states[user_id]
407
-
408
- except Exception as e:
409
- line_bot_api.push_message(user_id, TextSendMessage(text=f"❌ 搜尋過程發生錯誤:{e}"))
410
- if user_id in user_states: del user_states[user_id]
411
 
412
- # ================= 5. 多輪對話核心邏輯 =================
413
  @handler.add(MessageEvent, message=TextMessage)
414
  def handle_message(event):
415
  user_id = event.source.user_id
416
  user_msg = event.message.text.strip()
417
 
418
- # 附近店家 - 步驟 1:選擇品牌
419
  if user_msg == "附近店家":
420
- user_states[user_id] = {"step": "ASK_BRAND"}
421
 
422
  brand_buttons = QuickReply(items=[
423
  QuickReplyButton(action=MessageAction(label="屈臣氏", text="屈臣氏")),
@@ -428,16 +545,19 @@ def handle_message(event):
428
 
429
  line_bot_api.reply_message(
430
  event.reply_token,
431
- TextSendMessage(text="請問你想尋找哪一間藥妝店呢?", quick_reply=brand_buttons)
 
 
 
432
  )
433
  return
434
 
435
- # 附近店家 - 步驟 2:要求定位
436
  state = user_states.get(user_id)
437
  if state and state.get("step") == "ASK_BRAND":
438
  if user_msg in ["屈臣氏", "康是美", "寶雅", "全都要"]:
439
- state["search_brand"] = user_msg
440
- state["step"] = "ASK_LOCATION"
441
 
442
  location_button = QuickReply(items=[
443
  QuickReplyButton(action=LocationAction(label="📍 點我分享位置"))
@@ -445,7 +565,10 @@ def handle_message(event):
445
 
446
  line_bot_api.reply_message(
447
  event.reply_token,
448
- TextSendMessage(text=f"沒問題!請分享您的位置,精靈來幫您找最近的【{user_msg}】!", quick_reply=location_button)
 
 
 
449
  )
450
  else:
451
  line_bot_api.reply_message(event.reply_token, TextSendMessage(text="請點擊下方的按鈕選擇品牌喔!"))
@@ -455,6 +578,7 @@ def handle_message(event):
455
  if user_msg == "精靈顧問" or user_msg == "重新開始":
456
  user_states[user_id] = {"step": "ASK_SKIN"}
457
 
 
458
  skin_buttons = QuickReply(items=[
459
  QuickReplyButton(action=MessageAction(label="乾性肌", text="乾性肌")),
460
  QuickReplyButton(action=MessageAction(label="油性肌", text="油性肌")),
@@ -476,7 +600,10 @@ def handle_message(event):
476
  )
477
  return
478
 
 
479
  state = user_states.get(user_id)
 
 
480
  if not state:
481
  line_bot_api.reply_message(
482
  event.reply_token,
@@ -486,12 +613,13 @@ def handle_message(event):
486
 
487
  current_step = state.get("step")
488
 
489
- # 【步驟一】處理膚質選擇
490
  if current_step == "ASK_SKIN":
491
  if user_msg in ["乾性肌", "油性肌", "混合肌", "敏感肌"]:
492
- state["skin_type"] = user_msg
493
- state["step"] = "ASK_ISSUE"
494
 
 
495
  issue_buttons = QuickReply(items=[
496
  QuickReplyButton(action=MessageAction(label="嚴重脫皮", text="嚴重脫皮")),
497
  QuickReplyButton(action=MessageAction(label="暗沉無光", text="暗沉無光")),
@@ -506,12 +634,13 @@ def handle_message(event):
506
  else:
507
  line_bot_api.reply_message(event.reply_token, TextSendMessage(text="請選擇下方的膚質選項,或是輸入「重新開始」喔!"))
508
 
509
- # 【步驟二】處理困擾選擇
510
  elif current_step == "ASK_ISSUE":
511
  if user_msg in ["嚴重脫皮", "暗沉無光", "痘痘粉刺", "泛紅乾癢"]:
512
- state["issue"] = user_msg
513
- state["step"] = "ASK_PREFERENCE"
514
 
 
515
  pref_buttons = QuickReply(items=[
516
  QuickReplyButton(action=MessageAction(label="偏好清爽凝露", text="偏好清爽凝露")),
517
  QuickReplyButton(action=MessageAction(label="偏好滋潤乳霜", text="偏好滋潤乳霜")),
@@ -525,47 +654,184 @@ def handle_message(event):
525
  else:
526
  line_bot_api.reply_message(event.reply_token, TextSendMessage(text="請選擇下方的肌膚問題選項,或是輸入「重新開始」喔!"))
527
 
528
- # 【步驟三】處理偏好選擇 -> 產生建議
529
  elif current_step == "ASK_PREFERENCE":
530
  if user_msg in ["偏好清爽凝露", "偏好滋潤乳霜", "都可以"]:
531
  skin_type = state["skin_type"]
532
  issue = state["issue"]
533
  preference = user_msg
534
 
 
535
  line_bot_api.reply_message(
536
  event.reply_token,
537
  TextSendMessage(text="🧚‍♀️ 精靈正在為您翻閱保養圖鑑,請稍候幾秒鐘...")
538
  )
539
 
 
540
  prompt = f"""
541
  你現在是一位「台灣藥妝店皮膚科精靈顧問🧚‍♀️」,擁有10年以上保養諮詢經驗。
542
- 使用者資料:
543
- * 膚質{skin_type}
544
- * 困擾:{issue}
545
- * 偏好:{preference}
546
- 請根據以上資訊提供個人化保養建議。
547
- 格式要求固定包括精靈診斷、保養建議、推薦產品。
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
548
  """
 
549
  try:
 
550
  response = model.generate_content(prompt)
551
  ai_reply = response.text
 
 
552
  line_bot_api.push_message(user_id, TextSendMessage(text=ai_reply))
 
553
  except Exception as e:
554
- line_bot_api.push_message(user_id, TextSendMessage(text=f"❌ 精靈魔法動失敗!錯誤原因:\n{e}"))
 
 
555
 
 
556
  del user_states[user_id]
557
  else:
558
  line_bot_api.reply_message(event.reply_token, TextSendMessage(text="請選擇下方的質地偏好選項,或是輸入「重新開始」喔!"))
559
 
560
- # 處理純文字商品查詢
 
561
  elif current_step == "ASK_PHOTO_ITEM":
 
562
  text_prompt = f"請幫我查詢「{user_msg}」這款產品在台灣藥妝店的合理售價範圍與推薦購買商家。請簡單溫柔地回答,並條列出品名、價格、商家。"
563
  try:
 
564
  line_bot_api.reply_message(event.reply_token, TextSendMessage(text="✨ 收到文字!精靈正在幫您查詢這款產品,請稍候..."))
 
 
565
  response = model.generate_content(text_prompt)
 
 
566
  line_bot_api.push_message(user_id, TextSendMessage(text=f"🧚‍♀️ 查詢成功!\n\n{response.text}"))
 
567
  except Exception as e:
568
  line_bot_api.push_message(user_id, TextSendMessage(text=f"❌ 查詢失敗,錯誤原因:\n{e}"))
 
 
569
  del user_states[user_id]
570
 
571
  if __name__ == "__main__":
 
7
  import io
8
  import json
9
  from PIL import Image
10
+ from linebot.models import MessageEvent, TextMessage, TextSendMessage, QuickReply, QuickReplyButton, MessageAction, ImageMessage, FlexSendMessage, LocationMessage, LocationAction
 
 
 
 
11
  import urllib.parse
12
  import math
13
+ import urllib.parse
14
 
15
  app = Flask(__name__)
16
 
 
20
  genai.configure(api_key=os.environ.get('GEMINI_API_KEY'))
21
  model = genai.GenerativeModel('gemini-2.5-flash')
22
 
23
+ # 建立我們的虛擬藥妝資料庫 (Mock Database)
24
  fake_database = {
25
  "CeraVe適樂膚 長效潤澤修護霜 340g": [
26
  {"store": "屈臣氏", "name": "CeraVe適樂膚 長效潤澤修護霜 340g", "price": "619","link": "https://www.watsons.com.tw/cerave-cerave%E9%81%A9%E6%A8%82%E8%86%9A%E9%95%B7%E6%95%88%E6%BD%A4%E6%BE%A4%E4%BF%AE%E8%AD%B7%E9%9C%9C-340g/p/BP_153671","logo": "https://openhouse.osa.nycu.edu.tw/media/data/company_logos/3._%E5%B1%88%E8%87%A3%E6%B0%8Flogo.png"},
 
34
  ]
35
  }
36
 
37
+
38
+ # ================= 2. 建立狀態儲存區(暫存每個用戶的進度) =================
39
  user_states = {}
40
 
41
  @app.route("/callback", methods=['POST'])
 
48
  abort(400)
49
  return 'OK'
50
 
51
+ # ================= 【新增】專門處理使用者傳送照片的邏輯 =================
52
  @handler.add(MessageEvent, message=ImageMessage)
53
  def handle_image(event):
54
  user_id = event.source.user_id
55
  state = user_states.get(user_id)
56
 
57
+ # 檢查使用者是否處於拍照精靈的等待狀態
58
  if state and state.get("step") == "ASK_PHOTO_ITEM":
59
+ # 先給個回覆讓使用者知道精靈在看了
60
  line_bot_api.reply_message(
61
  event.reply_token,
62
  TextSendMessage(text="✨ 收到照片!精靈正在用魔法辨識這款產品,請稍候...")
63
  )
64
 
65
  try:
66
+ # 1. 從 LINE 伺服器下載使用者傳的照片
67
  message_content = line_bot_api.get_message_content(event.message.id)
68
  image_bytes = io.BytesIO(message_content.content)
69
  pil_image = Image.open(image_bytes)
70
 
71
+ # 2. 設定 Prompt,要求 Gemini 直接分析這張圖片
72
  vision_prompt = """
73
  請辨識這張圖片中的保養品。
74
  如果是 CeraVe (適樂膚) 的產品,請直接回答:CeraVe適樂膚 長效潤澤修護霜 340g
75
  如果是 Avene (雅漾) 活泉水,請直接回答:Avene 雅漾舒護活泉水150ml
76
  如果都不是,請發揮你的專業,判斷它是什麼真實的品牌與品名,並「直接回答品牌與品名」。
77
 
78
+ 注意:非常重要!無論如何,你只能輸出「商品名稱」,絕對不要輸出任何其他廢話、標點符號、價格商家資訊。
79
  """
80
 
81
+ # 3. 把圖片和 Prompt 一起丟給 Gemini (支援多模態)
82
  response = model.generate_content([vision_prompt, pil_image])
83
  product_name = response.text.strip()
84
 
85
+ # --- 第 3 步:雙軌制 (假資料庫 vs AI估價) ---
86
  if product_name in fake_database:
87
+ # 🌟 路線 A:命中資料庫,使用妳設定的真實數據!
88
  data_list = fake_database[product_name]
89
+ price_tag = "資料庫真實價" # 讓圖卡顯示這是真實數據
90
  else:
91
+ # 🤖 路線 B:資料庫沒有,呼叫 AI 進行現場估價!
92
+
93
  estimate_prompt = f"""
94
  請估算「{product_name}」這款產品在台灣屈臣氏、康是美、寶雅的合理售價。
95
  請務必「只回傳」一個標準的 JSON 陣列,不要有任何額外的說明文字。格式嚴格如下:
 
102
  est_response = model.generate_content(estimate_prompt)
103
  clean_text = est_response.text.replace("```json", "").replace("```", "").strip()
104
  data_list = json.loads(clean_text)
105
+ price_tag = "AI 估計價" # 讓圖卡顯示這是 AI 猜的
106
 
107
+ # --- 第 4 步:將資料塞進橫向圖卡 (不管哪條路線,最後都會來到這裡) ---
108
+ # --- 4 步:將資料塞進橫向圖卡 ---
109
+ # --- 第 4 步:將資料塞進 Toyota 風格的旗艦版圖卡 ---
110
+ # --- 第 4 步:將資料塞進 Toyota 風格的旗艦版圖卡 ---
111
+ # --- 第 4 步:將資料塞進 Toyota 風格的旗艦版圖卡 (終極防彈版) ---
112
+ # --- 第 4 步:將資料塞進 Toyota 風格 (店家 Logo 版) ---
113
+ # ================= 【霸氣點名版】保證全品牌上榜 + 曜石黑完美排版 =================
114
+ @handler.add(MessageEvent, message=LocationMessage)
115
+ def handle_location(event):
116
+ user_id = event.source.user_id
117
+ user_lat = event.message.latitude
118
+ user_lon = event.message.longitude
119
+
120
+ state = user_states.get(user_id, {})
121
+ brand_preference = state.get("search_brand", "全都要")
122
+
123
+ # ✅ 修正一:對齊正確的金鑰變數名稱
124
+ google_api_key = os.environ.get('Maps_API_KEY')
125
+ if not google_api_key:
126
+ line_bot_api.reply_message(event.reply_token, TextSendMessage(text="❌ 系統尚未設定 Google Maps API 金鑰。"))
127
+ return
128
+
129
+ # 計算距離的數學魔法
130
+ def calculate_distance(lat1, lon1, lat2, lon2):
131
+ import math
132
+ R = 6371.0
133
+ lat1_rad, lon1_rad = math.radians(lat1), math.radians(lon1)
134
+ lat2_rad, lon2_rad = math.radians(lat2), math.radians(lon2)
135
+ dlon = lon2_rad - lon1_rad
136
+ dlat = lat2_rad - lat1_rad
137
+ a = math.sin(dlat / 2)**2 + math.cos(lat1_rad) * math.cos(lat2_rad) * math.sin(dlon / 2)**2
138
+ c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
139
+ return int(R * c * 1000)
140
+
141
+ # 查詢「今天確切營業時間」的魔法
142
+ def get_exact_hours(place_id, api_key):
143
+ url = f"https://maps.googleapis.com/maps/api/place/details/json?place_id={place_id}&fields=opening_hours&language=zh-TW&key={api_key}"
144
+ try:
145
+ res = requests.get(url).json()
146
+ if 'result' in res and 'opening_hours' in res['result']:
147
+ weekday_text = res['result']['opening_hours'].get('weekday_text', [])
148
+ if weekday_text:
149
+ from datetime import datetime, timezone, timedelta
150
+ tw_tz = timezone(timedelta(hours=8))
151
+ today_idx = datetime.now(tw_tz).weekday()
152
+ today_str = weekday_text[today_idx]
153
+ if ": " in today_str:
154
+ return today_str.split(": ", 1)[1]
155
+ except Exception:
156
+ pass
157
+ return "詳情請洽門市"
158
+
159
+ # 決定要搜尋的名單
160
+ brands_to_search = ["屈臣氏", "康是美", "寶雅"] if brand_preference == "全都要" else [brand_preference]
161
+ search_radius = 3000
162
+
163
+ try:
164
+ line_bot_api.reply_message(event.reply_token, TextSendMessage(text="🧚‍♀️ 精靈正在為您精準定位各品牌門市,請稍候..."))
165
+
166
+ nearest_stores = []
167
+
168
+ for brand in brands_to_search:
169
+ 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}"
170
+ response = requests.get(url).json()
171
+
172
+ if response['status'] == 'OK' and response.get('results'):
173
+ brand_stores = []
174
+ for place in response['results']:
175
+ store_lat = place['geometry']['location']['lat']
176
+ store_lon = place['geometry']['location']['lng']
177
+ distance = calculate_distance(user_lat, user_lon, store_lat, store_lon)
178
+
179
+ photo_ref = ""
180
+ if 'photos' in place and len(place['photos']) > 0:
181
+ photo_ref = place['photos'][0]['photo_reference']
182
+
183
+ open_now = place.get('opening_hours', {}).get('open_now')
184
+ if open_now is True:
185
+ status_light = "🟢 營業中"
186
+ elif open_now is False:
187
+ status_light = "🔴 休息中"
188
+ else:
189
+ status_light = "🕒 營業狀態"
190
+
191
+ brand_stores.append({
192
+ "place_id": place.get('place_id'),
193
+ "name": place.get('name', '未知名稱'),
194
+ "rating": place.get('rating', '無評分'),
195
+ "lat": store_lat,
196
+ "lon": store_lon,
197
+ "distance": distance,
198
+ "address": place.get('formatted_address', ''),
199
+ "status_light": status_light,
200
+ "photo_ref": photo_ref
201
+ })
202
+
203
+ sorted_brand_stores = sorted(brand_stores, key=lambda x: x['distance'])
204
 
205
+ if brand_preference == "全都要":
206
+ if sorted_brand_stores:
207
+ nearest_stores.append(sorted_brand_stores[0])
208
+ else:
209
+ nearest_stores.extend(sorted_brand_stores[:3])
210
+
211
+ if not nearest_stores:
212
+ line_bot_api.push_message(user_id, TextSendMessage(text="🥺 哎呀,精靈在您附近 3 公里內都找不到藥妝店呢..."))
213
+ if user_id in user_states: del user_states[user_id]
214
+ return
215
+
216
+ nearest_stores = sorted(nearest_stores, key=lambda x: x['distance'])
217
+
218
+ # 準備製作符合高級風格的 Flex Message 卡片
219
+ bubbles = []
220
+ for store in nearest_stores:
221
+ exact_hours = get_exact_hours(store['place_id'], google_api_key)
222
+ display_hours = f"{store['status_light']} ({exact_hours})"
223
+
224
+ map_query = urllib.parse.quote(f"{store['name']} {store['address']}")
225
+ map_url = f"https://www.google.com/maps/search/?api=1&query={map_query}"
226
+
227
+ if store['photo_ref']:
228
+ image_url = f"https://maps.googleapis.com/maps/api/place/photo?maxwidth=600&photo_reference={store['photo_ref']}&key={google_api_key}"
229
+ else:
230
+ image_url = "https://images.unsplash.com/photo-1576426863848-c21f53c60b19?q=80&w=600&auto=format&fit=crop"
231
+
232
+ # ✅ 修正二:乾淨、排版漂亮且結構完全正確的 card 字典
233
+ card = {
234
+ "type": "bubble",
235
+ "size": "mega",
236
+ "hero": {
237
+ "type": "image",
238
+ "url": image_url,
239
+ "size": "full",
240
+ "aspectRatio": "16:9",
241
+ "aspectMode": "cover"
242
+ },
243
+ "body": {
244
  "type": "box",
245
  "layout": "vertical",
 
 
246
  "contents": [
247
+ {"type": "text", "text": store['name'], "weight": "bold", "size": "xl", "wrap": True, "color": "#111111"},
248
  {
249
  "type": "box",
250
+ "layout": "vertical",
251
+ "spacing": "sm",
252
+ "margin": "lg",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
253
  "contents": [
254
+ {
255
+ "type": "box",
256
+ "layout": "baseline",
257
+ "spacing": "sm",
258
+ "contents": [
259
+ {"type": "text", "text": "目前距離", "size": "sm", "color": "#8c8c8c", "flex": 3},
260
+ {"type": "text", "text": f"{store['distance']} 公尺", "size": "sm", "color": "#111111", "weight": "bold", "flex": 5}
261
+ ]
262
+ },
263
+ {
264
+ "type": "box",
265
+ "layout": "baseline",
266
+ "spacing": "sm",
267
+ "contents": [
268
+ {"type": "text", "text": "營業時間", "size": "sm", "color": "#8c8c8c", "flex": 3},
269
+ {"type": "text", "text": display_hours, "size": "sm", "color": "#111111", "weight": "bold", "flex": 5, "wrap": True}
270
+ ]
271
+ },
272
+ {
273
+ "type": "box",
274
+ "layout": "baseline",
275
+ "spacing": "sm",
276
+ "contents": [
277
+ {"type": "text", "text": "店家評分", "size": "sm", "color": "#8c8c8c", "flex": 3},
278
+ {"type": "text", "text": f"⭐ {store['rating']}", "size": "sm", "color": "#fbbc04", "weight": "bold", "flex": 5}
279
+ ]
280
+ }
281
  ]
282
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
283
  ]
284
+ },
285
+ "footer": {
286
  "type": "box",
287
  "layout": "vertical",
288
  "contents": [
289
+ {
290
+ "type": "button",
291
+ "style": "primary",
292
+ "color": "#000000",
293
+ "height": "md",
294
+ "action": {
295
+ "type": "uri",
296
+ "label": "開啟地圖導航",
297
+ "uri": map_url
298
+ }
299
  }
 
300
  ]
 
301
  }
302
+ }
303
+ bubbles.append(card)
304
+
305
+ flex_message = FlexSendMessage.new_from_json_dict({
306
+ "type": "flex",
307
+ "alt_text": "🔍 附近的藥妝店地圖出爐囉!",
308
+ "contents": {
309
+ "type": "carousel",
310
+ "contents": bubbles
311
+ }
312
+ })
313
+
314
+ line_bot_api.push_message(user_id, flex_message)
315
+ if user_id in user_states: del user_states[user_id]
316
+
317
+ except Exception as e:
318
+ line_bot_api.push_message(user_id, TextSendMessage(text=f"❌ 搜尋過程發生錯誤:{e}"))
319
+ if user_id in user_states: del user_states[user_id]
320
 
321
+ # ================= 【新增】專門處理使用者傳送位置的邏輯 =================
322
+ # ================= 【升級版】全台通用的真實附近店家搜尋 =================
323
+ # ================= 【終極版】客製化品牌 + 營業時間 + Flex 卡片 =================
324
+ # ================= 【極致美學版】客製化品牌 + 店家美照 + 精確時間 + 曜石黑圖卡 =================
325
+ # ================= 【霸氣點名版】保證全品牌上榜 + 曜石黑完美排版 =================
326
  @handler.add(MessageEvent, message=LocationMessage)
327
  def handle_location(event):
328
  user_id = event.source.user_id
 
332
  state = user_states.get(user_id, {})
333
  brand_preference = state.get("search_brand", "全都要")
334
 
335
+ google_api_key = os.environ.get('Maps_API_KEY')
 
336
  if not google_api_key:
337
  line_bot_api.reply_message(event.reply_token, TextSendMessage(text="❌ 系統尚未設定 Google Maps API 金鑰。"))
338
  return
339
 
340
+ # 計算距離的數學魔法
341
  def calculate_distance(lat1, lon1, lat2, lon2):
342
+ import math
343
  R = 6371.0
344
  lat1_rad, lon1_rad = math.radians(lat1), math.radians(lon1)
345
  lat2_rad, lon2_rad = math.radians(lat2), math.radians(lon2)
 
349
  c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
350
  return int(R * c * 1000)
351
 
352
+ # 查詢「今天確切營業時間」的魔法
353
  def get_exact_hours(place_id, api_key):
354
  url = f"https://maps.googleapis.com/maps/api/place/details/json?place_id={place_id}&fields=opening_hours&language=zh-TW&key={api_key}"
355
  try:
 
357
  if 'result' in res and 'opening_hours' in res['result']:
358
  weekday_text = res['result']['opening_hours'].get('weekday_text', [])
359
  if weekday_text:
360
+ from datetime import datetime, timezone, timedelta
361
  tw_tz = timezone(timedelta(hours=8))
362
  today_idx = datetime.now(tw_tz).weekday()
363
  today_str = weekday_text[today_idx]
 
367
  pass
368
  return "詳情請洽門市"
369
 
370
+ # 🌟 核心改寫:決定要搜尋的名單。全都要就分開查 3 次!
371
  brands_to_search = ["屈臣氏", "康是美", "寶雅"] if brand_preference == "全都要" else [brand_preference]
372
+ search_radius = 3000 # 稍微擴大到 3 公里,確保偏遠的寶雅也能抓到
373
 
374
  try:
375
  line_bot_api.reply_message(event.reply_token, TextSendMessage(text="🧚‍♀️ 精靈正在為您精準定位各品牌門市,請稍候..."))
376
 
377
  nearest_stores = []
378
 
379
+ # 🌟 針對每個品牌,獨立逼迫 Google 交出名單
380
  for brand in brands_to_search:
381
  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}"
382
  response = requests.get(url).json()
 
412
  "photo_ref": photo_ref
413
  })
414
 
415
+ # 依距離排序這個品牌的所有門市
416
  sorted_brand_stores = sorted(brand_stores, key=lambda x: x['distance'])
417
 
418
  if brand_preference == "全都要":
419
+ # 只抓該品牌離妳最近的「唯一代表」加入總榜單
420
+ nearest_stores.append(sorted_brand_stores[0])
421
  else:
422
+ # 如果只搜一家,就給最近的 3 個選項
423
  nearest_stores.extend(sorted_brand_stores[:3])
424
 
425
  if not nearest_stores:
 
427
  if user_id in user_states: del user_states[user_id]
428
  return
429
 
430
+ # 🌟 總榜單出爐後,依照這幾家店的距離再排序一次
431
  nearest_stores = sorted(nearest_stores, key=lambda x: x['distance'])
432
 
433
+ # 準備製作符合 Toyota 高級風格的 Flex Message 卡片
434
  bubbles = []
435
  for store in nearest_stores:
436
  exact_hours = get_exact_hours(store['place_id'], google_api_key)
 
444
  else:
445
  image_url = "https://images.unsplash.com/photo-1576426863848-c21f53c60b19?q=80&w=600&auto=format&fit=crop"
446
 
447
+ # ✅ 修正 2:乾淨俐落、不重複嵌套的規格表!
448
  card = {
449
  "type": "bubble",
450
  "size": "mega",
 
525
  "contents": bubbles
526
  }
527
  })
 
 
 
 
 
 
 
528
 
529
+ # ================= 3. 多輪對話核心邏輯 =================
530
  @handler.add(MessageEvent, message=TextMessage)
531
  def handle_message(event):
532
  user_id = event.source.user_id
533
  user_msg = event.message.text.strip()
534
 
535
+ # ================= 【升級】附近店家 - 步驟 1:選擇品牌 =================
536
  if user_msg == "附近店家":
537
+ user_states[user_id] = {"step": "ASK_BRAND"} # 記錄用戶進入找店流程
538
 
539
  brand_buttons = QuickReply(items=[
540
  QuickReplyButton(action=MessageAction(label="屈臣氏", text="屈臣氏")),
 
545
 
546
  line_bot_api.reply_message(
547
  event.reply_token,
548
+ TextSendMessage(
549
+ text="請問你想尋找哪一間藥妝店呢?",
550
+ quick_reply=brand_buttons
551
+ )
552
  )
553
  return
554
 
555
+ # ================= 【升級】附近店家 - 步驟 2:要求定位 =================
556
  state = user_states.get(user_id)
557
  if state and state.get("step") == "ASK_BRAND":
558
  if user_msg in ["屈臣氏", "康是美", "寶雅", "全都要"]:
559
+ state["search_brand"] = user_msg # 把使用者選的品牌存起來
560
+ state["step"] = "ASK_LOCATION" # 推進到下一步
561
 
562
  location_button = QuickReply(items=[
563
  QuickReplyButton(action=LocationAction(label="📍 點我分享位置"))
 
565
 
566
  line_bot_api.reply_message(
567
  event.reply_token,
568
+ TextSendMessage(
569
+ text=f"沒問題!請分享您的位置,精靈來幫您找最近的【{user_msg}】!",
570
+ quick_reply=location_button
571
+ )
572
  )
573
  else:
574
  line_bot_api.reply_message(event.reply_token, TextSendMessage(text="請點擊下方的按鈕選擇品牌喔!"))
 
578
  if user_msg == "精靈顧問" or user_msg == "重新開始":
579
  user_states[user_id] = {"step": "ASK_SKIN"}
580
 
581
+ # 建立第一步:膚質按鈕
582
  skin_buttons = QuickReply(items=[
583
  QuickReplyButton(action=MessageAction(label="乾性肌", text="乾性肌")),
584
  QuickReplyButton(action=MessageAction(label="油性肌", text="油性肌")),
 
600
  )
601
  return
602
 
603
+ # 取得該用戶當前的對話狀態
604
  state = user_states.get(user_id)
605
+
606
+ # 如果用戶不在諮詢流程中,輸入其他字則做基本回應
607
  if not state:
608
  line_bot_api.reply_message(
609
  event.reply_token,
 
613
 
614
  current_step = state.get("step")
615
 
616
+ # 【步驟一】處理膚質選擇 -> 進入詢問困擾
617
  if current_step == "ASK_SKIN":
618
  if user_msg in ["乾性肌", "油性肌", "混合肌", "敏感肌"]:
619
+ state["skin_type"] = user_msg # 記錄膚質
620
+ state["step"] = "ASK_ISSUE" # 進到下一步
621
 
622
+ # 建立第二步:困擾按鈕
623
  issue_buttons = QuickReply(items=[
624
  QuickReplyButton(action=MessageAction(label="嚴重脫皮", text="嚴重脫皮")),
625
  QuickReplyButton(action=MessageAction(label="暗沉無光", text="暗沉無光")),
 
634
  else:
635
  line_bot_api.reply_message(event.reply_token, TextSendMessage(text="請選擇下方的膚質選項,或是輸入「重新開始」喔!"))
636
 
637
+ # 【步驟二】處理困擾選擇 -> 進入詢問偏好
638
  elif current_step == "ASK_ISSUE":
639
  if user_msg in ["嚴重脫皮", "暗沉無光", "痘痘粉刺", "泛紅乾癢"]:
640
+ state["issue"] = user_msg # 記錄困擾
641
+ state["step"] = "ASK_PREFERENCE" # 進到下一步
642
 
643
+ # 建立第三步:偏好按鈕
644
  pref_buttons = QuickReply(items=[
645
  QuickReplyButton(action=MessageAction(label="偏好清爽凝露", text="偏好清爽凝露")),
646
  QuickReplyButton(action=MessageAction(label="偏好滋潤乳霜", text="偏好滋潤乳霜")),
 
654
  else:
655
  line_bot_api.reply_message(event.reply_token, TextSendMessage(text="請選擇下方的肌膚問題選項,或是輸入「重新開始」喔!"))
656
 
657
+ # 【步驟三】處理偏好選擇 -> 產生最終建議並結束
658
  elif current_step == "ASK_PREFERENCE":
659
  if user_msg in ["偏好清爽凝露", "偏好滋潤乳霜", "都可以"]:
660
  skin_type = state["skin_type"]
661
  issue = state["issue"]
662
  preference = user_msg
663
 
664
+ # 先回傳一個「思考中」的提示,因為呼叫 AI 需要 1~3 秒
665
  line_bot_api.reply_message(
666
  event.reply_token,
667
  TextSendMessage(text="🧚‍♀️ 精靈正在為您翻閱保養圖鑑,請稍候幾秒鐘...")
668
  )
669
 
670
+ # --- 開始呼叫 AI 產生客製化建議 ---
671
  prompt = f"""
672
  你現在是一位「台灣藥妝店皮膚科精靈顧問🧚‍♀️」,擁有10年以上保養諮詢經驗。
673
+
674
+ 使用者資料
675
+
676
+ * 膚質:{skin_type}
677
+ * 困擾:{issue}
678
+ * 偏好{preference}
679
+
680
+ 請根據以上資訊提供個人化保養建議。
681
+
682
+ 【輸出規則】
683
+
684
+ 1. 回覆控制在 180~250 字。
685
+ 2. 語氣溫柔、親切,像專業美容顧問。
686
+ 3. 使用繁體中文。
687
+ 4. 不要使用表格。
688
+ 5. 不要輸出任何前言、結語、免責聲明。
689
+ 6. 不要出現「以下是分析報告」、「分析結果」等字樣。
690
+ 7. 不要使用分隔線(────)。
691
+ 8. 必須完全依照下列格式輸出:
692
+
693
+ 🧚‍♀️ 哈囉親愛的!
694
+
695
+ 🩺 精靈診斷
696
+ (用1~2句話分析目前膚況,需根據使用者的膚質與困擾客製化)
697
+
698
+ 💡 保養建議
699
+ • (具體建議1)
700
+ • (具體建議2)
701
+ • (具體建議3)
702
+
703
+ 🛍️ 推薦產品
704
+
705
+ ① (完整品牌+產品名稱)
706
+ ⭐ (20字內推薦原因)
707
+
708
+ ② (完整品牌+產品名稱)
709
+ ⭐ (20字內推薦原因)
710
+
711
+ ③ (完整品牌+產品名稱)
712
+ ⭐ (20字內推薦原因)
713
+
714
+ 【產品規則】
715
+ 【精靈診斷輸出規則】
716
+
717
+ * 用 1~2 句話描述即可。
718
+ * 必須根據「膚質 + 困擾」客製化,不可套話。
719
+ * 要說明目前肌膚「正在發生的狀況」或「核心問題」。
720
+ * 可以包含簡單原因(例如:缺水、屏障受損、油水平衡失調)。
721
+ * 語氣溫柔、像專業皮膚顧問,但不要太長。
722
+
723
+ ✅ 範例風格:
724
+ 你的肌膚目前偏乾且屏障較弱,因此出現脫皮與緊繃感。
725
+ 油脂分泌較旺盛,毛孔堵塞導致粉刺與痘痘問題。
726
+ 肌膚處於敏感狀態,容易泛紅與乾癢。
727
+
728
+ 【保養建議輸出規則】
729
+
730
+ * 輸出 3 點條列(•)
731
+ * 每點格式固定:
732
+ * 主動作要具體(如:溫和清潔、加強保濕、避免去角質)
733
+ * 原因控制在 1 句,不要太長
734
+ * 可提及膚況關鍵字(缺水、油脂、屏障、泛紅等)
735
+ * 語氣專業但簡潔,不要教科書感
736
+
737
+ ✅ 範例格式:
738
+
739
+ • 溫和清潔: 使用低刺激洗面乳,避免過度清潔導致乾燥與刺激
740
+ • 立即保濕:洗臉後立刻補水,幫助鎖住水分改善脫皮
741
+ • 避免去角質: 減少刺激性保養,讓肌膚屏障有時間修復
742
+
743
+ ❌ 不要太長解釋
744
+ ❌ 不要超過 1 句原因
745
+ ❌ 不要使用專業論文語氣
746
+
747
+
748
+ 【產品推薦規則】
749
+
750
+ * 推薦 3 項產品即可。
751
+ * 必須是台灣藥妝店可購買的真實商品。
752
+ * 每項產品需包含:
753
+ 1. 完整品牌與品名
754
+ 2. 1句推薦原因
755
+ * 推薦原因控制在 25~50 字。
756
+ * 可簡單提及 1~2 個關鍵成分(例如:玻尿酸、神經醯胺、積雪草、維他命B5、菸鹼醯胺等)。
757
+ * 說明該成分對使用者膚況的幫助。
758
+ * 不要介紹品牌歷史。
759
+ * 不要描述過多技術名詞。
760
+ * 不要超過五句話。
761
+
762
+
763
+ 【範例風格】
764
+
765
+ 🧚‍♀️ 哈囉親愛的!
766
+
767
+ 🩺 精靈診斷
768
+ 你的肌膚目前有明顯缺水現象,因此出現脫皮狀況。
769
+
770
+ 💡 保養建議
771
+ • 洗臉後立即補充保濕產品
772
+ • 暫停去角質與酸類保養
773
+ • 選擇溫和產品加強修護
774
+
775
+ 🛍️ 推薦產品
776
+
777
+ 推薦原因請採用以下風格:
778
+
779
+ ❌ 過短:
780
+ ⭐ 保濕效果佳
781
+
782
+ ❌ 過長:
783
+ ⭐ 採用獨家智慧型玻尿酸水循環科技...
784
+
785
+ ✅ 理想格式:
786
+ ⭐ 含多重玻尿酸,能加強補水並改善乾燥脫皮。
787
+
788
+ ✅ 理想格式:
789
+ ⭐ 添加神經醯胺,有助修護肌膚屏障並減少乾燥。
790
+
791
+ ✅ 理想格式:
792
+ ⭐ 含維他命B5與積雪草,可舒緩泛紅與不適感。
793
+
794
+
795
+
796
  """
797
+
798
  try:
799
+ # 傳送 Prompt 給 Gemini
800
  response = model.generate_content(prompt)
801
  ai_reply = response.text
802
+
803
+ # 改用 push_message 主動推播結果
804
  line_bot_api.push_message(user_id, TextSendMessage(text=ai_reply))
805
+
806
  except Exception as e:
807
+ # 🛠️ 這裡做了修改:如果錯誤,直接把真實的錯誤訊息(e)用 LINE 傳給你!
808
+ error_msg = f"❌ 精靈魔法發動失敗!詳細錯誤原因如下:\n\n{e}"
809
+ line_bot_api.push_message(user_id, TextSendMessage(text=error_msg))
810
 
811
+ # 階段性任務完成,清除狀態
812
  del user_states[user_id]
813
  else:
814
  line_bot_api.reply_message(event.reply_token, TextSendMessage(text="請選擇下方的質地偏好選項,或是輸入「重新開始」喔!"))
815
 
816
+
817
+
818
  elif current_step == "ASK_PHOTO_ITEM":
819
+ # 如果使用者沒傳照片,而是傳純文字,就走這條路
820
  text_prompt = f"請幫我查詢「{user_msg}」這款產品在台灣藥妝店的合理售價範圍與推薦購買商家。請簡單溫柔地回答,並條列出品名、價格、商家。"
821
  try:
822
+ # 先回傳提示
823
  line_bot_api.reply_message(event.reply_token, TextSendMessage(text="✨ 收到文字!精靈正在幫您查詢這款產品,請稍候..."))
824
+
825
+ # 呼叫 AI
826
  response = model.generate_content(text_prompt)
827
+
828
+ # 用 push_message 推播結果
829
  line_bot_api.push_message(user_id, TextSendMessage(text=f"🧚‍♀️ 查詢成功!\n\n{response.text}"))
830
+
831
  except Exception as e:
832
  line_bot_api.push_message(user_id, TextSendMessage(text=f"❌ 查詢失敗,錯誤原因:\n{e}"))
833
+
834
+ # 任務完成,清除狀態
835
  del user_states[user_id]
836
 
837
  if __name__ == "__main__":