- 新增3张通知路由表模型(notify_area, camera_area_binding, area_person_binding) - 新增VLM复核服务,通过qwen3-vl-flash对告警截图二次确认 - 新增企微通知服务,告警确认后推送文本卡片给责任人 - 新增通知调度服务,编排VLM复核→查表路由→企微推送流水线 - 新增企微回调接口,支持手动结单/确认处理/标记误报 - 新增通知管理API,区域/摄像头绑定/人员绑定CRUD - 告警上报主流程(edge_compat + yudao_aiot_alarm)接入异步通知 - 扩展配置项支持VLM和企微环境变量 - 添加openai==1.68.0依赖(通过DashScope兼容端点调用)
161 lines
5.0 KiB
Python
161 lines
5.0 KiB
Python
"""
|
||
企微通知服务
|
||
|
||
封装企业微信 API,发送告警文本卡片。
|
||
V1 使用应用消息 + 文本卡片,后期扩展为模板卡片。
|
||
"""
|
||
|
||
import httpx
|
||
import logging
|
||
import time
|
||
from typing import Optional, List
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class WeChatService:
|
||
"""企微通知服务(单例)"""
|
||
|
||
def __init__(self):
|
||
self._enabled = False
|
||
self._corp_id = ""
|
||
self._agent_id = ""
|
||
self._secret = ""
|
||
self._token = ""
|
||
self._encoding_aes_key = ""
|
||
self._access_token = ""
|
||
self._token_expire_at = 0
|
||
|
||
def init(self, config):
|
||
"""初始化企微配置"""
|
||
self._enabled = config.enabled and bool(config.corp_id) and bool(config.secret)
|
||
self._corp_id = config.corp_id
|
||
self._agent_id = config.agent_id
|
||
self._secret = config.secret
|
||
self._token = config.token
|
||
self._encoding_aes_key = config.encoding_aes_key
|
||
|
||
if self._enabled:
|
||
logger.info(f"企微通知服务已启用: corp_id={self._corp_id}")
|
||
else:
|
||
logger.info("企微通知服务未启用")
|
||
|
||
@property
|
||
def enabled(self) -> bool:
|
||
return self._enabled
|
||
|
||
async def _get_access_token(self) -> str:
|
||
"""获取企微 access_token(带缓存)"""
|
||
if self._access_token and time.time() < self._token_expire_at - 60:
|
||
return self._access_token
|
||
|
||
url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken"
|
||
params = {"corpid": self._corp_id, "corpsecret": self._secret}
|
||
|
||
async with httpx.AsyncClient(timeout=10) as client:
|
||
resp = await client.get(url, params=params)
|
||
data = resp.json()
|
||
|
||
if data.get("errcode") != 0:
|
||
raise Exception(f"获取 access_token 失败: {data}")
|
||
|
||
self._access_token = data["access_token"]
|
||
self._token_expire_at = time.time() + data.get("expires_in", 7200)
|
||
logger.info("企微 access_token 已更新")
|
||
return self._access_token
|
||
|
||
async def send_alarm_card(
|
||
self,
|
||
user_ids: List[str],
|
||
alarm_id: str,
|
||
alarm_type: str,
|
||
area_name: str,
|
||
camera_name: str,
|
||
description: str,
|
||
snapshot_url: str,
|
||
event_time: str,
|
||
alarm_level: int = 2,
|
||
) -> bool:
|
||
"""
|
||
发送告警文本卡片
|
||
|
||
Args:
|
||
user_ids: 企微 userid 列表
|
||
alarm_id: 告警ID
|
||
alarm_type: 告警类型
|
||
area_name: 区域名称
|
||
camera_name: 摄像头名称
|
||
description: VLM 生成的场景描述
|
||
snapshot_url: 截图 URL
|
||
event_time: 告警时间
|
||
alarm_level: 告警级别
|
||
|
||
Returns:
|
||
是否发送成功
|
||
"""
|
||
if not self._enabled:
|
||
logger.debug("企微未启用,跳过发送")
|
||
return False
|
||
|
||
try:
|
||
access_token = await self._get_access_token()
|
||
|
||
# 告警类型中文映射
|
||
type_names = {
|
||
"leave_post": "人员离岗",
|
||
"intrusion": "周界入侵",
|
||
}
|
||
type_name = type_names.get(alarm_type, alarm_type)
|
||
|
||
# 告警级别映射
|
||
level_names = {1: "提醒", 2: "一般", 3: "严重", 4: "紧急"}
|
||
level_name = level_names.get(alarm_level, "一般")
|
||
|
||
# 构造文本卡片消息
|
||
content = (
|
||
f"<div class=\"highlight\">{level_name} | {type_name}</div>\n"
|
||
f"<div class=\"gray\">区域:{area_name}</div>\n"
|
||
f"<div class=\"gray\">摄像头:{camera_name}</div>\n"
|
||
f"<div class=\"gray\">时间:{event_time}</div>\n"
|
||
f"<div class=\"normal\">{description}</div>"
|
||
)
|
||
|
||
msg = {
|
||
"touser": "|".join(user_ids),
|
||
"msgtype": "textcard",
|
||
"agentid": int(self._agent_id) if self._agent_id else 0,
|
||
"textcard": {
|
||
"title": f"【{level_name}】{type_name}告警",
|
||
"description": content,
|
||
"url": snapshot_url or "https://work.weixin.qq.com",
|
||
"btntxt": "查看详情",
|
||
},
|
||
}
|
||
|
||
url = f"https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={access_token}"
|
||
async with httpx.AsyncClient(timeout=10) as client:
|
||
resp = await client.post(url, json=msg)
|
||
data = resp.json()
|
||
|
||
if data.get("errcode") != 0:
|
||
logger.error(f"企微发送失败: {data}")
|
||
return False
|
||
|
||
logger.info(f"企微通知已发送: alarm={alarm_id}, users={user_ids}")
|
||
return True
|
||
|
||
except Exception as e:
|
||
logger.error(f"企微发送异常: {e}")
|
||
return False
|
||
|
||
|
||
# 全局单例
|
||
_wechat_service: Optional[WeChatService] = None
|
||
|
||
|
||
def get_wechat_service() -> WeChatService:
|
||
global _wechat_service
|
||
if _wechat_service is None:
|
||
_wechat_service = WeChatService()
|
||
return _wechat_service
|