From 588b0d745b7873def2b29f194f6eb4df10c24ead Mon Sep 17 00:00:00 2001 From: OG T Date: Thu, 16 Apr 2026 15:35:19 +0800 Subject: [PATCH] =?UTF-8?q?fix(aiops):=20=E4=BF=AE=E5=BE=A9=20sensors=3D0/?= =?UTF-8?q?0=20=E6=A0=B9=E5=9B=A0=20=E2=80=94=20MCPToolRegistry=20?= =?UTF-8?q?=E5=BE=9E=E6=9C=AA=E5=9C=A8=20startup=20=E5=88=9D=E5=A7=8B?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 三個問題同時修復: 1. main.py: 補上 init_mcp_tool_registry() 呼叫 - ADR-081 Phase 1 建立了 MCPToolRegistry 但從未在 lifespan startup 被呼叫 - 導致 PreDecisionInvestigator sensors=0/0,evidence_summary 永遠空白 - 空白 evidence → Diagnostician 永遠 ABSTAIN 2. signal_producer.py: str(dict) → json.dumps() - labels/annotations 用 Python str() 序列化,寫入 Redis 後無法反序列化 3. brain/incident_engine.py: 新增 _parse_dict_field() helper - 從 Redis 讀回的 labels/annotations 可能是 JSON 字串 - isinstance(..., dict) 防禦不足,需先 json.loads() 2026-04-16 ogt + Claude Sonnet 4.6(亞太): 飛輪感官修復 Co-Authored-By: Claude Sonnet 4.6 --- apps/api/src/main.py | 9 +++++++++ apps/api/src/services/signal_producer.py | 5 +++-- .../lewooogo_brain/engines/incident_engine.py | 20 +++++++++++++++++-- 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/apps/api/src/main.py b/apps/api/src/main.py index f2cd5e56..d96d2094 100644 --- a/apps/api/src/main.py +++ b/apps/api/src/main.py @@ -230,6 +230,15 @@ async def lifespan(_app: FastAPI) -> AsyncGenerator[None, None]: register_all_providers() logger.info("mcp_providers_registered") + # ADR-081 Phase 1: MCPToolRegistry 初始化(PreDecisionInvestigator 感官工具) + # 2026-04-16 ogt + Claude Sonnet 4.6: 修復 sensors=0/0 根因 — init 從未在 startup 被呼叫 + try: + from src.services.mcp_tool_registry import init_mcp_tool_registry + await init_mcp_tool_registry() + logger.info("mcp_tool_registry_initialized") + except Exception as e: + logger.warning("mcp_tool_registry_init_failed", error=str(e)) + # Phase 6.5: Telegram 心跳監控(每 30 分鐘發送到 SRE 戰情室群組) # 2026-04-16 ogt + Claude Sonnet 4.6: 恢復 — 使用者確認必須繼續在 SRE 戰情室發送 # 上次停用原因(forwarded_to_separate_group)有誤,群組就是 SRE_GROUP_CHAT_ID diff --git a/apps/api/src/services/signal_producer.py b/apps/api/src/services/signal_producer.py index fa58631b..1a4e3ec4 100644 --- a/apps/api/src/services/signal_producer.py +++ b/apps/api/src/services/signal_producer.py @@ -12,6 +12,7 @@ Signal Producer Service - Phase 17 P0 Router 層違規修復 - Router -> Service -> Redis """ +import json from dataclasses import dataclass from typing import Any @@ -74,8 +75,8 @@ class SignalProducerService: "namespace": signal.namespace, "target": signal.target, "message": signal.message, - "labels": str(signal.labels or {}), - "annotations": str(signal.annotations or {}), + "labels": json.dumps(signal.labels or {}), + "annotations": json.dumps(signal.annotations or {}), "received_at": now_taipei().isoformat(), } diff --git a/packages/lewooogo-brain/src/lewooogo_brain/engines/incident_engine.py b/packages/lewooogo-brain/src/lewooogo_brain/engines/incident_engine.py index d5317f33..991d22d7 100644 --- a/packages/lewooogo-brain/src/lewooogo_brain/engines/incident_engine.py +++ b/packages/lewooogo-brain/src/lewooogo_brain/engines/incident_engine.py @@ -30,6 +30,22 @@ from lewooogo_brain.interfaces.incident_processor import ( ) +def _parse_dict_field(value: Any) -> dict: + """將 Redis 讀回的欄位(可能是 JSON 字串或 dict)轉為 dict。 + signal_producer 以 json.dumps() 寫入;舊資料可能是 str(dict) repr,一律 fallback 空 dict。 + 2026-04-16 ogt + Claude Sonnet 4.6: 修復 labels/annotations 永遠為 {} 的根因 + """ + if isinstance(value, dict): + return value + if isinstance(value, str) and value: + try: + result = json.loads(value) + return result if isinstance(result, dict) else {} + except (json.JSONDecodeError, ValueError): + return {} + return {} + + # ============================================================================= # Memory Provider Protocol (依賴注入用) # ============================================================================= @@ -237,8 +253,8 @@ class IncidentEngine(IIncidentProcessor): severity=severity, source=data.get("source", "unknown"), fired_at=datetime.now(timezone.utc), - labels=data.get("labels", {}) if isinstance(data.get("labels"), dict) else {}, - annotations=data.get("annotations", {}) if isinstance(data.get("annotations"), dict) else {}, + labels=_parse_dict_field(data.get("labels")), + annotations=_parse_dict_field(data.get("annotations")), ) def _compute_fingerprint(self, data: dict[str, Any]) -> str: