Spaces:
Sleeping
Sleeping
Commit ·
65e575b
1
Parent(s): 921a78a
late
Browse files- core/config.py +35 -9
- static/frontend/index.html +20 -12
core/config.py
CHANGED
|
@@ -5,6 +5,7 @@ Bloom Ware 統一配置管理中心
|
|
| 5 |
|
| 6 |
import os
|
| 7 |
import json
|
|
|
|
| 8 |
from typing import Optional, Dict, Any
|
| 9 |
from dotenv import load_dotenv
|
| 10 |
|
|
@@ -22,8 +23,9 @@ class Settings:
|
|
| 22 |
# ===== Firebase 配置 =====
|
| 23 |
FIREBASE_PROJECT_ID: str = os.getenv("FIREBASE_PROJECT_ID", "")
|
| 24 |
|
| 25 |
-
# Firebase 憑證:
|
| 26 |
_firebase_creds_json: Optional[str] = os.getenv("FIREBASE_CREDENTIALS_JSON")
|
|
|
|
| 27 |
_firebase_service_account_path: Optional[str] = os.getenv("FIREBASE_SERVICE_ACCOUNT_PATH")
|
| 28 |
|
| 29 |
@classmethod
|
|
@@ -32,23 +34,34 @@ class Settings:
|
|
| 32 |
取得 Firebase 憑證
|
| 33 |
|
| 34 |
優先順序:
|
| 35 |
-
1. 環境變數 FIREBASE_CREDENTIALS_JSON(生產環境,
|
| 36 |
-
2.
|
|
|
|
| 37 |
|
| 38 |
Returns:
|
| 39 |
dict: Firebase Service Account 憑證字典
|
| 40 |
|
| 41 |
Raises:
|
| 42 |
-
ValueError: 當
|
| 43 |
"""
|
|
|
|
| 44 |
if cls._firebase_creds_json:
|
| 45 |
-
# 生產環境:從環境變數讀取完整 JSON 字串
|
| 46 |
try:
|
| 47 |
return json.loads(cls._firebase_creds_json)
|
| 48 |
except json.JSONDecodeError as e:
|
| 49 |
raise ValueError(f"FIREBASE_CREDENTIALS_JSON 格式錯誤: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
elif cls._firebase_service_account_path:
|
| 51 |
-
# 開發環境:從檔案讀取
|
| 52 |
try:
|
| 53 |
with open(cls._firebase_service_account_path, 'r', encoding='utf-8') as f:
|
| 54 |
return json.load(f)
|
|
@@ -56,12 +69,15 @@ class Settings:
|
|
| 56 |
raise ValueError(f"Firebase 憑證檔案不存在: {cls._firebase_service_account_path}")
|
| 57 |
except json.JSONDecodeError as e:
|
| 58 |
raise ValueError(f"Firebase 憑證檔案格式錯誤: {e}")
|
|
|
|
|
|
|
| 59 |
else:
|
| 60 |
raise ValueError(
|
| 61 |
"Firebase 憑證未設定!\n"
|
| 62 |
"請設定以下其中一項:\n"
|
| 63 |
-
"1. FIREBASE_CREDENTIALS_JSON(
|
| 64 |
-
"2.
|
|
|
|
| 65 |
)
|
| 66 |
|
| 67 |
# ===== OpenAI 配置 =====
|
|
@@ -155,7 +171,17 @@ class Settings:
|
|
| 155 |
print(f"環境模式: {cls.ENVIRONMENT}")
|
| 156 |
print(f"是否為生產環境: {cls.IS_PRODUCTION}")
|
| 157 |
print(f"Firebase 專案 ID: {cls.FIREBASE_PROJECT_ID}")
|
| 158 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
print(f"OpenAI 模型: {cls.OPENAI_MODEL}")
|
| 160 |
print(f"OpenAI Timeout: {cls.OPENAI_TIMEOUT}s")
|
| 161 |
print(f"Google OAuth 回調 URI: {cls.GOOGLE_REDIRECT_URI}")
|
|
|
|
| 5 |
|
| 6 |
import os
|
| 7 |
import json
|
| 8 |
+
import base64
|
| 9 |
from typing import Optional, Dict, Any
|
| 10 |
from dotenv import load_dotenv
|
| 11 |
|
|
|
|
| 23 |
# ===== Firebase 配置 =====
|
| 24 |
FIREBASE_PROJECT_ID: str = os.getenv("FIREBASE_PROJECT_ID", "")
|
| 25 |
|
| 26 |
+
# Firebase 憑證:支援三種方式
|
| 27 |
_firebase_creds_json: Optional[str] = os.getenv("FIREBASE_CREDENTIALS_JSON")
|
| 28 |
+
_firebase_creds_base64: Optional[str] = os.getenv("FIREBASE_SERVICE_ACCOUNT_JSON_BASE64")
|
| 29 |
_firebase_service_account_path: Optional[str] = os.getenv("FIREBASE_SERVICE_ACCOUNT_PATH")
|
| 30 |
|
| 31 |
@classmethod
|
|
|
|
| 34 |
取得 Firebase 憑證
|
| 35 |
|
| 36 |
優先順序:
|
| 37 |
+
1. 環境變數 FIREBASE_CREDENTIALS_JSON(生產環境,JSON 字串)
|
| 38 |
+
2. 環境變數 FIREBASE_SERVICE_ACCOUNT_JSON_BASE64(base64 編碼的 JSON)
|
| 39 |
+
3. 檔案路徑 FIREBASE_SERVICE_ACCOUNT_PATH(開發環境)
|
| 40 |
|
| 41 |
Returns:
|
| 42 |
dict: Firebase Service Account 憑證字典
|
| 43 |
|
| 44 |
Raises:
|
| 45 |
+
ValueError: 當所有方式都未設定時
|
| 46 |
"""
|
| 47 |
+
# 方式 1: 直接 JSON 字串
|
| 48 |
if cls._firebase_creds_json:
|
|
|
|
| 49 |
try:
|
| 50 |
return json.loads(cls._firebase_creds_json)
|
| 51 |
except json.JSONDecodeError as e:
|
| 52 |
raise ValueError(f"FIREBASE_CREDENTIALS_JSON 格式錯誤: {e}")
|
| 53 |
+
|
| 54 |
+
# 方式 2: Base64 編碼的 JSON
|
| 55 |
+
elif cls._firebase_creds_base64:
|
| 56 |
+
try:
|
| 57 |
+
decoded_bytes = base64.b64decode(cls._firebase_creds_base64)
|
| 58 |
+
decoded_str = decoded_bytes.decode('utf-8')
|
| 59 |
+
return json.loads(decoded_str)
|
| 60 |
+
except Exception as e:
|
| 61 |
+
raise ValueError(f"FIREBASE_SERVICE_ACCOUNT_JSON_BASE64 解碼失敗: {e}")
|
| 62 |
+
|
| 63 |
+
# 方式 3: 從檔案讀取
|
| 64 |
elif cls._firebase_service_account_path:
|
|
|
|
| 65 |
try:
|
| 66 |
with open(cls._firebase_service_account_path, 'r', encoding='utf-8') as f:
|
| 67 |
return json.load(f)
|
|
|
|
| 69 |
raise ValueError(f"Firebase 憑證檔案不存在: {cls._firebase_service_account_path}")
|
| 70 |
except json.JSONDecodeError as e:
|
| 71 |
raise ValueError(f"Firebase 憑證檔案格式錯誤: {e}")
|
| 72 |
+
|
| 73 |
+
# 三種方式都沒設定
|
| 74 |
else:
|
| 75 |
raise ValueError(
|
| 76 |
"Firebase 憑證未設定!\n"
|
| 77 |
"請設定以下其中一項:\n"
|
| 78 |
+
"1. FIREBASE_CREDENTIALS_JSON(JSON 字串)\n"
|
| 79 |
+
"2. FIREBASE_SERVICE_ACCOUNT_JSON_BASE64(base64 編碼)\n"
|
| 80 |
+
"3. FIREBASE_SERVICE_ACCOUNT_PATH(檔案路徑)"
|
| 81 |
)
|
| 82 |
|
| 83 |
# ===== OpenAI 配置 =====
|
|
|
|
| 171 |
print(f"環境模式: {cls.ENVIRONMENT}")
|
| 172 |
print(f"是否為生產環境: {cls.IS_PRODUCTION}")
|
| 173 |
print(f"Firebase 專案 ID: {cls.FIREBASE_PROJECT_ID}")
|
| 174 |
+
|
| 175 |
+
# 判斷 Firebase 憑證來源
|
| 176 |
+
if cls._firebase_creds_json:
|
| 177 |
+
firebase_source = "環境變數 (JSON)"
|
| 178 |
+
elif cls._firebase_creds_base64:
|
| 179 |
+
firebase_source = "環境變數 (Base64)"
|
| 180 |
+
elif cls._firebase_service_account_path:
|
| 181 |
+
firebase_source = "檔案"
|
| 182 |
+
else:
|
| 183 |
+
firebase_source = "未設定 ❌"
|
| 184 |
+
print(f"Firebase 憑證來源: {firebase_source}")
|
| 185 |
print(f"OpenAI 模型: {cls.OPENAI_MODEL}")
|
| 186 |
print(f"OpenAI Timeout: {cls.OPENAI_TIMEOUT}s")
|
| 187 |
print(f"Google OAuth 回調 URI: {cls.GOOGLE_REDIRECT_URI}")
|
static/frontend/index.html
CHANGED
|
@@ -20,9 +20,12 @@
|
|
| 20 |
color: #1A1A1A;
|
| 21 |
overflow: hidden;
|
| 22 |
-webkit-font-smoothing: antialiased;
|
| 23 |
-
/*
|
| 24 |
-
height: 100vh; /* fallback
|
| 25 |
-
height: 100svh; /*
|
|
|
|
|
|
|
|
|
|
| 26 |
}
|
| 27 |
|
| 28 |
/* === 控制面板 === */
|
|
@@ -119,10 +122,13 @@
|
|
| 119 |
left: 0;
|
| 120 |
right: 0;
|
| 121 |
bottom: 0;
|
|
|
|
| 122 |
width: 100vw;
|
| 123 |
-
width:
|
| 124 |
-
height: 100vh; /* fallback */
|
| 125 |
-
height: 100svh; /*
|
|
|
|
|
|
|
| 126 |
z-index: 100;
|
| 127 |
display: flex;
|
| 128 |
flex-direction: column;
|
|
@@ -1098,9 +1104,9 @@
|
|
| 1098 |
/* ChatWindow 折疊圖標 */
|
| 1099 |
.chat-icon {
|
| 1100 |
position: fixed;
|
| 1101 |
-
/*
|
| 1102 |
-
bottom:
|
| 1103 |
-
left:
|
| 1104 |
z-index: 101;
|
| 1105 |
width: 56px;
|
| 1106 |
height: 56px;
|
|
@@ -1126,8 +1132,9 @@
|
|
| 1126 |
/* 退出按鈕 */
|
| 1127 |
.exit-button {
|
| 1128 |
position: absolute;
|
| 1129 |
-
|
| 1130 |
-
|
|
|
|
| 1131 |
z-index: 50;
|
| 1132 |
padding: 12px 20px;
|
| 1133 |
background: rgba(255, 255, 255, 0.95);
|
|
@@ -1153,7 +1160,8 @@
|
|
| 1153 |
/* 情緒標籤 */
|
| 1154 |
.emotion-indicator {
|
| 1155 |
position: absolute;
|
| 1156 |
-
|
|
|
|
| 1157 |
left: 50%;
|
| 1158 |
transform: translateX(-50%);
|
| 1159 |
z-index: 50;
|
|
|
|
| 20 |
color: #1A1A1A;
|
| 21 |
overflow: hidden;
|
| 22 |
-webkit-font-smoothing: antialiased;
|
| 23 |
+
/* 2025 最佳實踐:多層 fallback 確保相容性 */
|
| 24 |
+
height: 100vh; /* fallback 1: 傳統瀏覽器 */
|
| 25 |
+
height: 100svh; /* fallback 2: 小視窗高度 */
|
| 26 |
+
height: 100dvh; /* 主要方案: 動態視窗高度(iframe 最佳) */
|
| 27 |
+
/* 針對 WebKit 瀏覽器的額外修正 */
|
| 28 |
+
height: -webkit-fill-available;
|
| 29 |
}
|
| 30 |
|
| 31 |
/* === 控制面板 === */
|
|
|
|
| 122 |
left: 0;
|
| 123 |
right: 0;
|
| 124 |
bottom: 0;
|
| 125 |
+
/* 2025 最佳實踐:漸進式增強 */
|
| 126 |
width: 100vw;
|
| 127 |
+
width: 100dvw; /* 動態寬度(iframe 最佳) */
|
| 128 |
+
height: 100vh; /* fallback 1 */
|
| 129 |
+
height: 100svh; /* fallback 2 */
|
| 130 |
+
height: 100dvh; /* 主要方案 */
|
| 131 |
+
height: -webkit-fill-available; /* WebKit 修正 */
|
| 132 |
z-index: 100;
|
| 133 |
display: flex;
|
| 134 |
flex-direction: column;
|
|
|
|
| 1104 |
/* ChatWindow 折疊圖標 */
|
| 1105 |
.chat-icon {
|
| 1106 |
position: fixed;
|
| 1107 |
+
/* iframe 環境適配:使用固定值避免 viewport 計算問題 */
|
| 1108 |
+
bottom: 40px;
|
| 1109 |
+
left: 40px;
|
| 1110 |
z-index: 101;
|
| 1111 |
width: 56px;
|
| 1112 |
height: 56px;
|
|
|
|
| 1132 |
/* 退出按鈕 */
|
| 1133 |
.exit-button {
|
| 1134 |
position: absolute;
|
| 1135 |
+
/* iframe 環境適配:使用固定值 */
|
| 1136 |
+
top: 40px;
|
| 1137 |
+
right: 40px;
|
| 1138 |
z-index: 50;
|
| 1139 |
padding: 12px 20px;
|
| 1140 |
background: rgba(255, 255, 255, 0.95);
|
|
|
|
| 1160 |
/* 情緒標籤 */
|
| 1161 |
.emotion-indicator {
|
| 1162 |
position: absolute;
|
| 1163 |
+
/* iframe 環境適配:使用固定值 */
|
| 1164 |
+
top: 40px;
|
| 1165 |
left: 50%;
|
| 1166 |
transform: translateX(-50%);
|
| 1167 |
z-index: 50;
|