ForcePilot/backend/package/yuxi/channels/adapters/dingding/dedup.py
Kris 9c01d480cc feat(dingding): 完整实现钉钉适配器功能模块
- 新增钉钉工具调用审计日志功能
- 新增参数校验装饰器与基础工具集(文档、审批、多维表格)
- 完善事件类型映射与会话ID生成逻辑
- 新增消息去重、访问策略匹配、配置校验与UI提示
- 新增酷应用卡片、目录管理、表情反应支持
- 优化消息发送逻辑,添加401重试机制
- 新增配置化的权限控制与流式输出支持
2026-05-13 16:06:10 +08:00

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()