diff --git a/apps/api/src/services/decision_manager.py b/apps/api/src/services/decision_manager.py
index 684ca81c4..5d2f2dc2a 100644
--- a/apps/api/src/services/decision_manager.py
+++ b/apps/api/src/services/decision_manager.py
@@ -215,6 +215,8 @@ async def _push_decision_to_telegram(
# 2026-04-09 Claude Sonnet 4.6: Tool Calling 模型/後端
nemotron_tool_model = proposal_data.get("nemotron_tool_model", "")
nemotron_tool_backend = proposal_data.get("nemotron_tool_backend", "")
+ # 2026-04-16 ogt + Claude Sonnet 4.6: Playbook 匹配名稱穿透到 TG 卡片 (ADR-076)
+ _playbook_name = proposal_data.get("playbook_name", "")
# 建立 approval_id (使用 incident_id 作為追蹤)
# 2026-03-27 ogt: 修復 INC-INC-INC- 重複前綴 bug
@@ -331,6 +333,7 @@ async def _push_decision_to_telegram(
incident_id=incident.incident_id,
alert_category=_alert_category,
notification_type=_notification_type,
+ playbook_name=_playbook_name,
)
# 2026-04-09 Claude Sonnet 4.6: 存 message_id → 後續狀態更新在原訊息延續
@@ -511,8 +514,8 @@ async def _generate_playbook_draft_if_new(incident: "Incident") -> None:
KnowledgeEntryCreate(
title=f"[AI草稿] {alertname} Playbook",
content=content,
- entry_type=EntryType.PLAYBOOK,
- category="auto_generated",
+ entry_type=EntryType.AUTO_RUNBOOK,
+ category="ai_system",
tags=[alertname, severity, "ai_draft", "mcp_phase4c"],
source=EntrySource.AI_EXTRACTED,
status=EntryStatus.DRAFT,
@@ -539,6 +542,20 @@ async def _generate_playbook_draft_if_new(incident: "Incident") -> None:
entry_id=entry.entry_id,
)
+ # 2026-04-16 ogt + Claude Sonnet 4.6: KM 建立後推 Telegram 通知 (ADR-076)
+ # best-effort — 不阻塞主流程
+ try:
+ from src.services.telegram_gateway import get_telegram_gateway as _get_tg
+ _tg = _get_tg()
+ await _tg.send_as_nemotron(
+ f"📚 KM 新條目 — {entry.entry_id}\n"
+ f"🏷️ 分類:auto_generated 狀態:DRAFT\n"
+ f"📋 告警:{alertname} 事件:{incident.incident_id}\n"
+ f"標題:{str(getattr(entry, 'title', alertname))[:60]}"
+ )
+ except Exception as _tg_err:
+ _sl.get_logger(__name__).debug("km_tg_notify_failed", error=str(_tg_err))
+
except Exception as e:
import structlog as _sl
_sl.get_logger(__name__).debug("playbook_draft_failed", error=str(e))
@@ -2314,6 +2331,7 @@ class DecisionManager:
import json as _json
from src.utils.timezone import now_taipei
+ # 2026-04-16 ogt + Claude Sonnet 4.6: 補入 playbook_id / alert_category (ADR-076)
entry = {
"ts": now_taipei().isoformat(),
"confidence": proposal_data.get("confidence", 0.0),
@@ -2321,6 +2339,9 @@ class DecisionManager:
"provider": proposal_data.get("provider", proposal_data.get("source", "")),
"source": proposal_data.get("source", ""),
"diagnosis": str(proposal_data.get("diagnosis", proposal_data.get("description", "")))[:200],
+ "playbook_id": proposal_data.get("playbook_id", ""),
+ "playbook_name": proposal_data.get("playbook_name", ""),
+ "alert_category": proposal_data.get("alert_category", ""),
}
async with get_db_context() as db:
diff --git a/apps/api/src/services/runbook_generator.py b/apps/api/src/services/runbook_generator.py
index a70b1172e..6b06ecd7c 100644
--- a/apps/api/src/services/runbook_generator.py
+++ b/apps/api/src/services/runbook_generator.py
@@ -90,7 +90,7 @@ class NemotronRunbookGenerator:
title=f"[AUTO] {incident.incident_id} — {playbook.name}",
content=content,
entry_type=EntryType.AUTO_RUNBOOK,
- category="auto_generated",
+ category="ai_system",
tags=list(incident.affected_services or []) + ["auto_runbook", "nemotron"],
source=EntrySource.AI_EXTRACTED,
status=EntryStatus.DRAFT,
diff --git a/apps/api/src/services/telegram_gateway.py b/apps/api/src/services/telegram_gateway.py
index b8730f8e2..4de6fa985 100644
--- a/apps/api/src/services/telegram_gateway.py
+++ b/apps/api/src/services/telegram_gateway.py
@@ -164,6 +164,9 @@ class TelegramMessage:
ai_provider: str = "" # ollama/gemini/claude/expert_system/mock
# 2026-04-04 ogt: 底層模型名稱 (e.g. qwen2.5:7b-instruct, nemotron-70b)
ai_model: str = ""
+ # 2026-04-16 ogt + Claude Sonnet 4.6: 告警分類與修復鏈路顯示 (ADR-076)
+ alert_category: str = "" # host/k8s/database/service/external_site/secops 等
+ playbook_name: str = "" # 匹配到的 Playbook 名稱(空字串=規則匹配)
# ==========================================================================
# Phase 22: Nemotron 協作欄位 (ADR-044)
@@ -237,7 +240,7 @@ class TelegramMessage:
# 2026-03-29 ogt: ADR-037 異常頻率顯示
frequency_block = ""
- if self.anomaly_frequency and self.anomaly_frequency.get("count_24h", 0) > 1:
+ if self.anomaly_frequency and self.anomaly_frequency.get("count_24h", 0) >= 1:
freq = self.anomaly_frequency
escalation_emoji = {
None: "",
@@ -304,12 +307,37 @@ class TelegramMessage:
"LOW": "低風險",
}.get(self.risk_level.upper(), self.risk_level)
+ # ADR-076 分類標籤 (2026-04-16 ogt + Claude Sonnet 4.6)
+ _category_map = {
+ "host": "🖥️ 主機", "host_resource": "🖥️ 主機",
+ "k8s": "☸️ K8s", "kubernetes": "☸️ K8s",
+ "database": "🗄️ 資料庫",
+ "service": "⚙️ 服務",
+ "external_site": "🌐 外部網站",
+ "secops": "🔐 安全",
+ "auto_repair": "🔧 自動修復",
+ "alertchain_health": "📡 告警鏈路",
+ "flywheel_health": "🔄 飛輪健康",
+ "docker": "🐳 Docker",
+ "infrastructure": "🏗️ 基礎設施",
+ }
+ category_line = ""
+ if self.alert_category:
+ cat_display = html.escape(_category_map.get(self.alert_category, self.alert_category))
+ category_line = f"🏷️ 分類:{cat_display}\n"
+
+ # Playbook 顯示
+ playbook_line = ""
+ if self.playbook_name:
+ playbook_line = f"📖 Playbook:{html.escape(self.playbook_name)}\n"
+
# ADR-075 TYPE-3 格式組裝
message = (
f"{self.status_emoji} ACTION REQUIRED | {html.escape(risk_zh)}\n"
f"──────────────────────\n"
f"📋 {html.escape(incident_id)}\n"
f"🎯 資源:{safe_resource}\n"
+ f"{category_line}"
f"\n"
f"🧠 AI 深度診斷\n"
f"├─ 分析:{safe_root_cause}\n"
@@ -317,6 +345,7 @@ class TelegramMessage:
f"└─ {ai_source}\n"
f"\n"
f"⚡ 建議修復動作\n"
+ f"{playbook_line}"
f"{safe_action}\n"
)
if ai_cost_display:
@@ -448,10 +477,27 @@ class TelegramMessage:
)
# 2026-04-05 Claude Code: 重設計訊息格式,提升易讀性
+ # 2026-04-16 ogt + Claude Sonnet 4.6: 補 alert_category + playbook_name 顯示
+ _category_map = {
+ "host": "🖥️ 主機", "host_resource": "🖥️ 主機",
+ "k8s": "☸️ K8s", "kubernetes": "☸️ K8s",
+ "database": "🗄️ 資料庫", "service": "⚙️ 服務",
+ "external_site": "🌐 外部網站", "secops": "🔐 安全",
+ "auto_repair": "🔧 自動修復", "docker": "🐳 Docker",
+ }
+ category_line = ""
+ if self.alert_category:
+ cat_display = html.escape(_category_map.get(self.alert_category, self.alert_category))
+ category_line = f"🏷️ {cat_display}\n"
+ playbook_line = ""
+ if self.playbook_name:
+ playbook_line = f"📖 {html.escape(self.playbook_name)}\n"
+
# 組裝訊息
message = (
f"{self.status_emoji} {html.escape(self.risk_level)} {html.escape(incident_id)}\n"
f"{safe_resource}\n"
+ f"{category_line}"
f"\n"
f"{conf_line}\n"
f"👥 {resp_display}\n"
@@ -460,7 +506,7 @@ class TelegramMessage:
if nemotron_block:
message += f"\n{nemotron_block}"
message += (
- f"\n🔧 建議: {safe_action}\n"
+ f"\n🔧 建議: {playbook_line}{safe_action}\n"
f"⏱️ 停機: {safe_downtime}"
)
@@ -1488,6 +1534,8 @@ class TelegramGateway:
# 2026-04-12 ogt: ADR-075 告警分類與通知類型(斷點 B 修復)
alert_category: str = "",
notification_type: str = "",
+ # 2026-04-16 ogt + Claude Sonnet 4.6: 修復鏈路顯示 (ADR-076)
+ playbook_name: str = "",
) -> dict:
"""
推送待簽核卡片到 Telegram (v7.0 含 SignOz 整合)
@@ -1561,6 +1609,9 @@ class TelegramGateway:
# 2026-04-09 Claude Sonnet 4.6: Tool Calling 模型/後端
nemotron_tool_model=nemotron_tool_model,
nemotron_tool_backend=nemotron_tool_backend,
+ # 2026-04-16 ogt + Claude Sonnet 4.6: 修復鏈路顯示 (ADR-076)
+ alert_category=alert_category,
+ playbook_name=playbook_name,
)
# 格式化訊息 — Phase 22: 如果 Nemotron 啟用,使用雙軌格式