feat(api): P1 Telegram 按鈕優化 - 稍後/靜默
新增按鈕: - ⏰ 稍後 (snooze): 延遲 30 分鐘後再提醒 - 🔕 靜默 1h (silence): 同類資源告警靜默 1 小時 實作細節: - telegram_gateway.py: 新增 _handle_snooze/_handle_silence - decision_manager.py: 發送前檢查 silence 狀態 - Redis Key: telegram_snooze:{approval_id}, telegram_silence:{resource_name} - Skill 03 v1.5 → v1.6 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -73,6 +73,18 @@ async def _push_decision_to_telegram(
|
||||
)
|
||||
return
|
||||
|
||||
# 🔴 靜默檢查:此資源是否被靜默 (2026-03-27 P1 優化)
|
||||
target = incident.affected_services[0] if incident.affected_services else "unknown"
|
||||
silence_key = f"telegram_silence:{target}"
|
||||
if await redis.exists(silence_key):
|
||||
logger.info(
|
||||
"telegram_push_silenced",
|
||||
reason="Resource is silenced",
|
||||
incident_id=incident.incident_id,
|
||||
resource=target,
|
||||
)
|
||||
return
|
||||
|
||||
# 檢查是否有設定 Bot Token
|
||||
if not settings.OPENCLAW_TG_BOT_TOKEN:
|
||||
logger.debug(
|
||||
|
||||
@@ -31,12 +31,21 @@ import httpx
|
||||
import structlog
|
||||
|
||||
from src.core.config import settings
|
||||
from src.core.redis_client import get_redis
|
||||
from src.services.security_interceptor import (
|
||||
NonceReplayError,
|
||||
UserNotWhitelistedError,
|
||||
get_security_interceptor,
|
||||
)
|
||||
|
||||
# =============================================================================
|
||||
# Snooze/Silence Redis Keys (2026-03-27 P1 優化)
|
||||
# =============================================================================
|
||||
SNOOZE_KEY_PREFIX = "telegram_snooze:" # {approval_id} -> 稍後提醒
|
||||
SILENCE_KEY_PREFIX = "telegram_silence:" # {resource_name} -> 靜默
|
||||
SNOOZE_TTL_SECONDS = 30 * 60 # 30 分鐘
|
||||
SILENCE_TTL_SECONDS = 60 * 60 # 1 小時
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
@@ -331,11 +340,14 @@ class TelegramGateway:
|
||||
auto_tuning_command: str = "",
|
||||
) -> dict:
|
||||
"""
|
||||
建立 Inline Keyboard (簽核按鈕 + 自動調優)
|
||||
建立 Inline Keyboard (簽核按鈕 + 延遲/靜默 + 自動調優)
|
||||
|
||||
SOUL.md 規範 + v7.0:
|
||||
[✅ 簽核] [❌ 拒絕]
|
||||
[⚡ 執行自動調優]
|
||||
2026-03-27 ogt: P1 優化 - 新增稍後/靜默按鈕
|
||||
|
||||
佈局:
|
||||
第一行: [✅ 簽核] [❌ 拒絕]
|
||||
第二行: [⏰ 稍後] [🔕 靜默 1h]
|
||||
第三行: [⚡ 執行自動調優] (可選)
|
||||
|
||||
Args:
|
||||
approval_id: 簽核單 ID
|
||||
@@ -348,8 +360,10 @@ class TelegramGateway:
|
||||
# 產生 Nonce (防重放)
|
||||
approve_nonce = self._security.generate_callback_nonce(approval_id, "approve")
|
||||
reject_nonce = self._security.generate_callback_nonce(approval_id, "reject")
|
||||
snooze_nonce = self._security.generate_callback_nonce(approval_id, "snooze")
|
||||
silence_nonce = self._security.generate_callback_nonce(approval_id, "silence")
|
||||
|
||||
# 基本按鈕行
|
||||
# 第一行: 主要操作
|
||||
buttons = [
|
||||
[
|
||||
{
|
||||
@@ -360,10 +374,21 @@ class TelegramGateway:
|
||||
"text": "❌ 拒絕",
|
||||
"callback_data": reject_nonce,
|
||||
},
|
||||
]
|
||||
],
|
||||
# 第二行: 延遲/靜默 (2026-03-27 P1 優化)
|
||||
[
|
||||
{
|
||||
"text": "⏰ 稍後",
|
||||
"callback_data": snooze_nonce,
|
||||
},
|
||||
{
|
||||
"text": "🔕 靜默 1h",
|
||||
"callback_data": silence_nonce,
|
||||
},
|
||||
],
|
||||
]
|
||||
|
||||
# 自動調優按鈕 (v7.0)
|
||||
# 第三行: 自動調優按鈕 (v7.0)
|
||||
if include_auto_tuning and auto_tuning_command:
|
||||
tuning_nonce = self._security.generate_callback_nonce(approval_id, "tune")
|
||||
buttons.append([
|
||||
@@ -560,6 +585,59 @@ class TelegramGateway:
|
||||
"auto_tuning_result": auto_tuning_result,
|
||||
}
|
||||
|
||||
# ===================================================================
|
||||
# Step 2.5: 處理稍後/靜默 (2026-03-27 P1 優化)
|
||||
# ===================================================================
|
||||
if action == "snooze":
|
||||
snooze_result = await self._handle_snooze(
|
||||
approval_id=approval_id,
|
||||
username=username,
|
||||
)
|
||||
await self._answer_callback(
|
||||
callback_query_id,
|
||||
"snooze",
|
||||
text="⏰ 30 分鐘後再提醒",
|
||||
)
|
||||
await self._update_message_after_action(
|
||||
message_id=message_id,
|
||||
action="snooze",
|
||||
username=username,
|
||||
original_text=original_text,
|
||||
)
|
||||
return {
|
||||
"action": action,
|
||||
"approval_id": approval_id,
|
||||
"user": user,
|
||||
"success": True,
|
||||
"snooze_result": snooze_result,
|
||||
}
|
||||
|
||||
if action == "silence":
|
||||
silence_result = await self._handle_silence(
|
||||
approval_id=approval_id,
|
||||
username=username,
|
||||
original_text=original_text,
|
||||
)
|
||||
await self._answer_callback(
|
||||
callback_query_id,
|
||||
"silence",
|
||||
text="🔕 此類告警靜默 1 小時",
|
||||
)
|
||||
await self._update_message_after_action(
|
||||
message_id=message_id,
|
||||
action="silence",
|
||||
username=username,
|
||||
original_text=original_text,
|
||||
extra_info=silence_result.get("resource_name", ""),
|
||||
)
|
||||
return {
|
||||
"action": action,
|
||||
"approval_id": approval_id,
|
||||
"user": user,
|
||||
"success": True,
|
||||
"silence_result": silence_result,
|
||||
}
|
||||
|
||||
# ===================================================================
|
||||
# Step 3: 回應 Callback Query (簽核/拒絕)
|
||||
# ===================================================================
|
||||
@@ -686,6 +764,116 @@ class TelegramGateway:
|
||||
"error": str(e),
|
||||
}
|
||||
|
||||
async def _handle_snooze(
|
||||
self,
|
||||
approval_id: str,
|
||||
username: str,
|
||||
) -> dict:
|
||||
"""
|
||||
處理稍後提醒 (2026-03-27 P1 優化)
|
||||
|
||||
功能: 延遲 30 分鐘後再提醒此告警
|
||||
|
||||
Args:
|
||||
approval_id: 簽核單 ID
|
||||
username: 執行者名稱
|
||||
|
||||
Returns:
|
||||
dict: 處理結果
|
||||
"""
|
||||
try:
|
||||
redis = get_redis()
|
||||
snooze_key = f"{SNOOZE_KEY_PREFIX}{approval_id}"
|
||||
|
||||
# 設置 30 分鐘延遲標記
|
||||
await redis.setex(
|
||||
snooze_key,
|
||||
SNOOZE_TTL_SECONDS,
|
||||
f"{username}:{datetime.now(UTC).isoformat()}",
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"telegram_snooze_set",
|
||||
approval_id=approval_id,
|
||||
username=username,
|
||||
ttl_minutes=SNOOZE_TTL_SECONDS // 60,
|
||||
)
|
||||
|
||||
return {
|
||||
"snoozed": True,
|
||||
"approval_id": approval_id,
|
||||
"snooze_until": datetime.now(UTC).isoformat(),
|
||||
"ttl_minutes": SNOOZE_TTL_SECONDS // 60,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error("snooze_error", error=str(e), approval_id=approval_id)
|
||||
return {
|
||||
"snoozed": False,
|
||||
"error": str(e),
|
||||
}
|
||||
|
||||
async def _handle_silence(
|
||||
self,
|
||||
approval_id: str,
|
||||
username: str,
|
||||
original_text: str,
|
||||
) -> dict:
|
||||
"""
|
||||
處理靜默 1 小時 (2026-03-27 P1 優化)
|
||||
|
||||
功能: 同類告警 (相同資源) 1 小時內不再發送
|
||||
|
||||
Args:
|
||||
approval_id: 簽核單 ID
|
||||
username: 執行者名稱
|
||||
original_text: 原始訊息 (用於解析資源名稱)
|
||||
|
||||
Returns:
|
||||
dict: 處理結果
|
||||
"""
|
||||
try:
|
||||
redis = get_redis()
|
||||
|
||||
# 從原始訊息解析資源名稱 (格式: 🎯 資源: xxx)
|
||||
resource_name = "unknown"
|
||||
for line in original_text.split("\n"):
|
||||
if "🎯 資源:" in line or "🎯 資源: " in line:
|
||||
resource_name = line.split(":")[-1].strip()
|
||||
break
|
||||
|
||||
silence_key = f"{SILENCE_KEY_PREFIX}{resource_name}"
|
||||
|
||||
# 設置 1 小時靜默標記
|
||||
await redis.setex(
|
||||
silence_key,
|
||||
SILENCE_TTL_SECONDS,
|
||||
f"{username}:{datetime.now(UTC).isoformat()}:{approval_id}",
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"telegram_silence_set",
|
||||
approval_id=approval_id,
|
||||
resource_name=resource_name,
|
||||
username=username,
|
||||
ttl_hours=SILENCE_TTL_SECONDS // 3600,
|
||||
)
|
||||
|
||||
return {
|
||||
"silenced": True,
|
||||
"approval_id": approval_id,
|
||||
"resource_name": resource_name,
|
||||
"silence_until": datetime.now(UTC).isoformat(),
|
||||
"ttl_hours": SILENCE_TTL_SECONDS // 3600,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error("silence_error", error=str(e), approval_id=approval_id)
|
||||
return {
|
||||
"silenced": False,
|
||||
"error": str(e),
|
||||
}
|
||||
|
||||
async def _answer_callback(
|
||||
self,
|
||||
callback_query_id: str,
|
||||
@@ -700,6 +888,10 @@ class TelegramGateway:
|
||||
text = "❌ 已拒絕"
|
||||
elif action == "tune":
|
||||
text = "⚡ 調優中..."
|
||||
elif action == "snooze":
|
||||
text = "⏰ 30 分鐘後再提醒"
|
||||
elif action == "silence":
|
||||
text = "🔕 此類告警靜默 1 小時"
|
||||
else:
|
||||
text = "✓ 已處理"
|
||||
|
||||
@@ -725,7 +917,7 @@ class TelegramGateway:
|
||||
- 必須在底部加上分隔線與簽核狀態
|
||||
- 移除所有按鈕
|
||||
"""
|
||||
# 構建鋼印
|
||||
# 構建鋼印 (2026-03-27 ogt: 新增 snooze/silence)
|
||||
if action == "approve":
|
||||
stamp = f"✅ 已由 @{username} 授權執行"
|
||||
elif action == "reject":
|
||||
@@ -734,6 +926,11 @@ class TelegramGateway:
|
||||
stamp = f"⚡ 已由 @{username} 觸發自動調優 (Shadow Mode)"
|
||||
if extra_info:
|
||||
stamp += "\n📝 指令已記錄"
|
||||
elif action == "snooze":
|
||||
stamp = f"⏰ @{username} 已設定 30 分鐘後再提醒"
|
||||
elif action == "silence":
|
||||
resource_info = f" ({extra_info})" if extra_info else ""
|
||||
stamp = f"🔕 @{username} 已靜默此類告警 1 小時{resource_info}"
|
||||
else:
|
||||
stamp = f"✓ 已由 @{username} 處理"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user