mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-01 17:00:21 +08:00
refactor: 移除 Python 后端源码,全面迁移至 Rust gateway 架构
- 删除全部 Python 源码 (src/) 及 Alembic 迁移脚本,归档至 _deprecated_py_src/ - 重构 Rust gateway ai_pipeline: 拆分 planner/finalize 模块,新增 contracts/adaptation 层 - 重组 handlers 模块为 admin/public/proxy/internal/shared 子模块结构 - 新增 executor 模块,引入 Rust 原生数据库迁移 (aether-data/migrations) - 简化 CI/Docker 构建流程,移除 base image 二级构建,统一为单一 app image - 移除 Python 相关基础设施文件 (entrypoint.sh, gunicorn_conf.py, Dockerfile.base)
This commit is contained in:
15
_deprecated_py_src/plugins/notification/__init__.py
Normal file
15
_deprecated_py_src/plugins/notification/__init__.py
Normal file
@@ -0,0 +1,15 @@
|
||||
"""
|
||||
通知插件
|
||||
"""
|
||||
|
||||
from .base import Notification, NotificationLevel, NotificationPlugin
|
||||
from .email import EmailNotificationPlugin
|
||||
from .webhook import WebhookNotificationPlugin
|
||||
|
||||
__all__ = [
|
||||
"NotificationPlugin",
|
||||
"NotificationLevel",
|
||||
"Notification",
|
||||
"WebhookNotificationPlugin",
|
||||
"EmailNotificationPlugin",
|
||||
]
|
||||
416
_deprecated_py_src/plugins/notification/base.py
Normal file
416
_deprecated_py_src/plugins/notification/base.py
Normal file
@@ -0,0 +1,416 @@
|
||||
"""
|
||||
通知插件基类
|
||||
定义通知的接口和数据结构
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from abc import abstractmethod
|
||||
from datetime import datetime, timezone
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
|
||||
from src.plugins.common import BasePlugin
|
||||
|
||||
|
||||
class NotificationLevel(Enum):
|
||||
"""通知级别"""
|
||||
|
||||
INFO = "info"
|
||||
WARNING = "warning"
|
||||
ERROR = "error"
|
||||
CRITICAL = "critical"
|
||||
|
||||
|
||||
class Notification:
|
||||
"""通知对象"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
title: str,
|
||||
message: str,
|
||||
level: NotificationLevel = NotificationLevel.INFO,
|
||||
notification_type: str | None = None,
|
||||
source: str | None = None,
|
||||
timestamp: datetime | None = None,
|
||||
metadata: dict[str, Any] | None = None,
|
||||
recipient: str | None = None,
|
||||
tags: list[str] | None = None,
|
||||
):
|
||||
self.title = title
|
||||
self.message = message
|
||||
self.level = level
|
||||
self.notification_type = notification_type or "system"
|
||||
self.source = source or "aether"
|
||||
self.timestamp = timestamp or datetime.now(timezone.utc)
|
||||
self.metadata = metadata or {}
|
||||
self.recipient = recipient
|
||||
self.tags = tags or []
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
"""转换为字典"""
|
||||
return {
|
||||
"title": self.title,
|
||||
"message": self.message,
|
||||
"level": self.level.value,
|
||||
"type": self.notification_type,
|
||||
"source": self.source,
|
||||
"timestamp": self.timestamp.isoformat(),
|
||||
"metadata": self.metadata,
|
||||
"recipient": self.recipient,
|
||||
"tags": self.tags,
|
||||
}
|
||||
|
||||
def to_json(self) -> str:
|
||||
"""转换为JSON"""
|
||||
return json.dumps(self.to_dict(), default=str)
|
||||
|
||||
def format_message(self, template: str | None = None) -> str:
|
||||
"""格式化消息"""
|
||||
if template:
|
||||
return template.format(
|
||||
title=self.title,
|
||||
message=self.message,
|
||||
level=self.level.value,
|
||||
type=self.notification_type,
|
||||
source=self.source,
|
||||
timestamp=self.timestamp.isoformat(),
|
||||
**self.metadata,
|
||||
)
|
||||
else:
|
||||
# 默认格式
|
||||
return f"[{self.level.value.upper()}] {self.title}\n{self.message}"
|
||||
|
||||
|
||||
class NotificationPlugin(BasePlugin):
|
||||
"""
|
||||
通知插件基类
|
||||
所有通知插件必须实现这个接口
|
||||
|
||||
提供统一的重试机制,子类只需实现 _do_send 和 _do_send_batch 方法
|
||||
"""
|
||||
|
||||
def __init__(self, name: str = "notification", config: dict[str, Any] | None = None):
|
||||
# 调用父类初始化,设置metadata
|
||||
super().__init__(
|
||||
name=name, config=config, description="Notification Plugin", version="1.0.0"
|
||||
)
|
||||
|
||||
self.min_level = NotificationLevel[self.config.get("min_level", "INFO").upper()]
|
||||
self.batch_size = self.config.get("batch_size", 10)
|
||||
self.flush_interval = self.config.get("flush_interval", 60) # 秒
|
||||
self.retry_count = self.config.get("retry_count", 3)
|
||||
self.retry_delay = self.config.get("retry_delay", 5) # 秒
|
||||
self.retry_backoff = self.config.get("retry_backoff", 2.0) # 指数退避因子
|
||||
|
||||
# 统计信息
|
||||
self._send_attempts = 0
|
||||
self._send_successes = 0
|
||||
self._send_failures = 0
|
||||
self._retry_total = 0
|
||||
|
||||
async def send(self, notification: Notification) -> bool:
|
||||
"""
|
||||
发送单个通知(带重试机制)
|
||||
|
||||
Args:
|
||||
notification: 通知对象
|
||||
|
||||
Returns:
|
||||
是否发送成功
|
||||
"""
|
||||
if not self.should_send(notification):
|
||||
return False
|
||||
|
||||
self._send_attempts += 1
|
||||
last_error = None
|
||||
|
||||
for attempt in range(self.retry_count):
|
||||
try:
|
||||
result = await self._do_send(notification)
|
||||
if result:
|
||||
self._send_successes += 1
|
||||
return True
|
||||
except Exception as e:
|
||||
last_error = e
|
||||
|
||||
# 如果不是最后一次尝试,等待后重试
|
||||
if attempt < self.retry_count - 1:
|
||||
self._retry_total += 1
|
||||
delay = self.retry_delay * (self.retry_backoff**attempt)
|
||||
await asyncio.sleep(delay)
|
||||
|
||||
# 所有重试都失败
|
||||
self._send_failures += 1
|
||||
if last_error:
|
||||
# 可以在这里记录日志,但不抛出异常
|
||||
pass
|
||||
return False
|
||||
|
||||
@abstractmethod
|
||||
async def _do_send(self, notification: Notification) -> bool:
|
||||
"""
|
||||
实际发送单个通知(子类实现)
|
||||
|
||||
Args:
|
||||
notification: 通知对象
|
||||
|
||||
Returns:
|
||||
是否发送成功
|
||||
"""
|
||||
pass
|
||||
|
||||
async def send_batch(self, notifications: list[Notification]) -> dict[str, Any]:
|
||||
"""
|
||||
批量发送通知(带重试机制)
|
||||
|
||||
Args:
|
||||
notifications: 通知列表
|
||||
|
||||
Returns:
|
||||
发送结果统计
|
||||
"""
|
||||
# 过滤应该发送的通知
|
||||
to_send = [n for n in notifications if self.should_send(n)]
|
||||
|
||||
if not to_send:
|
||||
return {"total": 0, "sent": 0, "failed": 0}
|
||||
|
||||
self._send_attempts += len(to_send)
|
||||
last_error = None
|
||||
result = None
|
||||
|
||||
for attempt in range(self.retry_count):
|
||||
try:
|
||||
result = await self._do_send_batch(to_send)
|
||||
if result and result.get("sent", 0) == len(to_send):
|
||||
self._send_successes += result.get("sent", 0)
|
||||
return result
|
||||
elif result:
|
||||
# 部分成功
|
||||
self._send_successes += result.get("sent", 0)
|
||||
self._send_failures += result.get("failed", 0)
|
||||
return result
|
||||
except Exception as e:
|
||||
last_error = e
|
||||
|
||||
# 如果不是最后一次尝试,等待后重试
|
||||
if attempt < self.retry_count - 1:
|
||||
self._retry_total += 1
|
||||
delay = self.retry_delay * (self.retry_backoff**attempt)
|
||||
await asyncio.sleep(delay)
|
||||
|
||||
# 所有重试都失败
|
||||
self._send_failures += len(to_send)
|
||||
return {
|
||||
"total": len(to_send),
|
||||
"sent": 0,
|
||||
"failed": len(to_send),
|
||||
"error": str(last_error) if last_error else "Unknown error",
|
||||
}
|
||||
|
||||
@abstractmethod
|
||||
async def _do_send_batch(self, notifications: list[Notification]) -> dict[str, Any]:
|
||||
"""
|
||||
实际批量发送通知(子类实现)
|
||||
|
||||
Args:
|
||||
notifications: 通知列表
|
||||
|
||||
Returns:
|
||||
发送结果统计 {"total": int, "sent": int, "failed": int}
|
||||
"""
|
||||
pass
|
||||
|
||||
def should_send(self, notification: Notification) -> bool:
|
||||
"""判断是否应该发送通知"""
|
||||
if not self.enabled:
|
||||
return False
|
||||
|
||||
# 级别过滤
|
||||
level_values = {level: i for i, level in enumerate(NotificationLevel)}
|
||||
if level_values[notification.level] < level_values[self.min_level]:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
async def send_error(
|
||||
self,
|
||||
error: Exception,
|
||||
context: dict[str, Any] | None = None,
|
||||
recipient: str | None = None,
|
||||
) -> bool:
|
||||
"""发送错误通知"""
|
||||
notification = Notification(
|
||||
title=f"Error: {type(error).__name__}",
|
||||
message=str(error),
|
||||
level=NotificationLevel.ERROR,
|
||||
notification_type="error",
|
||||
metadata=context or {},
|
||||
recipient=recipient,
|
||||
tags=["error", type(error).__name__],
|
||||
)
|
||||
return await self.send(notification)
|
||||
|
||||
async def send_warning(
|
||||
self,
|
||||
title: str,
|
||||
message: str,
|
||||
context: dict[str, Any] | None = None,
|
||||
recipient: str | None = None,
|
||||
) -> bool:
|
||||
"""发送警告通知"""
|
||||
notification = Notification(
|
||||
title=title,
|
||||
message=message,
|
||||
level=NotificationLevel.WARNING,
|
||||
notification_type="warning",
|
||||
metadata=context or {},
|
||||
recipient=recipient,
|
||||
tags=["warning"],
|
||||
)
|
||||
return await self.send(notification)
|
||||
|
||||
async def send_info(
|
||||
self,
|
||||
title: str,
|
||||
message: str,
|
||||
context: dict[str, Any] | None = None,
|
||||
recipient: str | None = None,
|
||||
) -> bool:
|
||||
"""发送信息通知"""
|
||||
notification = Notification(
|
||||
title=title,
|
||||
message=message,
|
||||
level=NotificationLevel.INFO,
|
||||
notification_type="info",
|
||||
metadata=context or {},
|
||||
recipient=recipient,
|
||||
tags=["info"],
|
||||
)
|
||||
return await self.send(notification)
|
||||
|
||||
async def send_critical(
|
||||
self,
|
||||
title: str,
|
||||
message: str,
|
||||
context: dict[str, Any] | None = None,
|
||||
recipient: str | None = None,
|
||||
) -> bool:
|
||||
"""发送严重通知"""
|
||||
notification = Notification(
|
||||
title=title,
|
||||
message=message,
|
||||
level=NotificationLevel.CRITICAL,
|
||||
notification_type="critical",
|
||||
metadata=context or {},
|
||||
recipient=recipient,
|
||||
tags=["critical"],
|
||||
)
|
||||
return await self.send(notification)
|
||||
|
||||
async def send_usage_alert(
|
||||
self,
|
||||
user_id: str,
|
||||
usage_percent: float,
|
||||
limit: int,
|
||||
current: int,
|
||||
resource_type: str = "tokens",
|
||||
) -> bool:
|
||||
"""发送使用量警告"""
|
||||
level = NotificationLevel.INFO
|
||||
if usage_percent >= 90:
|
||||
level = NotificationLevel.CRITICAL
|
||||
elif usage_percent >= 75:
|
||||
level = NotificationLevel.WARNING
|
||||
|
||||
notification = Notification(
|
||||
title=f"Usage Alert: {resource_type.capitalize()}",
|
||||
message=f"User {user_id} has used {usage_percent:.1f}% of their {resource_type} quota ({current}/{limit})",
|
||||
level=level,
|
||||
notification_type="usage_alert",
|
||||
metadata={
|
||||
"user_id": user_id,
|
||||
"usage_percent": usage_percent,
|
||||
"limit": limit,
|
||||
"current": current,
|
||||
"resource_type": resource_type,
|
||||
},
|
||||
tags=["usage", resource_type],
|
||||
)
|
||||
return await self.send(notification)
|
||||
|
||||
async def send_provider_status(
|
||||
self,
|
||||
provider: str,
|
||||
status: str,
|
||||
error: str | None = None,
|
||||
latency: float | None = None,
|
||||
) -> bool:
|
||||
"""发送提供商状态通知"""
|
||||
level = NotificationLevel.INFO
|
||||
if status == "down":
|
||||
level = NotificationLevel.CRITICAL
|
||||
elif status == "degraded":
|
||||
level = NotificationLevel.WARNING
|
||||
|
||||
message = f"Provider {provider} is {status}"
|
||||
if error:
|
||||
message += f": {error}"
|
||||
if latency:
|
||||
message += f" (latency: {latency:.2f}s)"
|
||||
|
||||
notification = Notification(
|
||||
title=f"Provider Status: {provider}",
|
||||
message=message,
|
||||
level=level,
|
||||
notification_type="provider_status",
|
||||
metadata={"provider": provider, "status": status, "error": error, "latency": latency},
|
||||
tags=["provider", status],
|
||||
)
|
||||
return await self.send(notification)
|
||||
|
||||
async def get_stats(self) -> dict[str, Any]:
|
||||
"""
|
||||
获取统计信息
|
||||
|
||||
Returns:
|
||||
统计信息字典,包含基础重试统计和子类特定统计
|
||||
"""
|
||||
base_stats = {
|
||||
"plugin_name": self.name,
|
||||
"enabled": self.enabled,
|
||||
"send_attempts": self._send_attempts,
|
||||
"send_successes": self._send_successes,
|
||||
"send_failures": self._send_failures,
|
||||
"retry_total": self._retry_total,
|
||||
"success_rate": (
|
||||
self._send_successes / self._send_attempts * 100 if self._send_attempts > 0 else 0
|
||||
),
|
||||
"config": {
|
||||
"min_level": self.min_level.value,
|
||||
"retry_count": self.retry_count,
|
||||
"retry_delay": self.retry_delay,
|
||||
"retry_backoff": self.retry_backoff,
|
||||
"batch_size": self.batch_size,
|
||||
"flush_interval": self.flush_interval,
|
||||
},
|
||||
}
|
||||
|
||||
# 获取子类特定的统计信息
|
||||
extra_stats = await self._get_extra_stats()
|
||||
if extra_stats:
|
||||
base_stats.update(extra_stats)
|
||||
|
||||
return base_stats
|
||||
|
||||
async def _get_extra_stats(self) -> dict[str, Any]:
|
||||
"""
|
||||
获取子类特定的统计信息(子类可选重写)
|
||||
|
||||
Returns:
|
||||
额外的统计信息
|
||||
"""
|
||||
return {}
|
||||
381
_deprecated_py_src/plugins/notification/email.py
Normal file
381
_deprecated_py_src/plugins/notification/email.py
Normal file
@@ -0,0 +1,381 @@
|
||||
"""
|
||||
邮件通知插件
|
||||
通过SMTP发送邮件通知
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import smtplib
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
from typing import Any
|
||||
|
||||
aiosmtplib: Any
|
||||
try:
|
||||
import aiosmtplib as _aiosmtplib
|
||||
except ImportError:
|
||||
AIOSMTPLIB_AVAILABLE = False
|
||||
aiosmtplib = None
|
||||
else:
|
||||
AIOSMTPLIB_AVAILABLE = True
|
||||
aiosmtplib = _aiosmtplib
|
||||
|
||||
from src.core.logger import logger
|
||||
from src.utils.async_utils import run_in_executor
|
||||
|
||||
from .base import Notification, NotificationLevel, NotificationPlugin
|
||||
|
||||
|
||||
class EmailNotificationPlugin(NotificationPlugin):
|
||||
"""
|
||||
邮件通知插件
|
||||
支持HTML和纯文本邮件
|
||||
"""
|
||||
|
||||
def __init__(self, name: str = "email", config: dict[str, Any] | None = None):
|
||||
super().__init__(name, config or {})
|
||||
|
||||
# SMTP配置
|
||||
self.smtp_host = config.get("smtp_host") if config else None
|
||||
self.smtp_port = config.get("smtp_port", 587) if config else 587
|
||||
self.smtp_user = config.get("smtp_user") if config else None
|
||||
self.smtp_password = config.get("smtp_password") if config else None
|
||||
self.use_tls = config.get("use_tls", True) if config else True
|
||||
self.use_ssl = config.get("use_ssl", False) if config else False
|
||||
|
||||
# 邮件配置
|
||||
self.from_email = config.get("from_email") if config else None
|
||||
self.from_name = config.get("from_name", "Aether") if config else "Aether"
|
||||
self.to_emails = config.get("to_emails", []) if config else []
|
||||
self.cc_emails = config.get("cc_emails", []) if config else []
|
||||
self.bcc_emails = config.get("bcc_emails", []) if config else []
|
||||
|
||||
# 模板配置
|
||||
self.use_html = config.get("use_html", True) if config else True
|
||||
self.subject_prefix = config.get("subject_prefix", "[Aether]") if config else "[Aether]"
|
||||
|
||||
# 缓冲配置
|
||||
self._buffer: list[Notification] = []
|
||||
self._buffer_max_size = config.get("buffer_max_size", 500) if config else 500
|
||||
self._lock = asyncio.Lock()
|
||||
self._flush_task: asyncio.Task[None] | None = None
|
||||
|
||||
# 验证配置
|
||||
config_errors = []
|
||||
if not self.smtp_host:
|
||||
config_errors.append("缺少 smtp_host")
|
||||
if not self.from_email:
|
||||
config_errors.append("缺少 from_email")
|
||||
if not self.to_emails:
|
||||
config_errors.append("缺少 to_emails")
|
||||
|
||||
if config_errors:
|
||||
self.enabled = False
|
||||
for error in config_errors:
|
||||
logger.warning(f"Email 插件配置错误: {error},插件已禁用")
|
||||
return
|
||||
|
||||
# 注意: 不在这里启动刷新任务,因为可能还没有运行的事件循环
|
||||
# 需要在应用启动后调用 initialize() 方法来启动任务
|
||||
|
||||
async def initialize(self) -> bool:
|
||||
"""
|
||||
初始化插件(在事件循环运行后调用)
|
||||
启动后台任务等需要事件循环的操作
|
||||
|
||||
Returns:
|
||||
初始化成功返回 True,失败返回 False
|
||||
"""
|
||||
if not self.enabled:
|
||||
# 配置无效,插件被禁用
|
||||
return False
|
||||
|
||||
if self._flush_task is None:
|
||||
self._start_flush_task()
|
||||
|
||||
return True
|
||||
|
||||
def _start_flush_task(self) -> None:
|
||||
"""启动定时刷新任务"""
|
||||
|
||||
async def flush_loop() -> None:
|
||||
while self.enabled:
|
||||
await asyncio.sleep(self.flush_interval)
|
||||
await self.flush()
|
||||
|
||||
try:
|
||||
# 获取当前运行的事件循环
|
||||
loop = asyncio.get_running_loop()
|
||||
self._flush_task = loop.create_task(flush_loop())
|
||||
except RuntimeError:
|
||||
# 没有运行的事件循环,任务将在 initialize() 中创建
|
||||
logger.warning("Email 插件刷新任务等待事件循环创建")
|
||||
pass
|
||||
|
||||
def _format_html_email(self, notifications: list[Notification]) -> str:
|
||||
"""格式化HTML邮件"""
|
||||
# 颜色映射
|
||||
color_map = {
|
||||
NotificationLevel.INFO: "#28a745",
|
||||
NotificationLevel.WARNING: "#ffc107",
|
||||
NotificationLevel.ERROR: "#dc3545",
|
||||
NotificationLevel.CRITICAL: "#721c24",
|
||||
}
|
||||
|
||||
# 构建HTML
|
||||
html = """
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; line-height: 1.6; }
|
||||
.notification { margin: 20px 0; padding: 15px; border-left: 5px solid; }
|
||||
.info { border-left-color: #28a745; background-color: #d4edda; }
|
||||
.warning { border-left-color: #ffc107; background-color: #fff3cd; }
|
||||
.error { border-left-color: #dc3545; background-color: #f8d7da; }
|
||||
.critical { border-left-color: #721c24; background-color: #f8d7da; }
|
||||
.title { font-weight: bold; font-size: 1.2em; margin-bottom: 10px; }
|
||||
.metadata { margin-top: 10px; font-size: 0.9em; color: #666; }
|
||||
.timestamp { font-size: 0.8em; color: #999; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h2>Notifications from Aether</h2>
|
||||
"""
|
||||
|
||||
for notification in notifications:
|
||||
level_class = notification.level.value
|
||||
html += f"""
|
||||
<div class="notification {level_class}">
|
||||
<div class="title">{notification.title}</div>
|
||||
<div class="message">{notification.message}</div>
|
||||
"""
|
||||
|
||||
if notification.metadata:
|
||||
html += '<div class="metadata">'
|
||||
for key, value in notification.metadata.items():
|
||||
html += f"<strong>{key}:</strong> {value}<br>"
|
||||
html += "</div>"
|
||||
|
||||
html += f"""
|
||||
<div class="timestamp">{notification.timestamp.isoformat()}</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
html += """
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
return html
|
||||
|
||||
def _format_text_email(self, notifications: list[Notification]) -> str:
|
||||
"""格式化纯文本邮件"""
|
||||
lines = ["Notifications from Aether", "=" * 50, ""]
|
||||
|
||||
for notification in notifications:
|
||||
lines.append(f"[{notification.level.value.upper()}] {notification.title}")
|
||||
lines.append("-" * 40)
|
||||
lines.append(notification.message)
|
||||
|
||||
if notification.metadata:
|
||||
lines.append("")
|
||||
for key, value in notification.metadata.items():
|
||||
lines.append(f" {key}: {value}")
|
||||
|
||||
lines.append(f"\nTime: {notification.timestamp.isoformat()}")
|
||||
lines.append("=" * 50)
|
||||
lines.append("")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
async def _send_email_async(self, subject: str, body: str, is_html: bool = True) -> bool:
|
||||
"""异步发送邮件"""
|
||||
if AIOSMTPLIB_AVAILABLE:
|
||||
# 使用异步SMTP
|
||||
message = MIMEMultipart("alternative")
|
||||
message["Subject"] = f"{self.subject_prefix} {subject}"
|
||||
message["From"] = f"{self.from_name} <{self.from_email}>"
|
||||
message["To"] = ", ".join(self.to_emails)
|
||||
|
||||
if self.cc_emails:
|
||||
message["Cc"] = ", ".join(self.cc_emails)
|
||||
|
||||
# 添加内容
|
||||
if is_html:
|
||||
message.attach(MIMEText(body, "html"))
|
||||
else:
|
||||
message.attach(MIMEText(body, "plain"))
|
||||
|
||||
try:
|
||||
# 发送邮件
|
||||
if self.use_ssl:
|
||||
await aiosmtplib.send(
|
||||
message,
|
||||
hostname=self.smtp_host,
|
||||
port=self.smtp_port,
|
||||
use_tls=True,
|
||||
username=self.smtp_user,
|
||||
password=self.smtp_password,
|
||||
)
|
||||
else:
|
||||
await aiosmtplib.send(
|
||||
message,
|
||||
hostname=self.smtp_host,
|
||||
port=self.smtp_port,
|
||||
start_tls=self.use_tls,
|
||||
username=self.smtp_user,
|
||||
password=self.smtp_password,
|
||||
)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"异步邮件发送失败: {e}")
|
||||
return False
|
||||
else:
|
||||
# 使用同步SMTP(在线程中运行)
|
||||
return await run_in_executor(self._send_email_sync, subject, body, is_html)
|
||||
|
||||
def _send_email_sync(self, subject: str, body: str, is_html: bool = True) -> bool:
|
||||
"""同步发送邮件"""
|
||||
message = MIMEMultipart("alternative")
|
||||
message["Subject"] = f"{self.subject_prefix} {subject}"
|
||||
message["From"] = f"{self.from_name} <{self.from_email}>"
|
||||
message["To"] = ", ".join(self.to_emails)
|
||||
|
||||
if self.cc_emails:
|
||||
message["Cc"] = ", ".join(self.cc_emails)
|
||||
|
||||
# 添加内容
|
||||
if is_html:
|
||||
message.attach(MIMEText(body, "html"))
|
||||
else:
|
||||
message.attach(MIMEText(body, "plain"))
|
||||
|
||||
try:
|
||||
smtp_host = self.smtp_host
|
||||
assert smtp_host is not None
|
||||
|
||||
# 连接SMTP服务器
|
||||
server: smtplib.SMTP
|
||||
if self.use_ssl:
|
||||
server = smtplib.SMTP_SSL(smtp_host, self.smtp_port)
|
||||
else:
|
||||
server = smtplib.SMTP(smtp_host, self.smtp_port)
|
||||
if self.use_tls:
|
||||
server.starttls()
|
||||
|
||||
# 登录
|
||||
if self.smtp_user and self.smtp_password:
|
||||
server.login(self.smtp_user, self.smtp_password)
|
||||
|
||||
# 发送邮件
|
||||
all_recipients = self.to_emails + self.cc_emails + self.bcc_emails
|
||||
server.send_message(message, to_addrs=all_recipients)
|
||||
server.quit()
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"同步邮件发送失败: {e}")
|
||||
return False
|
||||
|
||||
async def _do_send(self, notification: Notification) -> bool:
|
||||
"""
|
||||
实际发送单个通知
|
||||
|
||||
Note: 对于 CRITICAL 级别通知,直接发送;其他级别加入缓冲区
|
||||
"""
|
||||
# 添加到缓冲区
|
||||
async with self._lock:
|
||||
# 缓冲区溢出保护:丢弃最旧的通知
|
||||
if len(self._buffer) >= self._buffer_max_size:
|
||||
drop_count = len(self._buffer) - self._buffer_max_size + 1
|
||||
del self._buffer[:drop_count]
|
||||
logger.warning("Email 通知缓冲区溢出,丢弃 {} 条旧通知", drop_count)
|
||||
|
||||
self._buffer.append(notification)
|
||||
|
||||
# 如果是严重通知,立即发送
|
||||
if notification.level == NotificationLevel.CRITICAL:
|
||||
return await self._flush_buffer()
|
||||
|
||||
# 如果缓冲区满,自动刷新
|
||||
if len(self._buffer) >= self.batch_size:
|
||||
return await self._flush_buffer()
|
||||
|
||||
return True
|
||||
|
||||
async def _do_send_batch(self, notifications: list[Notification]) -> dict[str, int]:
|
||||
"""实际批量发送通知"""
|
||||
if not notifications:
|
||||
return {"total": 0, "sent": 0, "failed": 0}
|
||||
|
||||
# 准备邮件内容
|
||||
subject = f"Batch Notifications ({len(notifications)} items)"
|
||||
|
||||
# 检查是否有严重通知
|
||||
critical_count = sum(1 for n in notifications if n.level == NotificationLevel.CRITICAL)
|
||||
if critical_count > 0:
|
||||
subject = f"[CRITICAL] {subject}"
|
||||
|
||||
# 格式化邮件内容
|
||||
if self.use_html:
|
||||
body = self._format_html_email(notifications)
|
||||
else:
|
||||
body = self._format_text_email(notifications)
|
||||
|
||||
# 发送邮件
|
||||
success = await self._send_email_async(subject, body, self.use_html)
|
||||
|
||||
return {
|
||||
"total": len(notifications),
|
||||
"sent": len(notifications) if success else 0,
|
||||
"failed": 0 if success else len(notifications),
|
||||
}
|
||||
|
||||
async def _flush_buffer(self) -> bool:
|
||||
"""刷新缓冲的通知(内部方法,不带锁)"""
|
||||
if not self._buffer:
|
||||
return True
|
||||
|
||||
notifications = self._buffer[:]
|
||||
self._buffer.clear()
|
||||
|
||||
# 批量发送(直接调用 _do_send_batch 避免重复统计)
|
||||
result = await self._do_send_batch(notifications)
|
||||
return result["failed"] == 0
|
||||
|
||||
async def flush(self) -> bool:
|
||||
"""刷新缓冲的通知"""
|
||||
async with self._lock:
|
||||
return await self._flush_buffer()
|
||||
|
||||
async def _get_extra_stats(self) -> dict[str, Any]:
|
||||
"""获取 Email 特定的统计信息"""
|
||||
return {
|
||||
"type": "email",
|
||||
"smtp_host": self.smtp_host,
|
||||
"smtp_port": self.smtp_port,
|
||||
"from_email": self.from_email,
|
||||
"recipients_count": len(self.to_emails),
|
||||
"buffer_size": len(self._buffer),
|
||||
"use_html": self.use_html,
|
||||
"aiosmtplib_available": AIOSMTPLIB_AVAILABLE,
|
||||
}
|
||||
|
||||
async def close(self) -> None:
|
||||
"""关闭插件"""
|
||||
# 刷新缓冲
|
||||
await self.flush()
|
||||
|
||||
# 取消刷新任务
|
||||
if self._flush_task:
|
||||
self._flush_task.cancel()
|
||||
|
||||
def __del__(self) -> None:
|
||||
"""清理资源"""
|
||||
try:
|
||||
from src.utils.async_utils import safe_create_task
|
||||
|
||||
safe_create_task(self.close())
|
||||
except Exception:
|
||||
pass
|
||||
321
_deprecated_py_src/plugins/notification/webhook.py
Normal file
321
_deprecated_py_src/plugins/notification/webhook.py
Normal file
@@ -0,0 +1,321 @@
|
||||
"""
|
||||
Webhook通知插件
|
||||
通过HTTP Webhook发送通知
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import aiohttp
|
||||
|
||||
try:
|
||||
import aiohttp
|
||||
|
||||
AIOHTTP_AVAILABLE = True
|
||||
except ImportError:
|
||||
AIOHTTP_AVAILABLE = False
|
||||
|
||||
from src.core.logger import logger
|
||||
|
||||
from .base import Notification, NotificationLevel, NotificationPlugin
|
||||
|
||||
|
||||
class WebhookNotificationPlugin(NotificationPlugin):
|
||||
"""
|
||||
Webhook通知插件
|
||||
支持多种Webhook格式(Slack, Discord, 通用)
|
||||
"""
|
||||
|
||||
def __init__(self, name: str = "webhook", config: dict[str, Any] | None = None):
|
||||
super().__init__(name, config)
|
||||
|
||||
if not AIOHTTP_AVAILABLE:
|
||||
self.enabled = False
|
||||
logger.warning("aiohttp not installed, webhook plugin disabled")
|
||||
return
|
||||
|
||||
# Webhook配置
|
||||
self.webhook_url = config.get("webhook_url") if config else None
|
||||
self.webhook_type = (
|
||||
config.get("webhook_type", "generic") if config else "generic"
|
||||
) # generic, slack, discord, teams
|
||||
self.secret = config.get("secret") if config else None # 用于签名
|
||||
self.timeout = config.get("timeout", 30) if config else 30
|
||||
self.headers = config.get("headers", {}) if config else {}
|
||||
|
||||
# 缓冲配置
|
||||
self._buffer: list[Notification] = []
|
||||
self._buffer_max_size = config.get("buffer_max_size", 500) if config else 500
|
||||
self._lock = asyncio.Lock()
|
||||
self._session: aiohttp.ClientSession | None = None
|
||||
self._flush_task = None
|
||||
|
||||
if not self.webhook_url:
|
||||
self.enabled = False
|
||||
logger.warning("No webhook URL configured")
|
||||
return
|
||||
|
||||
# 启动刷新任务
|
||||
self._start_flush_task()
|
||||
|
||||
def _start_flush_task(self) -> None:
|
||||
"""启动定时刷新任务"""
|
||||
|
||||
async def flush_loop() -> Any:
|
||||
while self.enabled:
|
||||
await asyncio.sleep(self.flush_interval)
|
||||
await self.flush()
|
||||
|
||||
self._flush_task = asyncio.create_task(flush_loop())
|
||||
|
||||
async def _get_session(self) -> aiohttp.ClientSession:
|
||||
"""获取HTTP会话"""
|
||||
if not self._session:
|
||||
self._session = aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=self.timeout))
|
||||
return self._session
|
||||
|
||||
def _generate_signature(self, payload: str) -> str:
|
||||
"""生成请求签名"""
|
||||
if not self.secret:
|
||||
return ""
|
||||
|
||||
# 使用HMAC-SHA256生成签名
|
||||
signature = hmac.new(
|
||||
self.secret.encode("utf-8"), payload.encode("utf-8"), hashlib.sha256
|
||||
).hexdigest()
|
||||
|
||||
return signature
|
||||
|
||||
def _format_for_slack(self, notification: Notification) -> dict[str, Any]:
|
||||
"""格式化为Slack消息"""
|
||||
# Slack颜色映射
|
||||
color_map = {
|
||||
NotificationLevel.INFO: "#36a64f",
|
||||
NotificationLevel.WARNING: "warning",
|
||||
NotificationLevel.ERROR: "danger",
|
||||
NotificationLevel.CRITICAL: "#ff0000",
|
||||
}
|
||||
|
||||
return {
|
||||
"text": notification.title,
|
||||
"attachments": [
|
||||
{
|
||||
"color": color_map.get(notification.level, "#808080"),
|
||||
"title": notification.title,
|
||||
"text": notification.message,
|
||||
"fields": (
|
||||
[
|
||||
{"title": k, "value": str(v), "short": True}
|
||||
for k, v in notification.metadata.items()
|
||||
]
|
||||
if notification.metadata
|
||||
else []
|
||||
),
|
||||
"footer": notification.source,
|
||||
"ts": int(notification.timestamp.timestamp()),
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
def _format_for_discord(self, notification: Notification) -> dict[str, Any]:
|
||||
"""格式化为Discord消息"""
|
||||
# Discord颜色映射
|
||||
color_map = {
|
||||
NotificationLevel.INFO: 0x00FF00,
|
||||
NotificationLevel.WARNING: 0xFFA500,
|
||||
NotificationLevel.ERROR: 0xFF0000,
|
||||
NotificationLevel.CRITICAL: 0x8B0000,
|
||||
}
|
||||
|
||||
embeds = [
|
||||
{
|
||||
"title": notification.title,
|
||||
"description": notification.message,
|
||||
"color": color_map.get(notification.level, 0x808080),
|
||||
"fields": (
|
||||
[
|
||||
{"name": k, "value": str(v), "inline": True}
|
||||
for k, v in notification.metadata.items()
|
||||
]
|
||||
if notification.metadata
|
||||
else []
|
||||
),
|
||||
"footer": {"text": notification.source},
|
||||
"timestamp": notification.timestamp.isoformat(),
|
||||
}
|
||||
]
|
||||
|
||||
return {"embeds": embeds}
|
||||
|
||||
def _format_for_teams(self, notification: Notification) -> dict[str, Any]:
|
||||
"""格式化为Microsoft Teams消息"""
|
||||
# Teams颜色映射
|
||||
color_map = {
|
||||
NotificationLevel.INFO: "00ff00",
|
||||
NotificationLevel.WARNING: "ffa500",
|
||||
NotificationLevel.ERROR: "ff0000",
|
||||
NotificationLevel.CRITICAL: "8b0000",
|
||||
}
|
||||
|
||||
facts = (
|
||||
[{"name": k, "value": str(v)} for k, v in notification.metadata.items()]
|
||||
if notification.metadata
|
||||
else []
|
||||
)
|
||||
|
||||
return {
|
||||
"@type": "MessageCard",
|
||||
"@context": "https://schema.org/extensions",
|
||||
"themeColor": color_map.get(notification.level, "808080"),
|
||||
"title": notification.title,
|
||||
"text": notification.message,
|
||||
"sections": [{"facts": facts}] if facts else [],
|
||||
"summary": notification.title,
|
||||
}
|
||||
|
||||
def _format_payload(self, notification: Notification) -> dict[str, Any]:
|
||||
"""根据Webhook类型格式化负载"""
|
||||
if self.webhook_type == "slack":
|
||||
return self._format_for_slack(notification)
|
||||
elif self.webhook_type == "discord":
|
||||
return self._format_for_discord(notification)
|
||||
elif self.webhook_type == "teams":
|
||||
return self._format_for_teams(notification)
|
||||
else:
|
||||
# 通用格式
|
||||
return notification.to_dict()
|
||||
|
||||
async def _do_send(self, notification: Notification) -> bool:
|
||||
"""
|
||||
实际发送单个通知
|
||||
|
||||
Note: 对于 CRITICAL 级别通知,直接发送;其他级别加入缓冲区
|
||||
"""
|
||||
# 添加到缓冲区
|
||||
async with self._lock:
|
||||
# 缓冲区溢出保护:丢弃最旧的通知
|
||||
if len(self._buffer) >= self._buffer_max_size:
|
||||
drop_count = len(self._buffer) - self._buffer_max_size + 1
|
||||
del self._buffer[:drop_count]
|
||||
logger.warning("Webhook 通知缓冲区溢出,丢弃 {} 条旧通知", drop_count)
|
||||
|
||||
self._buffer.append(notification)
|
||||
|
||||
# 如果是严重通知,立即发送
|
||||
if notification.level == NotificationLevel.CRITICAL:
|
||||
return await self._flush_buffer()
|
||||
|
||||
# 如果缓冲区满,自动刷新
|
||||
if len(self._buffer) >= self.batch_size:
|
||||
return await self._flush_buffer()
|
||||
|
||||
return True
|
||||
|
||||
async def _do_send_batch(self, notifications: list[Notification]) -> dict[str, Any]:
|
||||
"""实际批量发送通知"""
|
||||
success_count = 0
|
||||
failed_count = 0
|
||||
errors = []
|
||||
|
||||
if not notifications:
|
||||
return {"total": 0, "sent": 0, "failed": 0}
|
||||
|
||||
# 批量发送
|
||||
for notification in notifications:
|
||||
try:
|
||||
payload = self._format_payload(notification)
|
||||
payload_str = json.dumps(payload)
|
||||
|
||||
headers = dict(self.headers)
|
||||
headers["Content-Type"] = "application/json"
|
||||
|
||||
# 添加签名
|
||||
if self.secret:
|
||||
signature = self._generate_signature(payload_str)
|
||||
headers["X-Signature"] = signature
|
||||
headers["X-Timestamp"] = str(int(time.time()))
|
||||
|
||||
# 发送请求
|
||||
session = await self._get_session()
|
||||
async with session.post(
|
||||
self.webhook_url, data=payload_str, headers=headers
|
||||
) as response:
|
||||
if response.status < 300:
|
||||
success_count += 1
|
||||
else:
|
||||
failed_count += 1
|
||||
error_text = await response.text()
|
||||
errors.append(f"HTTP {response.status}: {error_text}")
|
||||
|
||||
except Exception as e:
|
||||
failed_count += 1
|
||||
errors.append(str(e))
|
||||
|
||||
return {
|
||||
"total": len(notifications),
|
||||
"sent": success_count,
|
||||
"failed": failed_count,
|
||||
"errors": errors,
|
||||
}
|
||||
|
||||
async def _flush_buffer(self) -> bool:
|
||||
"""刷新缓冲的通知(内部方法,不带锁)"""
|
||||
if not self._buffer:
|
||||
return True
|
||||
|
||||
notifications = self._buffer[:]
|
||||
self._buffer.clear()
|
||||
|
||||
# 批量发送(直接调用 _do_send_batch 避免重复统计)
|
||||
result = await self._do_send_batch(notifications)
|
||||
return result["failed"] == 0
|
||||
|
||||
async def flush(self) -> bool:
|
||||
"""刷新缓冲的通知"""
|
||||
async with self._lock:
|
||||
return await self._flush_buffer()
|
||||
|
||||
async def _get_extra_stats(self) -> dict[str, Any]:
|
||||
"""获取 Webhook 特定的统计信息"""
|
||||
return {
|
||||
"type": "webhook",
|
||||
"webhook_type": self.webhook_type,
|
||||
"webhook_url": (
|
||||
self.webhook_url.split("?")[0] if self.webhook_url else None
|
||||
), # 隐藏查询参数
|
||||
"buffer_size": len(self._buffer),
|
||||
"has_secret": bool(self.secret),
|
||||
}
|
||||
|
||||
async def _do_shutdown(self) -> None:
|
||||
"""清理资源"""
|
||||
await self.close()
|
||||
|
||||
async def close(self) -> Any:
|
||||
"""关闭插件"""
|
||||
# 刷新缓冲
|
||||
await self.flush()
|
||||
|
||||
# 取消刷新任务
|
||||
if self._flush_task:
|
||||
self._flush_task.cancel()
|
||||
|
||||
# 关闭HTTP会话
|
||||
if self._session:
|
||||
await self._session.close()
|
||||
|
||||
def __del__(self) -> None:
|
||||
"""清理资源"""
|
||||
try:
|
||||
from src.utils.async_utils import safe_create_task
|
||||
|
||||
safe_create_task(self.close())
|
||||
except Exception:
|
||||
pass
|
||||
Reference in New Issue
Block a user