All checks were successful
CD Pipeline / build-and-deploy (push) Successful in 8m57s
**背景** 用戶報告執行狀態卡在「⚡ 執行中...」永不回報,導致自動修復機制完全癱瘓 (信心度修復後,執行失敗但無法推送 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>
173 lines
5.0 KiB
Python
173 lines
5.0 KiB
Python
"""
|
||
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
|