fix(telegram): TG-2 + TG-4 修 drift 按鈕 black hole
Some checks failed
CD Pipeline / build-and-deploy (push) Has been cancelled
Some checks failed
CD Pipeline / build-and-deploy (push) Has been cancelled
2026-04-19 凌晨(台北時區)— ogt + Claude Opus 4.7 (1M)
統帥截圖直擊: 按「查看 Diff」→ 變成「執行中」,且看不到還有 21 項。
全景盤點發現 9 個 Telegram 子系統 bug,本 commit 修 2 個最痛的:
## TG-2: drift_view/drift_adopt/drift_revert 3 按鈕**無 handler**
點擊 → fallthrough → UX 黑洞 / 誤觸發 approve 路徑。
修復: handle_callback 在 state guard 後(line 2752 後)加 Step 1.85
offroute: 3 個 drift_* action → _handle_drift_action 專職處理,
不走 nonce approve/reject dispatch,避免誤觸發執行流。
3 個按鈕實作:
- drift_view: 讀 drift_reports → 送新訊息展示全部 items
(HIGH/MEDIUM/INFO emoji + Git vs K8s 原值對照,上限 50 項 4000 字)
- drift_adopt: 呼叫 drift_adopt_service.adopt_drift()
- drift_revert: 呼叫 drift_remediator.revert()
## TG-4: drift card message_id 沒存 Redis → edit 回不了卡片
修復: send_drift_card 成功後 setex f"tg_drift:{incident_id}" TTL 24h,
供 _edit_drift_card_outcome 在 adopt/revert 執行後更新原卡片(先移除
按鈕 + 加「XX by @username (成功/失敗)」簽核戳)。
## 未包含(follow-up):
TG-1 INFO_ACTIONS 擴充(view) — 下一 commit
TG-3 handler 重複分派 — 評估中
TG-5 Bot webhook URL 未設 — 需統帥決策公開 URL
approval card NO_ACTION 誤標 FAILED — 下一 commit
approval card description 矛盾 / responsibility 未知 / 執行後 edit
全景 9 bug 清單詳見 project_phase7_round3_telegram_subsystem_audit(待建)。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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 <code>{html.escape(report_id)}</code>",
|
||||
"parse_mode": "HTML",
|
||||
})
|
||||
return
|
||||
|
||||
_lines = [f"📊 <b>完整 Drift Diff</b> — <code>{html.escape(report_id)}</code>"]
|
||||
_lines.append(f"Namespace: <code>{html.escape(_rpt.namespace)}</code>")
|
||||
_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} <b>{html.escape(_field)}</b>")
|
||||
_lines.append(f" Git: <code>{html.escape(_git)}</code>")
|
||||
_lines.append(f" K8s: <code>{html.escape(_k8s)}</code>")
|
||||
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 查詢失敗: <code>{html.escape(str(_e)[:150])}</code>",
|
||||
"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
|
||||
|
||||
Reference in New Issue
Block a user