餘震 C-2 局部完成:
- config.py 新增 EMBEDDING_HOST 常數(env: EMBEDDING_HOST → fallback HERMES_URL)
- 原計畫同步修 services/ollama_service.py:515,520 的 hardcoded fallback,
但 origin/main 4349db2 (feat: AiderHeal) 已主動移除整個
generate_embedding() 方法 — rebase 衝突解決時採納 origin 決定(--ours),
不重新引入已被刪除的方法
- IP 殘留 fix 自動隨方法刪除而消失;EMBEDDING_HOST 常數保留於 config 以
供未來若恢復 embedding 路徑時集中化使用
ADR-008 集中化原則仍然完整:所有殘留的 IP 硬編碼已都改為 config 讀取
(services/nemoton_dispatcher_service.py:287 已於前個 commit 處理)。
264 lines
14 KiB
Python
264 lines
14 KiB
Python
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')
|
||
HERMES_TIMEOUT = int(os.getenv('HERMES_TIMEOUT', '120')) # 秒;批量 300 筆預估 ~90s
|
||
|
||
# Embedding 服務(ADR-003 對齊:embedding 走 Hermes 主機,內網免認證)
|
||
# 預設 fallback 到 HERMES_URL;若需獨立 embedding 主機可透過 env 覆寫
|
||
EMBEDDING_HOST = os.getenv('EMBEDDING_HOST', HERMES_URL)
|
||
|
||
# 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 |