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

@@ -0,0 +1,230 @@
"""Operator-facing alert outcome contract.
This module is intentionally generic: it converts the existing truth-chain
stage, automation quality verdict, and remediation evidence state into one
small contract that Telegram, AwoooP, and result notifications can all share.
"""
from __future__ import annotations
from typing import Any
_ACTION_REQUIRED_CHANNELS = ("telegram_sre_war_room", "awooop_operator_console")
_RESULT_ONLY_CHANNELS = ("telegram_result_reply", "awooop_operator_console")
def _safe_int(value: Any) -> int:
try:
return int(value or 0)
except (TypeError, ValueError):
return 0
def _first_text(values: list[Any]) -> str | None:
for value in values:
if value:
return str(value)
return None
def _build_notification(
*,
mode: str,
channels: tuple[str, ...],
reason: str,
source_id: str | None,
) -> dict[str, Any]:
return {
"mode": mode,
"channels": list(channels),
"reason": reason,
"source_id": source_id,
"telegram": (
"reply_to_original_or_standalone_action_required"
if mode == "action_required"
else "reply_to_original_or_standalone_result"
),
"awooop": "status_chain_panel",
}
def build_operator_outcome(
*,
truth_status: dict[str, Any] | None = None,
automation_quality: dict[str, Any] | None = None,
remediation_state: str | None = None,
fetch_error: str | None = None,
source_id: str | None = None,
) -> dict[str, Any]:
"""Build a normalized operator outcome for an alert/incident.
The output deliberately answers three questions:
1. What happened?
2. Does a human need to intervene?
3. How will that human be notified / where should they act?
"""
truth_status = truth_status or {}
automation_quality = automation_quality or {}
facts = automation_quality.get("facts")
if not isinstance(facts, dict):
facts = {}
verdict = str(automation_quality.get("verdict") or "unknown")
stage = str(truth_status.get("current_stage") or "unknown")
stage_status = str(truth_status.get("stage_status") or "unknown")
blockers = [
str(item)
for item in [
*(truth_status.get("blockers") if isinstance(truth_status.get("blockers"), list) else []),
*(automation_quality.get("blockers") if isinstance(automation_quality.get("blockers"), list) else []),
]
if item
]
verification = str(facts.get("verification_result") or "missing")
has_repair_execution = _safe_int(facts.get("effective_execution_records")) > 0 or _safe_int(
facts.get("auto_repair_execution_records")
) > 0
has_nonrepair_operation = (
_safe_int(facts.get("automation_operation_records")) > 0
and not has_repair_execution
)
needs_human_from_truth = bool(truth_status.get("needs_human"))
first_blocker = _first_text(blockers)
if fetch_error:
state = "truth_chain_unavailable"
severity = "warning"
needs_human = True
next_action = "open_awooop_and_review_source_records"
summary = "真相鏈查詢失敗,需人工確認處置結果"
reason = str(fetch_error)[:240]
elif verdict == "auto_repaired_verified":
state = "completed_verified"
severity = "success"
needs_human = False
next_action = "monitor_for_regression"
summary = "已驗證自動修復完成"
reason = "execution_and_verification_succeeded"
elif verdict == "execution_failed" or stage == "execution_failed":
state = "execution_failed_manual_required"
severity = "critical"
needs_human = True
next_action = "manual_fix_or_rollback"
summary = "執行失敗,需人工介入"
reason = first_blocker or "execution_failed"
elif verdict == "manual_required_diagnostic_only" or has_nonrepair_operation:
state = "diagnostic_only_manual_review"
severity = "warning"
needs_human = True
next_action = "manual_review_or_collect_repair_evidence"
summary = "只完成診斷/觀察,尚未證明修復"
reason = first_blocker or "diagnostic_or_audit_only"
elif verdict == "auto_repaired_verification_degraded":
state = "verification_degraded_manual_required"
severity = "warning"
needs_human = True
next_action = "manual_verify_or_repair"
summary = "已執行但驗證退化,需人工確認"
reason = first_blocker or f"verification={verification}"
elif verdict == "execution_unverified" or (
has_repair_execution and verification == "missing"
):
state = "execution_unverified_manual_required"
severity = "warning"
needs_human = True
next_action = "run_or_review_post_execution_verification"
summary = "已執行但缺少驗證結果,需人工確認"
reason = first_blocker or "execution_without_verification_result"
elif verdict == "manual_required_no_action":
state = "no_action_manual_review"
severity = "warning"
needs_human = True
next_action = "manual_review_no_action_decision"
summary = "AI 選擇不執行修復,需人工判斷是否接手"
reason = first_blocker or "no_action_or_observe"
elif remediation_state == "read_only":
state = "read_only_dry_run_manual_gate"
severity = "warning"
needs_human = True
next_action = "approve_or_escalate_from_awooop"
summary = "只讀試跑完成,等待人工放行或轉交"
reason = first_blocker or "read_only_dry_run"
elif remediation_state == "write_observed":
state = "write_observed_manual_review"
severity = "critical"
needs_human = True
next_action = "review_write_evidence"
summary = "補救證據出現寫入旗標,需人工確認"
reason = first_blocker or "write_observed"
elif remediation_state in {"blocked", "fetch_failed"}:
state = "blocked_manual_required"
severity = "critical" if remediation_state == "blocked" else "warning"
needs_human = True
next_action = "manual_investigation"
summary = "自動化流程受阻,需人工處理"
reason = first_blocker or remediation_state
elif verdict == "approval_required" or stage == "approval_required":
state = "approval_required"
severity = "warning"
needs_human = True
next_action = "approve_reject_or_escalate"
summary = "等待人工審批,尚未執行"
reason = first_blocker or "pending_human_approval"
elif needs_human_from_truth:
state = "manual_required"
severity = "warning"
needs_human = True
next_action = "manual_investigation"
summary = "真相鏈判定需人工介入"
reason = first_blocker or f"{stage}/{stage_status}"
elif verdict in {"observed_not_executed", "received_only"}:
state = "observed_not_executed"
severity = "info"
needs_human = False
next_action = "collect_evidence_or_wait"
summary = "已收到/觀測,尚未進入修復執行"
reason = first_blocker or verdict
else:
state = "unknown_pending_observation"
severity = "warning"
needs_human = bool(blockers)
next_action = "review_status_chain"
summary = "處置結果尚未形成明確結論"
reason = first_blocker or f"{verdict}:{stage}/{stage_status}"
mode = "action_required" if needs_human else "result_only"
channels = _ACTION_REQUIRED_CHANNELS if needs_human else _RESULT_ONLY_CHANNELS
return {
"schema_version": "operator_outcome_v1",
"state": state,
"severity": severity,
"summary_zh": summary,
"needs_human": needs_human,
"human_action_required": needs_human,
"human_action_reason": reason,
"next_action": next_action,
"notification": _build_notification(
mode=mode,
channels=channels,
reason=reason,
source_id=source_id,
),
"evidence": {
"verdict": verdict,
"current_stage": stage,
"stage_status": stage_status,
"verification": verification,
"auto_repair_execution_records": _safe_int(
facts.get("auto_repair_execution_records")
),
"effective_execution_records": _safe_int(
facts.get("effective_execution_records")
),
"automation_operation_records": _safe_int(
facts.get("automation_operation_records")
),
"mcp_gateway_total": _safe_int(facts.get("mcp_gateway_total")),
"knowledge_entries": _safe_int(facts.get("knowledge_entries")),
},
"blockers": blockers[:8],
}