diff --git a/apps/api/src/services/proposal_service.py b/apps/api/src/services/proposal_service.py index 76d65eb59..9662246bb 100644 --- a/apps/api/src/services/proposal_service.py +++ b/apps/api/src/services/proposal_service.py @@ -19,11 +19,9 @@ Decision Proposal Service - Phase 6.4 決策輸出層 """ from datetime import UTC, datetime -from uuid import UUID import structlog -from src.core.redis_client import get_redis from src.db.base import get_db_context from src.db.models import IncidentRecord from src.models.approval import ( @@ -42,8 +40,11 @@ from src.models.incident import ( Severity, ) from src.services.approval_db import get_approval_service +from src.services.incident_engine import get_incident_engine +from src.services.incident_memory import get_incident_memory from src.services.openclaw import get_openclaw from src.services.trust_engine import normalize_action_pattern, trust_engine +from src.utils.incident_converter import local_to_brain logger = structlog.get_logger(__name__) @@ -52,8 +53,6 @@ logger = structlog.get_logger(__name__) # Constants # ============================================================================= -INCIDENT_KEY_PREFIX = "incident:" - # Severity → RiskLevel 對應 SEVERITY_TO_RISK = { Severity.P0: ApprovalRiskLevel.CRITICAL, # P0 (critical) → CRITICAL (2 簽核) @@ -292,67 +291,21 @@ class ProposalService: async def _load_incident(self, incident_id: str) -> Incident | None: """ - 載入 Incident (Redis 優先,DB 備援) + 載入 Incident (委派給 IncidentEngineAdapter) + + ADR-046 (2026-04-01 ogt): 透過 IncidentEngineAdapter.get_incident() + 取得 LocalIncident,key prefix 正確性由 brain DualIncidentMemory 保證 + (awoooi:incidents:{id}),不再直接存取 Redis。 """ - redis_client = get_redis() - key = f"{INCIDENT_KEY_PREFIX}{incident_id}" - - # 1. 嘗試從 Redis 載入 try: - data = await redis_client.get(key) - if data: - return Incident.model_validate_json(data) + return await get_incident_engine().get_incident(incident_id) except Exception as e: logger.warning( - "redis_load_failed", + "load_incident_failed", incident_id=incident_id, error=str(e), ) - - # 2. 從 DB 載入 - try: - async with get_db_context() as db: - from sqlalchemy import select - - stmt = select(IncidentRecord).where( - IncidentRecord.incident_id == incident_id - ) - result = await db.execute(stmt) - record = result.scalar_one_or_none() - - if record: - return self._record_to_incident(record) - except Exception as e: - logger.warning( - "db_load_failed", - incident_id=incident_id, - error=str(e), - ) - - return None - - def _record_to_incident(self, record: IncidentRecord) -> Incident: - """將 DB Record 轉換為 Incident""" - from src.models.incident import Signal - - signals = [ - Signal.model_validate(s) for s in (record.signals or []) - ] - - return Incident( - incident_id=record.incident_id, - status=IncidentStatus(record.status.lower()), - severity=Severity(record.severity), - signals=signals, - affected_services=record.affected_services or [], - proposal_ids=[UUID(pid) for pid in (record.proposal_ids or [])], - decision_chain=None, # DB 不存完整決策鏈 - outcome=None, # DB 存結果狀態而非完整 outcome - created_at=record.created_at, - updated_at=record.updated_at, - resolved_at=record.resolved_at, - closed_at=record.closed_at, - ) + return None # ========================================================================= # 輔助方法: 決定修復動作 @@ -466,17 +419,15 @@ class ProposalService: async def _persist_incident(self, incident: Incident) -> None: """ 更新 Incident 到 Redis + DB - """ - redis_client = get_redis() - key = f"{INCIDENT_KEY_PREFIX}{incident.incident_id}" - # 1. 更新 Redis + ADR-046 (2026-04-01 ogt): Redis 委派給 brain DualIncidentMemory + 確保 key prefix 一致性 (awoooi:incidents:{id})。 + DB 直接更新 status + proposal_ids (最小化變更)。 + """ + # 1. 更新 Redis (委派給 brain DualIncidentMemory) try: - await redis_client.set( - key, - incident.model_dump_json(), - ex=604800, # 7 days - ) + brain_incident = local_to_brain(incident) + await get_incident_memory().save_incident(brain_incident) except Exception as e: logger.warning( "redis_persist_failed",