this is a push

This commit is contained in:
2026-08-13 15:03:34 +08:00
parent 0f55259fff
commit ca61c132f2
109 changed files with 44036 additions and 198 deletions

View File

@@ -11,6 +11,7 @@ import time
from collections import defaultdict
from concurrent.futures import ThreadPoolExecutor
from dataclasses import dataclass, field
from datetime import datetime
from io import BytesIO
from typing import Any
from urllib.error import HTTPError, URLError
@@ -20,6 +21,7 @@ from PIL import Image, ImageDraw, ImageFont
LOGGER = logging.getLogger(__name__)
ALERT_CLASSES = ("fire", "smoke")
CHANNEL_NAMES = {"wechat": "企业微信", "feishu": "飞书"}
@dataclass(slots=True)
@@ -58,6 +60,18 @@ class AlertManager:
self.session_ttl_seconds = max(60.0, session_ttl_seconds)
self._states: dict[str, SessionState] = {}
self._lock = threading.Lock()
self._delivery_stats = {
channel: {
"delivered": 0,
"failed": 0,
"tests": 0,
"last_status": "idle",
"last_delivery_at": None,
"last_test_at": None,
"last_error": None,
}
for channel in CHANNEL_NAMES
}
self._executor = ThreadPoolExecutor(
max_workers=4,
thread_name_prefix="alert-dispatch",
@@ -74,6 +88,54 @@ class AlertManager:
def enabled(self) -> bool:
return any(self.channels.values())
@property
def channel_status(self) -> list[dict[str, Any]]:
configured = self.channels
with self._lock:
stats = {
channel: dict(values)
for channel, values in self._delivery_stats.items()
}
return [
{
"id": channel,
"name": CHANNEL_NAMES[channel],
"configured": configured[channel],
"signed": channel == "feishu" and bool(self.feishu_secret),
"capabilities": (
["Markdown 告警", "检测截图"]
if channel == "wechat"
else ["交互卡片", "签名校验"]
),
**stats[channel],
}
for channel in CHANNEL_NAMES
]
def test_channel(self, channel: str) -> dict[str, Any]:
if channel not in CHANNEL_NAMES:
raise ValueError(f"Unsupported robot channel: {channel}")
if not self.channels[channel]:
raise ValueError(f"{CHANNEL_NAMES[channel]}机器人尚未配置")
try:
if channel == "wechat":
self._send_wechat_test()
else:
self._send_feishu_test()
except (HTTPError, URLError, TimeoutError, ValueError) as error:
self._record_delivery(
channel,
success=False,
error=str(error),
test=True,
)
raise RuntimeError(str(error)) from error
self._record_delivery(channel, success=True, test=True)
return next(
status for status in self.channel_status if status["id"] == channel
)
def evaluate(
self,
session_id: str,
@@ -147,14 +209,88 @@ class AlertManager:
if self.wechat_webhook_url:
try:
self._send_wechat_alert(image, triggered, detections)
self._record_delivery("wechat", success=True)
except (HTTPError, URLError, TimeoutError, ValueError) as error:
self._record_delivery("wechat", success=False, error=str(error))
LOGGER.error("WeChat alert failed: %s", error)
if self.feishu_webhook_url:
try:
self._send_feishu_alert(triggered, detections)
self._record_delivery("feishu", success=True)
except (HTTPError, URLError, TimeoutError, ValueError) as error:
self._record_delivery("feishu", success=False, error=str(error))
LOGGER.error("Feishu alert failed: %s", error)
def _record_delivery(
self,
channel: str,
*,
success: bool,
error: str | None = None,
test: bool = False,
) -> None:
now = datetime.now().astimezone().isoformat(timespec="seconds")
with self._lock:
stats = self._delivery_stats[channel]
stats["last_status"] = "success" if success else "failed"
stats["last_error"] = None if success else error
stats["delivered" if success else "failed"] += 1
if test:
stats["tests"] += 1
stats["last_test_at"] = now
else:
stats["last_delivery_at"] = now
def _send_wechat_test(self) -> None:
if not self.wechat_webhook_url:
return
post_json(
self.wechat_webhook_url,
{
"msgtype": "markdown",
"markdown": {
"content": (
"燧安机器人连接测试\n"
"> 消息通道工作正常\n"
f"> 测试时间:{time.strftime('%Y-%m-%d %H:%M:%S')}"
)
},
},
)
def _send_feishu_test(self) -> None:
if not self.feishu_webhook_url:
return
payload: dict[str, Any] = {
"msg_type": "interactive",
"card": {
"header": {
"template": "green",
"title": {
"tag": "plain_text",
"content": "燧安机器人连接测试",
},
},
"elements": [
{
"tag": "markdown",
"content": (
"**消息通道工作正常**\n"
f"测试时间:{time.strftime('%Y-%m-%d %H:%M:%S')}"
),
}
],
},
}
if self.feishu_secret:
timestamp = str(int(time.time()))
payload["timestamp"] = timestamp
payload["sign"] = feishu_signature(
timestamp,
self.feishu_secret,
)
post_json(self.feishu_webhook_url, payload)
def _send_wechat_alert(
self,
image: Image.Image,