Files
awoooi/apps/api/src/services/notifications/manager.py
Your Name f9f2263c00
All checks were successful
CD Pipeline / build-and-deploy (push) Successful in 8m57s
fix(execution-feedback): 修復系統自動化反饋完全斷鏈的三層 P0 故障
**背景**
用戶報告執行狀態卡在「 執行中...」永不回報,導致自動修復機制完全癱瘓
(信心度修復後,執行失敗但無法推送 Telegram 卡片通知)

**L1 — Post-verify AttributeError(2 處)**
- approval_execution.py:757, 1010 調用不存在方法 IncidentService.get_incident()
- 正確方法:get_from_working_memory() fallback get_from_episodic_memory()
- 影響:post-verify 邏輯被 exception 無聲吞掉,下游 Telegram 推送完全卡住

**L2 — Notification Provider 未配置**
- 新增 notifications/telegram.py:複用既有 TelegramGateway.send_notification()
- 修改 manager.py:初始化時註冊 TelegramWebhookProvider
- 影響:執行完成後無任何 provider 發送推送,導致 Telegram 看不到結果

**L3 — Solver Agent 語意合成生成殘缺指令**
- 舊邏輯:action_title="重啟服務" → 合成 "kubectl rollout restart deployment -n awoooi-prod"(缺名)
- 下游 operation_parser 無法解析(regex 要求 deployment/<name>)
- 修法:優先從 parsed 提取 target 欄位;無名則 return [],降級到唯讀調查指令
- 測試全部通過:35/35,含 11 個新安全測試

**驗證**
- 被阻擋的惡意 kubectl_command 現在正確 fall-through 到語意合成路徑
- 無 target 名稱時返回空列表,不再生成殘缺指令
- Telegram 執行結果推送鏈路已完整

**預期效果**
- 執行失敗 → 立即收到「 執行失敗」Telegram 卡片(L1 + L2 修復)
- 自動化決策遵循白名單,避免生成無法執行的指令(L3 修復)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 03:29:38 +08:00

173 lines
5.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Notification Manager
====================
Phase 6: leWOOOgo Output Plugins
管理所有 NotificationProvider統一發送介面
"""
from src.core.logging import get_logger
from .base import (
NotificationMessage,
NotificationProvider,
NotificationResult,
NotificationStatus,
)
from .discord import DiscordWebhookProvider
logger = get_logger("awoooi.notifications.manager")
class NotificationManager:
"""
通知管理器
管理多個 NotificationProvider支援:
- 同時發送至多個頻道
- 優雅降級 (單一 Provider 失敗不影響其他)
- 結果追蹤
"""
def __init__(self):
self._providers: list[NotificationProvider] = []
self._initialized = False
def register(self, provider: NotificationProvider) -> None:
"""註冊 Provider"""
if provider.enabled:
self._providers.append(provider)
logger.info(
"notification_provider_registered",
provider=provider.name,
enabled=provider.enabled,
)
else:
logger.warning(
"notification_provider_disabled",
provider=provider.name,
)
def initialize(self) -> None:
"""初始化所有 Provider"""
if self._initialized:
return
# 註冊 Discord
discord = DiscordWebhookProvider()
self.register(discord)
# 2026-04-25 修復 L2註冊 Telegram provider
# 根本原因:執行完成後無 provider 發送 Telegram 通知
from .telegram import TelegramWebhookProvider
telegram = TelegramWebhookProvider()
self.register(telegram)
self._initialized = True
logger.info(
"notification_manager_initialized",
provider_count=len(self._providers),
providers=[p.name for p in self._providers],
)
async def send_all(self, message: NotificationMessage) -> list[NotificationResult]:
"""
發送通知至所有已註冊的 Provider
Returns:
list[NotificationResult]: 各 Provider 的發送結果
"""
if not self._initialized:
self.initialize()
if not self._providers:
logger.warning("no_notification_providers_available")
return [
NotificationResult(
status=NotificationStatus.SKIPPED,
provider="none",
message="No notification providers configured",
)
]
results = []
for provider in self._providers:
try:
result = await provider.send(message)
results.append(result)
logger.info(
"notification_sent",
provider=provider.name,
status=result.status.value,
)
except Exception as e:
logger.exception(
"notification_send_failed",
provider=provider.name,
error=str(e),
)
results.append(
NotificationResult(
status=NotificationStatus.FAILED,
provider=provider.name,
message="Exception during send",
error=str(e),
)
)
return results
async def test_all(self) -> dict[str, bool]:
"""
測試所有 Provider 連線
Returns:
dict[str, bool]: Provider 名稱 → 連線狀態
"""
if not self._initialized:
self.initialize()
results = {}
for provider in self._providers:
try:
results[provider.name] = await provider.test_connection()
except Exception as e:
logger.error(
"notification_test_failed",
provider=provider.name,
error=str(e),
)
results[provider.name] = False
return results
async def close(self) -> None:
"""關閉所有 Provider"""
for provider in self._providers:
if hasattr(provider, "close"):
await provider.close()
# =============================================================================
# Singleton Instance
# =============================================================================
_notification_manager: NotificationManager | None = None
def get_notification_manager() -> NotificationManager:
"""取得 NotificationManager 單例"""
global _notification_manager
if _notification_manager is None:
_notification_manager = NotificationManager()
_notification_manager.initialize()
return _notification_manager
async def close_notification_manager() -> None:
"""關閉 NotificationManager"""
global _notification_manager
if _notification_manager:
await _notification_manager.close()
_notification_manager = None