feat: EwoooC 初始化 — 完整專案推版至 Gitea
Some checks failed
CD Pipeline / deploy (push) Failing after 59s

- 建立 Gitea Actions CD pipeline (.gitea/workflows/cd.yaml)
- 部署模式: rsync Python 檔案至 188 → docker restart (volume mount)
- Dockerfile/requirements 變動時自動重建 Docker image
- 部署通知: Telegram (開始/成功/失敗)
- 健康檢查: https://mo.wooo.work/health (最多 5 次重試)
- 同步最新 CLAUDE.md / ADR-008 / memory (2026-04-19)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
ogt
2026-04-19 01:21:13 +08:00
commit 1b4f3a7bbe
504 changed files with 387725 additions and 0 deletions

View File

@@ -0,0 +1,60 @@
import os
import datetime
import re
import zipfile
def create_backup():
"""
建立系統完整備份 (Zip 壓縮檔)
檔名格式: momo_pro_system_backup_YYYYMMDD_HHMMSS_V{version}.zip
"""
# 1. 基礎路徑設定
base_dir = os.path.dirname(os.path.abspath(__file__))
backup_folder = os.path.join(base_dir, 'backups')
if not os.path.exists(backup_folder):
os.makedirs(backup_folder)
print(f"📂 已建立備份目錄: {backup_folder}")
# 2. 嘗試從 app.py 讀取版本號
version = "Unknown"
app_py_path = os.path.join(base_dir, 'app.py')
try:
if os.path.exists(app_py_path):
with open(app_py_path, 'r', encoding='utf-8') as f:
content = f.read()
# 尋找 SYSTEM_VERSION = "V9.0"
match = re.search(r'SYSTEM_VERSION\s*=\s*["\']([^"\']+)["\']', content)
if match:
version = match.group(1)
except Exception as e:
print(f"⚠️ 無法讀取版本號: {e}")
# 3. 產生備份檔名
timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
base_name = f"momo_pro_system_backup_{timestamp}_{version}.zip"
output_path = os.path.join(backup_folder, base_name)
print(f"📦 正在打包專案目錄: {base_dir}")
print(f"🎯 目標檔案: {output_path}")
# 4. 執行壓縮
try:
with zipfile.ZipFile(output_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
for root, dirs, files in os.walk(base_dir):
# 排除不需要備份的目錄
for ignore in ['backups', '__pycache__', '.git', '.idea', '.vscode', 'bin', 'bin 2']:
if ignore in dirs:
dirs.remove(ignore)
for file in files:
if file == '.DS_Store' or file.endswith('.pyc'): continue
file_path = os.path.join(root, file)
arcname = os.path.relpath(file_path, base_dir)
zipf.write(file_path, arcname)
print(f"✅ 備份完成!")
except Exception as e:
print(f"❌ 備份失敗: {e}")
if __name__ == "__main__":
create_backup()

View File

@@ -0,0 +1,127 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
SQLite 資料庫監控腳本
收集資料庫狀態、大小、查詢效能等指標
"""
import sqlite3
import os
import time
from datetime import datetime
from flask import Flask
from prometheus_client import Gauge, Counter, Histogram, generate_latest, REGISTRY
app = Flask(__name__)
# 定義 Prometheus 指標
db_size_bytes = Gauge('sqlite_database_size_bytes', '資料庫檔案大小bytes', ['database'])
db_record_count = Gauge('sqlite_record_count', '資料表記錄總數', ['database', 'table'])
db_query_duration = Histogram('sqlite_query_duration_seconds', '查詢執行時間', ['query_type'])
db_connection_errors = Counter('sqlite_connection_errors_total', '連接錯誤總數')
db_slow_queries = Counter('sqlite_slow_queries_total', '慢查詢總數(>1秒', ['table'])
# 資料庫路徑
DATABASE_PATH = '/home/ogt/momo_pro_system/data/momo_database.db'
def get_db_size():
"""獲取資料庫檔案大小"""
try:
if os.path.exists(DATABASE_PATH):
size = os.path.getsize(DATABASE_PATH)
db_size_bytes.labels(database='momo').set(size)
return size
return 0
except Exception as e:
print(f"Error getting database size: {e}")
return 0
def get_table_counts():
"""獲取各資料表的記錄數"""
try:
conn = sqlite3.connect(DATABASE_PATH, timeout=5)
cursor = conn.cursor()
# 獲取所有資料表
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")
tables = cursor.fetchall()
for (table_name,) in tables:
try:
start_time = time.time()
cursor.execute(f"SELECT COUNT(*) FROM {table_name}")
count = cursor.fetchone()[0]
duration = time.time() - start_time
# 記錄指標
db_record_count.labels(database='momo', table=table_name).set(count)
db_query_duration.labels(query_type='count').observe(duration)
# 檢測慢查詢
if duration > 1.0:
db_slow_queries.labels(table=table_name).inc()
except Exception as e:
print(f"Error counting table {table_name}: {e}")
conn.close()
except Exception as e:
print(f"Database connection error: {e}")
db_connection_errors.inc()
def measure_query_performance():
"""測試常見查詢的效能"""
try:
conn = sqlite3.connect(DATABASE_PATH, timeout=5)
cursor = conn.cursor()
# 測試查詢 1最近銷售數據
start_time = time.time()
cursor.execute("SELECT * FROM realtime_sales_monthly ORDER BY 日期 DESC LIMIT 100")
cursor.fetchall()
duration = time.time() - start_time
db_query_duration.labels(query_type='recent_sales').observe(duration)
if duration > 1.0:
db_slow_queries.labels(table='realtime_sales_monthly').inc()
# 測試查詢 2商品統計
start_time = time.time()
cursor.execute("SELECT 品牌, COUNT(*) FROM realtime_sales_monthly GROUP BY 品牌 LIMIT 50")
cursor.fetchall()
duration = time.time() - start_time
db_query_duration.labels(query_type='brand_stats').observe(duration)
conn.close()
except Exception as e:
print(f"Query performance test error: {e}")
db_connection_errors.inc()
@app.route('/metrics')
def metrics():
"""Prometheus metrics endpoint"""
# 收集最新指標
get_db_size()
get_table_counts()
measure_query_performance()
# 返回 Prometheus 格式的指標
return generate_latest(REGISTRY)
@app.route('/health')
def health():
"""健康檢查端點"""
try:
if os.path.exists(DATABASE_PATH):
conn = sqlite3.connect(DATABASE_PATH, timeout=2)
conn.close()
return 'OK', 200
return 'Database file not found', 503
except Exception as e:
return f'Error: {str(e)}', 503
if __name__ == '__main__':
print(f"Starting Database Exporter on http://127.0.0.1:9120/metrics")
app.run(host='127.0.0.1', port=9120)

View File

@@ -0,0 +1,198 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
SQLite 資料庫優化腳本
建議在低流量時段執行(晚上 8 點後)
功能:
1. 添加缺失的索引(提升查詢效能)
2. 執行 VACUUM 清理(回收空間、優化儲存)
3. 分析資料庫統計(更新查詢規劃器統計資訊)
"""
import sqlite3
import os
import sys
import time
from datetime import datetime
# 資料庫路徑
DB_PATH = '/home/ogt/momo_pro_system/data/momo_database.db'
# 日誌函數
def log(message, level="INFO"):
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
print(f"[{timestamp}] [{level}] {message}")
# 檢查資料庫大小
def get_db_size():
size_bytes = os.path.getsize(DB_PATH)
size_mb = size_bytes / (1024 * 1024)
return size_bytes, size_mb
# 檢查索引是否存在
def index_exists(cursor, index_name):
cursor.execute(
"SELECT name FROM sqlite_master WHERE type='index' AND name=?",
(index_name,)
)
return cursor.fetchone() is not None
# 添加索引
def add_indexes(conn):
log("開始添加索引...")
cursor = conn.cursor()
indexes = [
# products 表索引
("idx_products_category", "CREATE INDEX IF NOT EXISTS idx_products_category ON products (category)"),
("idx_products_status", "CREATE INDEX IF NOT EXISTS idx_products_status ON products (status)"),
("idx_products_updated_at", "CREATE INDEX IF NOT EXISTS idx_products_updated_at ON products (updated_at DESC)"),
# price_records 表索引
("idx_price_records_product_id", "CREATE INDEX IF NOT EXISTS idx_price_records_product_id ON price_records (product_id)"),
("idx_price_records_product_time", "CREATE INDEX IF NOT EXISTS idx_price_records_product_time ON price_records (product_id, timestamp DESC)"),
# promo_products 表索引
("idx_promo_crawled_at", "CREATE INDEX IF NOT EXISTS idx_promo_crawled_at ON promo_products (crawled_at DESC)"),
("idx_promo_batch_id", "CREATE INDEX IF NOT EXISTS idx_promo_batch_id ON promo_products (batch_id)"),
("idx_promo_status_change", "CREATE INDEX IF NOT EXISTS idx_promo_status_change ON promo_products (status_change)"),
]
added_count = 0
skipped_count = 0
for index_name, create_sql in indexes:
try:
if index_exists(cursor, index_name):
log(f" 索引已存在,跳過: {index_name}", "INFO")
skipped_count += 1
continue
log(f" 創建索引: {index_name}...", "INFO")
start_time = time.time()
cursor.execute(create_sql)
conn.commit()
elapsed = time.time() - start_time
log(f" ✓ 索引創建成功: {index_name} (耗時 {elapsed:.2f} 秒)", "SUCCESS")
added_count += 1
except Exception as e:
log(f" ✗ 索引創建失敗: {index_name} - {e}", "ERROR")
conn.rollback()
log(f"索引添加完成:新增 {added_count} 個,跳過 {skipped_count}", "INFO")
return added_count
# 執行 VACUUM
def run_vacuum(conn):
log("開始執行 VACUUM 清理...", "INFO")
log("⚠️ 注意:此操作會鎖定整個資料庫,期間無法讀寫", "WARNING")
try:
start_time = time.time()
conn.execute("VACUUM")
elapsed = time.time() - start_time
log(f"✓ VACUUM 執行成功 (耗時 {elapsed:.2f} 秒)", "SUCCESS")
return True
except Exception as e:
log(f"✗ VACUUM 執行失敗: {e}", "ERROR")
return False
# 分析資料庫統計
def analyze_database(conn):
log("開始分析資料庫統計...", "INFO")
try:
start_time = time.time()
conn.execute("ANALYZE")
elapsed = time.time() - start_time
log(f"✓ 資料庫分析完成 (耗時 {elapsed:.2f} 秒)", "SUCCESS")
return True
except Exception as e:
log(f"✗ 資料庫分析失敗: {e}", "ERROR")
return False
# 顯示優化前後對比
def show_statistics(before_size, after_size):
log("=" * 60)
log("資料庫優化統計報告", "INFO")
log("=" * 60)
log(f"優化前大小: {before_size:.2f} MB", "INFO")
log(f"優化後大小: {after_size:.2f} MB", "INFO")
if before_size > after_size:
saved = before_size - after_size
saved_pct = (saved / before_size) * 100
log(f"節省空間: {saved:.2f} MB ({saved_pct:.1f}%)", "SUCCESS")
else:
log("空間無變化(資料庫已經很緊湊)", "INFO")
log("=" * 60)
def main():
log("=" * 60)
log("SQLite 資料庫優化腳本啟動", "INFO")
log("=" * 60)
# 檢查資料庫是否存在
if not os.path.exists(DB_PATH):
log(f"錯誤:資料庫不存在 - {DB_PATH}", "ERROR")
sys.exit(1)
# 記錄開始時間和大小
before_bytes, before_mb = get_db_size()
log(f"當前資料庫大小: {before_mb:.2f} MB", "INFO")
try:
# 連接資料庫
log("連接到資料庫...", "INFO")
conn = sqlite3.connect(DB_PATH, timeout=30.0)
# 步驟 1: 添加索引
log("\n" + "=" * 60)
log("步驟 1/3: 添加索引", "INFO")
log("=" * 60)
added_count = add_indexes(conn)
# 步驟 2: 執行 VACUUM
log("\n" + "=" * 60)
log("步驟 2/3: 執行 VACUUM 清理", "INFO")
log("=" * 60)
vacuum_success = run_vacuum(conn)
# 步驟 3: 分析資料庫
log("\n" + "=" * 60)
log("步驟 3/3: 分析資料庫統計", "INFO")
log("=" * 60)
analyze_success = analyze_database(conn)
# 關閉連接
conn.close()
log("資料庫連接已關閉", "INFO")
# 顯示統計
after_bytes, after_mb = get_db_size()
log("")
show_statistics(before_mb, after_mb)
# 最終結果
log("")
log("=" * 60)
if added_count > 0 or vacuum_success:
log("✓ 資料庫優化完成!", "SUCCESS")
else:
log("資料庫優化執行完畢(無顯著變化)", "INFO")
log("=" * 60)
except sqlite3.OperationalError as e:
log(f"資料庫操作錯誤: {e}", "ERROR")
log("可能原因:資料庫正在被其他程序使用", "ERROR")
sys.exit(1)
except Exception as e:
log(f"未預期的錯誤: {e}", "ERROR")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,102 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
密碼雜湊生成工具
使用方法:
1. 直接執行python generate_password_hash.py
2. 輸入新密碼至少8字元包含英文和數字
3. 將生成的雜湊值複製到 .env 檔案的 LOGIN_PASSWORD 欄位
注意:執行後請立即刪除或清空終端機歷史記錄,避免明文密碼外洩
"""
from werkzeug.security import generate_password_hash
import sys
import getpass
def validate_password_strength(password):
"""
驗證密碼強度
要求:
- 至少 8 個字元
- 包含英文字母
- 包含數字
"""
if len(password) < 8:
return False, "密碼長度至少需要 8 個字元"
has_letter = any(c.isalpha() for c in password)
has_digit = any(c.isdigit() for c in password)
if not has_letter:
return False, "密碼必須包含英文字母"
if not has_digit:
return False, "密碼必須包含數字"
return True, None
def main():
print("=" * 60)
print("🔐 MOMO 監控系統 - 密碼雜湊生成工具")
print("=" * 60)
print()
print("密碼要求:")
print(" • 至少 8 個字元")
print(" • 包含英文字母(大小寫皆可)")
print(" • 包含數字")
print(" • 建議使用符號增加強度(如 !@#$%^&*")
print()
try:
# 使用 getpass 隱藏輸入(不會在終端顯示)
password = getpass.getpass("請輸入新密碼: ")
if not password:
print("\n❌ 密碼不能為空")
sys.exit(1)
# 驗證密碼強度
is_valid, error_message = validate_password_strength(password)
if not is_valid:
print(f"\n{error_message}")
sys.exit(1)
# 再次確認密碼
password_confirm = getpass.getpass("請再次輸入密碼: ")
if password != password_confirm:
print("\n❌ 兩次輸入的密碼不一致")
sys.exit(1)
# 生成雜湊
password_hash = generate_password_hash(password, method='pbkdf2:sha256', salt_length=16)
print("\n" + "=" * 60)
print("✅ 密碼雜湊生成成功!")
print("=" * 60)
print()
print("請將以下內容複製到 .env 檔案中:")
print()
print(f"LOGIN_PASSWORD={password_hash}")
print()
print("⚠️ 安全提醒:")
print(" 1. 請立即將上述雜湊值複製到 .env 檔案")
print(" 2. 重新啟動 app.py 使新密碼生效")
print(" 3. 清空終端機歷史記錄history -c")
print(" 4. 不要將 .env 檔案提交到 Git")
print(" 5. 定期更換密碼(建議每 90 天)")
print()
print("=" * 60)
except KeyboardInterrupt:
print("\n\n⚠️ 操作已取消")
sys.exit(0)
except Exception as e:
print(f"\n❌ 發生錯誤: {e}")
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,212 @@
#!/bin/bash
# ==========================================================
# Docker 服務遷移到 K8s 腳本
# 用途:將 n8n 和 Superset 從 Docker 遷移到 K8s
# ==========================================================
set -e
BACKUP_DIR="/home/wooo/backups/k8s-migration"
DATE=$(date +%Y%m%d_%H%M%S)
KUBECONFIG="/home/wooo/.kube/config"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}
error() {
echo "[ERROR] $1" >&2
exit 1
}
# 確認執行
confirm() {
read -p "$1 (y/N): " response
case "$response" in
[yY][eE][sS]|[yY]) return 0 ;;
*) return 1 ;;
esac
}
# 步驟 1備份資料
backup_data() {
log "=== 步驟 1備份資料 ==="
mkdir -p "$BACKUP_DIR"
# 備份 n8n
log "備份 n8n 資料..."
docker run --rm -v n8n_data:/data -v "$BACKUP_DIR":/backup \
alpine tar czf "/backup/n8n_data_${DATE}.tar.gz" -C /data .
log "✅ n8n 備份完成: $BACKUP_DIR/n8n_data_${DATE}.tar.gz"
# 備份 Superset PostgreSQL
log "備份 Superset PostgreSQL..."
docker exec superset-postgres pg_dump -U superset superset > \
"$BACKUP_DIR/superset_db_${DATE}.sql"
log "✅ Superset DB 備份完成: $BACKUP_DIR/superset_db_${DATE}.sql"
# 備份 Superset home
log "備份 Superset home..."
docker run --rm -v superset_superset_home:/data -v "$BACKUP_DIR":/backup \
alpine tar czf "/backup/superset_home_${DATE}.tar.gz" -C /data .
log "✅ Superset home 備份完成"
log "=== 備份完成 ==="
ls -la "$BACKUP_DIR"
}
# 步驟 2部署 K8s 服務
deploy_k8s() {
log "=== 步驟 2部署 K8s 服務 ==="
export KUBECONFIG="$KUBECONFIG"
# 建立 namespace
log "建立 tools namespace..."
kubectl apply -f /home/wooo/momo_pro_system/k8s/tools/00-namespace.yaml
# 部署 n8n
log "部署 n8n..."
kubectl apply -f /home/wooo/momo_pro_system/k8s/tools/01-n8n.yaml
# 部署 Superset
log "部署 Superset..."
kubectl apply -f /home/wooo/momo_pro_system/k8s/tools/02-superset.yaml
# 等待 Pod 就緒
log "等待 Pod 就緒..."
kubectl wait --for=condition=ready pod -l app=n8n -n tools --timeout=300s || true
kubectl wait --for=condition=ready pod -l app=superset -n tools --timeout=300s || true
log "=== K8s 部署完成 ==="
kubectl get pods -n tools
}
# 步驟 3還原資料到 K8s
restore_data() {
log "=== 步驟 3還原資料 ==="
export KUBECONFIG="$KUBECONFIG"
# 取得 n8n PVC 路徑
N8N_PV=$(kubectl get pvc n8n-data -n tools -o jsonpath='{.spec.volumeName}')
N8N_PATH="/var/lib/rancher/k3s/storage/${N8N_PV}"
# 還原 n8n 資料
log "還原 n8n 資料..."
LATEST_N8N=$(ls -t "$BACKUP_DIR"/n8n_data_*.tar.gz | head -1)
if [ -f "$LATEST_N8N" ]; then
sudo tar xzf "$LATEST_N8N" -C "$N8N_PATH"
log "✅ n8n 資料已還原"
fi
# 重啟 n8n Pod 以載入資料
kubectl rollout restart deployment/n8n -n tools
log "=== 資料還原完成 ==="
}
# 步驟 4更新 Nginx
update_nginx() {
log "=== 步驟 4更新 Nginx 配置 ==="
# 取得 K8s Service ClusterIP
export KUBECONFIG="$KUBECONFIG"
N8N_IP=$(kubectl get svc n8n -n tools -o jsonpath='{.spec.clusterIP}')
SUPERSET_IP=$(kubectl get svc superset -n tools -o jsonpath='{.spec.clusterIP}')
log "n8n ClusterIP: $N8N_IP"
log "Superset ClusterIP: $SUPERSET_IP"
log "請手動更新 /etc/nginx/sites-enabled/monitor:"
log " n8n_backend: $N8N_IP:5678"
log " superset_backend: $SUPERSET_IP:8088"
log "=== Nginx 更新提示完成 ==="
}
# 步驟 5停止 Docker 服務
stop_docker() {
log "=== 步驟 5停止 Docker 服務 ==="
if confirm "確定要停止 Docker 服務嗎?"; then
docker stop momo-n8n momo-superset superset-postgres superset-redis 2>/dev/null || true
log "✅ Docker 服務已停止"
else
log "跳過停止 Docker 服務"
fi
}
# 顯示狀態
show_status() {
log "=== 當前狀態 ==="
echo ""
echo "Docker 容器:"
docker ps --format "table {{.Names}}\t{{.Status}}" | grep -E "n8n|superset" || echo "無相關容器"
echo ""
echo "K8s Pods"
export KUBECONFIG="$KUBECONFIG"
kubectl get pods -n tools 2>/dev/null || echo "tools namespace 不存在"
}
# 主選單
main() {
echo "========================================"
echo " Docker → K8s 遷移工具"
echo "========================================"
echo ""
echo "1) 備份資料"
echo "2) 部署 K8s 服務"
echo "3) 還原資料到 K8s"
echo "4) 更新 Nginx 配置"
echo "5) 停止 Docker 服務"
echo "6) 顯示當前狀態"
echo "7) 完整遷移 (1-5)"
echo "0) 退出"
echo ""
read -p "請選擇操作: " choice
case $choice in
1) backup_data ;;
2) deploy_k8s ;;
3) restore_data ;;
4) update_nginx ;;
5) stop_docker ;;
6) show_status ;;
7)
backup_data
deploy_k8s
restore_data
update_nginx
stop_docker
;;
0) exit 0 ;;
*) error "無效選項" ;;
esac
}
# 如果有參數,直接執行對應步驟
if [ -n "$1" ]; then
case $1 in
backup) backup_data ;;
deploy) deploy_k8s ;;
restore) restore_data ;;
nginx) update_nginx ;;
stop) stop_docker ;;
status) show_status ;;
all)
backup_data
deploy_k8s
restore_data
update_nginx
stop_docker
;;
*) echo "用法: $0 [backup|deploy|restore|nginx|stop|status|all]" ;;
esac
else
main
fi

View File

@@ -0,0 +1,28 @@
[Unit]
Description=MOMO Pro System Complete Startup Service
Documentation=https://mo.wooo.work
After=docker.service k3s.service network-online.target
Wants=docker.service k3s.service network-online.target
Requires=docker.service
[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/home/wooo/momo_pro_system
# 等待系統完全啟動
ExecStartPre=/bin/sleep 60
# 執行啟動腳本
ExecStart=/home/wooo/momo_pro_system/scripts/tools/system_startup_complete.sh
# 超時設定10分鐘
TimeoutStartSec=600
# 日誌
StandardOutput=journal
StandardError=journal
SyslogIdentifier=momo-startup
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,22 @@
[Unit]
Description=MOMO Pro System Startup Service
After=docker.service network-online.target
Wants=docker.service network-online.target
Requires=docker.service
[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/home/wooo/momo_pro_system
ExecStartPre=/bin/sleep 30
ExecStart=/home/wooo/momo_pro_system/scripts/tools/system_startup.sh
TimeoutStartSec=600
StandardOutput=journal
StandardError=journal
# 環境變數(可選,用於 Telegram 通知)
Environment="TELEGRAM_BOT_TOKEN=8075645931:AAH-EGKMo8ZC4QJs-Nc1_0s92xHrGdQvdpg"
Environment="TELEGRAM_CHAT_ID=5619078117"
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,73 @@
# cSpell:ignore momo
"""
一次性執行爬蟲腳本
用於手動觸發商品資料和圖片的更新
"""
import os
import sys
import logging
# 設定路徑
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, BASE_DIR)
# 設定日誌
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(message)s',
handlers=[
logging.StreamHandler(sys.stdout)
]
)
from scheduler import run_momo_task, run_edm_task, run_festival_task
def main():
"""執行所有爬蟲任務"""
print("=" * 80)
print("🚀 開始執行完整爬蟲任務")
print("=" * 80)
print()
try:
# 1. 一般商品爬蟲
print("📦 [1/3] 執行一般商品爬蟲...")
print("-" * 80)
run_momo_task()
print()
print("✅ 一般商品爬蟲完成")
print()
# 2. EDM 促銷商品爬蟲
print("🎁 [2/3] 執行 EDM 促銷商品爬蟲...")
print("-" * 80)
run_edm_task()
print()
print("✅ EDM 促銷商品爬蟲完成")
print()
# 3. Festival 購物節商品爬蟲
print("🎉 [3/3] 執行購物節商品爬蟲...")
print("-" * 80)
run_festival_task()
print()
print("✅ 購物節商品爬蟲完成")
print()
print("=" * 80)
print("🎉 所有爬蟲任務執行完成!")
print("=" * 80)
except KeyboardInterrupt:
print("\n\n⚠️ 使用者中斷執行")
print("=" * 80)
sys.exit(1)
except Exception as e:
print(f"\n\n❌ 執行過程發生錯誤: {e}")
import traceback
traceback.print_exc()
print("=" * 80)
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,87 @@
#!/usr/bin/env python3
"""
獨立排程器服務
負責定時執行商品看板和活動看板的爬蟲任務
"""
import os
import sys
import time
import logging
import schedule
from datetime import datetime, timedelta, timezone
# 確保在正確的目錄運行
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
os.chdir(BASE_DIR)
sys.path.insert(0, BASE_DIR)
# 設定台北時區
TAIPEI_TZ = timezone(timedelta(hours=8))
# 設定日誌
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(message)s',
handlers=[
logging.FileHandler(os.path.join(BASE_DIR, "logs/scheduler.log"), encoding="utf-8"),
logging.StreamHandler()
]
)
logger = logging.getLogger("SchedulerService")
# 導入爬蟲任務
from scheduler import run_momo_task, run_edm_task, run_festival_task, run_auto_import_task
def safe_task_wrapper(task_func, task_name):
"""安全的任務包裝器,捕獲異常避免排程中斷"""
def wrapper():
try:
timestamp = datetime.now(TAIPEI_TZ).strftime('%Y-%m-%d %H:%M:%S')
logger.info(f"⏰ [{timestamp}] 開始執行任務: {task_name}")
task_func()
logger.info(f"✅ [{timestamp}] 任務完成: {task_name}")
except Exception as e:
logger.error(f"❌ 任務執行失敗: {task_name} | Error: {e}", exc_info=True)
return wrapper
def main():
"""主程序"""
logger.info("=" * 60)
logger.info("🚀 WOOO TECH 排程器服務啟動")
logger.info("=" * 60)
# 設定排程任務
# V-Opt 2026-01-14: 商品看板和 EDM 需每小時執行以監控價格變化
# 商品看板(主站爬蟲)- 每 1 小時執行
schedule.every(1).hours.do(safe_task_wrapper(run_momo_task, "商品看板爬蟲"))
logger.info("📅 已設定:商品看板爬蟲 - 每 1 小時執行一次")
# EDM 限時搶購看板 - 每 1 小時執行
schedule.every(1).hours.do(safe_task_wrapper(run_edm_task, "EDM 限時搶購爬蟲"))
logger.info("📅 已設定EDM 限時搶購爬蟲 - 每 1 小時執行一次")
# 購物節活動看板 - 每 6 小時執行
schedule.every(6).hours.do(safe_task_wrapper(run_festival_task, "購物節活動爬蟲"))
logger.info("📅 已設定:購物節活動爬蟲 - 每 6 小時執行一次")
# Google Drive 自動匯入 - 每 30 分鐘執行
schedule.every(30).minutes.do(safe_task_wrapper(run_auto_import_task, "Google Drive 自動匯入"))
logger.info("📅 已設定Google Drive 自動匯入 - 每 30 分鐘執行一次")
logger.info("=" * 60)
logger.info("✅ 所有排程任務已設定完成,開始監聽...")
logger.info("=" * 60)
# 主循環
try:
while True:
schedule.run_pending()
time.sleep(1)
except KeyboardInterrupt:
logger.info("🔌 收到中斷信號,排程器服務正在關閉...")
except Exception as e:
logger.error(f"🚨 排程器服務異常: {e}", exc_info=True)
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,148 @@
#!/usr/bin/env python3
"""
Telegram Bot 獨立執行腳本
用法:
python run_telegram_bot.py
環境變數:
TELEGRAM_BOT_TOKEN: Telegram Bot Token (必填)
功能:
- 啟動 Telegram Bot 監聽
- 每日 09:00 推播趨勢摘要
- 處理用戶指令:/trend, /search, /copy, /keywords, /daily, /settings
"""
import os
import sys
import asyncio
import logging
from datetime import datetime, time
from dotenv import load_dotenv
# 載入環境變數
load_dotenv()
# 設定日誌
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.StreamHandler(),
logging.FileHandler('logs/telegram_bot.log', encoding='utf-8')
]
)
logger = logging.getLogger('TelegramBot')
def check_dependencies():
"""檢查必要的套件"""
try:
from telegram import Update
from telegram.ext import Application
logger.info("✅ python-telegram-bot 已安裝")
return True
except ImportError:
logger.error("❌ 請安裝 python-telegram-bot: pip install python-telegram-bot")
return False
def check_token():
"""檢查 Bot Token"""
token = os.getenv('TELEGRAM_BOT_TOKEN')
if not token:
logger.error("❌ 請在 .env 設定 TELEGRAM_BOT_TOKEN")
logger.info(" 1. 在 Telegram 搜尋 @BotFather")
logger.info(" 2. 發送 /newbot 建立新 Bot")
logger.info(" 3. 複製 Token 到 .env 檔案")
return None
logger.info("✅ Bot Token 已設定")
return token
async def main():
"""主程式"""
print("=" * 60)
print(" MOMO Pro System - Telegram Bot")
print("=" * 60)
print()
# 檢查依賴
if not check_dependencies():
sys.exit(1)
# 檢查 Token
token = check_token()
if not token:
sys.exit(1)
# 導入 Bot 服務
try:
from services.telegram_bot_service import TelegramBotService
logger.info("✅ TelegramBotService 已載入")
except ImportError as e:
logger.error(f"❌ 無法載入 TelegramBotService: {e}")
sys.exit(1)
# 建立 Bot 服務
bot_service = TelegramBotService(token)
# 取得 Application
app = bot_service.get_application()
if not app:
logger.error("❌ 無法建立 Bot Application")
sys.exit(1)
# 設定每日推播排程 (每天 09:00)
from telegram.ext import JobQueue
async def daily_summary_job(context):
"""每日摘要推播任務"""
logger.info("📤 執行每日趨勢摘要推播...")
await bot_service.send_daily_summary()
# 啟動 Bot
logger.info("🚀 啟動 Telegram Bot...")
logger.info(" 指令列表:")
logger.info(" /trend [分類] - 查看熱門趨勢")
logger.info(" /search [關鍵字] - AI 網路搜尋")
logger.info(" /copy [商品名] - 生成行銷文案")
logger.info(" /keywords - 熱門關鍵字")
logger.info(" /daily - 每日趨勢摘要")
logger.info(" /settings - 通知設定")
print()
# 初始化並啟動
await app.initialize()
await app.start()
# 設定每日推播 (09:00 台北時間)
job_queue = app.job_queue
if job_queue:
# 計算下一個 09:00 的時間
target_time = time(hour=9, minute=0, second=0)
job_queue.run_daily(daily_summary_job, time=target_time, name='daily_summary')
logger.info("📅 已設定每日 09:00 推播趨勢摘要")
# 開始輪詢
await app.updater.start_polling(drop_pending_updates=True)
logger.info("✅ Bot 已啟動,按 Ctrl+C 停止")
# 保持運行
try:
while True:
await asyncio.sleep(1)
except KeyboardInterrupt:
logger.info("🛑 收到停止信號...")
finally:
await app.updater.stop()
await app.stop()
await app.shutdown()
logger.info("👋 Bot 已停止")
if __name__ == '__main__':
# 確保 logs 目錄存在
os.makedirs('logs', exist_ok=True)
# 執行
asyncio.run(main())

View File

@@ -0,0 +1,183 @@
#!/bin/bash
# ==========================================================
# MOMO Pro System - 系統啟動腳本 (v3.0)
# 重開機後自動啟動所有服務
# 2026-02-13 更新:
# - n8n 和 Superset 已遷移到 K8s (tools namespace)
# - 移除 Docker n8n/Superset 啟動
# - 新增 K8s tools namespace 重啟
# ==========================================================
LOG_FILE="/var/log/momo_startup.log"
TELEGRAM_BOT_TOKEN="${TELEGRAM_BOT_TOKEN:-8075645931:AAH-EGKMo8ZC4QJs-Nc1_0s92xHrGdQvdpg}"
TELEGRAM_CHAT_ID="${TELEGRAM_CHAT_ID:-5619078117}"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}
notify() {
curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
-d chat_id="${TELEGRAM_CHAT_ID}" \
-d text="🖥️ UAT Server 啟動通知
$1" \
-d parse_mode="HTML" > /dev/null 2>&1 || true
}
wait_container() {
local name=$1
local max_wait=${2:-60}
local count=0
while [ $count -lt $max_wait ]; do
if docker ps --format '{{.Names}}' | grep -q "^${name}$"; then
STATUS=$(docker inspect --format='{{.State.Status}}' "$name" 2>/dev/null)
if [ "$STATUS" = "running" ]; then
log "$name 運行中"
return 0
fi
fi
sleep 2
count=$((count + 2))
done
log "⚠️ $name 等待超時"
return 1
}
wait_k8s_pod() {
local label=$1
local namespace=$2
local max_wait=${3:-120}
export KUBECONFIG=/home/wooo/.kube/config
log "等待 K8s Pod ($label) 就緒..."
kubectl wait --for=condition=ready pod -l "$label" -n "$namespace" --timeout="${max_wait}s" 2>/dev/null
if [ $? -eq 0 ]; then
log "✅ K8s Pod ($label) 已就緒"
return 0
else
log "⚠️ K8s Pod ($label) 等待超時"
return 1
fi
}
main() {
log "=========================================="
log "🚀 系統啟動流程開始 (v3.0)"
log "=========================================="
notify "🔄 UAT 系統正在啟動..."
# 1. 確保 Docker 運行
log "=== 1. 檢查 Docker ==="
if ! systemctl is-active --quiet docker; then
systemctl start docker
sleep 10
fi
log "✅ Docker 服務正常"
# 2. 啟動 Docker Registry
log "=== 2. 啟動 Docker Registry ==="
cd /home/wooo/registry 2>/dev/null || cd /home/wooo/devops/registry 2>/dev/null
docker compose up -d 2>/dev/null || docker start docker-registry 2>/dev/null
wait_container "docker-registry" 60
# 3. 啟動 GitLab
log "=== 3. 啟動 GitLab ==="
docker start wooo-gitlab 2>/dev/null || true
# GitLab 啟動較慢,不等待完全就緒
# 4. 重啟 K8s momo namespace (主應用)
log "=== 4. 重啟 K8s momo 應用 ==="
export KUBECONFIG=/home/wooo/.kube/config
kubectl rollout restart deployment/momo-app -n momo 2>/dev/null || true
kubectl rollout restart deployment/momo-scheduler -n momo 2>/dev/null || true
# 5. 重啟 K8s tools namespace (n8n, Superset)
log "=== 5. 重啟 K8s tools 服務 ==="
kubectl rollout restart deployment/n8n -n tools 2>/dev/null || true
kubectl rollout restart deployment/superset -n tools 2>/dev/null || true
# 6. 等待 K8s Pod 就緒
log "=== 6. 等待 K8s Pod 就緒 ==="
wait_k8s_pod "app=momo-app" "momo" 120
wait_k8s_pod "app=n8n" "tools" 60
wait_k8s_pod "app=superset" "tools" 90
# 7. 健康檢查
log "=== 7. 健康檢查 ==="
FAILED=""
# 取得 K8s ClusterIP
N8N_IP=$(kubectl get svc n8n -n tools -o jsonpath='{.spec.clusterIP}' 2>/dev/null)
SUPERSET_IP=$(kubectl get svc superset -n tools -o jsonpath='{.spec.clusterIP}' 2>/dev/null)
# Registry
if curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:5002/v2/ | grep -q "200"; then
log "✅ Registry: OK"
else
log "❌ Registry: FAILED"
FAILED="${FAILED}Registry, "
fi
# MOMO App
if curl -s -o /dev/null -w '%{http_code}' https://mo.wooo.work/health | grep -q "200"; then
log "✅ MOMO App: OK"
else
log "❌ MOMO App: FAILED"
FAILED="${FAILED}MOMO App, "
fi
# n8n (via K8s ClusterIP)
if [ -n "$N8N_IP" ] && curl -s -o /dev/null -w '%{http_code}' "http://${N8N_IP}:5678/healthz" | grep -q "200"; then
log "✅ n8n (K8s): OK"
else
log "❌ n8n (K8s): FAILED"
FAILED="${FAILED}n8n, "
fi
# Superset (via K8s ClusterIP)
if [ -n "$SUPERSET_IP" ] && curl -s -o /dev/null -w '%{http_code}' "http://${SUPERSET_IP}:8088/health" | grep -q "200"; then
log "✅ Superset (K8s): OK"
else
log "❌ Superset (K8s): FAILED"
FAILED="${FAILED}Superset, "
fi
# GitLab (可能需要更長時間)
if curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8929/ | grep -q "200\|302"; then
log "✅ GitLab: OK"
else
log "⚠️ GitLab: 啟動中... (需要幾分鐘)"
fi
# 結果通知
log "=========================================="
if [ -z "$FAILED" ]; then
log "✅ 所有服務啟動成功"
notify "✅ 系統啟動完成
所有服務狀態:
• Registry ✅
• MOMO App (K8s) ✅
• n8n (K8s) ✅
• Superset (K8s) ✅
• GitLab ✅
$(date '+%Y-%m-%d %H:%M:%S')"
else
log "⚠️ 部分服務異常: $FAILED"
notify "⚠️ 系統啟動完成,但部分服務異常
異常服務: $FAILED
請檢查日誌: /var/log/momo_startup.log
$(date '+%Y-%m-%d %H:%M:%S')"
fi
log "=========================================="
}
# 執行
main "$@"

View File

@@ -0,0 +1,283 @@
#!/bin/bash
# =============================================================================
# WOOO TECH - MOMO Pro System 完整啟動腳本
# 用途: 系統重開機後自動啟動所有服務
# 版本: 2.0
# 日期: 2026-02-06
# =============================================================================
set -e
# 配置
LOG_FILE="/var/log/momo_startup.log"
TELEGRAM_BOT_TOKEN="8075645931:AAH-EGKMo8ZC4QJs-Nc1_0s92xHrGdQvdpg"
TELEGRAM_CHAT_ID="5619078117"
# 顏色輸出
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
# 日誌函數
log() {
local level=$1
shift
local message=$@
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo -e "${timestamp} [${level}] ${message}" | tee -a "$LOG_FILE"
}
log_info() { log "INFO" "$@"; }
log_warn() { log "WARN" "${YELLOW}$@${NC}"; }
log_error() { log "ERROR" "${RED}$@${NC}"; }
log_success() { log "SUCCESS" "${GREEN}$@${NC}"; }
# 發送 Telegram 通知
send_telegram() {
local message=$1
curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
-d chat_id="${TELEGRAM_CHAT_ID}" \
-d parse_mode="HTML" \
-d text="${message}" > /dev/null 2>&1 || true
}
# 等待服務就緒
wait_for_service() {
local name=$1
local check_cmd=$2
local max_attempts=${3:-30}
local attempt=1
log_info "等待 ${name} 就緒..."
while [ $attempt -le $max_attempts ]; do
if eval "$check_cmd" > /dev/null 2>&1; then
log_success "${name} 已就緒 (嘗試 ${attempt}/${max_attempts})"
return 0
fi
sleep 2
((attempt++))
done
log_error "${name} 啟動失敗 (超過 ${max_attempts} 次嘗試)"
return 1
}
# =============================================================================
# 服務啟動順序和依賴關係
# =============================================================================
#
# 啟動順序圖:
#
# ┌──────────────┐
# │ 1. Docker │ ← 所有容器服務的基礎
# └──────┬───────┘
# │
# ┌──────▼───────┐ ┌──────────────────┐
# │ 2. Harbor │ │ 3. K8s (K3s) │
# │ Registry │ │ 自動啟動 │
# └──────┬───────┘ └────────┬─────────┘
# │ │
# ┌──────▼───────┐ ┌────────▼─────────┐
# │ 4. GitLab │ │ 5. K8s Services │
# │ CI/CD │ │ - PostgreSQL │
# └──────────────┘ │ - momo-app │
# │ - scheduler │
# └────────┬─────────┘
# │
# ┌────────────────────────────▼───────────────────────────┐
# │ 6. 監控服務 (Docker) │
# │ - Prometheus │
# │ - Grafana │
# │ - Node Exporter │
# │ - Alertmanager │
# └────────────────────────────┬───────────────────────────┘
# │
# ┌────────▼─────────┐
# │ 7. 健康檢查 │
# │ 發送通知 │
# └──────────────────┘
#
# =============================================================================
main() {
local start_time=$(date +%s)
local errors=0
log_info "=========================================="
log_info "MOMO Pro System 啟動程序開始"
log_info "=========================================="
# =========================================================================
# 1. 確認 Docker 服務
# =========================================================================
log_info "[1/7] 確認 Docker 服務..."
if ! systemctl is-active --quiet docker; then
log_warn "Docker 未運行,正在啟動..."
systemctl start docker
wait_for_service "Docker" "docker info" 30 || { ((errors++)); log_error "Docker 啟動失敗"; }
else
log_success "Docker 已運行"
fi
# =========================================================================
# 2. 啟動 Harbor Registry
# =========================================================================
log_info "[2/7] 啟動 Harbor Registry..."
cd /home/wooo/devops/harbor/harbor
# 確保完全停止後再啟動
docker compose down --remove-orphans 2>/dev/null || true
sleep 5
docker compose up -d
wait_for_service "Harbor" "curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:5050/api/v2.0/ping | grep -q 200" 60 || {
((errors++))
log_error "Harbor 啟動失敗"
}
# =========================================================================
# 3. 確認 K3s 服務
# =========================================================================
log_info "[3/7] 確認 K3s 服務..."
if ! systemctl is-active --quiet k3s; then
log_warn "K3s 未運行,正在啟動..."
systemctl start k3s
wait_for_service "K3s" "kubectl get nodes" 60 || { ((errors++)); log_error "K3s 啟動失敗"; }
else
log_success "K3s 已運行"
fi
# =========================================================================
# 4. 啟動 GitLab
# =========================================================================
log_info "[4/7] 啟動 GitLab..."
if ! docker ps | grep -q gitlab; then
docker start gitlab gitlab-runner 2>/dev/null || {
log_warn "GitLab 容器不存在,嘗試從 compose 啟動..."
cd /home/wooo/devops/gitlab && docker compose up -d 2>/dev/null || true
}
fi
wait_for_service "GitLab" "curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8929 | grep -q -E '200|302'" 120 || {
((errors++))
log_warn "GitLab 啟動緩慢,可能需要更多時間"
}
# =========================================================================
# 5. 確認 K8s 服務
# =========================================================================
log_info "[5/7] 確認 K8s 服務..."
# 等待 PostgreSQL
log_info "等待 PostgreSQL..."
wait_for_service "PostgreSQL" "kubectl exec momo-postgres-0 -n momo -- pg_isready -U momo -d momo_analytics" 60 || {
log_warn "PostgreSQL 未就緒,重啟中..."
kubectl rollout restart statefulset/momo-postgres -n momo
sleep 30
}
# 等待 momo-app
log_info "等待 momo-app..."
wait_for_service "momo-app" "kubectl exec -n momo deploy/momo-app -- curl -s http://localhost:80/health | grep -q healthy" 90 || {
log_warn "momo-app 未就緒,重啟中..."
kubectl rollout restart deployment/momo-app -n momo
sleep 30
}
# 等待 scheduler
log_info "等待 scheduler..."
kubectl get pods -n momo -l app=momo-scheduler | grep -q Running || {
log_warn "scheduler 未運行,重啟中..."
kubectl rollout restart deployment/momo-scheduler -n momo
}
# =========================================================================
# 6. 啟動監控服務
# =========================================================================
log_info "[6/7] 啟動監控服務..."
cd /home/wooo/monitoring
docker compose up -d 2>/dev/null || log_warn "監控服務啟動失敗(可能已存在)"
# 確認 Grafana
wait_for_service "Grafana" "curl -s -o /dev/null http://127.0.0.1:3000/login" 30 || {
((errors++))
log_warn "Grafana 未就緒"
}
# =========================================================================
# 7. 最終健康檢查
# =========================================================================
log_info "[7/7] 執行最終健康檢查..."
local health_status=""
# 檢查 mo.wooo.work
if curl -s -o /dev/null -w '%{http_code}' https://mo.wooo.work/health 2>/dev/null | grep -q 200; then
health_status+="✅ mo.wooo.work: 正常\n"
else
health_status+="❌ mo.wooo.work: 異常\n"
((errors++))
fi
# 檢查 Harbor
if curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:5050/api/v2.0/ping 2>/dev/null | grep -q 200; then
health_status+="✅ Harbor: 正常\n"
else
health_status+="❌ Harbor: 異常\n"
((errors++))
fi
# 檢查 GitLab
if curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8929 2>/dev/null | grep -q -E '200|302'; then
health_status+="✅ GitLab: 正常\n"
else
health_status+="⚠️ GitLab: 啟動中\n"
fi
# 檢查 K8s Pods
local pod_status=$(kubectl get pods -n momo --no-headers 2>/dev/null | awk '{print $2}')
if echo "$pod_status" | grep -v "1/1" > /dev/null; then
health_status+="⚠️ K8s Pods: 部分異常\n"
else
health_status+="✅ K8s Pods: 全部正常\n"
fi
# =========================================================================
# 完成
# =========================================================================
local end_time=$(date +%s)
local duration=$((end_time - start_time))
log_info "=========================================="
log_info "啟動程序完成"
log_info "耗時: ${duration}"
log_info "錯誤數: ${errors}"
log_info "=========================================="
# 發送 Telegram 通知
local emoji="🟢"
local status_text="成功"
if [ $errors -gt 0 ]; then
emoji="🟡"
status_text="部分失敗 (${errors} 個錯誤)"
fi
send_telegram "$(cat <<EOF
${emoji} <b>MOMO Pro System 啟動通知</b>
📋 <b>狀態:</b> ${status_text}
⏱️ <b>耗時:</b> ${duration} 秒
🕐 <b>時間:</b> $(date '+%Y-%m-%d %H:%M:%S')
<b>服務健康檢查:</b>
$(echo -e "$health_status")
🏷️ <i>UAT Server (192.168.0.110)</i>
EOF
)"
return $errors
}
# 執行主程序
main "$@"