feat(flywheel): expose incident processing timeline
All checks were successful
CD Pipeline / build-and-deploy (push) Successful in 10m56s
All checks were successful
CD Pipeline / build-and-deploy (push) Successful in 10m56s
This commit is contained in:
@@ -234,6 +234,7 @@ async def create_approval(
|
||||
title=f"新授權請求建立: {approval.action[:50]}...",
|
||||
risk_level=approval.risk_level.value,
|
||||
approval_id=str(approval.id),
|
||||
incident_id=approval.incident_id,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
@@ -326,6 +327,7 @@ async def sign_approval(
|
||||
actor_role="signer",
|
||||
risk_level=approval.risk_level.value,
|
||||
approval_id=str(approval_id),
|
||||
incident_id=approval.incident_id,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
@@ -354,6 +356,7 @@ async def sign_approval(
|
||||
actor="OpenClaw",
|
||||
actor_role="executor",
|
||||
approval_id=str(approval_id),
|
||||
incident_id=approval.incident_id,
|
||||
)
|
||||
|
||||
execution_svc = get_execution_service()
|
||||
@@ -461,6 +464,7 @@ async def reject_approval(
|
||||
actor=request.rejector_name,
|
||||
actor_role="rejector",
|
||||
approval_id=str(approval_id),
|
||||
incident_id=approval.incident_id,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
@@ -615,6 +619,7 @@ async def bulk_approve(
|
||||
actor_role="signer",
|
||||
risk_level=signed_approval.risk_level.value,
|
||||
approval_id=approval_id_str,
|
||||
incident_id=signed_approval.incident_id,
|
||||
)
|
||||
|
||||
# 如果觸發執行,加入背景任務
|
||||
|
||||
@@ -30,6 +30,7 @@ from src.models.incident import Incident, IncidentStatus, Severity
|
||||
# Phase 16 R3.3b (2026-03-25 台北時區): Repository 層整合 - 已移至 Service 層
|
||||
from src.services.decision_manager import get_decision_manager
|
||||
from src.services.incident_service import get_incident_service
|
||||
from src.services.incident_timeline_service import fetch_incident_timeline
|
||||
from src.services.proposal_service import get_proposal_service
|
||||
from src.utils.timezone import now_taipei
|
||||
|
||||
@@ -92,6 +93,48 @@ class ProposalGenerateResponse(BaseModel):
|
||||
incident_status: str | None = None
|
||||
|
||||
|
||||
class IncidentTimelineEvent(BaseModel):
|
||||
"""事件處理歷程中的一筆原始或合成事件"""
|
||||
stage: str
|
||||
status: str
|
||||
title: str
|
||||
description: str | None = None
|
||||
actor: str | None = None
|
||||
timestamp: str | None = None
|
||||
source_table: str | None = None
|
||||
data: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class IncidentTimelineStage(BaseModel):
|
||||
"""事件處理歷程的標準階段"""
|
||||
stage: str
|
||||
label: str
|
||||
status: str
|
||||
timestamp: str | None = None
|
||||
title: str
|
||||
description: str | None = None
|
||||
actor: str | None = None
|
||||
source_table: str | None = None
|
||||
data: dict[str, Any] = Field(default_factory=dict)
|
||||
events: list[IncidentTimelineEvent] = Field(default_factory=list)
|
||||
|
||||
|
||||
class IncidentTimelineResponse(BaseModel):
|
||||
"""事件完整處理歷程回應"""
|
||||
incident_id: str
|
||||
title: str
|
||||
status: str
|
||||
severity: str
|
||||
started_at: str | None = None
|
||||
updated_at: str | None = None
|
||||
resolved_at: str | None = None
|
||||
affected_services: list[str] = Field(default_factory=list)
|
||||
approval_ids: list[str] = Field(default_factory=list)
|
||||
timeline: list[IncidentTimelineStage] = Field(default_factory=list)
|
||||
events: list[IncidentTimelineEvent] = Field(default_factory=list)
|
||||
ascii_timeline: str
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# GET /api/v1/incidents
|
||||
# =============================================================================
|
||||
@@ -271,6 +314,50 @@ async def get_incident(incident_id: str) -> IncidentResponse:
|
||||
) from e
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# GET /api/v1/incidents/{incident_id}/timeline
|
||||
# =============================================================================
|
||||
|
||||
@router.get(
|
||||
"/{incident_id}/timeline",
|
||||
response_model=IncidentTimelineResponse,
|
||||
summary="取得事件完整處理歷程",
|
||||
description="彙整 webhook、AI、目標、風險、安全閘、執行、驗證、KM 與結案事件。",
|
||||
)
|
||||
async def get_incident_timeline(incident_id: str) -> IncidentTimelineResponse:
|
||||
"""
|
||||
取得單一 Incident 的端到端處理歷程。
|
||||
"""
|
||||
try:
|
||||
timeline = await fetch_incident_timeline(incident_id)
|
||||
if timeline is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Incident not found: {incident_id}",
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"incident_timeline_fetched",
|
||||
incident_id=incident_id,
|
||||
stage_count=len(timeline.get("timeline", [])),
|
||||
event_count=len(timeline.get("events", [])),
|
||||
)
|
||||
return IncidentTimelineResponse.model_validate(timeline)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.exception(
|
||||
"get_incident_timeline_error",
|
||||
incident_id=incident_id,
|
||||
error=str(e),
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to get incident timeline: {str(e)}",
|
||||
) from e
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# POST /api/v1/incidents/{incident_id}/proposal
|
||||
# =============================================================================
|
||||
|
||||
@@ -25,6 +25,7 @@ logger = get_logger("awoooi.timeline")
|
||||
)
|
||||
async def get_timeline_events(
|
||||
limit: int = Query(default=100, ge=1, le=200, description="回傳筆數上限"),
|
||||
incident_id: str | None = Query(default=None, description="只回傳特定 Incident 的事件"),
|
||||
) -> dict:
|
||||
"""
|
||||
取得時間軸事件 (後端授權來源)
|
||||
@@ -34,12 +35,13 @@ async def get_timeline_events(
|
||||
count: 事件總數
|
||||
"""
|
||||
service = get_timeline_service()
|
||||
events = await service.get_events(limit=limit)
|
||||
events = await service.get_events(limit=limit, incident_id=incident_id)
|
||||
|
||||
logger.info(
|
||||
"timeline_events_fetched",
|
||||
count=len(events),
|
||||
limit=limit,
|
||||
incident_id=incident_id,
|
||||
)
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user