feat(telegram): ADR-050 P1 - 6鍵 Inline Keyboard + info actions 骨架
第一行: [✅ 批准] [❌ 拒絕] [🔕 靜默] (nonce 防重放) 第二行: [📋 詳情] [🔄 重診] [📊 歷史] (read-only, action:incident_id 格式) - security_interceptor: parse_callback_data 支援 2-part info action 格式 - telegram_gateway: _build_inline_keyboard 新增 incident_id 參數 - telegram.py: info_action 短路,不觸發 DB 操作 P2 待實作: detail/reanalyze/history 回傳實際資料 (目前回傳「功能開發中」) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -125,6 +125,12 @@ async def telegram_webhook(
|
||||
if not result.get("success"):
|
||||
return {"ok": False, "message": result.get("error")}
|
||||
|
||||
# =====================================================================
|
||||
# Step 2.5: ADR-050 Info Actions (read-only, 無需 DB 操作)
|
||||
# =====================================================================
|
||||
if result.get("info_action"):
|
||||
return {"ok": True, "message": f"info:{result['action']}", "approval_id": result["approval_id"]}
|
||||
|
||||
# =====================================================================
|
||||
# Step 3: 更新資料庫 (簽核/拒絕)
|
||||
# =====================================================================
|
||||
|
||||
@@ -423,15 +423,28 @@ class TelegramSecurityInterceptor:
|
||||
"""
|
||||
解析 Callback Data
|
||||
|
||||
格式: {action}:{approval_id}:{timestamp}:{random}
|
||||
格式一 (寫操作,nonce 防重放): {action}:{approval_id}:{timestamp}:{random}
|
||||
格式二 (讀操作,ADR-050): {action}:{incident_id} (2 parts)
|
||||
|
||||
Args:
|
||||
callback_data: Telegram callback_data 字串
|
||||
|
||||
Returns:
|
||||
dict: 解析結果 {action, approval_id, timestamp, nonce}
|
||||
dict: 解析結果
|
||||
- 格式一: {action, approval_id, timestamp, nonce, is_info_action: False}
|
||||
- 格式二: {action, incident_id, is_info_action: True}
|
||||
"""
|
||||
# 2026-04-01 Claude Code (ADR-050): 支援 read-only info actions (2-part format)
|
||||
INFO_ACTIONS = {"detail", "reanalyze", "history"}
|
||||
parts = callback_data.split(":")
|
||||
if len(parts) == 2 and parts[0] in INFO_ACTIONS:
|
||||
return {
|
||||
"action": parts[0],
|
||||
"incident_id": parts[1],
|
||||
"approval_id": parts[1], # 相容舊版呼叫
|
||||
"is_info_action": True,
|
||||
}
|
||||
|
||||
if len(parts) != 4:
|
||||
raise ValueError(f"Invalid callback_data format: {callback_data}")
|
||||
|
||||
@@ -440,6 +453,7 @@ class TelegramSecurityInterceptor:
|
||||
"approval_id": parts[1],
|
||||
"timestamp": int(parts[2]),
|
||||
"nonce": callback_data, # 整個字串作為 nonce
|
||||
"is_info_action": False,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1147,64 +1147,52 @@ class TelegramGateway:
|
||||
approval_id: str,
|
||||
include_auto_tuning: bool = True,
|
||||
auto_tuning_command: str = "",
|
||||
incident_id: str = "",
|
||||
) -> dict:
|
||||
"""
|
||||
建立 Inline Keyboard (簽核按鈕 + 延遲/靜默 + 自動調優)
|
||||
建立 Inline Keyboard (ADR-050 v2.0 六鍵佈局)
|
||||
|
||||
2026-03-27 ogt: P1 優化 - 新增稍後/靜默按鈕
|
||||
|
||||
佈局:
|
||||
第一行: [✅ 簽核] [❌ 拒絕]
|
||||
第二行: [⏰ 稍後] [🔕 靜默 1h]
|
||||
第三行: [⚡ 執行自動調優] (可選)
|
||||
2026-04-01 Claude Code (ADR-050): 重組為 6 鍵 + 可選自動調優
|
||||
- 第一行: [✅ 批准] [❌ 拒絕] [🔕 靜默] ← nonce 防重放
|
||||
- 第二行: [📋 詳情] [🔄 重診] [📊 歷史] ← incident_id format (read-only)
|
||||
- 第三行: [⚡ 自動調優] (可選)
|
||||
|
||||
Args:
|
||||
approval_id: 簽核單 ID
|
||||
approval_id: 簽核單 ID (用於 nonce 生成)
|
||||
include_auto_tuning: 是否包含自動調優按鈕
|
||||
auto_tuning_command: kubectl 調優指令
|
||||
incident_id: 關聯 Incident ID (用於 detail/reanalyze/history 按鈕)
|
||||
|
||||
Returns:
|
||||
dict: Telegram InlineKeyboardMarkup
|
||||
"""
|
||||
# 產生 Nonce (防重放)
|
||||
# 產生 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")
|
||||
|
||||
# 第一行: 主要操作
|
||||
# 第一行: 主要簽核操作 (nonce 保護)
|
||||
buttons = [
|
||||
[
|
||||
{
|
||||
"text": "✅ 簽核",
|
||||
"callback_data": approve_nonce,
|
||||
},
|
||||
{
|
||||
"text": "❌ 拒絕",
|
||||
"callback_data": reject_nonce,
|
||||
},
|
||||
],
|
||||
# 第二行: 延遲/靜默 (2026-03-27 P1 優化)
|
||||
[
|
||||
{
|
||||
"text": "⏰ 稍後",
|
||||
"callback_data": snooze_nonce,
|
||||
},
|
||||
{
|
||||
"text": "🔕 靜默 1h",
|
||||
"callback_data": silence_nonce,
|
||||
},
|
||||
{"text": "✅ 批准", "callback_data": approve_nonce},
|
||||
{"text": "❌ 拒絕", "callback_data": reject_nonce},
|
||||
{"text": "🔕 靜默", "callback_data": silence_nonce},
|
||||
],
|
||||
]
|
||||
|
||||
# 第二行: 資訊查詢按鈕 (ADR-050: read-only, format: action:incident_id)
|
||||
if incident_id:
|
||||
buttons.append([
|
||||
{"text": "📋 詳情", "callback_data": f"detail:{incident_id}"},
|
||||
{"text": "🔄 重診", "callback_data": f"reanalyze:{incident_id}"},
|
||||
{"text": "📊 歷史", "callback_data": f"history:{incident_id}"},
|
||||
])
|
||||
|
||||
# 第三行: 自動調優按鈕 (v7.0)
|
||||
if include_auto_tuning and auto_tuning_command:
|
||||
tuning_nonce = self._security.generate_callback_nonce(approval_id, "tune")
|
||||
buttons.append([
|
||||
{
|
||||
"text": "⚡ 執行自動調優",
|
||||
"callback_data": tuning_nonce,
|
||||
}
|
||||
{"text": "⚡ 執行自動調優", "callback_data": tuning_nonce}
|
||||
])
|
||||
|
||||
return {"inline_keyboard": buttons}
|
||||
@@ -1807,11 +1795,31 @@ class TelegramGateway:
|
||||
"""
|
||||
try:
|
||||
# ===================================================================
|
||||
# Step 1: 安全驗證 (白名單 + Nonce)
|
||||
# Step 1: 解析 Callback Data (支援兩種格式)
|
||||
# ===================================================================
|
||||
parsed = self._security.parse_callback_data(callback_data)
|
||||
action = parsed["action"]
|
||||
approval_id = parsed["approval_id"]
|
||||
|
||||
# ===================================================================
|
||||
# Step 1.5: ADR-050 Info Actions (read-only, 只需白名單驗證)
|
||||
# ===================================================================
|
||||
# 2026-04-01 Claude Code (ADR-050 P1): detail/reanalyze/history
|
||||
if parsed.get("is_info_action"):
|
||||
if not self._security.is_whitelisted(user_id):
|
||||
raise UserNotWhitelistedError(f"User {user_id} not in whitelist")
|
||||
await self._answer_callback(
|
||||
callback_query_id, action,
|
||||
text={"detail": "📋 功能開發中", "reanalyze": "🔄 功能開發中", "history": "📊 功能開發中"}.get(action, "⏳ 功能開發中"),
|
||||
)
|
||||
return {
|
||||
"action": action,
|
||||
"approval_id": approval_id,
|
||||
"user": {"id": user_id, "username": username},
|
||||
"success": True,
|
||||
"info_action": True,
|
||||
}
|
||||
|
||||
nonce = parsed["nonce"]
|
||||
|
||||
# 驗證使用者 + Nonce
|
||||
|
||||
Reference in New Issue
Block a user