feat(p38): Telegram 補 4 個 AI 觀測台命令(B-3 完成)
All checks were successful
CD Pipeline / deploy (push) Successful in 2m31s
All checks were successful
CD Pipeline / deploy (push) Successful in 2m31s
統帥盤點要求:6 個觀測頁是否都有 Telegram 對應? 盤點結果:promotion_review 已有 (pg_ok/pg_no inline button),剩 4 個缺。 新增 4 個 cmd handler 對應 4 個觀測頁面: 1. cmd:obs_ai_calls — AI 呼叫總覽(24h) - 總呼叫 / Token / cost / errors / RAG 命中 / cache 命中 - Top 5 provider 分組 2. cmd:obs_health — 主機健康監控 - 三主機 GCP-A / GCP-B / 111 即時 HTTP probe - 過去 24h uptime % + 平均 response_ms(讀 host_health_probes) 3. cmd:obs_budget — 預算控管 - 當月 spent vs budget 各 provider - 超 alert_pct 自動標記 ⚠️ / 超 100% 標記 🚨 4. cmd:obs_quality — Caller 反饋趨勢 - 過去 30 日 avg_score 最低 8 名 - 含 thumbs_up/down + trend 圖示 - 含 智能建議(feedback_quality_tracker) UI/UX: - main_menu_keyboard 加「🛰 AI 觀測台」入口 - 新 _submenu_observability() 在 menu_keyboards.py - _SUBMENUS 註冊 'observability' key - titles 映射加 observability 標題 - 4 個命令 cross-link(彼此互通 + 返回主選單) Telegram 6/6 對應達成: - promotion_review: pg_ok/pg_no inline button (既有) - ai_calls: cmd:obs_ai_calls (新增) - host_health: cmd:obs_health (新增) - budget: cmd:obs_budget (新增) - quality_trend: cmd:obs_quality (新增) - ppt_audit: 既有「有 issues 才推 Telegram」推送行為(不需查詢命令) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8085,6 +8085,217 @@ def handle_cmd(cmd, arg, chat_id, reply_to):
|
||||
)
|
||||
send_message(chat_id, photo_help, reply_to, _submenu_market())
|
||||
|
||||
# ── Phase 38: AI 觀測台 4 個指令(對應 /observability/* 6 頁的 4 個關鍵指標)───
|
||||
elif cmd == 'obs_ai_calls':
|
||||
# 24h AI 呼叫統計:總次數 / token / cost / RAG 命中 / 錯誤
|
||||
try:
|
||||
from database.manager import DatabaseManager
|
||||
from sqlalchemy import text as _sa
|
||||
session = DatabaseManager().get_session()
|
||||
row = session.execute(
|
||||
_sa("""
|
||||
SELECT COUNT(*),
|
||||
COALESCE(SUM(input_tokens + output_tokens), 0),
|
||||
COALESCE(SUM(cost_usd), 0),
|
||||
COUNT(*) FILTER (WHERE status NOT IN ('ok','cache_only')),
|
||||
COUNT(*) FILTER (WHERE rag_hit),
|
||||
COUNT(*) FILTER (WHERE cache_hit)
|
||||
FROM ai_calls
|
||||
WHERE called_at >= NOW() - INTERVAL '24 hours'
|
||||
"""),
|
||||
).fetchone()
|
||||
top_provider = session.execute(
|
||||
_sa("""
|
||||
SELECT provider, COUNT(*) AS calls, COALESCE(SUM(cost_usd), 0) AS cost
|
||||
FROM ai_calls
|
||||
WHERE called_at >= NOW() - INTERVAL '24 hours'
|
||||
GROUP BY provider ORDER BY calls DESC LIMIT 5
|
||||
"""),
|
||||
).fetchall()
|
||||
session.close()
|
||||
|
||||
calls, tokens, cost, errors, rag, cache = row or (0, 0, 0, 0, 0, 0)
|
||||
err_rate = (errors / calls * 100) if calls else 0
|
||||
cache_rate = (cache / calls * 100) if calls else 0
|
||||
rag_rate = (rag / calls * 100) if calls else 0
|
||||
|
||||
lines = [
|
||||
"📊 *AI 呼叫總覽(過去 24 小時)*", "",
|
||||
f"• 總呼叫:*{calls:,}* 次",
|
||||
f"• Token 用量:*{tokens:,}*",
|
||||
f"• 成本:*${cost:.2f} USD*",
|
||||
f"• 錯誤:*{errors}* 次({err_rate:.1f}%)",
|
||||
f"• RAG 命中:*{rag}* 次({rag_rate:.1f}%)",
|
||||
f"• 快取命中:*{cache}* 次({cache_rate:.1f}%)",
|
||||
"",
|
||||
"*依供應商分組(Top 5):*",
|
||||
]
|
||||
for p, c, ct in top_provider:
|
||||
lines.append(f"• `{p}`:{c} 次 · ${ct:.2f}")
|
||||
lines.append("")
|
||||
lines.append("詳細查詢:mo.wooo.work/observability/ai\\_calls")
|
||||
|
||||
kb = [_row(('🏥 主機健康', 'cmd:obs_health'), ('💰 預算控管', 'cmd:obs_budget')),
|
||||
_row(('💬 反饋趨勢', 'cmd:obs_quality'), ('← 返回主選單', 'menu:main'))]
|
||||
send_message(chat_id, '\n'.join(lines), reply_to, kb, parse_mode='Markdown')
|
||||
except Exception as e:
|
||||
send_message(chat_id, f"❌ 查詢 AI 呼叫統計失敗:{e}", reply_to, parse_mode=None)
|
||||
|
||||
elif cmd == 'obs_health':
|
||||
# 三主機 Ollama 即時 + 24h uptime
|
||||
try:
|
||||
from services.ollama_service import (
|
||||
OLLAMA_HOST_PRIMARY, OLLAMA_HOST_SECONDARY, OLLAMA_HOST_FALLBACK,
|
||||
_is_unhealthy,
|
||||
)
|
||||
import requests as _req
|
||||
from database.manager import DatabaseManager
|
||||
from sqlalchemy import text as _sa
|
||||
|
||||
lines = ["🏥 *主機健康監控*", ""]
|
||||
lines.append("*三主機即時狀態:*")
|
||||
for label, host in [
|
||||
('GCP-A (Primary)', OLLAMA_HOST_PRIMARY),
|
||||
('GCP-B (Secondary)', OLLAMA_HOST_SECONDARY),
|
||||
('111 (Fallback)', OLLAMA_HOST_FALLBACK),
|
||||
]:
|
||||
healthy = False
|
||||
try:
|
||||
r = _req.get(f"{host.rstrip('/')}/api/tags", timeout=3)
|
||||
healthy = (r.status_code == 200)
|
||||
except Exception:
|
||||
pass
|
||||
emoji = "✅" if healthy else "❌"
|
||||
mark = " ⚠️標記異常" if _is_unhealthy(host) else ""
|
||||
lines.append(f"{emoji} *{label}*:`{host}`{mark}")
|
||||
|
||||
# 24h uptime
|
||||
try:
|
||||
session = DatabaseManager().get_session()
|
||||
rows = session.execute(
|
||||
_sa("""
|
||||
SELECT host_label,
|
||||
COUNT(*) AS total,
|
||||
COUNT(*) FILTER (WHERE healthy) AS up,
|
||||
COALESCE(AVG(response_ms) FILTER (WHERE healthy), 0) AS avg_ms
|
||||
FROM host_health_probes
|
||||
WHERE probed_at >= NOW() - INTERVAL '24 hours'
|
||||
GROUP BY host_label ORDER BY host_label
|
||||
"""),
|
||||
).fetchall()
|
||||
session.close()
|
||||
if rows:
|
||||
lines.append("")
|
||||
lines.append("*過去 24h 在線率:*")
|
||||
for label, total, up, avg_ms in rows:
|
||||
pct = (up / total * 100) if total else 0
|
||||
lines.append(f"• {label}:*{pct:.1f}%* · {int(avg_ms)}ms 平均")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
lines.append("")
|
||||
lines.append("詳細查詢:mo.wooo.work/observability/host\\_health")
|
||||
kb = [_row(('📊 AI 呼叫', 'cmd:obs_ai_calls'), ('💰 預算控管', 'cmd:obs_budget')),
|
||||
_row(('💬 反饋趨勢', 'cmd:obs_quality'), ('← 返回主選單', '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_budget':
|
||||
# 當月預算 vs 實際 spent
|
||||
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()
|
||||
budgets = session.execute(
|
||||
_sa("""
|
||||
SELECT period, provider, budget_usd, alert_pct
|
||||
FROM ai_call_budgets
|
||||
ORDER BY period, provider NULLS FIRST
|
||||
"""),
|
||||
).fetchall()
|
||||
spent_rows = session.execute(
|
||||
_sa("""
|
||||
SELECT provider, COALESCE(SUM(cost_usd), 0)
|
||||
FROM ai_calls
|
||||
WHERE called_at >= :ms
|
||||
GROUP BY provider
|
||||
"""),
|
||||
{'ms': month_start},
|
||||
).fetchall()
|
||||
session.close()
|
||||
spent_map = {r[0]: float(r[1] or 0) for r in spent_rows}
|
||||
|
||||
lines = [f"💰 *預算控管({today.year}-{today.month:02d})*", ""]
|
||||
warn = False
|
||||
for period, provider, budget, alert_pct in budgets:
|
||||
spent = spent_map.get(provider, 0.0) if provider else sum(spent_map.values())
|
||||
ratio = (spent / float(budget)) if float(budget) > 0 else 0
|
||||
pct = ratio * 100
|
||||
if ratio >= 1.0:
|
||||
icon = "🚨"
|
||||
warn = True
|
||||
elif ratio >= float(alert_pct or 80) / 100:
|
||||
icon = "⚠️"
|
||||
warn = True
|
||||
else:
|
||||
icon = "✅"
|
||||
lines.append(f"{icon} `{period}` `{provider or '(全部)'}`:${spent:.2f} / ${float(budget):.2f} ({pct:.0f}%)")
|
||||
if not budgets:
|
||||
lines.append("(尚無預算設定,需先跑 migrations/025)")
|
||||
if warn:
|
||||
lines.append("")
|
||||
lines.append("⚠️ *已有供應商超出告警閾值*,請至 Web 介面確認。")
|
||||
lines.append("")
|
||||
lines.append("詳細查詢:mo.wooo.work/observability/budget")
|
||||
kb = [_row(('📊 AI 呼叫', 'cmd:obs_ai_calls'), ('🏥 主機健康', 'cmd:obs_health')),
|
||||
_row(('💬 反饋趨勢', 'cmd:obs_quality'), ('← 返回主選單', '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_quality':
|
||||
# 30d caller 反饋趨勢
|
||||
try:
|
||||
from services.feedback_quality_tracker import (
|
||||
compute_caller_quality_trend, get_caller_recommendations,
|
||||
)
|
||||
trends = compute_caller_quality_trend(days=30)
|
||||
recs = get_caller_recommendations(days=30)
|
||||
sorted_trends = sorted(trends.items(), key=lambda kv: kv[1].get('avg_score', 5))[:8]
|
||||
|
||||
lines = ["💬 *Caller 反饋趨勢(過去 30 日)*", ""]
|
||||
if not sorted_trends:
|
||||
lines.append("(過去 30 日無反饋資料)")
|
||||
else:
|
||||
lines.append("*平均分數最低 8 名:*")
|
||||
for caller, info in sorted_trends:
|
||||
avg = info.get('avg_score', 0)
|
||||
up = info.get('thumbs_up', 0)
|
||||
dn = info.get('thumbs_down', 0)
|
||||
n = info.get('total_feedback', 0)
|
||||
trend = info.get('trend', 'unknown')
|
||||
icon = {'positive': '📈', 'negative': '📉', 'neutral': '➖'}.get(trend, '❓')
|
||||
lines.append(f"{icon} `{caller}` *{avg:.2f}*/5 · 👍{up} 👎{dn} · N={n}")
|
||||
|
||||
if recs:
|
||||
lines.append("")
|
||||
lines.append("*智能建議:*")
|
||||
for r in recs[:5]:
|
||||
icon = "⚠️" if r.get('action') == 'review' else "✅"
|
||||
lines.append(f"{icon} `{r.get('caller')}`:{r.get('reason')}")
|
||||
|
||||
lines.append("")
|
||||
lines.append("詳細查詢:mo.wooo.work/observability/quality\\_trend")
|
||||
kb = [_row(('📊 AI 呼叫', 'cmd:obs_ai_calls'), ('🏥 主機健康', 'cmd:obs_health')),
|
||||
_row(('💰 預算控管', 'cmd:obs_budget'), ('← 返回主選單', '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)
|
||||
|
||||
else:
|
||||
# 不認識的指令 → 自然語言
|
||||
txt, kb = openclaw_answer(cmd + (' ' + arg if arg else ''), chat_id=chat_id)
|
||||
@@ -8213,6 +8424,7 @@ def telegram_webhook():
|
||||
'competitor': '📊 *競品比價日報* — 選擇分析日期',
|
||||
'competitor_ppt': '📄 *競品比價簡報* — 選擇時間範圍',
|
||||
'category': '🗂 *分類業績鑽取* — 點選分類深入分析',
|
||||
'observability': '🛰 *AI 觀測台* — 系統指標與成本控管',
|
||||
}
|
||||
if cq_message_id:
|
||||
result = edit_message_text(
|
||||
|
||||
@@ -84,6 +84,7 @@ def main_menu_keyboard():
|
||||
('📄 簡報報表', 'menu:reports'),
|
||||
('🌐 市場情報', 'menu:market'),
|
||||
('🔍 競品日報', 'menu:competitor'),
|
||||
('🛰 AI 觀測台', 'menu:observability'),
|
||||
('❓ 使用說明', 'cmd:help'),
|
||||
],
|
||||
row_size=2,
|
||||
@@ -269,6 +270,16 @@ def _submenu_competitor_ppt():
|
||||
])
|
||||
|
||||
|
||||
def _submenu_observability():
|
||||
"""Phase 38 AI 觀測台 — 對應 /observability/* 6 頁。"""
|
||||
return _menu_with_back([
|
||||
_row(('📊 AI 呼叫總覽 (24h)', 'cmd:obs_ai_calls'),
|
||||
('🏥 主機健康狀態', 'cmd:obs_health')),
|
||||
_row(('💰 預算控管 (當月)', 'cmd:obs_budget'),
|
||||
('💬 反饋趨勢 (30d)', 'cmd:obs_quality')),
|
||||
])
|
||||
|
||||
|
||||
_SUBMENUS = {
|
||||
'main': main_menu_keyboard,
|
||||
'sales': _submenu_sales,
|
||||
@@ -281,4 +292,5 @@ _SUBMENUS = {
|
||||
'competitor': _submenu_competitor,
|
||||
'competitor_ppt': _submenu_competitor_ppt,
|
||||
'category': _submenu_category,
|
||||
'observability': _submenu_observability,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user