diff --git a/.gitea/workflows/cd.yaml b/.gitea/workflows/cd.yaml index 54bcb00..499785e 100644 --- a/.gitea/workflows/cd.yaml +++ b/.gitea/workflows/cd.yaml @@ -130,8 +130,8 @@ jobs: if: steps.deploy_type.outputs.type == 'sync' run: | ssh -i ~/.ssh/id_deploy -o StrictHostKeyChecking=no ollama@192.168.0.188 \ - "docker restart momo-pro-system momo-scheduler 2>&1 && \ - echo '✅ 兩容器已重啟(app/scheduler)'" + "docker restart momo-pro-system momo-scheduler momo-telegram-bot 2>&1 && \ + echo '✅ 三容器已重啟(app/scheduler/telegram-bot)'" # ── 模式 B:重建 Docker Image(Dockerfile / requirements.txt 變動) ── - name: 同步所有檔案並重建 Image @@ -155,11 +155,11 @@ jobs: # 重建並重啟(先強制移除舊容器,避免 container name conflict) ssh -i ~/.ssh/id_deploy -o StrictHostKeyChecking=no ollama@192.168.0.188 \ "cd /home/ollama/momo-pro && \ - docker stop momo-pro-system momo-scheduler 2>/dev/null; \ - docker rm momo-pro-system momo-scheduler 2>/dev/null; \ + docker stop momo-pro-system momo-scheduler momo-telegram-bot 2>/dev/null; \ + docker rm momo-pro-system momo-scheduler momo-telegram-bot 2>/dev/null; \ docker compose build momo-app && \ - docker compose up -d --no-deps momo-app scheduler && \ - echo '✅ Image 重建完成'" + docker compose up -d --no-deps momo-app scheduler telegram-bot && \ + echo '✅ Image 重建完成(三容器)'" # ── 健康檢查(最多重試 5 次,每次間隔 10s) ─────────────────────────── - name: 健康檢查 diff --git a/docker-compose.yml b/docker-compose.yml index f7fadd7..ce4cca9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -125,6 +125,7 @@ services: condition: service_healthy networks: - momo-network + - momo-pro_default logging: driver: "json-file" options: @@ -248,6 +249,52 @@ services: max-size: "10m" max-file: "3" + # --------------------------------------------------------------------------- + # Telegram Bot - 互動指令 + 每日 09:00 推播 + # --------------------------------------------------------------------------- + telegram-bot: + build: + context: . + dockerfile: Dockerfile + image: ${MOMO_IMAGE:-registry.wooo.work/wooo/momo-pro-system}:${VERSION:-latest} + container_name: momo-telegram-bot + restart: unless-stopped + labels: + - "com.centurylinklabs.watchtower.enable=true" + init: true + volumes: + - ./data:/app/data + - ./logs:/app/logs + - ./config:/app/config + - ./config.py:/app/config.py:ro + - ./run_telegram_bot.py:/app/run_telegram_bot.py:ro + - ./services:/app/services:ro + - ./database:/app/database:ro + environment: + - FLASK_ENV=production + - PYTHONUNBUFFERED=1 + - TZ=Asia/Taipei + - USE_POSTGRESQL=true + - POSTGRES_HOST=momo-db + - POSTGRES_PORT=5432 + - POSTGRES_USER=${POSTGRES_USER:-momo} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-wooo_pg_2026} + - POSTGRES_DB=${POSTGRES_DB:-momo_analytics} + - EMBEDDING_HOST=${EMBEDDING_HOST:-http://192.168.0.111:11434} + env_file: + - .env + command: ["python", "run_telegram_bot.py"] + depends_on: + - postgres + networks: + - momo-network + - momo-pro_default + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + # =========================================================================== # Monitoring Services (使用 --profile monitoring 啟用) # =========================================================================== diff --git a/run_scheduler.py b/run_scheduler.py index d64e940..65c2dd1 100644 --- a/run_scheduler.py +++ b/run_scheduler.py @@ -1,41 +1,90 @@ -# run_scheduler.py +#!/usr/bin/env python3 +""" +run_scheduler.py — momo-scheduler 容器入口點 + +排程任務清單(對齊 app.py init_scheduler + scheduler.py 全任務): + 每 30 分鐘:auto_import、whitepage_check + 每 1 小時:momo、edm、festival + 每 4 小時:competitor_price_feeder、icaim_analysis + 每 6 小時:openclaw_meta_analysis、quality_rescore + 每 12 小時:dedup_batch + 每 1 天 :db_backup、backup_monitor + 每 1 週 :weekly_strategy(週一 06:00) +""" import asyncio import logging import threading import time + import schedule -from datetime import datetime, timedelta, timezone -from database.manager import get_session +# 匯入全部排程任務函式 +from scheduler import ( + run_momo_task, + run_edm_task, + run_festival_task, + run_auto_import_task, + run_whitepage_check, + run_competitor_price_feeder_task, + run_icaim_analysis_task, + run_weekly_strategy_task, + run_db_backup_task, + run_backup_monitor_task, + run_openclaw_meta_analysis_task, + run_dedup_batch_task, + run_quality_rescore_task, +) +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', +) logger = logging.getLogger(__name__) -# ICAIM completion callback — decision_tracker service reserved for future implementation -def on_icaim_task_complete(plan_id: int, sku: str): - """Triggered by ICAIM scheduler after task completion.""" - logger.info("[Scheduler] [ICAIM] on_icaim_task_complete: plan_id=%s sku=%s", plan_id, sku) +def _register_schedules(): + schedule.every(30).minutes.do(run_auto_import_task) + logger.info("📅 每 30 分鐘:auto_import") + + schedule.every(30).minutes.do(run_whitepage_check) + logger.info("📅 每 30 分鐘:whitepage_check") + + schedule.every(1).hours.do(run_momo_task) + logger.info("📅 每 1 小時:momo_task") + + schedule.every(1).hours.do(run_edm_task) + logger.info("📅 每 1 小時:edm_task") + + schedule.every(1).hours.do(run_festival_task) + logger.info("📅 每 1 小時:festival_task") + + schedule.every(4).hours.do(run_competitor_price_feeder_task) + logger.info("📅 每 4 小時:competitor_price_feeder") + + schedule.every(4).hours.do(run_icaim_analysis_task) + logger.info("📅 每 4 小時:icaim_analysis") + + schedule.every(6).hours.do(run_openclaw_meta_analysis_task) + logger.info("📅 每 6 小時:openclaw_meta_analysis") + + schedule.every(6).hours.do(run_quality_rescore_task) + logger.info("📅 每 6 小時:quality_rescore") + + schedule.every(12).hours.do(run_dedup_batch_task) + logger.info("📅 每 12 小時:dedup_batch") + + schedule.every().day.at("03:00").do(run_db_backup_task) + logger.info("📅 每日 03:00:db_backup") + + schedule.every().day.at("04:00").do(run_backup_monitor_task) + logger.info("📅 每日 04:00:backup_monitor") + + schedule.every().monday.at("06:00").do(run_weekly_strategy_task) + logger.info("📅 每週一 06:00:weekly_strategy") -# schedule settings (keep original schedule logic) -def run_icaim_task(): - """Simulate ICAIM task execution.""" - logger.info("[Scheduler] [ICAIM] executing ICAIM analysis task...") - plan_id = 123 - sku = "sample_sku" - on_icaim_task_complete(plan_id, sku) - logger.info("[Scheduler] [ICAIM] task completed, triggered follow_up schedule") - - -schedule.every(6).hours.do(run_icaim_task) -logger.info("📅 scheduled: ICAIM analysis task every 6 hours") - - -# B8 FIX: Elephant Alpha autonomous engine startup -# Runs in a dedicated daemon thread with its own asyncio event loop. -# Isolated from the schedule loop so a crash doesn't kill the scheduler. def _run_elephant_alpha_engine(): - """Daemon thread: runs EA autonomous monitoring in its own asyncio loop.""" + """Daemon thread: ElephantAlpha 自主監控引擎(獨立 asyncio loop)""" try: from services.elephant_alpha_autonomous_engine import autonomous_engine loop = asyncio.new_event_loop() @@ -48,24 +97,27 @@ def _run_elephant_alpha_engine(): loop.close() -_ea_thread = threading.Thread( - target=_run_elephant_alpha_engine, - daemon=True, - name="elephant-alpha-engine" -) -_ea_thread.start() -logger.info("🐘 [ElephantAlpha] Autonomous engine thread launched") - - -# start schedule loop (keep original main loop) if __name__ == "__main__": - logger.info("Scheduler started.") + logger.info("🚀 momo-scheduler 啟動中...") + + _register_schedules() + logger.info("✅ 全部排程任務已註冊") + + _ea_thread = threading.Thread( + target=_run_elephant_alpha_engine, + daemon=True, + name="elephant-alpha-engine", + ) + _ea_thread.start() + logger.info("🐘 [ElephantAlpha] Autonomous engine thread launched") + + logger.info("⏰ 排程主迴圈啟動,等待任務觸發...") while True: try: schedule.run_pending() time.sleep(1) except KeyboardInterrupt: - logger.info("Scheduler stopped.") + logger.info("⛔ Scheduler stopped.") break except Exception as e: logger.error(f"Scheduler error: {e}") diff --git a/run_telegram_bot.py b/run_telegram_bot.py new file mode 100644 index 0000000..ecf023d --- /dev/null +++ b/run_telegram_bot.py @@ -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())