From 0dd4b486c5e4245b414364aec3cde43b26efff7e Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 18 May 2026 09:47:40 +0800 Subject: [PATCH] fix(telegram): keep info callbacks nonfatal --- apps/api/src/services/telegram_gateway.py | 30 +++++++++++++---- .../tests/test_telegram_message_templates.py | 29 ++++++++++++++++ docs/LOGBOOK.md | 33 +++++++++++++++++++ 3 files changed, 85 insertions(+), 7 deletions(-) diff --git a/apps/api/src/services/telegram_gateway.py b/apps/api/src/services/telegram_gateway.py index 704c45bd2..6e43e050d 100644 --- a/apps/api/src/services/telegram_gateway.py +++ b/apps/api/src/services/telegram_gateway.py @@ -4017,17 +4017,17 @@ class TelegramGateway: if action == "detail": # ADR-050 P2: 取得事件詳情,傳送新訊息 (保留原始簽核卡片+按鈕) # 2026-04-01 Claude Code (ADR-050 P2) - await self._answer_callback(callback_query_id, action, text="📋 詳情傳送中...") + await self._answer_callback_nonfatal(callback_query_id, action, text="📋 詳情傳送中...") await self._send_incident_detail(incident_id) elif action == "history": # ADR-050 P2: 取得頻率統計 # 2026-04-01 Claude Code (ADR-050 P2) - await self._answer_callback(callback_query_id, action, text="📊 歷史統計傳送中...") + await self._answer_callback_nonfatal(callback_query_id, action, text="📊 歷史統計傳送中...") await self._send_incident_history(incident_id) elif action == "reanalyze": # ADR-050 P2: 觸發重診 # 2026-04-01 Claude Code (ADR-050 P2): reanalyze button handler - await self._answer_callback(callback_query_id, action, text="🔄 重診排程中...") + await self._answer_callback_nonfatal(callback_query_id, action, text="🔄 重診排程中...") await self._send_reanalyze_result(incident_id) elif action == "drift_view_page": # 2026-04-20 P0.1 ogt + Claude Opus 4.7: drift_view 分頁切頁 @@ -4037,7 +4037,7 @@ class TelegramGateway: _page_num = int(_page_str) except ValueError: _rid, _page_num = incident_id, 0 - await self._answer_callback( + await self._answer_callback_nonfatal( callback_query_id, action, text=f"📄 切換至第 {_page_num + 1} 頁..." ) await self._send_drift_diff_detail(_rid or incident_id, page=_page_num) @@ -4408,7 +4408,7 @@ class TelegramGateway: except UserNotWhitelistedError as e: logger.warning("telegram_callback_denied", error=str(e), user_id=user_id) - await self._answer_callback( + await self._answer_callback_nonfatal( callback_query_id, "denied", text="⛔ 您沒有簽核權限", @@ -4417,7 +4417,7 @@ class TelegramGateway: except NonceReplayError as e: logger.warning("telegram_callback_replay", error=str(e)) - await self._answer_callback( + await self._answer_callback_nonfatal( callback_query_id, "replay", text="⚠️ 此操作已處理過", @@ -4426,7 +4426,7 @@ class TelegramGateway: except Exception as e: logger.error("telegram_callback_error", error=str(e)) - await self._answer_callback( + await self._answer_callback_nonfatal( callback_query_id, "error", text="❌ 處理失敗", @@ -5017,6 +5017,22 @@ class TelegramGateway: "show_alert": False, }) + async def _answer_callback_nonfatal( + self, + callback_query_id: str, + action: str, + text: str | None = None, + ) -> None: + """Best-effort callback toast; never block the actual DB-backed reply.""" + try: + await self._answer_callback(callback_query_id, action, text=text) + except Exception as exc: + logger.warning( + "telegram_answer_callback_nonfatal_failed", + action=action, + error=str(exc), + ) + async def _update_message_after_action( self, message_id: int, diff --git a/apps/api/tests/test_telegram_message_templates.py b/apps/api/tests/test_telegram_message_templates.py index 66739c7ee..d631fee79 100644 --- a/apps/api/tests/test_telegram_message_templates.py +++ b/apps/api/tests/test_telegram_message_templates.py @@ -195,6 +195,35 @@ async def test_send_html_line_message_attaches_awooop_markup_to_first_chunk(monk assert all("reply_markup" not in payload for _, payload in sent_requests[1:]) +@pytest.mark.asyncio +async def test_info_callback_sends_history_when_answer_callback_is_stale(monkeypatch): + """Telegram answerCallbackQuery 400 不得阻斷詳情/歷史 DB truth-chain reply。""" + gateway = TelegramGateway() + sent_history = [] + + monkeypatch.setattr(gateway._security, "is_whitelisted", lambda _user_id: True) + + async def stale_answer(*_args, **_kwargs): + raise telegram_gateway_module.TelegramGatewayError("HTTP error: 400") + + async def fake_history(incident_id: str): + sent_history.append(incident_id) + + monkeypatch.setattr(gateway, "_answer_callback", stale_answer) + monkeypatch.setattr(gateway, "_send_incident_history", fake_history) + + result = await gateway.handle_callback( + callback_query_id="stale-callback", + callback_data="history:INC-20260513-79ED5E", + user_id=123456, + message_id=789, + ) + + assert result["success"] is True + assert result["info_action"] is True + assert sent_history == ["INC-20260513-79ED5E"] + + class TestTelegramMessageFormat: """測試現有 TelegramMessage 格式化""" diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index 250ba6d58..22a869b48 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -1,3 +1,36 @@ +## 2026-05-18 | T45 Telegram 詳情/歷史 callback 400 非致命化 + +**背景**:Telegram 截圖顯示「詳情 / 歷史」按鈕仍可能回到 `HTTP error: 400` 或看起來像流程中斷。實查 callback 路徑後,read-only actions (`detail` / `history` / `reanalyze` / `drift_view_page`) 會先呼叫 `answerCallbackQuery`,再送 DB-backed 詳情/歷史訊息。若 Telegram 對 callback toast 回 400(常見於 callback 過期、舊訊息重點、client 狀態異常),原流程會被例外中斷,導致後面的 truth-chain reply 沒有送出。 + +**修正**: +- `TelegramGateway.handle_callback()`: + - read-only info actions 改用 `_answer_callback_nonfatal()`。 + - `UserNotWhitelistedError` / `NonceReplayError` / generic exception 的 callback toast 也改為 non-fatal,避免錯誤處理本身再因 Telegram 400 放大。 +- 新增 `_answer_callback_nonfatal()`: + - `answerCallbackQuery` 失敗只記錄 `telegram_answer_callback_nonfatal_failed`。 + - 不阻斷真正重要的 DB truth-chain reply / history / detail message。 +- `apps/api/tests/test_telegram_message_templates.py` 新增測試: + - 模擬 `answerCallbackQuery` 回 `HTTP error: 400`。 + - 驗證 `history:INC-...` 仍會送出 `_send_incident_history()`,並回 `success=True`。 + +**local verification**: +- `DATABASE_URL='sqlite+aiosqlite:///:memory:' /Users/ogt/awoooi/apps/api/.venv/bin/python -m pytest apps/api/tests/test_telegram_message_templates.py -q`:37 passed。 +- `/Users/ogt/awoooi/apps/api/.venv/bin/ruff check --select E9,F821,F401 apps/api/src/services/telegram_gateway.py apps/api/tests/test_telegram_message_templates.py`:pass。 +- 全檔 ruff 仍會命中 `telegram_gateway.py` 既有 import/order 與歷史 style debt,本輪未順手大改以避免擴大風險。 +- `git diff --check`:pass。 + +**目前整體進度**: +- Alertmanager 低風險自動修復主線:約 98%。 +- 完整 AI 自動化管理產品化:約 99%。 +- 告警詳情/歷史/主卡/前端 deep-link 可追溯:約 99%。 +- Telegram approval / reject callback:約 97%。 +- Truth-chain 對「自動修復成功但驗證降級」的判讀:約 99%。 +- AwoooP MCP 使用可見性:約 90%。 +- 188 OpenClaw runtime hygiene:約 90%。 +- Token hygiene:約 65%。 +- CI/CD secret masking hygiene:約 60%。 +- Gitea infra-lint 可執行性:100%。 + ## 2026-05-18 | T44 Gitea workflow secret env 泄漏面收斂 **背景**:T43 production verification 時,Gitea job log 顯示 step-level env 有 secret 展開風險。這會讓部署 SSH key、DB URL、Telegram token、AI provider key 等敏感值在 CI log 中留下痕跡,會直接破壞 token hygiene,也會污染後續 AwoooP / SignOz / log matching 的可信度。