feat(flywheel): expose incident processing timeline
All checks were successful
CD Pipeline / build-and-deploy (push) Successful in 10m56s

This commit is contained in:
Your Name
2026-04-29 23:38:30 +08:00
parent dae0aa2312
commit 4a57c2d04f
12 changed files with 984 additions and 12 deletions

View File

@@ -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,
)
# 如果觸發執行,加入背景任務

View File

@@ -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
# =============================================================================

View File

@@ -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 {