diff --git a/apps/api/src/services/telegram_gateway.py b/apps/api/src/services/telegram_gateway.py index abc531a2f..d0c60c9f2 100644 --- a/apps/api/src/services/telegram_gateway.py +++ b/apps/api/src/services/telegram_gateway.py @@ -3122,6 +3122,12 @@ class TelegramGateway: @NemoTronAwoooI_Bot → 只有 NemoClaw 回應 其他訊息 → 兩個 AI 並行回應 """ + # ── 指令路由 (2026-04-03 ogt: 方案B slash commands) ────────────────── + cmd = text.strip().split()[0].lower().split("@")[0] if text.strip() else "" + if cmd.startswith("/"): + await self._handle_group_command(cmd, chat_id, message_id) + return + from src.services.chat_manager import get_chat_manager as _get_cm chat_mgr = _get_cm() @@ -3205,6 +3211,106 @@ class TelegramGateway: logger.info("group_message_handled", user_id=user_id, text=text[:50]) + async def _handle_group_command(self, cmd: str, chat_id: int, message_id: int | None) -> None: # noqa: ARG002 + """ + SRE 群組 Slash Commands (2026-04-03 ogt: 方案B) + + /status → K8s Cluster 健康狀態 + /incidents → 活躍告警列表 + /cost → 本月 AI 費用統計 + /pods → 異常 Pod 列表 + /help → 指令說明 + """ + from src.repositories.k8s_repository import get_k8s_repository + from src.repositories.incident_repository import get_incident_repository + from src.core.redis_client import get_redis + from src.utils.timezone import now_taipei + + if cmd == "/status": + try: + k8s = get_k8s_repository() + s = await k8s.get_pod_status_summary(namespace="awoooi-prod") + running, total = s.get("running", 0), s.get("total", 0) + problems = s.get("problem_pods", []) + lines = [f"🖥 Cluster 狀態", f"• Pods: {running}/{total} Running"] + if problems: + lines.append(f"• 異常: {len(problems)} 個") + for p in problems[:5]: + lines.append(f" ⚠️ {p}") + else: + lines.append("• 全部正常 ✅") + msg = "\n".join(lines) + except Exception as e: + msg = f"🖥 Cluster 狀態\n⚠️ 無法取得: {e}" + await self.send_as_openclaw(text=msg, reply_to_message_id=message_id) + + elif cmd == "/incidents": + try: + repo = get_incident_repository() + incidents = await repo.get_active() + if incidents: + lines = ["🚨 活躍告警"] + for inc in incidents[:10]: + lines.append(f"• {inc.incident_id} SEV{inc.severity.value} — {inc.status.value}") + msg = "\n".join(lines) + else: + msg = "🚨 活躍告警\n✅ 目前無告警" + except Exception as e: + msg = f"🚨 活躍告警\n⚠️ 無法取得: {e}" + await self.send_as_openclaw(text=msg, reply_to_message_id=message_id) + + elif cmd == "/cost": + redis = get_redis() + month = now_taipei().strftime("%Y-%m") + try: + gemini_cost = float(await redis.get(f"gemini_cost:{month}") or 0) + claude_cost = float(await redis.get(f"claude_cost:{month}") or 0) + total = gemini_cost + claude_cost + msg = ( + f"💰 {month} AI 費用統計\n" + f"• 🦞 OpenClaw (Gemini Flash-Lite): ${gemini_cost:.4f} / $10.00 上限\n" + f"• 🤖 NemoClaw (Claude Haiku 4.5): ${claude_cost:.4f}\n" + f"• 合計: ${total:.4f}" + ) + except Exception as e: + msg = f"💰 費用統計\n⚠️ 無法取得: {e}" + await self.send_as_openclaw(text=msg, reply_to_message_id=message_id) + + elif cmd == "/pods": + try: + k8s = get_k8s_repository() + s = await k8s.get_pod_status_summary(namespace="awoooi-prod") + problems = s.get("problem_pods", []) + if problems: + lines = [f"⚠️ 異常 Pod ({len(problems)} 個)"] + for p in problems[:15]: + lines.append(f"• {p}") + msg = "\n".join(lines) + else: + msg = "⚠️ 異常 Pod\n✅ 全部 Pod 正常" + except Exception as e: + msg = f"⚠️ 異常 Pod\n⚠️ 無法取得: {e}" + await self.send_as_openclaw(text=msg, reply_to_message_id=message_id) + + elif cmd == "/help": + msg = ( + "🤖 SRE 戰情室指令\n\n" + "/status — 查詢 K8s Cluster 狀態\n" + "/incidents — 列出活躍告警\n" + "/cost — 查詢本月 AI 費用\n" + "/pods — 列出異常 Pod\n" + "/help — 顯示此說明\n\n" + "對話方式:\n" + "• 直接輸入 → 小O + 小賀 同時回應\n" + "• 小O 或 @OpenClawAwoooI_Bot → 只有 OpenClaw\n" + "• 小賀 或 @NemoTronAwoooI_Bot → 只有 NemoClaw\n" + "• Reply 某個 Bot 的訊息 → 只有那個 Bot 回應" + ) + await self.send_as_openclaw(text=msg, reply_to_message_id=message_id) + + else: + logger.debug("group_unknown_command", cmd=cmd) + async def _send_chat_action(self, chat_id: int, action: str) -> None: """發送聊天狀態 (e.g., typing)""" if not self._http_client: return