- 新增钉钉工具调用审计日志功能 - 新增参数校验装饰器与基础工具集(文档、审批、多维表格) - 完善事件类型映射与会话ID生成逻辑 - 新增消息去重、访问策略匹配、配置校验与UI提示 - 新增酷应用卡片、目录管理、表情反应支持 - 优化消息发送逻辑,添加401重试机制 - 新增配置化的权限控制与流式输出支持
90 lines
2.8 KiB
Python
90 lines
2.8 KiB
Python
from __future__ import annotations
|
|
|
|
import time
|
|
from collections import OrderedDict
|
|
|
|
from yuxi.utils.logging_config import logger
|
|
|
|
_DEFAULT_MAX_ENTRIES = 10000
|
|
_DEFAULT_TTL_SECONDS = 86400
|
|
|
|
|
|
class DingDingDedupGuard:
|
|
def __init__(self, ttl: int = _DEFAULT_TTL_SECONDS, max_entries: int = _DEFAULT_MAX_ENTRIES):
|
|
self._ttl = ttl
|
|
self._max_entries = max_entries
|
|
self._pending: dict[str, float] = {}
|
|
self._committed: OrderedDict[str, float] = OrderedDict()
|
|
|
|
def _gc(self) -> None:
|
|
now = time.time()
|
|
stale = [k for k, ts in self._committed.items() if now - ts > self._ttl]
|
|
for k in stale:
|
|
del self._committed[k]
|
|
while len(self._committed) > self._max_entries:
|
|
self._committed.popitem(last=False)
|
|
|
|
def has_processed(self, message_id: str) -> bool:
|
|
if not message_id:
|
|
return False
|
|
self._gc()
|
|
return message_id in self._committed
|
|
|
|
def record_as_processed(self, message_id: str) -> None:
|
|
if not message_id:
|
|
return
|
|
self._gc()
|
|
self._pending.pop(message_id, None)
|
|
if message_id in self._committed:
|
|
self._committed.move_to_end(message_id)
|
|
self._committed[message_id] = time.time()
|
|
else:
|
|
if len(self._committed) >= self._max_entries:
|
|
self._committed.popitem(last=False)
|
|
self._committed[message_id] = time.time()
|
|
|
|
def claim(self, message_id: str) -> bool:
|
|
if not message_id:
|
|
return True
|
|
self._gc()
|
|
if message_id in self._committed:
|
|
logger.debug(f"[DingDing] Dedup: duplicate message rejected {message_id}")
|
|
return False
|
|
if message_id in self._pending:
|
|
logger.debug(f"[DingDing] Dedup: already pending {message_id}")
|
|
return False
|
|
self._pending[message_id] = time.time()
|
|
return True
|
|
|
|
def commit(self, message_id: str) -> None:
|
|
if not message_id:
|
|
return
|
|
self._pending.pop(message_id, None)
|
|
self._committed[message_id] = time.time()
|
|
self._gc()
|
|
|
|
def release(self, message_id: str) -> None:
|
|
if not message_id:
|
|
return
|
|
self._pending.pop(message_id, None)
|
|
|
|
def finalize_processing(self, message_id: str) -> None:
|
|
self.commit(message_id)
|
|
|
|
def check_and_mark(self, message_id: str) -> bool:
|
|
if self.has_processed(message_id):
|
|
return True
|
|
self.record_as_processed(message_id)
|
|
return False
|
|
|
|
def stats(self) -> dict[str, int]:
|
|
self._gc()
|
|
return {
|
|
"committed": len(self._committed),
|
|
"pending": len(self._pending),
|
|
}
|
|
|
|
def clear(self) -> None:
|
|
self._pending.clear()
|
|
self._committed.clear()
|