feat(telegram): extract incident refs from callback buttons
This commit is contained in:
@@ -951,10 +951,31 @@ def _reply_markup_summary(payload: dict) -> dict[str, object]:
|
||||
}
|
||||
|
||||
|
||||
def _reply_markup_incident_ids(payload: dict) -> list[str]:
|
||||
"""Extract safe incident refs from button callback_data without storing it."""
|
||||
reply_markup = payload.get("reply_markup")
|
||||
if not isinstance(reply_markup, dict):
|
||||
return []
|
||||
|
||||
incident_ids: set[str] = set()
|
||||
for row in reply_markup.get("inline_keyboard") or []:
|
||||
if not isinstance(row, list):
|
||||
continue
|
||||
for button in row:
|
||||
if not isinstance(button, dict):
|
||||
continue
|
||||
callback_data = str(button.get("callback_data") or "")
|
||||
incident_ids.update(_INCIDENT_ID_RE.findall(callback_data))
|
||||
return sorted(incident_ids)
|
||||
|
||||
|
||||
def _outbound_source_envelope(method: str, payload: dict) -> dict[str, object]:
|
||||
"""Build a redaction-friendly source envelope for Channel Hub replay."""
|
||||
text = str(payload.get("text") or payload.get("caption") or "")
|
||||
incident_ids = sorted(set(_INCIDENT_ID_RE.findall(text)))
|
||||
incident_ids = sorted(
|
||||
set(_INCIDENT_ID_RE.findall(text))
|
||||
| set(_reply_markup_incident_ids(payload))
|
||||
)
|
||||
code_refs = sorted(set(match.group(1) for match in _CODE_REF_RE.finditer(text)))
|
||||
return {
|
||||
"adapter": "legacy_telegram_gateway",
|
||||
|
||||
@@ -49,3 +49,30 @@ def test_outbound_source_envelope_keeps_replay_context_without_raw_payload() ->
|
||||
assert "approval-id-secret" not in str(envelope)
|
||||
assert "1234567890:" not in str(envelope)
|
||||
assert "ACTION REQUIRED" not in str(envelope)
|
||||
|
||||
|
||||
def test_outbound_source_envelope_reads_incident_refs_from_buttons() -> None:
|
||||
payload = {
|
||||
"chat_id": "-100123",
|
||||
"text": "ACTION REQUIRED without incident id in visible text",
|
||||
"reply_markup": {
|
||||
"inline_keyboard": [
|
||||
[
|
||||
{"text": "詳情", "callback_data": "detail:INC-20260525-ABC123"},
|
||||
{"text": "歷史", "callback_data": "history:INC-20260525-ABC123"},
|
||||
],
|
||||
[
|
||||
{"text": "重診", "callback_data": "reanalyze:INC-20260525-DEF456"},
|
||||
],
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
envelope = _outbound_source_envelope("sendMessage", payload)
|
||||
|
||||
assert envelope["source_refs"]["incident_ids"] == [
|
||||
"INC-20260525-ABC123",
|
||||
"INC-20260525-DEF456",
|
||||
]
|
||||
assert envelope["reply_markup"]["buttons"][0]["callback_prefix"] == "detail"
|
||||
assert "detail:INC-20260525-ABC123" not in str(envelope)
|
||||
|
||||
@@ -20531,3 +20531,66 @@ GET /api/v1/health:
|
||||
- KM governance:約 84.6%。
|
||||
- AI Provider lane visibility:約 92.2%。
|
||||
- 完整 AI 自動化管理產品化:約 97.35%。
|
||||
|
||||
---
|
||||
|
||||
## 2026-05-25 T186 — Telegram Outbound Source Refs Future Coverage
|
||||
|
||||
**背景**:
|
||||
|
||||
- T183-T185 已把 callback evidence / snapshot status 做到可觀測。
|
||||
- 下一個 source matching 缺口是 Telegram outbound `source_refs.incident_ids` 覆蓋率偏低:
|
||||
|
||||
```text
|
||||
production read-only aggregate:
|
||||
message_type triggered_by total source_refs incident_refs reply_markup callback_reply
|
||||
final legacy_gateway 2465 2174 229 56 2
|
||||
approval_request legacy_gateway 1477 1259 678 1261 0
|
||||
error legacy_gateway 1307 1271 20 5 1
|
||||
|
||||
reply_markup first callback prefix:
|
||||
approve 382 total / 332 with incident refs
|
||||
silence 370 total / 95 with incident refs
|
||||
detail 104 total / 99 with incident refs
|
||||
drift_view 144 total / 0 with incident refs
|
||||
```
|
||||
|
||||
- 既有 mirror 只從 Telegram visible text 抽 `INC-*`;若卡片文字沒有 incident,但按鈕 callback_data 有 `detail:INC-*` / `history:INC-*`,未來 source matching 會漏掉。
|
||||
- 既有 DB 只保存 redacted `callback_prefix`,不保存 raw callback_data,因此舊資料不能安全反推補齊;本輪先補「未來出站鏡像」。
|
||||
|
||||
**本輪修正**:
|
||||
|
||||
- `_outbound_source_envelope()` 新增安全抽取:
|
||||
- 從 `reply_markup.inline_keyboard[*].callback_data` 只抽 `INC-*`。
|
||||
- 仍只保存 `source_refs.incident_ids` 與既有 `callback_prefix` 摘要。
|
||||
- 不保存 raw callback_data、approval nonce、Telegram token 或原始訊息文字。
|
||||
- 新增 regression test,確保:
|
||||
- visible text 沒 incident 時,也能從 `detail:INC-*` / `history:INC-*` / `reanalyze:INC-*` 抽 incident refs。
|
||||
- envelope 不含 raw callback_data。
|
||||
|
||||
**local validation(完成)**:
|
||||
|
||||
```text
|
||||
python3 -m py_compile apps/api/src/services/telegram_gateway.py apps/api/tests/test_telegram_gateway_error_sanitizer.py
|
||||
git diff --check
|
||||
PYTHONPATH=. DATABASE_URL='postgresql+asyncpg://test:test@localhost/test' /Users/ogt/.pyenv/shims/pytest tests/test_telegram_gateway_error_sanitizer.py -q
|
||||
3 passed in 0.56s
|
||||
PYTHONPATH=. DATABASE_URL='postgresql+asyncpg://test:test@localhost/test' /Users/ogt/.pyenv/shims/pytest tests/test_telegram_message_templates.py tests/test_telegram_button_consistency.py -q
|
||||
64 passed in 0.63s
|
||||
```
|
||||
|
||||
**目前整體進度**:
|
||||
|
||||
- AwoooP 告警可觀測鏈:約 99.60%。
|
||||
- 低風險自動修復閉環:約 95.8%。
|
||||
- 前端 AI 自動化管理介面同步:約 99.15%。
|
||||
- 首頁 KPI / 小龍蝦流程 truth alignment:約 96.5%。
|
||||
- Telegram 詳情 / 歷史可追溯:約 99.0%。
|
||||
- Telegram outbound / callback DB coverage 可視化:約 99.0%。
|
||||
- callback / DB replayability:約 98.5%。
|
||||
- MCP / 自建 MCP 可視化:約 95.1%。
|
||||
- Sentry / SigNoz source correlation:約 93.9%。
|
||||
- Ansible / PlayBook 可視化:約 92.6%。
|
||||
- KM governance:約 84.6%。
|
||||
- AI Provider lane visibility:約 92.2%。
|
||||
- 完整 AI 自動化管理產品化:約 97.4%。
|
||||
|
||||
Reference in New Issue
Block a user