Files
ewoooc/config.py
ogt b9fe98f591 refactor: centralize config — HERMES_URL, SSH params, validate_critical_config()
- config.py: add HERMES_URL (default 192.168.0.111:11434), SSH jump params, validate_critical_config()
- services/hermes_analyst_service.py: remove hardcoded HERMES_URL, import from config
- app.py: call validate_critical_config() on startup, log warnings for optional missing vars

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 21:27:47 +08:00

259 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import os
import json
from dotenv import load_dotenv
# 載入 .env 環境變數
load_dotenv()
# 基本路徑設定
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
DATA_DIR = os.path.join(BASE_DIR, 'data')
LOG_DIR = os.path.join(BASE_DIR, 'logs')
UPLOAD_FOLDER = os.path.join(BASE_DIR, 'web/static/uploads')
# 建立必要目錄
for d in [DATA_DIR, LOG_DIR, UPLOAD_FOLDER]:
os.makedirs(d, exist_ok=True)
# ==========================================
# 資料庫設定
# ==========================================
# 支援 SQLite 和 PostgreSQL 兩種資料庫
# 預設使用 SQLite (本地開發),可透過環境變數切換到 PostgreSQL
USE_POSTGRESQL = os.getenv('USE_POSTGRESQL', 'false').lower() == 'true'
if USE_POSTGRESQL:
# PostgreSQL 連線設定
POSTGRES_HOST = os.getenv('POSTGRES_HOST', 'momo-postgres')
POSTGRES_PORT = os.getenv('POSTGRES_PORT', '5432')
POSTGRES_USER = os.getenv('POSTGRES_USER', 'momo')
POSTGRES_PASSWORD = os.getenv('POSTGRES_PASSWORD')
POSTGRES_DB = os.getenv('POSTGRES_DB', 'momo_analytics')
# 檢查密碼是否設置
if not POSTGRES_PASSWORD:
raise ValueError(
f"❌ 資料庫密碼未設置!請檢查:\n"
f"1. .env 檔案中是否設置了 POSTGRES_PASSWORD\n"
f"2. Docker Compose 環境變數是否正確傳遞\n"
f"3. 容器重啟後環境變數是否生效\n"
f"當前環境變數USE_POSTGRESQL=true, POSTGRES_HOST={POSTGRES_HOST}, POSTGRES_USER={POSTGRES_USER}"
)
DATABASE_PATH = f"postgresql://{POSTGRES_USER}:{POSTGRES_PASSWORD}@{POSTGRES_HOST}:{POSTGRES_PORT}/{POSTGRES_DB}"
DATABASE_TYPE = 'postgresql'
else:
# SQLite 連線設定 (開發環境或備用)
DATABASE_PATH = f"sqlite:///{os.path.join(DATA_DIR, 'momo_database.db')}"
DATABASE_TYPE = 'sqlite'
# ==========================================
# 安全設定(從環境變數讀取)
# ==========================================
import sys as _sys
LOGIN_PASSWORD = os.getenv('LOGIN_PASSWORD')
if not LOGIN_PASSWORD:
print("[FATAL] LOGIN_PASSWORD 環境變數未設定,拒絕啟動。請在 .env 或 Docker 環境設定此值。", file=_sys.stderr)
_sys.exit(1)
SECRET_KEY = os.getenv('SECRET_KEY')
if not SECRET_KEY or SECRET_KEY in ('your_flask_secret_key', 'change_me', ''):
print("[FATAL] SECRET_KEY 環境變數未設定或仍為不安全預設值拒絕啟動。請執行python3 -c \"import secrets; print(secrets.token_hex(32))\" 產生安全金鑰。", file=_sys.stderr)
_sys.exit(1)
# ==========================================
# 通訊模組設定(從環境變數讀取)
# ==========================================
# --- Telegram Bot ---
TELEGRAM_BOT_TOKEN = os.getenv('TELEGRAM_BOT_TOKEN', '')
try:
TELEGRAM_CHAT_IDS = json.loads(os.getenv('TELEGRAM_CHAT_IDS', '[]'))
except json.JSONDecodeError:
TELEGRAM_CHAT_IDS = []
# --- Line Notify ---
LINE_ENABLED = os.getenv('LINE_ENABLED', 'false').lower() == 'true' # 預設關閉
LINE_CHANNEL_ACCESS_TOKEN = os.getenv('LINE_CHANNEL_ACCESS_TOKEN', '')
LINE_GROUP_ID = os.getenv('LINE_GROUP_ID', '')
# --- Email (SMTP) ---
EMAIL_HOST = os.getenv('EMAIL_HOST', 'smtp.gmail.com')
EMAIL_PORT = int(os.getenv('EMAIL_PORT', '587'))
EMAIL_HOST_USER = os.getenv('EMAIL_HOST_USER', '')
EMAIL_HOST_PASSWORD = os.getenv('EMAIL_HOST_PASSWORD', '') # 注意:若使用 Gmail需設定「應用程式密碼」
EMAIL_SENDER = os.getenv('EMAIL_SENDER', '')
EMAIL_RECEIVER = os.getenv('EMAIL_RECEIVER', '')
# ==========================================
# 網路設定(從環境變數讀取)
# ==========================================
PUBLIC_URL = os.getenv('PUBLIC_URL', 'https://mo.wooo.work')
# 補上 EXCEL_EXPORT_DIR 定義
EXCEL_EXPORT_DIR = os.path.join(DATA_DIR, 'excel_exports')
# 更新建立目錄清單 (確保系統會自動建立這個資料夾)
for d in [DATA_DIR, LOG_DIR, UPLOAD_FOLDER, EXCEL_EXPORT_DIR]:
os.makedirs(d, exist_ok=True)
# MOMO 分類清單 (從 JSON 檔案動態讀取)
def load_momo_categories():
import json
import time
categories_path = os.path.join(DATA_DIR, 'categories.json')
# 預設分類,用於首次啟動或檔案遺失時
default_categories = [
{"name": "保養超值組", "url": "https://www.momoshop.com.tw/category/DgrpCategory.jsp?d_code=1111700012"},
{"name": "化妝水", "url": "https://www.momoshop.com.tw/category/DgrpCategory.jsp?d_code=1111700001"},
{"name": "精華液", "url": "https://www.momoshop.com.tw/category/DgrpCategory.jsp?d_code=1111700002&p_orderType=4&showType=chessboardType"},
{"name": "乳液", "url": "https://www.momoshop.com.tw/category/DgrpCategory.jsp?d_code=1111700003&p_orderType=4&showType=chessboardType"},
{"name": "乳霜", "url": "https://www.momoshop.com.tw/category/DgrpCategory.jsp?d_code=1111700004&p_orderType=4&showType=chessboardType"},
{"name": "凝膠", "url": "https://www.momoshop.com.tw/category/DgrpCategory.jsp?d_code=1111700005&p_orderType=4&showType=chessboardType"},
{"name": "面膜", "url": "https://www.momoshop.com.tw/category/DgrpCategory.jsp?d_code=1111700006&p_orderType=4&showType=chessboardType"},
{"name": "眼霜", "url": "https://www.momoshop.com.tw/category/DgrpCategory.jsp?d_code=1111700007&p_orderType=4&showType=chessboardType"},
{"name": "護唇膏", "url": "https://www.momoshop.com.tw/category/DgrpCategory.jsp?d_code=1111700008&p_orderType=4&showType=chessboardType"},
{"name": "防曬", "url": "https://www.momoshop.com.tw/category/DgrpCategory.jsp?d_code=1111700009&p_orderType=4&showType=chessboardType"},
{"name": "素顏霜", "url": "https://www.momoshop.com.tw/category/DgrpCategory.jsp?d_code=1111700010&p_orderType=4&showType=chessboardType"},
{"name": "美顏霜", "url": "https://www.momoshop.com.tw/category/DgrpCategory.jsp?d_code=1111700011&p_orderType=4&showType=chessboardType"},
{"name": "身體護理", "url": "https://www.momoshop.com.tw/category/MgrpCategory.jsp?m_code=1100901499&sourcePageType=4"},
{"name": "手部保養", "url": "https://www.momoshop.com.tw/category/MgrpCategory.jsp?m_code=1100901655&sourcePageType=4"},
{"name": "足部保養", "url": "https://www.momoshop.com.tw/category/MgrpCategory.jsp?m_code=1100901656&sourcePageType=4"},
{"name": "局部護理", "url": "https://www.momoshop.com.tw/category/MgrpCategory.jsp?m_code=1100901505&sourcePageType=4"},
{"name": "止汗體香", "url": "https://www.momoshop.com.tw/category/MgrpCategory.jsp?m_code=1100901503&sourcePageType=4"},
{"name": "嬰幼身體保養品牌旗艦", "url": "https://www.momoshop.com.tw/category/MgrpCategory.jsp?m_code=1100901724&sourcePageType=4"},
{"name": "嬰幼本月主打", "url": "https://www.momoshop.com.tw/category/MgrpCategory.jsp?m_code=2705200081&sourcePageType=4"},
{"name": "嬰幼清潔用品", "url": "https://www.momoshop.com.tw/category/MgrpCategory.jsp?m_code=2705200349&p_orderType=4&showType=chessboardType"},
{"name": "嬰幼保養護膚", "url": "https://www.momoshop.com.tw/category/MgrpCategory.jsp?m_code=2705200350&p_orderType=4&showType=chessboardType"},
{"name": "媽咪孕期保養", "url": "https://www.momoshop.com.tw/category/MgrpCategory.jsp?m_code=2705200352&p_orderType=4&showType=chessboardType"},
{"name": "送禮超值組", "url": "https://www.momoshop.com.tw/category/MgrpCategory.jsp?m_code=2705200348&p_orderType=4&showType=chessboardType"},
{"name": "嬰幼品牌總覽", "url": "https://www.momoshop.com.tw/category/MgrpCategory.jsp?m_code=2705200348&p_orderType=4&showType=chessboardType"},
{"name": "私密保養本月主打", "url": "https://www.momoshop.com.tw/category/MgrpCategory.jsp?m_code=1105900060&sourcePageType=4"},
{"name": "私密保養", "url": "https://www.momoshop.com.tw/category/MgrpCategory.jsp?m_code=1105900002&sourcePageType=4"},
{"name": "私密清潔", "url": "https://www.momoshop.com.tw/category/MgrpCategory.jsp?m_code=1105900003&sourcePageType=4"},
{"name": "除毛", "url": "https://www.momoshop.com.tw/category/MgrpCategory.jsp?m_code=1105900154&sourcePageType=4"},
{"name": "私密保養推薦品牌", "url": "https://www.momoshop.com.tw/category/MgrpCategory.jsp?m_code=1105900023&sourcePageType=4"}
]
if not os.path.exists(categories_path):
# 如果檔案不存在,使用預設值並為每項加上 ID 後建立新檔案
categories_with_id = []
for i, cat in enumerate(default_categories):
cat['id'] = int(time.time() * 1000) + i
categories_with_id.append(cat)
try:
with open(categories_path, 'w', encoding='utf-8') as f:
json.dump(categories_with_id, f, ensure_ascii=False, indent=4)
return categories_with_id
except Exception as e:
print(f"Error creating categories.json: {e}")
return default_categories
try:
# V-Fix: 檢查檔案大小,如果過大 (例如超過 1MB) 則視為異常,直接使用預設值,避免讀取卡死
if os.path.exists(categories_path):
try:
if os.path.getsize(categories_path) > 1024 * 1024:
print(f"⚠️ Warning: categories.json is too large ({os.path.getsize(categories_path)} bytes). Using defaults.")
return default_categories
except OSError:
return default_categories
with open(categories_path, 'r', encoding='utf-8') as f:
try:
content = f.read().strip()
except Exception:
return default_categories
if not content:
return default_categories
data = json.loads(content)
# 若讀取到的列表為空,則回傳預設值 (修正空檔案導致爬蟲不工作的問題)
if not data:
return default_categories
return data
except (json.JSONDecodeError, FileNotFoundError, OSError):
# 如果檔案損毀或讀取失敗,回傳預設值
return default_categories
MOMO_CATEGORIES = load_momo_categories()
# ==========================================
# 密碼安全設定
# ==========================================
PASSWORD_MIN_LENGTH = int(os.getenv('PASSWORD_MIN_LENGTH', '8'))
PASSWORD_REQUIRE_UPPERCASE = os.getenv('PASSWORD_REQUIRE_UPPERCASE', 'true').lower() == 'true'
PASSWORD_REQUIRE_LOWERCASE = os.getenv('PASSWORD_REQUIRE_LOWERCASE', 'true').lower() == 'true'
PASSWORD_REQUIRE_DIGIT = os.getenv('PASSWORD_REQUIRE_DIGIT', 'true').lower() == 'true'
PASSWORD_REQUIRE_SPECIAL = os.getenv('PASSWORD_REQUIRE_SPECIAL', 'false').lower() == 'true'
PASSWORD_SPECIAL_CHARS = os.getenv('PASSWORD_SPECIAL_CHARS', '!@#$%^&*()_+-=[]{}|;:,.<>?')
PASSWORD_EXPIRY_DAYS = int(os.getenv('PASSWORD_EXPIRY_DAYS', '90'))
# ==========================================
# 外部服務連結 (分析報表選單)
# ==========================================
METABASE_URL = os.getenv('METABASE_URL', '') # Metabase BI 連結
GRIST_URL = os.getenv('GRIST_URL', '') # Grist 資料協作連結
# ==========================================
# AI 服務設定
# ==========================================
# Hermes AI Service (競價情報分析)
HERMES_URL = os.getenv('HERMES_URL', 'http://192.168.0.111:11434')
# SSH Jump Configuration (AIOps AutoHeal)
SSH_JUMP_HOST = os.getenv('SSH_JUMP_HOST', '192.168.0.110')
SSH_JUMP_USER = os.getenv('SSH_JUMP_USER', 'wooo')
SSH_TARGET_HOST = os.getenv('SSH_TARGET_HOST', '192.168.0.188')
SSH_TARGET_USER = os.getenv('SSH_TARGET_USER', 'ollama')
# Ollama 本地 AI 服務
OLLAMA_HOST = os.getenv('OLLAMA_HOST', 'https://ollama.wooo.work/ollama')
OLLAMA_MODEL = os.getenv('OLLAMA_MODEL', 'gemma3:4b')
# Google Gemini AI 雲端服務
GEMINI_API_KEY = os.getenv('GEMINI_API_KEY', '')
GEMINI_MODEL = os.getenv('GEMINI_MODEL', 'gemini-1.5-flash')
# 預設 AI 提供者: 'ollama' (本地免費) 或 'gemini' (雲端付費)
AI_PROVIDER = os.getenv('AI_PROVIDER', 'ollama')
# YouTube API Key (用於趨勢爬蟲)
YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
# ==========================================
# 系統版本與路徑
# ==========================================
SYSTEM_VERSION = "V10.3"
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
public_url = PUBLIC_URL # 用於模板顯示
# ==========================================
# 模組化路由設定
# ==========================================
# 控制是否啟用模組化路由,設為 True 時會自動清理 app.py 中的重複路由
USE_MODULAR_ROUTES = {
'system': True, # 系統設定、日誌、備份
'edm': True, # EDM 與節慶儀表板
'monthly': True, # 月結分析
'dashboard': True, # 首頁商品看板
'daily_sales': True, # 當日業績分析
'api': True, # 通用 API
'export': True, # 匯出功能
'import': True, # 匯入功能
'sales': True, # 業績分析
}
def validate_critical_config():
"""啟動時驗證選用配置,缺少則回傳 warning 清單(非 fatal"""
warnings = []
optional_vars = ['HERMES_URL', 'GOOGLE_DRIVE_CREDENTIALS']
for var in optional_vars:
if not os.getenv(var):
warnings.append(f"[Config] 選用設定 {var} 未設,部分功能可能停用")
return warnings