fix: Telegram bot 全功能修復 — 16個await按鈕/AI對話/模型遷移/DB schema
All checks were successful
CD Pipeline / deploy (push) Successful in 1m35s

## Telegram Bot 功能修復
- 補全 16 個 await: 按鈕的 handler(日期選擇/目標設定/促銷追蹤等),
  新增 _handle_await_callback + _process_await_input 完整狀態機
- cmd: 按鈕加入  即時回饋 + try/except 防 BadRequest
- handle_callback 加頂層 try/except 錯誤兜底
- 補 momo:cmd:suggestion + momo:menu:main callback handler
- 修復 _enhanced_keyword_matching context NameError

## AI 模型遷移(hermes3@111 → qwen2.5@188)
- hermes_analyst_service: URL 192.168.0.111→188, hermes3→qwen2.5:7b-instruct
- code_review_pipeline: 改用 HERMES_URL/HERMES_MODEL 常數
- elephant_alpha_orchestrator / nemoton_dispatcher: registry/footprint 同步
- aider_heal_executor: OLLAMA_API_BASE fallback 改 188
- ai_routes: footprint display 字串改 qwen2.5:7b-instruct

## ElephantAlpha 404 修復
- elephant_service: openrouter→NVIDIA NIM, nvidia/llama-3.1-nemotron-ultra-253b-v1
- ai_provider: 模型 ID 同步更新

## TELEGRAM_CHAT_ID 環境變數修正
- cicd_routes + aider_heal_executor: 優先讀 TELEGRAM_CHAT_IDS[0],
  fallback TELEGRAM_CHAT_ID,修復通知靜默失敗

## AI 對話 logging 改善
- telegram_ai_integration: Hermes 降級改 WARNING,OpenClaw 失敗加 exc_info
- hermes_analyst_service: 連線失敗 log 加 host/model context

## DB Schema 修復
- migrations/019: action_plans 補齊全欄位,DROP NOT NULL action_type
- autoheal_models: ActionPlan ORM 同步為超集 schema

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
ogt
2026-04-25 03:30:14 +08:00
parent 86d80d3f2a
commit d5c0feab5e
13 changed files with 509 additions and 129 deletions

View File

@@ -28,22 +28,34 @@ class AgentContext(Base):
class ActionPlan(Base):
"""
行動計畫表NemoTron 輸出,等待審核與執行追蹤)。
行動計畫表 — 統一 schema超集)。
Group A01-init.sql / CodeReview / OpenClaw:
action_type, description, priority, metadata_json
Group Bmigration 017 / watcher_agent / ai_orchestrator:
session_id, plan_type, sku, payload, created_by, approved_by, executed_at
migration 019 已在 DB 補齊所有欄位。
"""
__tablename__ = 'action_plans'
id = Column(Integer, primary_key=True, autoincrement=True)
# Group A columns
action_type = Column(String(100), nullable=True) # code_review_fix / openclaw_recommendation
description = Column(Text) # 人類可讀的行動說明
status = Column(String(50), default='pending') # pending/auto_pending/pending_review/executed
priority = Column(Integer, default=3) # 1=critical 2=high 3=medium 4=low
metadata_json = Column(Text) # JSON: pipeline_id/commit_sha/findings
created_at = Column(DateTime, default=datetime.now)
# Group B columns (ADR-012 / NemoTron)
session_id = Column(String(64), nullable=True)
plan_type = Column(String(50), nullable=True) # price_adjust / restock / campaign
sku = Column(String(100), nullable=True, index=True)
sku = Column(String(100), nullable=True)
payload = Column(Text) # JSON 行動內容
status = Column(String(20), default='pending') # pending/approved/rejected/executed
created_by = Column(String(50)) # nemotron / openclaw
approved_by = Column(String(100), nullable=True) # Telegram user_id
created_at = Column(DateTime, default=datetime.now)
executed_at = Column(DateTime, nullable=True)
__table_args__ = (
Index('idx_action_plans_type', 'action_type'),
Index('idx_action_plan_sku_status', 'sku', 'status'),
Index('idx_action_plan_created', 'created_at'),
{'extend_existing': True},

View File

@@ -0,0 +1,199 @@
-- Migration 019: Fix action_plans schema — add ALL columns needed by all callers
-- 建立日期2026-04-25
-- 問題根源:
-- (A) code_review_pipeline_service.py / openclaw_strategist_service.py 寫入
-- action_type / description / priority / metadata_json來自 01-init.sql 定義)
-- (B) watcher_agent.py / ai_orchestrator.py 寫入
-- session_id / plan_type / sku / payload / created_by / approved_by來自 migration 017
-- 兩組 callers 使用不同 schema造成寫入失敗。
-- 解決方案:讓 action_plans 成為兩組欄位的超集superset
-- 修復方式:逐欄 ADD COLUMN IF NOT EXISTS冪等不鎖表
-- 回滾路徑(若需還原):
-- ALTER TABLE action_plans DROP COLUMN IF EXISTS action_type;
-- ALTER TABLE action_plans DROP COLUMN IF EXISTS description;
-- ALTER TABLE action_plans DROP COLUMN IF EXISTS priority;
-- ALTER TABLE action_plans DROP COLUMN IF EXISTS metadata_json;
-- ALTER TABLE action_plans DROP COLUMN IF EXISTS session_id;
-- ALTER TABLE action_plans DROP COLUMN IF EXISTS plan_type;
-- ALTER TABLE action_plans DROP COLUMN IF EXISTS sku;
-- ALTER TABLE action_plans DROP COLUMN IF EXISTS payload;
-- ALTER TABLE action_plans DROP COLUMN IF EXISTS created_by;
-- ALTER TABLE action_plans DROP COLUMN IF EXISTS approved_by;
-- ALTER TABLE action_plans DROP COLUMN IF EXISTS executed_at;
-- ── Group A: 01-init.sql / CodeReview / OpenClaw ──────────────────────────────
-- action_type — 'code_review_fix' | 'openclaw_recommendation'
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'action_plans' AND column_name = 'action_type'
) THEN
ALTER TABLE action_plans ADD COLUMN action_type VARCHAR(100);
RAISE NOTICE 'action_plans.action_type column added';
ELSE
RAISE NOTICE 'action_plans.action_type already exists, skipped';
END IF;
END $$;
-- description — 人類可讀行動說明
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'action_plans' AND column_name = 'description'
) THEN
ALTER TABLE action_plans ADD COLUMN description TEXT;
RAISE NOTICE 'action_plans.description column added';
ELSE
RAISE NOTICE 'action_plans.description already exists, skipped';
END IF;
END $$;
-- priority — 1=critical 2=high 3=medium 4=low
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'action_plans' AND column_name = 'priority'
) THEN
ALTER TABLE action_plans ADD COLUMN priority INTEGER DEFAULT 3;
RAISE NOTICE 'action_plans.priority column added';
ELSE
RAISE NOTICE 'action_plans.priority already exists, skipped';
END IF;
END $$;
-- metadata_json — pipeline_id / commit_sha / findings JSON 擴展
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'action_plans' AND column_name = 'metadata_json'
) THEN
ALTER TABLE action_plans ADD COLUMN metadata_json TEXT;
RAISE NOTICE 'action_plans.metadata_json column added';
ELSE
RAISE NOTICE 'action_plans.metadata_json already exists, skipped';
END IF;
END $$;
-- ── Group B: Migration 017 / watcher_agent / ai_orchestrator ─────────────────
-- session_id — Agent session identifier
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'action_plans' AND column_name = 'session_id'
) THEN
ALTER TABLE action_plans ADD COLUMN session_id VARCHAR(64);
RAISE NOTICE 'action_plans.session_id column added';
ELSE
RAISE NOTICE 'action_plans.session_id already exists, skipped';
END IF;
END $$;
-- plan_type — 'price_adjust' | 'restock' | 'campaign'
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'action_plans' AND column_name = 'plan_type'
) THEN
ALTER TABLE action_plans ADD COLUMN plan_type VARCHAR(50);
RAISE NOTICE 'action_plans.plan_type column added';
ELSE
RAISE NOTICE 'action_plans.plan_type already exists, skipped';
END IF;
END $$;
-- sku — 商品 SKU
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'action_plans' AND column_name = 'sku'
) THEN
ALTER TABLE action_plans ADD COLUMN sku VARCHAR(100);
RAISE NOTICE 'action_plans.sku column added';
ELSE
RAISE NOTICE 'action_plans.sku already exists, skipped';
END IF;
END $$;
-- payload — JSON 行動內容NemoTron 輸出)
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'action_plans' AND column_name = 'payload'
) THEN
ALTER TABLE action_plans ADD COLUMN payload TEXT;
RAISE NOTICE 'action_plans.payload column added';
ELSE
RAISE NOTICE 'action_plans.payload already exists, skipped';
END IF;
END $$;
-- created_by — 'nemotron' | 'openclaw' | 'code_review_pipeline'
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'action_plans' AND column_name = 'created_by'
) THEN
ALTER TABLE action_plans ADD COLUMN created_by VARCHAR(50);
RAISE NOTICE 'action_plans.created_by column added';
ELSE
RAISE NOTICE 'action_plans.created_by already exists, skipped';
END IF;
END $$;
-- approved_by — Telegram user_id
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'action_plans' AND column_name = 'approved_by'
) THEN
ALTER TABLE action_plans ADD COLUMN approved_by VARCHAR(100);
RAISE NOTICE 'action_plans.approved_by column added';
ELSE
RAISE NOTICE 'action_plans.approved_by already exists, skipped';
END IF;
END $$;
-- executed_at — 執行時間戳
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'action_plans' AND column_name = 'executed_at'
) THEN
ALTER TABLE action_plans ADD COLUMN executed_at TIMESTAMP;
RAISE NOTICE 'action_plans.executed_at column added';
ELSE
RAISE NOTICE 'action_plans.executed_at already exists, skipped';
END IF;
END $$;
-- ── 索引補充 ──────────────────────────────────────────────────────────────────
CREATE INDEX IF NOT EXISTS idx_action_plans_type ON action_plans(action_type);
CREATE INDEX IF NOT EXISTS idx_action_plan_sku_status ON action_plans(sku, status);
CREATE INDEX IF NOT EXISTS idx_action_plan_created ON action_plans(created_at);
-- ── 修正 action_type NOT NULL 殘留問題 ──────────────────────────────────────
-- 根因action_type 在 01-init.sql 原始定義中為 NOT NULL
-- migration 019 的 IF NOT EXISTS 跳過了該欄位NOT NULL 約束未被修正。
-- watcher_agent.py / ai_orchestrator.py 插入時不填 action_type → 違反約束。
-- 修正DROP NOT NULL + 設定 DEFAULT 'auto'(冪等,不鎖表)
ALTER TABLE action_plans ALTER COLUMN action_type DROP NOT NULL;
ALTER TABLE action_plans ALTER COLUMN action_type SET DEFAULT 'auto';
DO $$
BEGIN
RAISE NOTICE 'Migration 019 done: action_plans unified schema (Group A + Group B columns) + action_type NOT NULL fix';
END $$;

View File

@@ -1637,7 +1637,7 @@ def api_icaim_trigger():
if result.threats:
hermes_stats = {
'model': 'hermes3:latest',
'model': 'qwen2.5:7b-instruct',
'duration_sec': hermes_duration,
'tokens': result.hermes_tokens,
}

View File

@@ -558,7 +558,12 @@ def run_diagnosis(env):
# =============================================================================
TELEGRAM_BOT_TOKEN = os.environ.get('TELEGRAM_BOT_TOKEN', '')
TELEGRAM_CHAT_ID = os.environ.get('TELEGRAM_CHAT_ID', '')
_chat_ids_raw = os.environ.get('TELEGRAM_CHAT_IDS', '[]')
try:
_chat_ids_list = json.loads(_chat_ids_raw)
TELEGRAM_CHAT_ID = str(_chat_ids_list[0]) if _chat_ids_list else os.environ.get('TELEGRAM_CHAT_ID', '')
except Exception:
TELEGRAM_CHAT_ID = os.environ.get('TELEGRAM_CHAT_ID', '')
if not TELEGRAM_BOT_TOKEN or not TELEGRAM_CHAT_ID:
import logging

View File

@@ -134,7 +134,7 @@ class AIProviderService:
'elephant': {
'connected': elephant_connected,
'model': self._elephant.model if elephant_connected else None,
'available_models': [{'id': 'openrouter/elephant-alpha', 'name': 'Elephant Alpha'}],
'available_models': [{'id': 'nvidia/llama-3.1-nemotron-ultra-253b-v1', 'name': 'Nemotron Ultra 253B'}],
'type': 'cloud',
'cost': 'efficient'
},

View File

@@ -13,6 +13,7 @@ ADR-014: Autonomous Code Heal Pipeline
L5 - Telegram 通知每次修復結果(成功/失敗/回滾)
"""
import json
import os
import re
import time
@@ -41,14 +42,19 @@ HEALTH_CHECK_URL: str = (
os.getenv("MOMO_BASE_URL", "https://mo.wooo.work").rstrip("/") + "/health"
)
OLLAMA_API_BASE: str = os.getenv("OLLAMA_API_BASE", "http://192.168.0.111:11434")
OLLAMA_API_BASE: str = os.getenv("OLLAMA_API_BASE", "http://192.168.0.188:11434")
AIDER_MODEL: str = os.getenv("AIDER_MODEL", "ollama/qwen3-coder-next")
MAX_DIFF_LINES: int = int(os.getenv("AIDER_MAX_DIFF_LINES", "50"))
MAX_HOURLY_FIX: int = int(os.getenv("AIDER_MAX_HOURLY_FIX", "5"))
TELEGRAM_BOT_TOKEN: str = os.getenv("TELEGRAM_BOT_TOKEN", "")
TELEGRAM_CHAT_ID: str = os.getenv("TELEGRAM_CHAT_ID", "")
_chat_ids_raw = os.getenv("TELEGRAM_CHAT_IDS", "[]")
try:
_chat_ids_list = json.loads(_chat_ids_raw)
TELEGRAM_CHAT_ID: str = str(_chat_ids_list[0]) if _chat_ids_list else os.getenv("TELEGRAM_CHAT_ID", "")
except Exception:
TELEGRAM_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID", "")
# 允許 Aider 修改的路徑(正規表示式)
ALLOWED_FILE_PATTERN = re.compile(

View File

@@ -29,6 +29,7 @@ from typing import Any, Dict, List, Optional
from database.manager import get_session
from sqlalchemy import text
from services.hermes_analyst_service import HERMES_URL as _HERMES_URL, HERMES_MODEL as _HERMES_MODEL
logger = logging.getLogger(__name__)
@@ -189,7 +190,7 @@ class CodeReviewPipeline:
# ── Step 2Hermes 掃描 ───────────────────────────────────────────────────
def _hermes_scan(self, files: Dict[str, str]) -> List[Dict]:
"""直呼內網 Ollamahttp://192.168.0.111:11434免認證"""
"""直呼內網 Ollamahttp://192.168.0.188:11434免認證"""
try:
import requests as _req
@@ -213,8 +214,8 @@ class CodeReviewPipeline:
只輸出 JSON 陣列,不含其他文字。無問題時輸出 []"""
resp = _req.post(
"http://192.168.0.111:11434/api/generate",
json={"model": "hermes3:latest", "prompt": prompt,
f"{_HERMES_URL}/api/generate",
json={"model": _HERMES_MODEL, "prompt": prompt,
"stream": False, "options": {"temperature": 0.1}},
timeout=120,
)

View File

@@ -69,7 +69,7 @@ class ElephantAlphaOrchestrator:
self.agents = {
"hermes": AgentCapability(
name="Hermes Analyst",
model="hermes3:latest",
model="qwen2.5:7b-instruct",
strengths=["price_competition_analysis", "threat_detection", "market_intelligence"],
limitations=["context_window", "real_time_data"],
cost_per_token=0.0,
@@ -112,7 +112,7 @@ CURRENT ARCHITECTURE:
- Your role: Autonomous decision-making and agent orchestration
AGENT CAPABILITIES:
1. HERMES (hermes3:latest)
1. HERMES (qwen2.5:7b-instruct)
- Strengths: Price competition analysis, threat detection, market intelligence
- Limitations: Limited context window, no real-time data access
- Best for: Analyzing large datasets, identifying patterns, threat assessment

View File

@@ -15,15 +15,15 @@ from dataclasses import dataclass
logger = logging.getLogger(__name__)
# Elephant Alpha 設定
OPENROUTER_API_KEY = os.getenv('OPENROUTER_API_KEY', '')
ELEPHANT_ALPHA_URL = "https://openrouter.ai/api/v1/chat/completions"
DEFAULT_ELEPHANT_MODEL = "openrouter/elephant-alpha"
# Elephant Alpha 設定NVIDIA NIM API
NVIDIA_API_KEY = os.getenv('NVIDIA_API_KEY', '')
ELEPHANT_ALPHA_URL = "https://integrate.api.nvidia.com/v1/chat/completions"
DEFAULT_ELEPHANT_MODEL = "nvidia/llama-3.1-nemotron-ultra-253b-v1"
ELEPHANT_TIMEOUT = int(os.getenv('ELEPHANT_TIMEOUT', '120')) # 預設 2 分鐘
# Elephant Alpha 定價 (USD per 1M tokens) - 預估效能型定價
# Elephant Alpha 定價 (USD per 1M tokens) - NVIDIA NIM 定價
ELEPHANT_PRICING = {
'openrouter/elephant-alpha': {'input': 0.10, 'output': 0.40},
'nvidia/llama-3.1-nemotron-ultra-253b-v1': {'input': 0.10, 'output': 0.40},
}
@dataclass
@@ -48,7 +48,7 @@ class ElephantService:
"""
初始化 Elephant 服務
"""
self.api_key = api_key or OPENROUTER_API_KEY
self.api_key = api_key or NVIDIA_API_KEY
self.model = model or DEFAULT_ELEPHANT_MODEL
# W3-A: 護欄 2 — 斷線降級 cache (300s TTL不每次 ping OpenRouter)
@@ -79,7 +79,7 @@ class ElephantService:
@staticmethod
def calculate_cost(model: str, input_tokens: int, output_tokens: int) -> Dict[str, float]:
"""計算費用"""
pricing = ELEPHANT_PRICING.get(model, ELEPHANT_PRICING['openrouter/elephant-alpha'])
pricing = ELEPHANT_PRICING.get(model, ELEPHANT_PRICING['nvidia/llama-3.1-nemotron-ultra-253b-v1'])
input_cost = (input_tokens / 1_000_000) * pricing['input']
output_cost = (output_tokens / 1_000_000) * pricing['output']
return {

View File

@@ -4,7 +4,7 @@
Hermes 3 競價情報分析服務 (Module 2)
角色:分析師 (Analyst)
模型:hermes3:latest @ 192.168.0.111:11434
模型:qwen2.5:7b-instruct @ 192.168.0.188:11434
輸入SQL 漏斗篩選後的候選商品(~300筆
輸出Top N 威脅清單(結構化 JSON→ 交給 NemoTron dispatcher
@@ -25,8 +25,8 @@ from sqlalchemy import text
logger = logging.getLogger(__name__)
HERMES_MODEL = "hermes3:latest"
HERMES_URL = "http://192.168.0.111:11434"
HERMES_MODEL = "qwen2.5:7b-instruct"
HERMES_URL = "http://192.168.0.188:11434"
HERMES_TIMEOUT = 120 # 秒,批量 300 筆最長預估 ~90s
TOP_N = 20 # 輸出前 N 個威脅,控制 NemoTron 每次消耗配額
@@ -118,7 +118,11 @@ class HermesAnalystService:
None, self._call_hermes_intent, message
)
except Exception as e:
logger.warning(f"[Hermes.handle_l1] LLM 意圖分析失敗(降級規則引擎): {e}")
logger.error(
f"[Hermes.handle_l1] run_in_executor 例外,降級規則引擎"
f"{type(e).__name__}: {e}",
exc_info=True,
)
if llm_result:
return llm_result
@@ -167,7 +171,10 @@ class HermesAnalystService:
"metadata": {"source": "hermes_llm"},
}
except Exception as e:
logger.info(f"[Hermes.intent] 降級規則引擎({type(e).__name__}: {e}")
logger.warning(
f"[Hermes.intent] Ollama 連線失敗,降級規則引擎"
f"host={HERMES_URL} model={HERMES_MODEL} error={type(e).__name__}: {e}"
)
return None
def _rule_based_intent(self, message: str) -> dict:
@@ -527,7 +534,7 @@ if __name__ == "__main__":
]
fake_pchome = {"A001": 280, "A003": 980, "A005": 760, "A007": 590, "A009": 350}
print("=== Hermes 3 競價分析 CLI 測試 ===\n")
print("=== AI分析師 競價分析 CLI 測試 ===\n")
raw = service._batch_analyze(fake_candidates, fake_pchome)
for t in raw:

View File

@@ -283,8 +283,8 @@ def _build_footprint_json(hermes_stats: Optional[dict], nim_stats: Optional[dict
result = {}
if hermes_stats:
result["analyst"] = {
"model": "hermes3:latest",
"host": "192.168.0.111",
"model": "qwen2.5:7b-instruct",
"host": "192.168.0.188",
"duration_sec": hermes_stats.get("duration_sec", 0),
"tokens": hermes_stats.get("tokens", 0),
"cost_usd": 0,
@@ -317,11 +317,11 @@ def _build_footprint_block(hermes_stats: Optional[dict], nim_stats: Optional[dic
dur = hermes_stats.get("duration_sec", 0)
tok = hermes_stats.get("tokens", "?")
lines.append(
f"• 🔍 分析: Hermes 3 8B (本地 111) | "
f"• 🔍 分析: Qwen2.5 7B (本地 188) | "
f"耗時: {dur:.1f}s | Tokens: {tok} | $0 成本"
)
else:
lines.append("• 🔍 分析: Hermes 3 8B (本地 111) | $0 成本")
lines.append("• 🔍 分析: Qwen2.5 7B (本地 188) | $0 成本")
if nim_stats:
tok = nim_stats.get("total_tokens", "?")

View File

@@ -442,112 +442,185 @@ class TrendTelegramBot:
await query.answer()
data = query.data
# ===== 主選單按鈕 =====
if data == "menu_main" or data == "menu:main":
await query.edit_message_text(
"👋 *MOMO 趨勢助手 Bot* — 請選擇功能類別:",
parse_mode='Markdown',
reply_markup=self._get_main_menu_keyboard()
)
try:
# ===== 主選單按鈕 =====
if data == "menu_main" or data == "menu:main":
await query.edit_message_text(
"👋 *MOMO 趨勢助手 Bot* — 請選擇功能類別:",
parse_mode='Markdown',
reply_markup=self._get_main_menu_keyboard()
)
# ===== 新的完整菜單系統 =====
elif data.startswith("menu:"):
await self._handle_main_menu_callback(query, data)
# ===== 新的完整菜單系統 =====
elif data.startswith("menu:"):
await self._handle_main_menu_callback(query, data)
# ===== 舊的簡單菜單(向下相容) =====
elif data == "menu_trend":
await query.edit_message_text(
"📊 *熱門趨勢*\n\n請選擇分類:",
parse_mode='Markdown',
reply_markup=self._get_category_keyboard("trend")
)
# ===== 舊的簡單菜單(向下相容) =====
elif data == "menu_trend":
await query.edit_message_text(
"📊 *熱門趨勢*\n\n請選擇分類:",
parse_mode='Markdown',
reply_markup=self._get_category_keyboard("trend")
)
elif data == "menu_search":
context.user_data['waiting_for'] = 'search_query'
await query.edit_message_text(
"🔍 *AI 搜尋*\n\n請輸入要搜尋的關鍵字:\n\n"
"例如:夏季防曬推薦、母親節禮物",
parse_mode='Markdown',
reply_markup=InlineKeyboardMarkup([[
InlineKeyboardButton("🔙 返回主選單", callback_data="menu:main")
]])
)
elif data == "menu_search":
context.user_data['waiting_for'] = 'search_query'
await query.edit_message_text(
"🔍 *AI 搜尋*\n\n請輸入要搜尋的關鍵字:\n\n"
"例如:夏季防曬推薦、母親節禮物",
parse_mode='Markdown',
reply_markup=InlineKeyboardMarkup([[
InlineKeyboardButton("🔙 返回主選單", callback_data="menu:main")
]])
)
elif data == "menu_copy":
context.user_data['waiting_for'] = 'copy_product'
await query.edit_message_text(
"✍️ *生成文案*\n\n請輸入商品名稱:\n\n"
"例如:防曬乳、保濕面膜",
parse_mode='Markdown',
reply_markup=InlineKeyboardMarkup([[
InlineKeyboardButton("🔙 返回主選單", callback_data="menu:main")
]])
)
elif data == "menu_copy":
context.user_data['waiting_for'] = 'copy_product'
await query.edit_message_text(
"✍️ *生成文案*\n\n請輸入商品名稱:\n\n"
"例如:防曬乳、保濕面膜",
parse_mode='Markdown',
reply_markup=InlineKeyboardMarkup([[
InlineKeyboardButton("🔙 返回主選單", callback_data="menu:main")
]])
)
elif data == "menu_keywords":
await query.edit_message_text(
"🏷️ *熱門關鍵字*\n\n請選擇分類:",
parse_mode='Markdown',
reply_markup=self._get_category_keyboard("keywords")
)
elif data == "menu_keywords":
await query.edit_message_text(
"🏷️ *熱門關鍵字*\n\n請選擇分類:",
parse_mode='Markdown',
reply_markup=self._get_category_keyboard("keywords")
)
elif data == "menu_daily":
await query.edit_message_text("📰 正在載入今日趨勢摘要...")
await self._show_daily_summary(query)
elif data == "menu_daily":
await query.edit_message_text("📰 正在載入今日趨勢摘要...")
await self._show_daily_summary(query)
elif data == "menu_settings":
await self._show_settings(query)
elif data == "menu_settings":
await self._show_settings(query)
# ===== 趨勢分類按鈕 =====
elif data.startswith("trend_"):
category = data[6:]
await query.edit_message_text(f"🔄 正在查詢 {category} 趨勢...")
await self._show_trend_by_category(query, category)
# ===== 趨勢分類按鈕 =====
elif data.startswith("trend_"):
category = data[6:]
await query.edit_message_text(f"🔄 正在查詢 {category} 趨勢...")
await self._show_trend_by_category(query, category)
# ===== 關鍵字分類按鈕 =====
elif data.startswith("keywords_"):
category = data[9:]
await query.edit_message_text(f"🔄 正在查詢 {category} 熱門關鍵字...")
await self._show_keywords_by_category(query, category)
# ===== 關鍵字分類按鈕 =====
elif data.startswith("keywords_"):
category = data[9:]
await query.edit_message_text(f"🔄 正在查詢 {category} 熱門關鍵字...")
await self._show_keywords_by_category(query, category)
# ===== 設定按鈕 =====
elif data.startswith("settings_"):
await self._handle_settings_callback(query, data)
# ===== 設定按鈕 =====
elif data.startswith("settings_"):
await self._handle_settings_callback(query, data)
# ===== 降價決策按鈕(僅支援 momo:pa:xxx / momo:pr:xxx 格式)=====
elif data.startswith("momo:pa:"):
await self._handle_price_approve(query, data.split(":")[-1])
# ===== 降價決策按鈕(僅支援 momo:pa:xxx / momo:pr:xxx 格式)=====
elif data.startswith("momo:pa:"):
await self._handle_price_approve(query, data.split(":")[-1])
elif data.startswith("momo:pr:"):
await self._handle_price_reject(query, data.split(":")[-1])
elif data.startswith("momo:pr:"):
await self._handle_price_reject(query, data.split(":")[-1])
# ===== L3 運維決策按鈕momo:ops:<action>:<task_name>=====
elif data.startswith("momo:ops:"):
await self._handle_ops_callback(query, data)
# ===== L3 運維決策按鈕momo:ops:<action>:<task_name>=====
elif data.startswith("momo:ops:"):
await self._handle_ops_callback(query, data)
# ===== 批次定價決策momo:bpa / bpr:<batch_id>=====
elif data.startswith("momo:bpa:"):
await self._handle_batch_price_decision(query, data.split(":", 2)[-1], "approve")
# ===== 批次定價決策momo:bpa / bpr:<batch_id>=====
elif data.startswith("momo:bpa:"):
await self._handle_batch_price_decision(query, data.split(":", 2)[-1], "approve")
elif data.startswith("momo:bpr:"):
await self._handle_batch_price_decision(query, data.split(":", 2)[-1], "reject")
elif data.startswith("momo:bpr:"):
await self._handle_batch_price_decision(query, data.split(":", 2)[-1], "reject")
# ===== 事件忽略momo:eig:<event_id>=====
elif data.startswith("momo:eig:"):
await self._handle_event_ignore(query, data.split(":", 2)[-1])
# ===== 事件忽略momo:eig:<event_id>=====
elif data.startswith("momo:eig:"):
await self._handle_event_ignore(query, data.split(":", 2)[-1])
# ===== OpenClaw 指令按鈕cmd:<cmd>:<arg>=====
elif data.startswith("cmd:"):
parts = data[4:].split(":", 1)
cmd = parts[0]
arg = parts[1] if len(parts) > 1 else ""
chat_id = query.message.chat_id
import threading as _t
_t.Thread(
target=self._forward_cmd_to_openclaw,
args=(cmd, arg, chat_id),
daemon=True,
).start()
# ===== AI 回應建議按鈕 =====
elif data == "momo:menu:main":
await query.edit_message_text(
"👋 *MOMO 趨勢助手 Bot* — 請選擇功能類別:",
parse_mode='Markdown',
reply_markup=self._get_main_menu_keyboard()
)
elif data.startswith("momo:cmd:suggestion:"):
suggestion_text = data.split(":", 3)[-1].replace("_", " ")
await query.edit_message_text(f"⏳ 正在查詢:{suggestion_text}...")
chat_id = query.message.chat_id
import threading as _t
_t.Thread(
target=self._forward_cmd_to_openclaw,
args=("suggestion", suggestion_text, chat_id),
daemon=True,
).start()
# ===== 待輸入狀態按鈕await:xxx=====
elif data.startswith("await:"):
await self._handle_await_callback(query, data[6:], context)
# ===== OpenClaw 指令按鈕cmd:<cmd>:<arg>=====
elif data.startswith("cmd:"):
parts = data[4:].split(":", 1)
cmd = parts[0]
arg = parts[1] if len(parts) > 1 else ""
chat_id = query.message.chat_id
try:
await query.edit_message_text("⏳ 正在處理,請稍候...")
except Exception:
pass
import threading as _t
_t.Thread(
target=self._forward_cmd_to_openclaw,
args=(cmd, arg, chat_id),
daemon=True,
).start()
except Exception as _e:
logger.error(f"[handle_callback] error processing '{data}': {_e}", exc_info=True)
try:
await query.edit_message_text(
"❌ 系統處理異常,請重試或返回主選單",
reply_markup=InlineKeyboardMarkup([[
InlineKeyboardButton("🔙 返回主選單", callback_data="menu:main")
]])
)
except Exception:
pass
async def _handle_await_callback(self, query, await_type: str, context):
"""處理所有 await: 類型的按鈕,設定 waiting_for 狀態並提示用戶輸入"""
prompts = {
'date_range_sales': ('📅 業績查詢 — 指定日期區間', '請輸入日期區間\n格式YYYY/MM/DD-YYYY/MM/DD\n例如2026/04/01-2026/04/07'),
'date_top': ('📅 商品排行 — 指定日期', '請輸入日期\n格式YYYY/MM/DD\n例如2026/04/25'),
'date_competitor': ('📅 競品報告 — 指定日期', '請輸入日期\n格式YYYY/MM/DD\n例如2026/04/25'),
'date_ppt_daily': ('📅 日報簡報 — 指定日期', '請輸入日期\n格式YYYY/MM/DD\n例如2026/04/25'),
'date_ppt_monthly': ('📅 月報簡報 — 指定月份', '請輸入月份\n格式YYYY/MM\n例如2026/04'),
'date_ppt_vendor': ('📅 廠商簡報 — 指定月份', '請輸入月份\n格式YYYY/MM\n例如2026/04'),
'date_trend_month': ('📅 月份趨勢 — 指定月份', '請輸入月份\n格式YYYY/MM\n例如2026/04'),
'date_trend_year': ('📅 年度趨勢 — 指定年份', '請輸入年份\n格式YYYY\n例如2026'),
'date_trend_quarter':('📅 季度趨勢 — 指定季度', '請輸入季度\n格式YYYY/QQ=1~4\n例如2026/2'),
'goal_daily': ('🎯 日目標設定', '請輸入日目標金額(元,純數字)\n例如500000'),
'goal_monthly': ('🎯 月目標設定', '請輸入月目標金額(元,純數字)\n例如15000000'),
'goal_quarterly': ('🎯 季目標設定', '請輸入季目標金額(元,純數字)\n例如45000000'),
'goal_half': ('🎯 半年目標設定', '請輸入半年目標金額(元,純數字)\n例如90000000'),
'goal_yearly': ('🎯 年目標設定', '請輸入年目標金額(元,純數字)\n例如180000000'),
'promo_range': ('🎉 促銷效益追蹤', '請輸入促銷日期區間\n格式YYYY/MM/DD-YYYY/MM/DD\n例如2026/04/01-2026/04/07'),
'search_compare': ('🔍 競品關鍵字比價', '請輸入商品關鍵字\n例如:防曬乳 SPF50'),
}
if await_type not in prompts:
await query.answer(f"未知類型:{await_type}", show_alert=True)
return
title, hint = prompts[await_type]
context.user_data['waiting_for'] = await_type
await query.edit_message_text(
f"*{title}*\n\n{hint}\n\n請直接在對話框輸入:",
parse_mode='Markdown',
reply_markup=InlineKeyboardMarkup([[
InlineKeyboardButton("❌ 取消", callback_data="menu:main")
]])
)
def _forward_cmd_to_openclaw(self, cmd: str, arg: str, chat_id: int):
"""轉發 cmd:* 指令到 OpenClaw Flask 內部 API"""
@@ -1290,6 +1363,11 @@ class TrendTelegramBot:
await self._process_copy(update, text)
return
if waiting_for and waiting_for.startswith(('date_', 'goal_', 'promo_', 'search_compare')):
context.user_data['waiting_for'] = None
await self._process_await_input(update, context, waiting_for, text)
return
# Enhanced natural language processing with AI integration
try:
# Import AI integration
@@ -1312,12 +1390,12 @@ class TrendTelegramBot:
await self._handle_simple_ai_response(update, ai_result)
else:
# Fallback to enhanced keyword matching
await self._enhanced_keyword_matching(update, text)
await self._enhanced_keyword_matching(update, context, text)
except Exception as e:
logger.error(f"[handle_message] AI processing error: {e}", exc_info=True)
# Fallback to enhanced keyword matching
await self._enhanced_keyword_matching(update, text)
await self._enhanced_keyword_matching(update, context, text)
async def _handle_complex_ai_response(self, update: Update, ai_result: dict):
"""Handle complex AI responses"""
@@ -1354,11 +1432,11 @@ class TrendTelegramBot:
keyboard.append([])
keyboard[-1].append({
'text': suggestion,
'callback_data': f'cmd:suggestion:{suggestion.lower().replace(" ", "_")}'
'callback_data': f'momo:cmd:suggestion:{suggestion.lower().replace(" ", "_")}'
})
# Add main menu button
keyboard.append([{'text': 'Main Menu', 'callback_data': 'menu:main'}])
keyboard.append([{'text': '主選單', 'callback_data': 'momo:menu:main'}])
await update.message.reply_text(
response_text,
@@ -1371,7 +1449,7 @@ class TrendTelegramBot:
reply_markup=self._get_main_menu_keyboard()
)
async def _enhanced_keyword_matching(self, update: Update, text: str):
async def _enhanced_keyword_matching(self, update: Update, context, text: str):
"""Enhanced keyword matching as fallback with Traditional Chinese responses"""
import re
from datetime import datetime, timedelta
@@ -1493,6 +1571,69 @@ class TrendTelegramBot:
"Product analysis",
"Market intelligence"
])
async def _process_await_input(self, update, context, await_type: str, text: str):
"""處理所有 await: 狀態的用戶輸入,路由到對應 cmd"""
import re
chat_id = update.effective_chat.id
date_cmd_map = {
'date_range_sales': ('sales', text.strip()),
'date_top': ('top', text.strip()),
'date_competitor': ('ppt', 'competitor ' + text.strip()),
'date_ppt_daily': ('ppt', 'daily ' + text.strip()),
'date_ppt_monthly': ('ppt', 'monthly ' + text.strip()),
'date_ppt_vendor': ('ppt', 'vendor ' + text.strip()),
'date_trend_month': ('trend', text.strip()),
'date_trend_year': ('trend', text.strip()),
'date_trend_quarter':('trend', text.strip()),
'promo_range': ('promo', text.strip()),
'search_compare': ('competitor', text.strip()),
}
goal_types = {
'goal_daily': 'daily',
'goal_monthly': 'monthly',
'goal_quarterly': 'quarterly',
'goal_half': 'half',
'goal_yearly': 'yearly',
}
if await_type in date_cmd_map:
cmd, arg = date_cmd_map[await_type]
await update.message.reply_text(f"⏳ 正在處理 `{text.strip()}`...", parse_mode='Markdown')
import threading as _t
_t.Thread(
target=self._forward_cmd_to_openclaw,
args=(cmd, arg, chat_id),
daemon=True,
).start()
elif await_type in goal_types:
period = goal_types[await_type]
amount_str = re.sub(r'[^\d]', '', text)
if not amount_str:
await update.message.reply_text(
"⚠️ 格式錯誤請輸入純數字例如500000",
reply_markup=InlineKeyboardMarkup([[
InlineKeyboardButton("🔙 返回目標管理", callback_data="menu:goals")
]])
)
return
await update.message.reply_text(
f"⏳ 正在設定{period}目標 {int(amount_str):,} 元...",
parse_mode='Markdown'
)
import threading as _t
_t.Thread(
target=self._forward_cmd_to_openclaw,
args=('goal', f'set:{period}:{amount_str}', chat_id),
daemon=True,
).start()
else:
await update.message.reply_text(
"⚠️ 無法識別輸入類型,請重新選擇功能",
reply_markup=self._get_main_menu_keyboard()
)
async def _process_search(self, update: Update, query: str):
"""處理搜尋請求"""
await update.message.reply_text(f"🔍 正在搜尋「{query}」...")

View File

@@ -50,6 +50,11 @@ class TelegramAIIntegration:
# L1: Semantic understanding (Hermes)
l1_result = await self.orchestrator.handle_l1(event, session_id)
if not l1_result or l1_result.get("metadata", {}).get("source") != "hermes_llm":
logger.warning(
f"[TelegramAIIntegration] Hermes LLM 未回應,走規則引擎降級"
f"session={session_id} source={( l1_result or {}).get('metadata', {}).get('source', 'none')}"
)
# Check if this is a complex query requiring L2 processing
if self._is_complex_query(user_message, l1_result):
@@ -76,7 +81,11 @@ class TelegramAIIntegration:
},
)
except Exception as e:
logger.warning(f"[TelegramAIIntegration] OpenClaw 策略師呼叫失敗: {e}")
logger.error(
f"[TelegramAIIntegration] OpenClaw 策略師呼叫失敗"
f"{type(e).__name__}: {e}",
exc_info=True,
)
strategist_text = ""
response = self._format_complex_response(l1_result, l2_result, user_message)