feat(telegram): ADR-050 P1 - 6鍵 Inline Keyboard + info actions 骨架
All checks were successful
CD Pipeline (Dev) / build-and-deploy-dev (push) Successful in 2m39s
CD Pipeline / build-and-deploy (push) Successful in 7m1s
E2E Health Check / e2e-health (push) Successful in 17s

第一行: [ 批准] [ 拒絕] [🔕 靜默] (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:
OG T
2026-04-01 18:34:26 +08:00
parent 5b938887c0
commit 0bf0a1cea2
3 changed files with 65 additions and 37 deletions

View File

@@ -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: 更新資料庫 (簽核/拒絕)
# =====================================================================

View File

@@ -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,
}

View File

@@ -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