fix: restore full scheduler + telegram-bot + fix momo-app network isolation
All checks were successful
CD Pipeline / deploy (push) Successful in 1m55s
All checks were successful
CD Pipeline / deploy (push) Successful in 1m55s
三個關鍵修復: 1. momo-app 加入 momo-pro_default 網路 → 修復 momo-db DNS 解析失敗(crash loop) 2. 新增 telegram-bot compose 服務 → momo-telegram-bot 容器從未啟動,小龍蝦群組零訊息 3. 重寫 run_scheduler.py → 完整載入 scheduler.py 13 個真實排程任務 4. 新增 run_telegram_bot.py 至 repo(原本只存在 server,未納入版控) 5. cd.yaml 同步更新:三容器 restart/rebuild(app/scheduler/telegram-bot) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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: 健康檢查
|
||||
|
||||
@@ -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 啟用)
|
||||
# ===========================================================================
|
||||
|
||||
126
run_scheduler.py
126
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}")
|
||||
|
||||
148
run_telegram_bot.py
Normal file
148
run_telegram_bot.py
Normal 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())
|
||||
Reference in New Issue
Block a user