fix(api): 補修復候選人工草案包
All checks were successful
CD Pipeline / tests (push) Successful in 1m28s
Code Review / ai-code-review (push) Successful in 14s
CD Pipeline / build-and-deploy (push) Successful in 4m0s
CD Pipeline / post-deploy-checks (push) Successful in 1m48s

This commit is contained in:
Your Name
2026-06-11 18:23:49 +08:00
parent 99efc62745
commit febe9ecfcd
5 changed files with 238 additions and 2 deletions

View File

@@ -322,6 +322,14 @@ class RepairCandidateService:
metadata["repair_candidate_blocker_summary"] = self._humanize_blockers(
metadata["repair_candidate_blockers"]
)
draft_package = self._build_draft_package(
blockers=metadata["repair_candidate_blockers"],
playbook=playbook,
evidence=evidence,
)
metadata["playbook_draft_required"] = True
metadata["repair_candidate_next_step"] = draft_package["next_step"]
metadata["repair_candidate_draft_package"] = draft_package
metadata["fallback_action"] = fallback_action
return RepairCandidateResult(
evidence=evidence,
@@ -367,6 +375,93 @@ class RepairCandidateService:
}
return "".join(labels.get(blocker, blocker) for blocker in blockers)
def _build_draft_package(
self,
*,
blockers: list[str],
playbook: Playbook | None,
evidence: EvidenceSnapshot | None,
) -> dict[str, Any]:
"""Describe the concrete owner-review package needed to unblock repair.
The package is a handoff contract only. It must not be interpreted as
approval to mutate runtime state or auto-create an approved PlayBook.
"""
blocker_set = set(blockers)
if "incident_not_found" in blocker_set:
lane = "restore_truth_chain_before_repair"
next_step = "先修復 incident / approval 真相鏈綁定,再重跑 MCP evidence 與 PlayBook 匹配。"
elif "mcp_evidence_missing" in blocker_set:
lane = "rerun_mcp_evidence_collection"
next_step = (
"先按重診收集 MCP evidence成功後再建立服務專屬 PlayBook 草案,"
"禁止只憑通用規則批准修復。"
)
elif {
"playbook_not_matched",
"playbook_not_found",
"playbook_generic_fallback_not_repair",
} & blocker_set:
lane = "create_service_specific_repair_playbook"
next_step = (
"建立專屬 PlayBook 草案:綁定 alertname / target selector補 MCP evidence refs、"
"修復命令、rollback、verifier plan 與 owner review通用兜底不可執行。"
)
elif "playbook_observe_only" in blocker_set:
lane = "promote_diagnostic_to_repair_playbook"
next_step = (
"把診斷命令保留為 MCP evidence collector另建獨立修復步驟、rollback "
"與 verifier經 owner review 後才可進入批准。"
)
elif "playbook_command_not_safely_routable" in blocker_set:
lane = "route_command_through_safe_mcp_or_ansible"
next_step = (
"將命令改走 allowlisted MCP / Ansible route補 blast radius、rollback "
"與 verifier plan再送 owner review。"
)
elif {
"playbook_not_approved",
"playbook_trust_below_gate",
} & blocker_set:
lane = "owner_review_playbook_trust_gate"
next_step = (
"由 owner review PlayBook 狀態與 trust score補成功/失敗證據後才可進入修復候選。"
)
else:
lane = "repair_candidate_owner_review"
next_step = (
"建立人工處置包並補 PlayBook 草案欄位;完成 owner review 後再重跑候選生成。"
)
evidence_ref = None
if evidence and evidence.snapshot_id:
evidence_ref = evidence.snapshot_id
return {
"schema_version": "repair_candidate_draft_package_v1",
"status": "draft_required",
"lane": lane,
"next_step": next_step,
"matched_playbook_id": playbook.playbook_id if playbook else None,
"matched_playbook_name": playbook.name if playbook else None,
"evidence_snapshot_id": evidence_ref,
"required_fields": [
"alertname",
"target_selector",
"mcp_evidence_refs",
"repair_command",
"rollback_command",
"verifier_plan",
"owner_review",
],
"blocked_operations": [
"auto_execute",
"approve_no_action_as_repair",
"generic_fallback_repair",
],
}
def _build_description(
self,
*,

View File

@@ -270,6 +270,9 @@ def _format_manual_handoff_package_lines(
alert_category: str = "",
suggested_action: str | None = None,
verdict: str | None = None,
repair_candidate_blocker_summary: str = "",
repair_candidate_next_step: str = "",
repair_candidate_required_fields: list[str] | None = None,
compact: bool = False,
) -> list[str]:
"""Build a safe manual handoff package for no-action / degraded alerts.
@@ -287,6 +290,11 @@ def _format_manual_handoff_package_lines(
evidence_hint = _manual_evidence_hint(resource_name, alert_category)
incident_ref = incident_id or "--"
required_fields = [
str(field)
for field in (repair_candidate_required_fields or [])
if str(field).strip()
]
lines = [
"",
"🧰 <b>人工處置包</b>",
@@ -296,6 +304,25 @@ def _format_manual_handoff_package_lines(
"├ 3. 在 AwoooP 建立修復候選命令、風險、rollback、verifier、owner",
"└ 4. 修復後回寫execution result、verifier、KM / PlayBook trust",
]
insert_at = 3
if repair_candidate_blocker_summary:
lines.insert(
insert_at,
f"├ 阻擋:{html.escape(str(repair_candidate_blocker_summary)[:260])}",
)
insert_at += 1
if repair_candidate_next_step:
lines.insert(
insert_at,
f"├ 下一步:{html.escape(str(repair_candidate_next_step)[:360])}",
)
insert_at += 1
if required_fields:
field_text = ", ".join(required_fields[:7])
lines.insert(
insert_at,
f"├ PlayBook 草案欄位:<code>{html.escape(field_text)}</code>",
)
if not compact:
lines.append("按鈕:<b>處置包</b> 看完整證據,<b>重診</b> 重新收集,<b>Runs</b> 追蹤狀態")
return lines
@@ -1938,6 +1965,9 @@ class TelegramMessage:
automation_state: str = "" # diagnosis_collected_manual_required / diagnosis_failed_manual_required
automation_quality: dict | None = None # truth-chain automation_quality 摘要
remediation_summary: dict | None = None # ADR-100 read-only dry-run history 摘要
repair_candidate_blocker_summary: str = "" # 修復候選阻擋原因摘要
repair_candidate_next_step: str = "" # 修復候選阻擋後的下一步
repair_candidate_required_fields: list[str] | None = None # PlayBook 草案必填欄位
# ==========================================================================
# Phase 22: Nemotron 協作欄位 (ADR-044)
@@ -2117,6 +2147,9 @@ class TelegramMessage:
alert_category=self.alert_category,
suggested_action=self.suggested_action,
verdict=verdict,
repair_candidate_blocker_summary=self.repair_candidate_blocker_summary,
repair_candidate_next_step=self.repair_candidate_next_step,
repair_candidate_required_fields=self.repair_candidate_required_fields,
)
if not lines:
return ""
@@ -4126,6 +4159,10 @@ class TelegramGateway:
# 2026-04-16 ogt + Claude Sonnet 4.6: 修復鏈路顯示 (ADR-076)
playbook_name: str = "",
automation_state: str = "",
# 2026-06-11 Codex: no-action 修復候選阻擋時的人工處置包欄位。
repair_candidate_blocker_summary: str = "",
repair_candidate_next_step: str = "",
repair_candidate_required_fields: list[str] | None = None,
) -> dict:
"""
推送待簽核卡片到 Telegram (v7.0 含 SignOz 整合)
@@ -4232,6 +4269,9 @@ class TelegramGateway:
automation_state=automation_state,
automation_quality=automation_quality,
remediation_summary=remediation_summary,
repair_candidate_blocker_summary=repair_candidate_blocker_summary,
repair_candidate_next_step=repair_candidate_next_step,
repair_candidate_required_fields=repair_candidate_required_fields,
)
# 格式化訊息 — Phase 22: 如果 Nemotron 啟用,使用雙軌格式