新增 Zalo OA 官方账号完整集成能力,包含: 1. 基础通信能力:消息编解码、目标归一化、文本分块 2. 安全与校验:Webhook 签名验证、DM 策略管理、配对流程 3. 辅助工具:重复事件去重、请求限流、异常告警 4. 管理功能:账号多实例管理、配置验证、健康诊断 5. 扩展能力:媒体托管、视觉识别、TTS 语音合成 6. 运维支持:审计日志、状态监控、目录同步
82 lines
2.6 KiB
Python
82 lines
2.6 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import time
|
|
from collections import deque
|
|
from enum import StrEnum
|
|
from typing import Any
|
|
|
|
from yuxi.utils.logging_config import logger
|
|
|
|
MAX_AUDIT_ENTRIES = 500
|
|
|
|
|
|
class AuditEventType(StrEnum):
|
|
MESSAGE_SENT = "message.sent"
|
|
MESSAGE_RECEIVED = "message.received"
|
|
MESSAGE_FAILED = "message.failed"
|
|
MESSAGE_RECALLED = "message.recalled"
|
|
TOKEN_REFRESHED = "token.refreshed"
|
|
TOKEN_REFRESH_FAILED = "token.refresh_failed"
|
|
AUTH_FAILURE = "auth.failure"
|
|
WEBOOK_RECEIVED = "webhook.received"
|
|
WEBOOK_RATE_LIMITED = "webhook.rate_limited"
|
|
PAIRING_CREATED = "pairing.created"
|
|
PAIRING_VERIFIED = "pairing.verified"
|
|
CONFIG_CHANGED = "config.changed"
|
|
CONNECTION_STATE = "connection.state"
|
|
BROADCAST_START = "broadcast.start"
|
|
BROADCAST_END = "broadcast.end"
|
|
|
|
|
|
class AuditLogger:
|
|
def __init__(self, enabled: bool = True, log_level: str = "info", max_entries: int = MAX_AUDIT_ENTRIES):
|
|
self._enabled = enabled
|
|
self._log_level = log_level
|
|
self._entries: deque[dict[str, Any]] = deque(maxlen=max_entries)
|
|
|
|
@property
|
|
def enabled(self) -> bool:
|
|
return self._enabled
|
|
|
|
def record(self, event_type: AuditEventType | str, details: dict[str, Any] | None = None):
|
|
if not self._enabled:
|
|
return
|
|
|
|
entry = {
|
|
"timestamp": time.time(),
|
|
"event_type": str(event_type),
|
|
"details": details or {},
|
|
}
|
|
self._entries.append(entry)
|
|
|
|
msg = json.dumps(entry, ensure_ascii=False, default=str)
|
|
if self._log_level == "debug":
|
|
logger.debug(f"[ZaloOA][Audit] {msg}")
|
|
elif self._log_level == "warning":
|
|
logger.warning(f"[ZaloOA][Audit] {msg}")
|
|
else:
|
|
logger.info(f"[ZaloOA][Audit] {msg}")
|
|
|
|
def get_entries(self, limit: int = 100, event_type: str = "") -> list[dict[str, Any]]:
|
|
entries = list(self._entries)
|
|
if event_type:
|
|
entries = [e for e in entries if e["event_type"] == event_type]
|
|
return sorted(entries, key=lambda e: e["timestamp"], reverse=True)[:limit]
|
|
|
|
def get_summary(self) -> dict[str, Any]:
|
|
entries = list(self._entries)
|
|
event_counts: dict[str, int] = {}
|
|
for entry in entries:
|
|
event_type = entry["event_type"]
|
|
event_counts[event_type] = event_counts.get(event_type, 0) + 1
|
|
return {
|
|
"total_entries": len(entries),
|
|
"event_counts": event_counts,
|
|
"latest_entry": entries[-1] if entries else None,
|
|
}
|
|
|
|
@property
|
|
def total_entries(self) -> int:
|
|
return len(self._entries)
|