diff --git a/routes/openclaw_bot_routes.py b/routes/openclaw_bot_routes.py
index a6a54d5..adeb46e 100644
--- a/routes/openclaw_bot_routes.py
+++ b/routes/openclaw_bot_routes.py
@@ -8318,6 +8318,157 @@ def handle_cmd(cmd, arg, chat_id, reply_to):
except Exception as e:
send_message(chat_id, f"❌ 查詢預算失敗:{e}", reply_to, parse_mode=None)
+ elif cmd == 'obs_overview':
+ # Phase 49: 觀測台總覽(一頁式 KPI)
+ try:
+ from database.manager import DatabaseManager
+ from sqlalchemy import text as _sa
+ from datetime import datetime as _dt
+ today = _dt.now()
+ month_start = _dt(today.year, today.month, 1)
+ session = DatabaseManager().get_session()
+ host_rows = session.execute(_sa("""
+ SELECT host_label, COUNT(*), COUNT(*) FILTER (WHERE healthy)
+ FROM host_health_probes
+ WHERE probed_at >= NOW() - INTERVAL '24 hours'
+ GROUP BY host_label ORDER BY host_label
+ """)).fetchall()
+ ai = session.execute(_sa("""
+ SELECT COUNT(*), COALESCE(SUM(cost_usd), 0),
+ COUNT(*) FILTER (WHERE status NOT IN ('ok','cache_only')),
+ COUNT(*) FILTER (WHERE rag_hit)
+ FROM ai_calls WHERE called_at >= NOW() - INTERVAL '24 hours'
+ """)).fetchone()
+ month_cost = session.execute(
+ _sa("SELECT COALESCE(SUM(cost_usd), 0) FROM ai_calls WHERE called_at >= :ms"),
+ {'ms': month_start},
+ ).fetchone()[0] or 0
+ ep_pending = session.execute(
+ _sa("SELECT COUNT(*) FROM learning_episodes WHERE promotion_status = 'awaiting_review' AND reviewed_at IS NULL"),
+ ).fetchone()[0] or 0
+ session.close()
+
+ ai_total = int(ai[0] or 0)
+ err_rate = (int(ai[2] or 0) / ai_total * 100) if ai_total else 0
+ rag_rate = (int(ai[3] or 0) / ai_total * 100) if ai_total else 0
+ lines = ["🛰 *觀測台總覽(24h)*", ""]
+ lines.append("*三主機在線率:*")
+ for label, total, up in host_rows:
+ pct = (float(up) / float(total) * 100) if total else 0
+ emoji = "✅" if pct >= 99 else "⚠️" if pct >= 90 else "🚨"
+ lines.append(f"{emoji} {label}:*{pct:.1f}%*")
+ lines.append("")
+ lines.append(f"📊 AI 呼叫:*{ai_total:,}* 次(錯誤 {err_rate:.1f}%)")
+ lines.append(f"💰 24h 成本:*${float(ai[1] or 0):.2f}* · 當月 *${float(month_cost):.2f}*")
+ lines.append(f"💡 RAG 命中率:*{rag_rate:.1f}%*")
+ if ep_pending:
+ lines.append(f"📋 待審 episodes:*{ep_pending}* 筆")
+ lines.append("")
+ lines.append("詳細:mo.wooo.work/observability/overview")
+ kb = [_row(('🤖 Agent 編排', 'cmd:obs_orchestration'), ('💼 商業面 AI', 'cmd:obs_business')),
+ _row(('🏥 主機健康', 'cmd:obs_health'), ('📊 AI 呼叫', 'cmd:obs_ai_calls')),
+ _row(('← 返回主選單', 'menu:main'))]
+ send_message(chat_id, '\n'.join(lines), reply_to, kb, parse_mode='Markdown')
+ except Exception as e:
+ send_message(chat_id, f"❌ 查詢觀測台總覽失敗:{e}", reply_to, parse_mode=None)
+
+ elif cmd == 'obs_orchestration':
+ # Phase 49: Agent 編排矩陣(4 Agent × Models)
+ try:
+ from database.manager import DatabaseManager
+ from sqlalchemy import text as _sa
+ agent_groups = [
+ ('🤖 OpenClaw', ['openclaw_qa', 'openclaw_daily', 'openclaw_meta', 'openclaw_monthly', 'openclaw_weekly', 'openclaw_bot_main', 'openclaw_bot_gemini', 'openclaw_bot_nim', 'sales_copy', 'code_review_openclaw', 'openclaw_daily_insight']),
+ ('🔍 Hermes', ['hermes_analyst', 'hermes_intent', 'code_review_hermes']),
+ ('🧬 NemoTron', ['nemotron_dispatch']),
+ ('🐘 ElephantAlpha', ['ea_engine', 'code_review_elephant']),
+ ]
+ session = DatabaseManager().get_session()
+ lines = ["🌐 *Agent 編排矩陣(24h)*", ""]
+ for label, callers in agent_groups:
+ row = session.execute(_sa("""
+ SELECT COUNT(*),
+ COALESCE(SUM(cost_usd), 0),
+ COUNT(*) FILTER (WHERE provider IN ('gcp_ollama','ollama_secondary','ollama_111','ollama_other')),
+ COUNT(*) FILTER (WHERE rag_hit),
+ COUNT(*) FILTER (WHERE status NOT IN ('ok','cache_only'))
+ FROM ai_calls
+ WHERE called_at >= NOW() - INTERVAL '24 hours'
+ AND caller = ANY(:c)
+ """), {'c': callers}).fetchone()
+ calls = int(row[0] or 0)
+ if calls == 0:
+ lines.append(f"{label}:(無呼叫)")
+ continue
+ cost = float(row[1] or 0)
+ ollama_pct = float(row[2] or 0) / calls * 100
+ rag_pct = float(row[3] or 0) / calls * 100
+ err_pct = float(row[4] or 0) / calls * 100
+ lines.append(f"{label}:*{calls:,}* 次 · ${cost:.2f}")
+ lines.append(f" 本地 Ollama {ollama_pct:.0f}% · RAG {rag_pct:.0f}% · 錯誤 {err_pct:.1f}%")
+ session.close()
+ lines.append("")
+ lines.append("詳細:mo.wooo.work/observability/agent\\_orchestration")
+ kb = [_row(('🛰 觀測台總覽', 'cmd:obs_overview'), ('💼 商業面 AI', 'cmd:obs_business')),
+ _row(('← 返回主選單', 'menu:main'))]
+ send_message(chat_id, '\n'.join(lines), reply_to, kb, parse_mode='Markdown')
+ except Exception as e:
+ send_message(chat_id, f"❌ 查詢 Agent 編排失敗:{e}", reply_to, parse_mode=None)
+
+ elif cmd == 'obs_business':
+ # Phase 49: 商業面 × AI 編排(AI 在做什麼生意)
+ try:
+ from database.manager import DatabaseManager
+ from sqlalchemy import text as _sa
+ session = DatabaseManager().get_session()
+ rec_rows = session.execute(_sa("""
+ SELECT strategy, COUNT(*), COALESCE(AVG(confidence), 0)
+ FROM ai_price_recommendations
+ WHERE created_at >= NOW() - INTERVAL '7 days'
+ GROUP BY strategy ORDER BY 2 DESC
+ """)).fetchall()
+ verdict_rows = session.execute(_sa("""
+ SELECT verdict, COUNT(*) FROM action_outcomes
+ WHERE created_at >= NOW() - INTERVAL '30 days'
+ GROUP BY verdict
+ """)).fetchall()
+ unfollowed = session.execute(_sa("""
+ SELECT COUNT(*) FROM ai_price_recommendations r
+ WHERE r.created_at >= NOW() - INTERVAL '7 days'
+ AND r.confidence >= 0.7
+ AND NOT EXISTS (
+ SELECT 1 FROM action_plans p
+ WHERE p.sku = r.sku
+ AND p.created_at >= r.created_at
+ AND p.created_at < r.created_at + INTERVAL '7 days'
+ )
+ """)).fetchone()[0] or 0
+ session.close()
+
+ lines = ["💼 *商業面 × AI 編排*", ""]
+ if rec_rows:
+ lines.append("*AI 價格決策 7d:*")
+ for strategy, cnt, conf in rec_rows:
+ lines.append(f"• {strategy}:*{int(cnt):,}* 筆(信心 {float(conf):.2f})")
+ else:
+ lines.append("(過去 7 日無 AI 價格決策)")
+ if unfollowed > 0:
+ lines.append("")
+ lines.append(f"⚠️ *未跟進機會:{unfollowed} 筆*(high-confidence 卻無 action_plan)")
+ if verdict_rows:
+ lines.append("")
+ lines.append("*Outcomes Verdict 30d:*")
+ for v, c in verdict_rows:
+ icon = "✅" if v == 'effective' else "❌" if v == 'backfired' else "➖"
+ lines.append(f"{icon} {v}:*{int(c):,}*")
+ lines.append("")
+ lines.append("詳細:mo.wooo.work/observability/business\\_intel")
+ kb = [_row(('🛰 觀測台總覽', 'cmd:obs_overview'), ('🌐 Agent 編排', 'cmd:obs_orchestration')),
+ _row(('← 返回主選單', 'menu:main'))]
+ send_message(chat_id, '\n'.join(lines), reply_to, kb, parse_mode='Markdown')
+ except Exception as e:
+ send_message(chat_id, f"❌ 查詢商業面失敗:{e}", reply_to, parse_mode=None)
+
elif cmd == 'obs_trigger_review':
# Phase 44 (L2):Telegram inline 觸發 Code Review Pipeline
try:
diff --git a/run_scheduler.py b/run_scheduler.py
index 9be7493..44956fe 100644
--- a/run_scheduler.py
+++ b/run_scheduler.py
@@ -585,6 +585,24 @@ def run_observability_daily_summary():
WHERE audited_at >= NOW() - INTERVAL '7 days'
"""),
).fetchone()
+ # Phase 49: 商業面未跟進機會(high-confidence 卻無 action_plan)
+ unfollowed_count = 0
+ try:
+ unfollowed_count = session.execute(
+ _sa("""
+ SELECT COUNT(*) FROM ai_price_recommendations r
+ WHERE r.created_at >= NOW() - INTERVAL '7 days'
+ AND r.confidence >= 0.7
+ AND NOT EXISTS (
+ SELECT 1 FROM action_plans p
+ WHERE p.sku = r.sku
+ AND p.created_at >= r.created_at
+ AND p.created_at < r.created_at + INTERVAL '7 days'
+ )
+ """),
+ ).fetchone()[0] or 0
+ except Exception:
+ pass
finally:
session.close()
@@ -632,16 +650,23 @@ def run_observability_daily_summary():
if ppt_failed:
lines.append(f" 失敗:{ppt_failed} 筆")
+ if unfollowed_count > 0:
+ lines.append("")
+ lines.append(f"⚠️ 商業面未跟進:{unfollowed_count} 筆"
+ f"(high-confidence AI 建議未轉化為 action_plan)")
+
lines.append("")
- lines.append('→ 開觀測台詳查')
+ lines.append('→ 開觀測台總覽')
from services.telegram_templates import send_telegram_with_result
reply_markup = {
"inline_keyboard": [
- [{"text": "🏥 主機健康", "callback_data": "cmd:obs_health"},
- {"text": "📊 AI 呼叫", "callback_data": "cmd:obs_ai_calls"}],
- [{"text": "💰 預算", "callback_data": "cmd:obs_budget"},
- {"text": "💬 反饋趨勢", "callback_data": "cmd:obs_quality"}],
+ [{"text": "🛰 觀測台總覽", "callback_data": "cmd:obs_overview"},
+ {"text": "🌐 Agent 編排", "callback_data": "cmd:obs_orchestration"}],
+ [{"text": "💼 商業面 AI", "callback_data": "cmd:obs_business"},
+ {"text": "🏥 主機健康", "callback_data": "cmd:obs_health"}],
+ [{"text": "📊 AI 呼叫", "callback_data": "cmd:obs_ai_calls"},
+ {"text": "💰 預算", "callback_data": "cmd:obs_budget"}],
],
}
send_telegram_with_result('\n'.join(lines), reply_markup=reply_markup, parse_mode='HTML')
diff --git a/services/openclaw_bot/menu_keyboards.py b/services/openclaw_bot/menu_keyboards.py
index 175ec23..acd94b5 100644
--- a/services/openclaw_bot/menu_keyboards.py
+++ b/services/openclaw_bot/menu_keyboards.py
@@ -271,8 +271,11 @@ def _submenu_competitor_ppt():
def _submenu_observability():
- """Phase 38 AI 觀測台 — 對應 /observability/* 6 頁。"""
+ """Phase 38-49 AI 觀測台 — 對應 /observability/* 9 頁。"""
return _menu_with_back([
+ _row(('🛰 觀測台總覽 (24h)', 'cmd:obs_overview'),
+ ('🌐 Agent 編排矩陣', 'cmd:obs_orchestration')),
+ _row(('💼 商業面 × AI', 'cmd:obs_business'),),
_row(('📊 AI 呼叫總覽 (24h)', 'cmd:obs_ai_calls'),
('🏥 主機健康狀態', 'cmd:obs_health')),
_row(('💰 預算控管 (當月)', 'cmd:obs_budget'),