feat(awooop): link telegram alerts to incident runs
This commit is contained in:
@@ -141,11 +141,13 @@ async def list_runs(
|
||||
project_id: str | None,
|
||||
state: str | None,
|
||||
remediation_status: str | None,
|
||||
incident_id: str | None,
|
||||
page: int,
|
||||
per_page: int,
|
||||
) -> dict[str, Any]:
|
||||
"""列出 runs,支援 project_id、state、remediation_status filter 與分頁。"""
|
||||
"""列出 runs,支援 project_id、state、remediation_status、incident_id filter 與分頁。"""
|
||||
_validate_remediation_status_filter(remediation_status)
|
||||
_validate_incident_id_filter(incident_id)
|
||||
|
||||
async with get_db_context("awoooi") as db:
|
||||
stmt = select(AwoooPRunState).order_by(AwoooPRunState.created_at.desc())
|
||||
@@ -155,7 +157,7 @@ async def list_runs(
|
||||
stmt = stmt.where(AwoooPRunState.state == state)
|
||||
|
||||
offset = (page - 1) * per_page
|
||||
if remediation_status:
|
||||
if remediation_status or incident_id:
|
||||
result = await db.execute(stmt)
|
||||
candidate_rows = list(result.scalars().all())
|
||||
context_limit = _list_filter_context_limit(len(candidate_rows))
|
||||
@@ -176,6 +178,10 @@ async def list_runs(
|
||||
remediation_summaries.get(row.run_id),
|
||||
remediation_status,
|
||||
)
|
||||
and _remediation_summary_matches_incident_id(
|
||||
remediation_summaries.get(row.run_id),
|
||||
incident_id,
|
||||
)
|
||||
]
|
||||
total = len(filtered_rows)
|
||||
rows = filtered_rows[offset : offset + per_page]
|
||||
@@ -521,14 +527,36 @@ def _validate_remediation_status_filter(value: str | None) -> None:
|
||||
)
|
||||
|
||||
|
||||
def _validate_incident_id_filter(value: str | None) -> None:
|
||||
if value is None:
|
||||
return
|
||||
if not _INCIDENT_ID_RE.fullmatch(value):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail="incident_id 格式錯誤,必須是 INC-YYYYMMDD-XXXX",
|
||||
)
|
||||
|
||||
|
||||
def _remediation_summary_matches_status(
|
||||
summary: dict[str, Any] | None,
|
||||
remediation_status: str,
|
||||
remediation_status: str | None,
|
||||
) -> bool:
|
||||
if remediation_status is None:
|
||||
return True
|
||||
status_value = str((summary or {}).get("status") or "no_evidence")
|
||||
return status_value == remediation_status
|
||||
|
||||
|
||||
def _remediation_summary_matches_incident_id(
|
||||
summary: dict[str, Any] | None,
|
||||
incident_id: str | None,
|
||||
) -> bool:
|
||||
if incident_id is None:
|
||||
return True
|
||||
incident_ids = (summary or {}).get("incident_ids")
|
||||
return isinstance(incident_ids, list) and incident_id in incident_ids
|
||||
|
||||
|
||||
async def _build_run_remediation_summaries(
|
||||
*,
|
||||
runs: list[AwoooPRunState],
|
||||
|
||||
@@ -30,6 +30,7 @@ import os
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from datetime import UTC, datetime
|
||||
from urllib.parse import quote
|
||||
from uuid import NAMESPACE_URL, UUID, uuid5
|
||||
|
||||
import httpx
|
||||
@@ -71,6 +72,7 @@ _TELEGRAM_BOT_URL_RE = re.compile(r"(api\.telegram\.org/bot)[^/\s]+")
|
||||
_INCIDENT_ID_RE = re.compile(r"\bINC-\d{8}-[A-Z0-9]{4,}\b")
|
||||
_CODE_REF_RE = re.compile(r"<code>([0-9a-f]{7,12})</code>", re.IGNORECASE)
|
||||
_TELEGRAM_HTML_CHUNK_LIMIT = 3600
|
||||
_AWOOOP_WEB_BASE_URL = "https://awoooi.wooo.work"
|
||||
|
||||
|
||||
def _top_gateway_bucket(
|
||||
@@ -196,6 +198,23 @@ def _format_remediation_history_lines(history: dict[str, object] | None) -> list
|
||||
]
|
||||
|
||||
|
||||
def _awooop_runs_url_for_incident(incident_id: str) -> str:
|
||||
safe_incident_id = quote(str(incident_id or ""), safe="")
|
||||
return (
|
||||
f"{_AWOOOP_WEB_BASE_URL}/zh-TW/awooop/runs"
|
||||
f"?project_id=awoooi&incident_id={safe_incident_id}"
|
||||
)
|
||||
|
||||
|
||||
def _awooop_runs_button_row(incident_id: str) -> list[dict[str, str]]:
|
||||
if not incident_id:
|
||||
return []
|
||||
return [{
|
||||
"text": "🧭 AwoooP",
|
||||
"url": _awooop_runs_url_for_incident(incident_id),
|
||||
}]
|
||||
|
||||
|
||||
def _latest_remediation_history_item(history: dict[str, object] | None) -> dict[str, object]:
|
||||
if not history:
|
||||
return {}
|
||||
@@ -2187,6 +2206,10 @@ class TelegramGateway:
|
||||
tuning_nonce = self._security.generate_callback_nonce(approval_id, "tune")
|
||||
buttons.append([{"text": "⚡ 執行自動調優", "callback_data": tuning_nonce}])
|
||||
|
||||
awooop_row = _awooop_runs_button_row(incident_id)
|
||||
if awooop_row:
|
||||
buttons.append(awooop_row)
|
||||
|
||||
return {"inline_keyboard": buttons}
|
||||
|
||||
# ── YAML Fallback 路徑(原有邏輯,不改動任何行為)────────────────────
|
||||
@@ -2232,6 +2255,9 @@ class TelegramGateway:
|
||||
{"text": "📋 詳情", "callback_data": f"detail:{incident_id}"},
|
||||
{"text": "🔕 忽略", "callback_data": silence_nonce},
|
||||
])
|
||||
awooop_row = _awooop_runs_button_row(incident_id)
|
||||
if awooop_row:
|
||||
rows.append(awooop_row)
|
||||
buttons = rows
|
||||
else:
|
||||
# 舊版通用鍵(向下相容)
|
||||
@@ -2249,6 +2275,9 @@ class TelegramGateway:
|
||||
{"text": "🔄 重診", "callback_data": f"reanalyze:{incident_id}"},
|
||||
{"text": "📊 歷史", "callback_data": f"history:{incident_id}"},
|
||||
])
|
||||
awooop_row = _awooop_runs_button_row(incident_id)
|
||||
if awooop_row:
|
||||
buttons.append(awooop_row)
|
||||
|
||||
logger.info(
|
||||
"telegram_keyboard_built",
|
||||
@@ -2837,12 +2866,14 @@ class TelegramGateway:
|
||||
|
||||
# read-only 查類按鈕(2-part info 格式,handler 已在 handle_callback 實作)
|
||||
# detail/history 均在 INFO_ACTIONS 白名單,無 nonce 無副作用
|
||||
keyboard = {
|
||||
"inline_keyboard": [[
|
||||
{"text": "📋 詳情", "callback_data": f"detail:{incident_id}"},
|
||||
{"text": "📊 歷史", "callback_data": f"history:{incident_id}"},
|
||||
]]
|
||||
}
|
||||
inline_keyboard = [[
|
||||
{"text": "📋 詳情", "callback_data": f"detail:{incident_id}"},
|
||||
{"text": "📊 歷史", "callback_data": f"history:{incident_id}"},
|
||||
]]
|
||||
awooop_row = _awooop_runs_button_row(incident_id)
|
||||
if awooop_row:
|
||||
inline_keyboard.append(awooop_row)
|
||||
keyboard = {"inline_keyboard": inline_keyboard}
|
||||
return await self._send_request(
|
||||
"sendMessage",
|
||||
{
|
||||
@@ -5189,13 +5220,17 @@ class TelegramGateway:
|
||||
|
||||
# Step 1: 換掉按鈕 (移除 Row 1 批准/拒絕/靜默,保留 Row 2 資訊按鈕)
|
||||
if keep_info_buttons:
|
||||
new_keyboard = {"inline_keyboard": [
|
||||
inline_keyboard = [
|
||||
[
|
||||
{"text": "📋 詳情", "callback_data": f"detail:{incident_id}"},
|
||||
{"text": "🔄 重診", "callback_data": f"reanalyze:{incident_id}"},
|
||||
{"text": "📊 歷史", "callback_data": f"history:{incident_id}"},
|
||||
],
|
||||
]}
|
||||
]
|
||||
awooop_row = _awooop_runs_button_row(incident_id)
|
||||
if awooop_row:
|
||||
inline_keyboard.append(awooop_row)
|
||||
new_keyboard = {"inline_keyboard": inline_keyboard}
|
||||
else:
|
||||
new_keyboard = {"inline_keyboard": []}
|
||||
|
||||
@@ -6942,6 +6977,9 @@ class TelegramGateway:
|
||||
{"text": "📋 詳情", "callback_data": f"detail:{incident_id}"},
|
||||
{"text": "📊 歷史", "callback_data": f"history:{incident_id}"},
|
||||
]]
|
||||
awooop_row = _awooop_runs_button_row(incident_id)
|
||||
if awooop_row:
|
||||
info_buttons.append(awooop_row)
|
||||
await self._send_request(
|
||||
"editMessageReplyMarkup",
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user