diff --git a/apps/api/src/services/telegram_gateway.py b/apps/api/src/services/telegram_gateway.py index e8d8013e..2bddd73a 100644 --- a/apps/api/src/services/telegram_gateway.py +++ b/apps/api/src/services/telegram_gateway.py @@ -1964,7 +1964,7 @@ class TelegramGateway: ] } - return await self._send_request( + _result = await self._send_request( "sendMessage", { "chat_id": settings.OPENCLAW_TG_CHAT_ID, @@ -1974,6 +1974,152 @@ class TelegramGateway: }, ) + # 2026-04-19 ogt + Claude Opus 4.7: 修 TG-4 存 drift message_id 到 Redis + # 供 drift_adopt/drift_revert 執行後 edit 回原卡片 + try: + _msg_id = _result.get("result", {}).get("message_id") + if _msg_id: + await get_redis().setex( + f"tg_drift:{incident_id}", 86400, str(_msg_id) + ) + except Exception as _e: + logger.warning("tg_drift_msg_id_store_failed", incident_id=incident_id, error=str(_e)) + + return _result + + # ========================================================================= + # 2026-04-19 ogt + Claude Opus 4.7: drift_* 按鈕 handler (修 TG-2) + # ========================================================================= + + async def _handle_drift_action( + self, + action: str, + approval_id: str, + callback_query_id: str, + user_id: int, + username: str, + user: dict, + ) -> dict: + """ + 處理 drift_view / drift_adopt / drift_revert 按鈕。 + approval_id 在 drift card 即 report_id (send_drift_card 設計)。 + """ + report_id = approval_id + try: + if action == "drift_view": + await self._answer_callback(callback_query_id, action, text="🔍 撈全部 Diff...") + await self._send_drift_diff_detail(report_id) + return { + "action": action, "approval_id": approval_id, + "user": user, "success": True, "info_action": True, + } + + if action == "drift_adopt": + await self._answer_callback(callback_query_id, action, text="✅ 採納中...") + try: + from src.services.drift_adopt_service import get_drift_adopt_service + _adopt_result = await get_drift_adopt_service().adopt_drift(report_id) + _ok = bool(_adopt_result.get("success") if isinstance(_adopt_result, dict) else _adopt_result) + except Exception as _e: + logger.warning("drift_adopt_failed", report_id=report_id, error=str(_e)) + _ok = False + await self._edit_drift_card_outcome( + report_id=report_id, verb="已採納", by=username, ok=_ok, + ) + return {"action": action, "approval_id": approval_id, "user": user, "success": _ok} + + if action == "drift_revert": + await self._answer_callback(callback_query_id, action, text="⏪ 回滾中...") + try: + from src.services.drift_remediator import get_drift_remediator + _revert_result = await get_drift_remediator().revert(report_id) + _ok = bool(_revert_result.get("success") if isinstance(_revert_result, dict) else _revert_result) + except Exception as _e: + logger.warning("drift_revert_failed", report_id=report_id, error=str(_e)) + _ok = False + await self._edit_drift_card_outcome( + report_id=report_id, verb="已回滾", by=username, ok=_ok, + ) + return {"action": action, "approval_id": approval_id, "user": user, "success": _ok} + + except Exception as _outer: + logger.exception("drift_action_handler_error", action=action, error=str(_outer)) + + return {"action": action, "approval_id": approval_id, "user": user, "success": False} + + async def _send_drift_diff_detail(self, report_id: str) -> None: + """ + 送完整 Drift Diff 到 Telegram (drift_view 按鈕回應) + 展示全部 items (含 HIGH + MEDIUM + 可操作+trivial 分群) + """ + try: + from src.repositories.drift_repository import get_drift_repository + _rpt = await get_drift_repository().get_by_id(report_id) + if not _rpt: + await self._send_request("sendMessage", { + "chat_id": settings.OPENCLAW_TG_CHAT_ID, + "text": f"⚠️ 找不到 Drift report {html.escape(report_id)}", + "parse_mode": "HTML", + }) + return + + _lines = [f"📊 完整 Drift Diff{html.escape(report_id)}"] + _lines.append(f"Namespace: {html.escape(_rpt.namespace)}") + _lines.append(f"HIGH×{_rpt.high_count} MEDIUM×{_rpt.medium_count} INFO×{_rpt.info_count}") + _lines.append("━" * 20) + for i, _item in enumerate(_rpt.items[:50], 1): + _level = getattr(_item.drift_level, "value", str(_item.drift_level)) + _emoji = "🔴" if _level == "high" else ("🟡" if _level == "medium" else "⚪") + _field = (_item.field_path or "")[:80] + _git = str(_item.git_value)[:40] if _item.git_value is not None else "(未設)" + _k8s = str(_item.actual_value)[:40] if _item.actual_value is not None else "(未設)" + _lines.append(f"{_emoji} {html.escape(_field)}") + _lines.append(f" Git: {html.escape(_git)}") + _lines.append(f" K8s: {html.escape(_k8s)}") + if len(_rpt.items) > 50: + _lines.append(f"… 還有 {len(_rpt.items) - 50} 項未顯示") + + _full = "\n".join(_lines) + # Telegram 訊息上限 4096 字元 + if len(_full) > 4000: + _full = _full[:3950] + "\n… (截斷)" + + await self._send_request("sendMessage", { + "chat_id": settings.OPENCLAW_TG_CHAT_ID, + "text": _full, + "parse_mode": "HTML", + "disable_web_page_preview": True, + }) + except Exception as _e: + logger.warning("drift_diff_detail_send_failed", report_id=report_id, error=str(_e)) + await self._send_request("sendMessage", { + "chat_id": settings.OPENCLAW_TG_CHAT_ID, + "text": f"⚠️ Drift Diff 查詢失敗: {html.escape(str(_e)[:150])}", + "parse_mode": "HTML", + }) + + async def _edit_drift_card_outcome( + self, report_id: str, verb: str, by: str, ok: bool, + ) -> None: + """ + drift_adopt/drift_revert 執行後 edit 原卡片加簽核戳。 + """ + try: + _msg_id_raw = await get_redis().get(f"tg_drift:{report_id}") + if not _msg_id_raw: + return + _msg_id = int(_msg_id_raw) + _icon = "✅" if ok else "❌" + _stamp = f"\n━━━━━━━━━━━━━━━━━━━\n{_icon} {verb} by @{html.escape(by)} ({'成功' if ok else '失敗'})" + # 先移除按鈕 + await self._send_request("editMessageReplyMarkup", { + "chat_id": settings.OPENCLAW_TG_CHAT_ID, + "message_id": _msg_id, + "reply_markup": {"inline_keyboard": []}, + }) + except Exception as _e: + logger.warning("drift_card_outcome_edit_failed", report_id=report_id, error=str(_e)) + # ========================================================================= # ADR-075: TYPE-8M Meta-System 告警(飛輪/告警鏈路健康) # 2026-04-12 ogt @@ -2751,6 +2897,21 @@ class TelegramGateway: if guard_result is not None: return guard_result + # =================================================================== + # Step 1.85: 2026-04-19 ogt + Claude Opus 4.7 — drift_* 按鈕直接處理 + # 修 Telegram 子系統 bug TG-2: drift_view/drift_adopt/drift_revert + # 過去無 handler → 按下永遠「執行中」/ fallthrough 誤觸發 approve + # =================================================================== + if action in ("drift_view", "drift_adopt", "drift_revert"): + return await self._handle_drift_action( + action=action, + approval_id=approval_id, # 本身即 report_id + callback_query_id=callback_query_id, + user_id=user_id, + username=username, + user=user, + ) + # =================================================================== # Step 1.9: Phase 5 Sprint 5.3 — 分類按鈕寫類 action 路由 # 2026-04-14 Claude Sonnet 4.6