diff --git a/apps/api/src/api/v1/telegram.py b/apps/api/src/api/v1/telegram.py index a8b6937e..4a641300 100644 --- a/apps/api/src/api/v1/telegram.py +++ b/apps/api/src/api/v1/telegram.py @@ -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: 更新資料庫 (簽核/拒絕) # ===================================================================== diff --git a/apps/api/src/services/security_interceptor.py b/apps/api/src/services/security_interceptor.py index ee053a05..de262923 100644 --- a/apps/api/src/services/security_interceptor.py +++ b/apps/api/src/services/security_interceptor.py @@ -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, } diff --git a/apps/api/src/services/telegram_gateway.py b/apps/api/src/services/telegram_gateway.py index c852f916..ffce0a96 100644 --- a/apps/api/src/services/telegram_gateway.py +++ b/apps/api/src/services/telegram_gateway.py @@ -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