import logging import os import tempfile import uuid from io import BytesIO import markdown from bs4 import BeautifulSoup from flask import Flask, abort, request, send_from_directory from google import genai from google.genai import types from google.genai.types import Tool, GenerateContentConfig, GoogleSearch from linebot.v3 import WebhookHandler from linebot.v3.exceptions import InvalidSignatureError from linebot.v3.messaging import ( ApiClient, Configuration, ImageMessage, MessagingApi, MessagingApiBlob, ReplyMessageRequest, TextMessage, ) from linebot.v3.webhooks import ( ImageMessageContent, MessageEvent, TextMessageContent, ) from PIL import Image from linebot.v3.webhooks import VideoMessageContent # === 初始化 Google Gemini === GOOGLE_API_KEY = os.environ.get("GOOGLE_API_KEY") client = genai.Client(api_key=GOOGLE_API_KEY) google_search_tool = Tool( google_search=GoogleSearch() ) chat = client.chats.create( model="gemini-2.0-flash", config=GenerateContentConfig( system_instruction=( "你是一位專業健身教練與營養顧問,擁有多年重量訓練與健身飲食指導經驗。" "請使用繁體中文,針對使用者的健身問題提供專業建議,包含動作教學、訓練計畫、飲食建議與常見錯誤修正等。回答請控制在 200 字內。" ), tools=[google_search_tool], response_modalities=["TEXT"], ) ) # === 初始設定 === static_tmp_path = tempfile.gettempdir() os.makedirs(static_tmp_path, exist_ok=True) base_url = os.getenv("SPACE_HOST") app = Flask(__name__) logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") app.logger.setLevel(logging.INFO) channel_secret = os.environ.get("YOUR_CHANNEL_SECRET") channel_access_token = os.environ.get("YOUR_CHANNEL_ACCESS_TOKEN") configuration = Configuration(access_token=channel_access_token) handler = WebhookHandler(channel_secret) def query(payload): response = chat.send_message(message=payload) return response.text[:200] # 回覆字數上限 200 字 @app.route("/images/") def serve_image(filename): return send_from_directory(static_tmp_path, filename) @app.route("/") def home(): return {"message": "Line Webhook Server"} @app.route("/", methods=["POST"]) def callback(): signature = request.headers.get("X-Line-Signature") body = request.get_data(as_text=True) app.logger.info(f"Request body: {body}") try: handler.handle(body, signature) except InvalidSignatureError: app.logger.warning("Invalid signature. Please check channel credentials.") abort(400) return "OK" @handler.add(MessageEvent, message=TextMessageContent) def handle_text_message(event): user_msg = event.message.text.strip() if user_msg == "飲食紀錄": reply = "點選下方連結開始記錄你的飲食 🍱:\nhttps://docs.google.com/forms/d/e/1FAIpQLScrNcLDvfODtj7V0IEmo_MMBUQG1LA3HfvbNsraM4-mQmJlOA/viewform?usp=dialog" elif user_msg == "訓練紀錄": reply = "點選下方連結記錄你的訓練 💪:\nhttps://docs.google.com/forms/d/e/1FAIpQLSc8crIyxQX-YJeaNzM5x1JUWbQA-qoQPiZB9cbqKuLOq3uQpA/viewform" elif user_msg == "運動補給品": reply = "推薦常見健身補給品:乳清蛋白、BCAA、肌酸與電解質飲。點此選購 👉 https://www.myprotein.tw/" elif user_msg.startswith("AI "): prompt = user_msg[3:].strip() try: response = client.models.generate_content( model="gemini-2.0-flash-exp-image-generation", contents=prompt, config=types.GenerateContentConfig(response_modalities=["TEXT", "IMAGE"]), ) for part in response.candidates[0].content.parts: if part.inline_data is not None: image = Image.open(BytesIO(part.inline_data.data)) filename = f"{uuid.uuid4().hex}.png" image_path = os.path.join(static_tmp_path, filename) image.save(image_path) image_url = f"https://{base_url}/images/{filename}" with ApiClient(configuration) as api_client: line_bot_api = MessagingApi(api_client) line_bot_api.reply_message( ReplyMessageRequest( reply_token=event.reply_token, messages=[ImageMessage(original_content_url=image_url, preview_image_url=image_url)], ) ) return except Exception as e: reply = "抱歉,生成圖片時發生錯誤。" else: reply = query(user_msg) with ApiClient(configuration) as api_client: line_bot_api = MessagingApi(api_client) line_bot_api.reply_message( ReplyMessageRequest( reply_token=event.reply_token, messages=[TextMessage(text=reply)] ) )