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