feat(awooop): 顯示修復候選升級合約
This commit is contained in:
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user