diff --git a/apps/api/src/services/decision_manager.py b/apps/api/src/services/decision_manager.py index 755dc737b..1218ca086 100644 --- a/apps/api/src/services/decision_manager.py +++ b/apps/api/src/services/decision_manager.py @@ -28,12 +28,81 @@ from uuid import uuid4 import structlog from src.core.redis_client import get_redis +from src.core.config import settings from src.models.incident import Incident from src.services.openclaw import get_openclaw logger = structlog.get_logger(__name__) +# ============================================================================= +# Telegram 推送 (Phase 6.5: 決策就緒通知) +# ============================================================================= + +async def _push_decision_to_telegram( + incident: Incident, + proposal_data: dict[str, Any], +) -> None: + """ + 決策就緒時推送到 Telegram + + Phase 6.5: 整合 Signal Worker 流程與 Telegram 通知 + """ + try: + # 延遲導入避免循環依賴 + from src.services.telegram_gateway import get_telegram_gateway, TelegramGatewayError + + # 檢查是否有設定 Bot Token + if not settings.OPENCLAW_TG_BOT_TOKEN: + logger.debug( + "telegram_push_skipped", + reason="Bot token not configured", + incident_id=incident.incident_id, + ) + return + + gateway = get_telegram_gateway() + + # 從 proposal_data 提取資料 + target = incident.affected_services[0] if incident.affected_services else "unknown" + risk_level = proposal_data.get("risk_level", "medium") + action = proposal_data.get("action", proposal_data.get("kubectl_command", "")) + description = proposal_data.get("description", "") + reasoning = proposal_data.get("reasoning", "") + confidence = proposal_data.get("confidence", 0.75) + source = proposal_data.get("source", "unknown") + + # 建立 approval_id (使用 incident_id 作為追蹤) + approval_id = f"INC-{incident.incident_id}" + + await gateway.send_approval_card( + approval_id=approval_id, + risk_level=risk_level, + resource_name=target[:50], + root_cause=f"[{source.upper()}] {reasoning[:80]}" if reasoning else description[:100], + suggested_action=action[:50] if action else "待分析", + estimated_downtime="5-15 min", + primary_responsibility="INFRA", + confidence=confidence, + namespace=incident.signals[0].labels.get("namespace", "default") if incident.signals else "default", + ) + + logger.info( + "telegram_decision_pushed", + incident_id=incident.incident_id, + source=source, + risk_level=risk_level, + ) + + except Exception as e: + # Telegram 失敗不影響主流程 + logger.warning( + "telegram_decision_push_failed", + incident_id=incident.incident_id, + error=str(e), + ) + + # ============================================================================= # Decision States # ============================================================================= @@ -298,6 +367,13 @@ class DecisionManager: # 4. 儲存最終結果 await self._save_token(token) + # 5. Phase 6.5: 推送到 Telegram (非阻塞) + if token.state == DecisionState.READY and token.proposal_data: + # 使用 asyncio.create_task 非阻塞執行 + asyncio.create_task( + _push_decision_to_telegram(incident, token.proposal_data) + ) + return token async def _dual_engine_analyze(