fix: Telegram bot 全功能修復 — 16個await按鈕/AI對話/模型遷移/DB schema
All checks were successful
CD Pipeline / deploy (push) Successful in 1m35s
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:
@@ -28,22 +28,34 @@ class AgentContext(Base):
|
||||
|
||||
class ActionPlan(Base):
|
||||
"""
|
||||
行動計畫表(NemoTron 輸出,等待審核與執行追蹤)。
|
||||
行動計畫表 — 統一 schema(超集)。
|
||||
Group A(01-init.sql / CodeReview / OpenClaw):
|
||||
action_type, description, priority, metadata_json
|
||||
Group B(migration 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},
|
||||
|
||||
199
migrations/019_fix_action_plans_schema.sql
Normal file
199
migrations/019_fix_action_plans_schema.sql
Normal 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 $$;
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
},
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 2:Hermes 掃描 ───────────────────────────────────────────────────
|
||||
|
||||
def _hermes_scan(self, files: Dict[str, str]) -> List[Dict]:
|
||||
"""直呼內網 Ollama(http://192.168.0.111:11434),免認證"""
|
||||
"""直呼內網 Ollama(http://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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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", "?")
|
||||
|
||||
@@ -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/Q(Q=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}」...")
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user