From 44840f5e7337167198ebc1fdbd50691b50294f11 Mon Sep 17 00:00:00 2001 From: OG T Date: Wed, 1 Apr 2026 09:11:57 +0800 Subject: [PATCH] =?UTF-8?q?fix(service):=20#123=20proposal=5Fservice.py=20?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3=20key=20prefix=20+=20=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E9=87=8D=E8=A4=87=E9=82=8F=E8=BC=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ADR-046 修復: proposal_service 使用錯誤 Redis key prefix "incident:" (brain 使用 "awoooi:incidents:"),導致 R-R2 後 load/persist 失效。 變更: - _load_incident(): 委派給 IncidentEngineAdapter.get_incident() (正確 key prefix,含 brain→local 型別轉換) - _persist_incident(): Redis 部分委派給 brain DualIncidentMemory 透過 local_to_brain() 轉換後儲存 (key prefix 一致) - 移除 _record_to_incident() 重複邏輯 (已由 IncidentEngineAdapter 處理) - 移除 INCIDENT_KEY_PREFIX 常數 - 移除 get_redis() 直接依賴 Co-Authored-By: Claude Sonnet 4.6 --- apps/api/src/services/proposal_service.py | 85 +++++------------------ 1 file changed, 18 insertions(+), 67 deletions(-) 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",