feat(approval): 批准/拒絕後立即回應 Telegram + 持久化 message_id 到 DB
All checks were successful
CD Pipeline / build-and-deploy (push) Successful in 13m9s
All checks were successful
CD Pipeline / build-and-deploy (push) Successful in 13m9s
問題:按下 TG 批准/拒絕按鈕後完全沒有任何回應,使用者不知道是否成功。
Telegram message_id 只存 Redis 24h TTL,過期後無法追蹤。
修正:
- approval_records 加 telegram_message_id / telegram_chat_id 欄位(已 ALTER TABLE)
- approval_db.update_telegram_message() — 持久化 message_id 到 DB
- decision_manager: 發送告警卡片後同時寫 Redis + DB
- telegram_gateway._notify_approval_result() — 批准/拒絕後:
1. editMessageReplyMarkup 移除批准/拒絕按鈕,保留資訊按鈕
2. sendMessage reply_to 在原訊息下回覆狀態行
3. fallback: send_notification 發新訊息
- _handle_group_command: chat_id 改為 _chat_id 消除 IDE lint
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -154,6 +154,19 @@ class ApprovalRecord(Base):
|
||||
comment="Associated Incident ID (INC-YYYYMMDD-XXXXXX)",
|
||||
)
|
||||
|
||||
# 2026-04-09 Claude Sonnet 4.6: Telegram 訊息持久化
|
||||
# Redis tg_msg:{id} TTL 24h 過期後仍可查詢,支援跨 Session 狀態更新
|
||||
telegram_message_id: Mapped[int | None] = mapped_column(
|
||||
Integer,
|
||||
nullable=True,
|
||||
comment="Telegram message_id of the approval card sent to operator",
|
||||
)
|
||||
telegram_chat_id: Mapped[int | None] = mapped_column(
|
||||
Integer,
|
||||
nullable=True,
|
||||
comment="Telegram chat_id where the approval card was sent",
|
||||
)
|
||||
|
||||
# Timestamps
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True),
|
||||
|
||||
@@ -610,6 +610,27 @@ class ApprovalDBService:
|
||||
.values(incident_id=incident_id)
|
||||
)
|
||||
|
||||
async def update_telegram_message(
|
||||
self, incident_id: str, telegram_message_id: int, telegram_chat_id: int | None = None
|
||||
) -> None:
|
||||
"""
|
||||
2026-04-09 Claude Sonnet 4.6: 持久化 Telegram message_id 到 DB
|
||||
讓告警訊息 ID 不再只存 Redis(24h TTL),支援長期狀態追蹤和訊息更新。
|
||||
以 incident_id 查找最新 PENDING approval record 並回填。
|
||||
"""
|
||||
async with get_db_context() as db:
|
||||
values: dict = {"telegram_message_id": telegram_message_id}
|
||||
if telegram_chat_id is not None:
|
||||
values["telegram_chat_id"] = telegram_chat_id
|
||||
await db.execute(
|
||||
update(ApprovalRecord)
|
||||
.where(
|
||||
ApprovalRecord.incident_id == incident_id,
|
||||
ApprovalRecord.status == ApprovalStatus.PENDING,
|
||||
)
|
||||
.values(**values)
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# Phase 6.4h: Proposals API 支援方法
|
||||
# =========================================================================
|
||||
|
||||
@@ -183,9 +183,22 @@ async def _push_decision_to_telegram(
|
||||
)
|
||||
|
||||
# 2026-04-09 Claude Sonnet 4.6: 存 message_id → 後續狀態更新在原訊息延續
|
||||
# 同時寫 Redis (快速查詢) 和 DB (持久化,不受 TTL 限制)
|
||||
tg_message_id = tg_result.get("result", {}).get("message_id") if isinstance(tg_result, dict) else None
|
||||
tg_chat_id = tg_result.get("result", {}).get("chat", {}).get("id") if isinstance(tg_result, dict) else None
|
||||
if tg_message_id:
|
||||
await redis.setex(f"tg_msg:{incident.incident_id}", 86400, str(tg_message_id))
|
||||
# 持久化到 DB
|
||||
try:
|
||||
from src.services.approval_db import get_approval_service as _get_approval_svc
|
||||
_approval_svc = _get_approval_svc()
|
||||
await _approval_svc.update_telegram_message(
|
||||
incident_id=incident.incident_id,
|
||||
telegram_message_id=tg_message_id,
|
||||
telegram_chat_id=tg_chat_id,
|
||||
)
|
||||
except Exception as _e:
|
||||
logger.warning("telegram_message_id_db_save_failed", incident_id=incident.incident_id, error=str(_e))
|
||||
|
||||
# 🔴 發送成功後設置去重 key (TTL 10 分鐘)
|
||||
await redis.setex(dedup_key, 600, "1")
|
||||
|
||||
@@ -3622,7 +3622,7 @@ class TelegramGateway:
|
||||
|
||||
logger.info("group_message_handled", user_id=user_id, text=text[:50])
|
||||
|
||||
async def _handle_group_command(self, cmd: str, chat_id: int, message_id: int | None) -> None: # noqa: ARG002
|
||||
async def _handle_group_command(self, cmd: str, _chat_id: int, message_id: int | None) -> None:
|
||||
"""
|
||||
SRE 群組 Slash Commands (2026-04-03 ogt: 方案B)
|
||||
|
||||
@@ -3730,6 +3730,95 @@ class TelegramGateway:
|
||||
await self._http_client.post(url, json={"chat_id": chat_id, "action": action})
|
||||
except: pass
|
||||
|
||||
async def _notify_approval_result(
|
||||
self,
|
||||
message_id: int | None,
|
||||
incident_id: str,
|
||||
action: str,
|
||||
username: str,
|
||||
execution_triggered: bool,
|
||||
) -> None:
|
||||
"""
|
||||
2026-04-09 Claude Sonnet 4.6: 批准/拒絕後立即更新 Telegram 訊息狀態。
|
||||
|
||||
策略:
|
||||
1. editMessageReplyMarkup — 移除批准/拒絕按鈕,保留資訊按鈕
|
||||
2. sendMessage reply_to → 在原訊息下方附加狀態行
|
||||
3. 如果 message_id 找不到,fallback 到 send_notification
|
||||
"""
|
||||
import html as _html
|
||||
|
||||
chat_id = settings.OPENCLAW_TG_CHAT_ID
|
||||
if not chat_id:
|
||||
return
|
||||
|
||||
# 找到原始告警訊息 ID(優先 Redis,fallback DB)
|
||||
orig_msg_id = message_id
|
||||
if not orig_msg_id:
|
||||
try:
|
||||
redis = await get_redis()
|
||||
_val = await redis.get(f"tg_msg:{incident_id}")
|
||||
if _val:
|
||||
orig_msg_id = int(_val)
|
||||
else:
|
||||
# DB fallback
|
||||
from src.services.approval_db import get_approval_service as _svc
|
||||
_approvals = await _svc().get_all_approvals(incident_id=incident_id)
|
||||
if _approvals and _approvals[0].telegram_message_id:
|
||||
orig_msg_id = _approvals[0].telegram_message_id
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if action == "approve":
|
||||
status_emoji = "✅"
|
||||
status_text = f"<b>已批准</b> by {_html.escape(username)}"
|
||||
suffix = "⚡ 執行中..." if execution_triggered else "👀 等待執行"
|
||||
else:
|
||||
status_emoji = "❌"
|
||||
status_text = f"<b>已拒絕</b> by {_html.escape(username)}"
|
||||
suffix = ""
|
||||
|
||||
status_line = f"{status_emoji} {status_text} {suffix}".strip()
|
||||
|
||||
if orig_msg_id:
|
||||
try:
|
||||
# 1. 移除批准/拒絕按鈕(只保留資訊按鈕列)
|
||||
info_buttons = [[
|
||||
{"text": "📋 詳情", "callback_data": f"detail:{incident_id}"},
|
||||
{"text": "📊 歷史", "callback_data": f"history:{incident_id}"},
|
||||
]]
|
||||
await self._http_client.post(
|
||||
f"https://api.telegram.org/bot{settings.OPENCLAW_TG_BOT_TOKEN}/editMessageReplyMarkup",
|
||||
json={
|
||||
"chat_id": chat_id,
|
||||
"message_id": orig_msg_id,
|
||||
"reply_markup": {"inline_keyboard": info_buttons},
|
||||
},
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
# 2. 在原訊息下回覆狀態
|
||||
await self._http_client.post(
|
||||
f"https://api.telegram.org/bot{settings.OPENCLAW_TG_BOT_TOKEN}/sendMessage",
|
||||
json={
|
||||
"chat_id": chat_id,
|
||||
"text": status_line,
|
||||
"parse_mode": "HTML",
|
||||
"reply_to_message_id": orig_msg_id,
|
||||
},
|
||||
)
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# fallback: 發新通知
|
||||
try:
|
||||
await self.send_notification(status_line, parse_mode="HTML")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
async def _execute_approval_action(
|
||||
self,
|
||||
action: str,
|
||||
@@ -3775,7 +3864,6 @@ class TelegramGateway:
|
||||
return
|
||||
|
||||
if action == "approve":
|
||||
# 2026-03-29 ogt: 正確呼叫 sign_approval (返回 tuple)
|
||||
approval, message, execution_triggered = await service.sign_approval(
|
||||
approval_id=approval_uuid,
|
||||
signer_id=f"tg_{user_id}",
|
||||
@@ -3791,9 +3879,16 @@ class TelegramGateway:
|
||||
status=approval.status.value,
|
||||
execution_triggered=execution_triggered,
|
||||
)
|
||||
# 2026-04-09 Claude Sonnet 4.6: 回應 Telegram — 更新訊息狀態 + answer callback
|
||||
await self._notify_approval_result(
|
||||
message_id=message_id,
|
||||
incident_id=approval_id,
|
||||
action="approve",
|
||||
username=username,
|
||||
execution_triggered=execution_triggered,
|
||||
)
|
||||
|
||||
elif action == "reject":
|
||||
# 2026-03-29 ogt: 正確呼叫 reject_approval (返回 tuple)
|
||||
approval, message = await service.reject_approval(
|
||||
approval_id=approval_uuid,
|
||||
rejector_id=f"tg_{user_id}",
|
||||
@@ -3807,9 +3902,16 @@ class TelegramGateway:
|
||||
approval_id=approval_id,
|
||||
user_id=user_id,
|
||||
)
|
||||
# 2026-04-09 Claude Sonnet 4.6: 回應 Telegram — 更新訊息狀態
|
||||
await self._notify_approval_result(
|
||||
message_id=message_id,
|
||||
incident_id=approval_id,
|
||||
action="reject",
|
||||
username=username,
|
||||
execution_triggered=False,
|
||||
)
|
||||
|
||||
elif action == "tune":
|
||||
# 自動調優已在 handle_callback 中處理
|
||||
logger.info(
|
||||
"telegram_auto_tuning_via_polling",
|
||||
approval_id=approval_id,
|
||||
|
||||
Reference in New Issue
Block a user