feat(awooop): 顯示修復候選升級合約
All checks were successful
Code Review / ai-code-review (push) Successful in 14s
CD Pipeline / tests (push) Successful in 1m44s
CD Pipeline / build-and-deploy (push) Successful in 4m50s
CD Pipeline / post-deploy-checks (push) Successful in 1m40s

This commit is contained in:
Your Name
2026-06-26 12:03:58 +08:00
parent 4b18a3d8c0
commit 0fec19c707
6 changed files with 477 additions and 1 deletions

View File

@@ -38,7 +38,7 @@ from src.db.awooop_models import (
AwoooPRunStepJournal,
)
from src.db.base import get_db_context
from src.db.models import IncidentRecord, MCPAuditLog
from src.db.models import ApprovalRecord, IncidentRecord, MCPAuditLog
from src.services.audit_sink import write_audit
from src.services.awooop_ansible_audit_service import summarize_ansible_execution
from src.services.awooop_approval_token import issue_approval_token, record_approval
@@ -5584,6 +5584,7 @@ def _build_awooop_status_chain(
source_id: str | None = None,
fetch_error: str | None = None,
source_correlation: dict[str, Any] | None = None,
repair_candidate_promotion: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""Build the shared read-only status chain used by Telegram and Operator UI."""
truth_status = (
@@ -5762,9 +5763,112 @@ def _build_awooop_status_chain(
"mcp": mcp_section,
"execution": execution_section,
"source_refs": source_section,
"repair_candidate_promotion": repair_candidate_promotion
or _empty_repair_candidate_promotion_projection(),
}
def _empty_repair_candidate_promotion_projection() -> dict[str, Any]:
return {
"schema_version": "repair_candidate_promotion_projection_v1",
"status": "not_available",
"source": "approval_metadata",
"available": False,
"reason": "repair_candidate_promotion_contract_not_found",
"contract": None,
"summary": "",
"runtime_execution_authorized": False,
"runtime_write_allowed": False,
}
def _repair_candidate_promotion_summary(contract: Mapping[str, Any]) -> str:
route = str(contract.get("route_id") or "--")
ready = _safe_int(contract.get("ready_count"))
total = _safe_int(contract.get("total_count"))
blocked = _safe_int(contract.get("blocked_count"))
status_value = str(contract.get("status") or "unknown")
return (
f"route={route}; promotion={ready}/{total}; "
f"blocked={blocked}; status={status_value}; runtime=false"
)
def _repair_candidate_projection_from_metadata(
metadata: Mapping[str, Any] | None,
) -> dict[str, Any] | None:
if not isinstance(metadata, Mapping):
return None
package = metadata.get("repair_candidate_draft_package")
package = package if isinstance(package, Mapping) else {}
contract = package.get("candidate_promotion_contract") or metadata.get(
"repair_candidate_promotion_contract"
)
if not isinstance(contract, Mapping):
return None
contract_dict = dict(contract)
summary = str(metadata.get("repair_candidate_promotion_summary") or "").strip()
if not summary:
summary = _repair_candidate_promotion_summary(contract_dict)
work_item = package.get("awooop_work_item")
if not isinstance(work_item, Mapping):
work_item = {}
return {
"schema_version": "repair_candidate_promotion_projection_v1",
"status": str(contract_dict.get("status") or "unknown"),
"source": "approval_metadata",
"available": True,
"approval_id": str(metadata.get("approval_id") or ""),
"work_item_id": str(work_item.get("work_item_id") or ""),
"work_item_url": str(
work_item.get("work_item_url")
or work_item.get("work_item_href")
or ""
),
"contract": contract_dict,
"summary": summary,
"runtime_execution_authorized": bool(
contract_dict.get("runtime_execution_authorized") is True
),
"runtime_write_allowed": bool(contract_dict.get("runtime_write_allowed") is True),
}
async def _fetch_repair_candidate_promotion_projection(
*,
incident_ids: list[str],
project_id: str,
) -> dict[str, Any]:
"""Read the latest repair-candidate promotion contract for status-chain UI.
The projection is read-only: it surfaces existing ApprovalRecord metadata
so Work Items can show why a draft is blocked before runtime execution.
"""
normalized_ids = [incident_id for incident_id in incident_ids if incident_id]
if not normalized_ids:
return _empty_repair_candidate_promotion_projection()
async with get_db_context(project_id or "awoooi") as db:
stmt = (
select(ApprovalRecord)
.where(ApprovalRecord.incident_id.in_(normalized_ids))
.where(ApprovalRecord.extra_metadata.is_not(None))
.order_by(ApprovalRecord.created_at.desc())
.limit(20)
)
result = await db.execute(stmt)
rows = list(result.scalars().all())
for row in rows:
metadata = dict(row.extra_metadata or {})
metadata["approval_id"] = str(row.id)
projection = _repair_candidate_projection_from_metadata(metadata)
if projection is not None:
return projection
return _empty_repair_candidate_promotion_projection()
async def _fetch_awooop_status_chain(
*,
incident_ids: list[str],
@@ -5808,6 +5912,24 @@ async def _fetch_awooop_status_chain(
missing_reason="source_correlation_fetch_failed",
)
try:
repair_candidate_promotion = await _fetch_repair_candidate_promotion_projection(
incident_ids=incident_ids,
project_id=project_id or "awoooi",
)
except Exception as exc:
logger.warning(
"operator_repair_candidate_promotion_projection_fetch_failed",
incident_ids=incident_ids,
project_id=project_id,
error=str(exc),
)
repair_candidate_promotion = {
**_empty_repair_candidate_promotion_projection(),
"status": "fetch_failed",
"reason": "repair_candidate_promotion_projection_fetch_failed",
}
return _build_awooop_status_chain(
incident_ids=incident_ids,
truth_chain=truth_chain,
@@ -5815,6 +5937,7 @@ async def _fetch_awooop_status_chain(
source_id=source_id,
fetch_error=fetch_error,
source_correlation=source_correlation,
repair_candidate_promotion=repair_candidate_promotion,
)