From 822789c81005dce03ef7f0eada4a851ac489f89a Mon Sep 17 00:00:00 2001 From: OoO Date: Mon, 4 May 2026 20:00:15 +0800 Subject: [PATCH] =?UTF-8?q?feat(p49):=20Telegram=20=E8=A3=9C=E5=AE=8C=209?= =?UTF-8?q?=20=E9=A0=81=E5=B0=8D=E6=87=89=20+=20daily=20summary=20?= =?UTF-8?q?=E5=8A=A0=E5=95=86=E6=A5=AD=E9=9D=A2=E6=9C=AA=E8=B7=9F=E9=80=B2?= =?UTF-8?q?=E8=AD=A6=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit M-B: Telegram 對應從 6/9 → 9/9 新增 3 個 cmd handler,對應 Phase 45-48 的 3 個新觀測頁: - cmd:obs_overview — 一頁式總覽(三主機 24h + AI 呼叫 + 月成本 + 待審 episode) - cmd:obs_orchestration — Agent 編排矩陣(4 Agent × Models 24h 數字) 本地 Ollama % / RAG 命中 % / 錯誤率 + cost - cmd:obs_business — 商業面 × AI(價格決策 7d by strategy + 未跟進機會 + Outcomes verdict 30d) services/openclaw_bot/menu_keyboards.py::_submenu_observability 升級為 9 項 M-C: daily summary(每日 09:30)加商業面警示 - 從 ai_price_recommendations × action_plans 跨表 JOIN 偵測 high-confidence (≥0.7) 卻無對應 action_plan 的「機會流失」 - 7d 內若有未跟進,daily summary 自動標 ⚠️ 警示 - 對應 Phase 48 business_intel 頁同個邏輯,閉環推送 inline keyboard 升級:日報附 6 個入口(總覽/編排/商業面/主機/AI/預算), 不再只有 4 個 Phase 38→49 累計 14 commits。觀測台戰役完整收官: - 9 頁全部對應 Telegram cmd - DB 22/22 = 100% 全覆蓋 - 6 個 L2 一鍵 + 3 種主動推送(即時/異常/日常) - 日報含商業面警示 Co-Authored-By: Claude Opus 4.7 (1M context) --- routes/openclaw_bot_routes.py | 151 ++++++++++++++++++++++++ run_scheduler.py | 35 +++++- services/openclaw_bot/menu_keyboards.py | 5 +- 3 files changed, 185 insertions(+), 6 deletions(-) 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'),