fix(alerts): 補齊處置結果與人工通知契約
Some checks failed
CD Pipeline / tests (push) Failing after 45s
CD Pipeline / build-and-deploy (push) Has been skipped
CD Pipeline / post-deploy-checks (push) Has been skipped
Code Review / ai-code-review (push) Successful in 12s

This commit is contained in:
Your Name
2026-05-31 15:41:09 +08:00
parent 03f2abf576
commit 3d8b395032
14 changed files with 888 additions and 75 deletions

View File

@@ -39,6 +39,7 @@ from opentelemetry import trace
from src.core.config import settings
from src.core.redis_client import get_redis
from src.services.operator_outcome import build_operator_outcome
from src.services.security_interceptor import (
NonceReplayError,
UserNotWhitelistedError,
@@ -169,6 +170,42 @@ def _format_automation_quality_lines(quality: dict[str, object] | None) -> list[
return lines
def _operator_outcome_from_blocks(
*,
truth_status: dict[str, object] | None,
quality: dict[str, object] | None,
remediation_state: str | None = None,
source_id: str | None = None,
) -> dict[str, object]:
if isinstance(quality, dict) and isinstance(quality.get("operator_outcome"), dict):
return dict(quality["operator_outcome"])
return build_operator_outcome(
truth_status=truth_status if isinstance(truth_status, dict) else {},
automation_quality=quality if isinstance(quality, dict) else {},
remediation_state=remediation_state,
source_id=source_id,
)
def _format_operator_outcome_lines(outcome: dict[str, object] | None) -> list[str]:
if not outcome:
return []
notification = (
outcome.get("notification")
if isinstance(outcome.get("notification"), dict)
else {}
)
channels = notification.get("channels") if isinstance(notification.get("channels"), list) else []
return [
"👤 <b>處置結論</b>",
f"├ 結果:<b>{html.escape(str(outcome.get('summary_zh') or '尚未形成明確結論'))}</b>",
f"├ 狀態:<code>{html.escape(str(outcome.get('state') or 'unknown'))}</code>",
f"├ 人工:<code>{'yes' if outcome.get('needs_human') else 'no'}</code> | "
f"通知:<code>{html.escape(','.join(str(item) for item in channels) or 'none')}</code>",
f"└ 下一步:<code>{html.escape(str(outcome.get('next_action') or '--'))}</code>",
]
def _format_remediation_history_lines(history: dict[str, object] | None) -> list[str]:
if not history or int(history.get("total") or 0) <= 0:
return []
@@ -413,6 +450,14 @@ def _format_awooop_status_chain_lines(
and repair_state != "auto_repaired_verified"
):
needs_human = True
outcome = _operator_outcome_from_blocks(
truth_status=truth_status,
quality=quality,
remediation_state=remediation_state,
)
if outcome:
needs_human = bool(needs_human or outcome.get("needs_human"))
next_step = str(outcome.get("next_action") or next_step)
lines = [
"",
@@ -450,6 +495,9 @@ def _format_awooop_status_chain_lines(
),
f"下一步: <code>{html.escape(next_step)}</code>",
]
outcome_lines = _format_operator_outcome_lines(outcome)
if outcome_lines:
lines.extend(["", *outcome_lines])
blockers = [str(item) for item in [*truth_blockers, *quality_blockers] if item]
if blockers:
@@ -1485,6 +1533,15 @@ def _callback_reply_awooop_status_chain_snapshot(
and repair_state != "auto_repaired_verified"
):
needs_human = True
outcome = _operator_outcome_from_blocks(
truth_status=truth_status,
quality=quality,
remediation_state=remediation_state,
source_id=incident_id,
)
if outcome:
needs_human = bool(needs_human or outcome.get("needs_human"))
next_step = str(outcome.get("next_action") or next_step)
truth_blockers = (
truth_status.get("blockers") if isinstance(truth_status.get("blockers"), list) else []
@@ -1511,6 +1568,7 @@ def _callback_reply_awooop_status_chain_snapshot(
"verification": str(verification),
"needs_human": needs_human,
"next_step": next_step,
"operator_outcome": outcome,
"blockers": blockers[:8],
"evidence": {
"auto_repair_records": auto_repair_records,
@@ -1782,6 +1840,13 @@ class TelegramMessage:
text = f"{self.root_cause} {self.suggested_action}".lower()
state = (self.automation_state or "").lower()
quality = self.automation_quality or {}
outcome = (
quality.get("operator_outcome")
if isinstance(quality.get("operator_outcome"), dict)
else None
)
if outcome and outcome.get("summary_zh"):
return str(outcome["summary_zh"])
facts = quality.get("facts") if isinstance(quality.get("facts"), dict) else {}
verdict = str(quality.get("verdict") or "")
has_repair_execution = _has_repair_execution_evidence(facts)
@@ -1828,6 +1893,25 @@ class TelegramMessage:
return "🟡 AI 已提出修復建議,等待人工批准"
return "🟡 安全閘門待審批"
def _operator_outcome(self) -> dict[str, object] | None:
quality = self.automation_quality if isinstance(self.automation_quality, dict) else {}
if isinstance(quality.get("operator_outcome"), dict):
return dict(quality["operator_outcome"])
if not quality:
return None
return build_operator_outcome(
truth_status=None,
automation_quality=quality,
remediation_state=_remediation_evidence_state(self.remediation_summary),
source_id=self.incident_id or self.approval_id,
)
def _format_operator_outcome_block(self) -> str:
lines = _format_operator_outcome_lines(self._operator_outcome())
if not lines:
return ""
return "\n".join(lines) + "\n"
def _format_automation_block(self) -> str:
"""Visible AI automation chain for every ACTION REQUIRED card.
2026-05-04 ogt: 加入 Token 用量 + 具體 Ollama 伺服器顯示
@@ -2080,6 +2164,7 @@ class TelegramMessage:
playbook_line = f"📖 Playbook<code>{html.escape(self.playbook_name)}</code>\n"
remediation_evidence_block = self._format_remediation_evidence_block()
flow_progress_block = self._format_flow_progress_block()
operator_outcome_block = self._format_operator_outcome_block()
automation_block = self._format_automation_block()
# ADR-075 TYPE-3 格式組裝
@@ -2092,6 +2177,7 @@ class TelegramMessage:
f"🧭 處置狀態:<b>{safe_automation_summary}</b>\n"
f"{remediation_evidence_block}\n"
f"{flow_progress_block}\n"
f"{operator_outcome_block}"
f"{automation_block}"
f"\n"
f"🧠 <b>AI 深度診斷</b>\n"