From 581b244ad15dc3786da68925aa21affa1a102774 Mon Sep 17 00:00:00 2001 From: OG T Date: Tue, 14 Apr 2026 20:41:22 +0800 Subject: [PATCH] =?UTF-8?q?feat(Phase=205=20Sprint=205.1):=20Telegram=20ca?= =?UTF-8?q?llback=5Fhandler=20=E6=8E=A5=E4=B8=8A=20dispatcher?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 整合點: _handle_callback_query 未知 action fallback 路徑 變更: 1. Line 2601 原「⚠️ 未知操作」改呼叫 _dispatch_category_action() 2. 新增 _dispatch_category_action() method: - 查 callback_action_spec registry - 若 action 不存在 → 回「未知操作」(行為不變) - 若存在 → acknowledge + 從 incident 取 labels + dispatch + reply 原卡片 效果: - check_process / check_port / check_log_* / check_health / open_signoz / open_flywheel 等 10 個查類按鈕現在有完整 flow(雖 Sprint 5.2 還沒接 MCP,但 stub 會 reply) - 當 CD 部署 + Sprint 5.2 實裝 MCP 接線後,查類按鈕自動上線 Sprint 5.1 DOD: - ✅ callback_handler 接線 _dispatch_category_action - ✅ Dispatcher 讀 incident labels 替換模板變數 - ✅ Reply to 原告警卡片(Redis tg_msg lookup) - ⏳ MCP 實際執行(Sprint 5.2) 回歸測試: 109/109 Co-Authored-By: Claude Haiku 4.5 --- apps/api/src/services/telegram_gateway.py | 95 ++++++++++++++++++++++- 1 file changed, 93 insertions(+), 2 deletions(-) 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: 傳送事件詳情訊息 (不修改原始簽核卡片)