feat(api): 預填修復候選 PlayBook 草案
Some checks failed
Code Review / ai-code-review (push) Successful in 15s
CD Pipeline / tests (push) Successful in 1m56s
CD Pipeline / post-deploy-checks (push) Has been cancelled
CD Pipeline / build-and-deploy (push) Has been cancelled

This commit is contained in:
Your Name
2026-06-18 10:00:44 +08:00
parent 1d3bd4fccb
commit 1b249c98f3
2 changed files with 166 additions and 1 deletions

View File

@@ -350,7 +350,13 @@ class RepairCandidateService:
namespace=namespace,
)
metadata["playbook_draft_required"] = True
metadata["repair_candidate_next_step"] = draft_package["next_step"]
metadata["repair_candidate_prefilled_draft_summary"] = self._draft_summary_for_operator(
draft_package.get("playbook_draft_template") or {}
)
metadata["repair_candidate_next_step"] = self._next_step_with_draft_summary(
str(draft_package["next_step"]),
str(metadata["repair_candidate_prefilled_draft_summary"]),
)
metadata["repair_candidate_draft_package"] = draft_package
metadata["fallback_action"] = fallback_action
return RepairCandidateResult(
@@ -487,6 +493,16 @@ class RepairCandidateService:
evidence=evidence,
playbook=playbook,
)
playbook_draft_template = self._build_playbook_draft_template(
coverage_gap=coverage_gap,
blockers=blockers,
lane=lane,
alertname=alertname,
target_resource=target_resource,
namespace=namespace,
evidence=evidence,
playbook=playbook,
)
incident_id = getattr(incident, "incident_id", "") if incident else ""
project_id = str(getattr(incident, "project_id", "awoooi") or "awoooi")
work_item: dict[str, Any] | None = None
@@ -517,6 +533,7 @@ class RepairCandidateService:
"next_step": next_step,
"required_fields": required_fields,
"coverage_gap": coverage_gap,
"playbook_draft_template": playbook_draft_template,
"blocked_operations": blocked_operations,
"target_href": f"/awooop/runs?{run_query}",
"work_item_href": f"/awooop/work-items?{work_item_query}",
@@ -542,12 +559,128 @@ class RepairCandidateService:
"evidence_snapshot_id": evidence_ref,
"required_fields": required_fields,
"coverage_gap": coverage_gap,
"playbook_draft_template": playbook_draft_template,
"blocked_operations": blocked_operations,
}
if work_item:
package["awooop_work_item"] = work_item
return package
def _draft_summary_for_operator(self, template: dict[str, Any]) -> str:
route = str(template.get("suggested_route") or "--")
repair = str(template.get("repair_command_template") or "--")
verifier = template.get("verifier_plan_template") or []
verifier_head = "--"
if isinstance(verifier, list) and verifier:
verifier_head = str(verifier[0])
return f"route={route}; repair_template={repair}; verifier={verifier_head}"
def _next_step_with_draft_summary(self, next_step: str, draft_summary: str) -> str:
if not draft_summary or draft_summary == "route=--; repair_template=--; verifier=--":
return next_step
return f"{next_step} 草案已預填:{draft_summary}"
def _build_playbook_draft_template(
self,
*,
coverage_gap: dict[str, Any],
blockers: list[str],
lane: str,
alertname: str,
target_resource: str,
namespace: str,
evidence: EvidenceSnapshot | None,
playbook: Playbook | None,
) -> dict[str, Any]:
"""Prefill a service-specific PlayBook draft without authorizing execution."""
target_kind = str(coverage_gap.get("target_kind") or "unknown")
target = (target_resource or "").strip()
ns = (namespace or "").strip()
command_template = "owner_supplied_service_specific_command_after_evidence_review"
rollback_template = "owner_supplied_rollback_or_safe_stop_after_evidence_review"
route = "owner_review_required"
verifier_plan = [
"rerun_mcp_evidence_collection",
"confirm_fingerprint_recurrence_stops_or_decreases",
"write_execution_result_and_verifier_outcome",
"update_km_and_playbook_trust_after_owner_review",
]
if target_kind == "k8s_workload":
workload = target.replace("deployment/", "").replace("pod/", "").replace("svc/", "")
workload_ref = workload or "<deployment>"
namespace_ref = ns or "<namespace>"
command_template = (
f"kubectl rollout restart deployment/{workload_ref} -n {namespace_ref}"
)
rollback_template = (
f"kubectl rollout undo deployment/{workload_ref} -n {namespace_ref}"
)
route = "k8s_rollout_after_owner_review"
verifier_plan.extend([
f"kubectl rollout status deployment/{workload_ref} -n {namespace_ref}",
f"kubectl get events -n {namespace_ref} --field-selector involvedObject.name={workload_ref}",
])
elif target_kind == "host_service":
service_ref = target or "<service>"
command_template = f"systemctl restart {service_ref}"
rollback_template = f"systemctl status {service_ref}; journalctl -u {service_ref} -n 120"
route = "host_service_route_after_owner_review"
verifier_plan.extend([
f"systemctl is-active {service_ref}",
f"journalctl -u {service_ref} -n 120 --no-pager",
])
elif target_kind == "service":
service_ref = target or "<service>"
command_template = f"service-specific repair for {service_ref}"
rollback_template = f"service-specific rollback for {service_ref}"
route = "service_specific_route_after_owner_review"
verifier_plan.append(f"confirm service health for {service_ref}")
owner_review_checklist = [
"確認 alertname 與 target selector 沒有誤配",
"確認 MCP evidence refs 足以支持修復假設",
"確認 repair command 不是純診斷命令或通用兜底命令",
"確認 rollback command 能在失敗時安全收斂",
"確認 verifier plan 能判斷成功、失敗與是否升級人工",
"確認 blast radius、maintenance window 與 owner 已填寫",
"確認 runtime_execution_authorized 仍為 false直到正式 approval gate 通過",
]
return {
"schema_version": "service_specific_playbook_draft_template_v1",
"status": "prefilled_owner_review_required",
"lane": lane,
"coverage_key": coverage_gap.get("coverage_key"),
"blocking_stage": coverage_gap.get("blocking_stage"),
"blockers": list(dict.fromkeys(blockers)),
"alert_selector": {
"alertname": alertname or None,
"namespace": ns or None,
"target_resource": target or None,
"target_kind": target_kind,
},
"mcp_evidence_refs": coverage_gap.get("required_mcp_evidence_refs") or [],
"matched_playbook": {
"playbook_id": playbook.playbook_id if playbook else None,
"name": playbook.name if playbook else None,
"status": playbook.status.value if playbook else None,
"trust_score": float(playbook.trust_score) if playbook else None,
},
"suggested_route": route,
"repair_command_template": command_template,
"rollback_command_template": rollback_template,
"verifier_plan_template": list(dict.fromkeys(verifier_plan)),
"owner_review_checklist": owner_review_checklist,
"evidence_snapshot_id": evidence.snapshot_id if evidence else None,
"template_is_executable": False,
"approval_required_before_execution": True,
"runtime_execution_authorized": False,
"writes_runtime_state": False,
"telegram_send_authorized": False,
}
def _build_coverage_gap(
self,
*,