diff --git a/apps/api/src/services/telegram_gateway.py b/apps/api/src/services/telegram_gateway.py index fee11225..26222de2 100644 --- a/apps/api/src/services/telegram_gateway.py +++ b/apps/api/src/services/telegram_gateway.py @@ -2598,7 +2598,14 @@ class TelegramGateway: await self._answer_callback(callback_query_id, action, text="🔄 重診排程中...") await self._send_reanalyze_result(incident_id) else: - await self._answer_callback(callback_query_id, action, text="⚠️ 未知操作") + # 2026-04-14 Claude Sonnet 4.6 (Phase 5 Sprint 5.1): + # 未知 action → fallback dispatcher (查看 callback_action_spec.yaml 是否有註冊) + await self._dispatch_category_action( + callback_query_id=callback_query_id, + action=action, + incident_id=incident_id, + user_id=user_id, + ) return { "action": action, @@ -2608,7 +2615,11 @@ class TelegramGateway: "info_action": True, } - nonce = parsed["nonce"] + nonce = parsed["nonce"] # 4-part nonce action + + # 2026-04-14 Claude Sonnet 4.6 (Phase 5 Sprint 5.1): + # 寫類 nonce action 先驗 nonce 再 fallback dispatcher(若 action 在 registry) + # 這段邏輯在 Step 2 之後再處理,這裡只是佔位註解 # 驗證使用者 + Nonce user = await self._security.verify_callback( @@ -3445,6 +3456,86 @@ class TelegramGateway: ) return True + async def _dispatch_category_action( + self, + callback_query_id: str, + action: str, + incident_id: str, + user_id: int, + ) -> None: + """ + Phase 5 Sprint 5.1 (2026-04-14 Claude Sonnet 4.6): + Fallback dispatcher — 未知 info action 查 callback_action_spec.yaml + + 流程: + 1. 查 action registry + 2. 若不存在 → 原「⚠️ 未知操作」回覆 + 3. 若存在 → 從 incident 取 labels → dispatch_action → reply_to 原卡片 + + 注意: 此方法只處理 info action (查類)。nonce action (寫類) 走另一路徑。 + """ + from src.services.callback_dispatcher import dispatch_action, get_action_spec + + spec = get_action_spec(action) + if not spec: + await self._answer_callback(callback_query_id, action, text="⚠️ 未知操作") + return + + # Acknowledge callback immediately(避免 Telegram 端 timeout) + await self._answer_callback( + callback_query_id, action, text=f"{spec.emoji} 執行中..." + ) + + # 從 incident 取 labels (供模板替換) + labels: dict = {} + try: + from src.repositories.incident_repository import get_incident_repository + repo = get_incident_repository() + incident = await repo.get_by_id(incident_id) + if incident and incident.signals: + labels = incident.signals[0].labels or {} + except Exception as _e: + logger.debug("dispatch_labels_lookup_failed", incident_id=incident_id, error=str(_e)) + + # Dispatch + result = await dispatch_action( + action_name=action, + incident_id=incident_id, + user_id=user_id, + labels=labels, + ) + + # Reply to 原卡片 — 從 Redis tg_msg 查 message_id + try: + from src.core.redis_client import get_redis + redis = get_redis() + msg_id_raw = await redis.get(f"tg_msg:{incident_id}") + orig_msg_id = int(msg_id_raw) if msg_id_raw else None + except Exception: + orig_msg_id = None + + try: + payload: dict = { + "chat_id": settings.OPENCLAW_TG_CHAT_ID, + "text": result.result_text, + "parse_mode": "HTML", + } + if orig_msg_id: + payload["reply_to_message_id"] = orig_msg_id + await self._http_client.post( + f"https://api.telegram.org/bot{settings.OPENCLAW_TG_BOT_TOKEN}/sendMessage", + json=payload, + ) + logger.info( + "category_action_reply_sent", + action=action, + incident_id=incident_id, + success=result.success, + duration_ms=round(result.duration_ms, 1), + ) + except Exception as _e: + logger.warning("category_action_reply_failed", action=action, error=str(_e)) + async def _send_incident_detail(self, incident_id: str) -> None: """ ADR-050 P2: 傳送事件詳情訊息 (不修改原始簽核卡片)