73 lines
2.3 KiB
Python
73 lines
2.3 KiB
Python
|
|
"""Microsoft Teams 入站消息防抖。
|
|||
|
|
|
|||
|
|
相同 sender + conversation 连续消息文本合并,
|
|||
|
|
减少 Agent 调用次数,提升响应效率。
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
import time
|
|||
|
|
from typing import Any
|
|||
|
|
|
|||
|
|
DEBOUNCE_TTL_SECONDS = 3.0
|
|||
|
|
DEBOUNCE_MAX_TEXT_LENGTH = 4000
|
|||
|
|
|
|||
|
|
|
|||
|
|
class DebounceManager:
|
|||
|
|
def __init__(self, ttl: float = DEBOUNCE_TTL_SECONDS):
|
|||
|
|
self._ttl = ttl
|
|||
|
|
self._pending: dict[str, dict[str, Any]] = {}
|
|||
|
|
|
|||
|
|
@staticmethod
|
|||
|
|
def make_key(app_id: str, conversation_id: str, sender_id: str) -> str:
|
|||
|
|
return f"msteams:{app_id}:{conversation_id}:{sender_id}"
|
|||
|
|
|
|||
|
|
def merge(self, key: str, text: str, metadata: dict[str, Any] | None = None) -> dict[str, Any] | None:
|
|||
|
|
now = time.monotonic()
|
|||
|
|
existing = self._pending.get(key)
|
|||
|
|
if existing and (now - existing["timestamp"]) <= self._ttl:
|
|||
|
|
merged_text = existing["text"] + "\n" + text
|
|||
|
|
if len(merged_text) > DEBOUNCE_MAX_TEXT_LENGTH:
|
|||
|
|
merged_text = merged_text[:DEBOUNCE_MAX_TEXT_LENGTH]
|
|||
|
|
self._pending[key] = {
|
|||
|
|
"text": merged_text,
|
|||
|
|
"timestamp": now,
|
|||
|
|
"merge_count": existing.get("merge_count", 1) + 1,
|
|||
|
|
"metadata": {**(existing.get("metadata") or {}), **(metadata or {})},
|
|||
|
|
}
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
self._pending[key] = {
|
|||
|
|
"text": text,
|
|||
|
|
"timestamp": now,
|
|||
|
|
"merge_count": 1,
|
|||
|
|
"metadata": metadata or {},
|
|||
|
|
}
|
|||
|
|
self._cleanup(now)
|
|||
|
|
return self._pending[key]
|
|||
|
|
|
|||
|
|
def get(self, key: str) -> dict[str, Any] | None:
|
|||
|
|
return self._pending.get(key)
|
|||
|
|
|
|||
|
|
def consume(self, key: str) -> dict[str, Any] | None:
|
|||
|
|
entry = self._pending.pop(key, None)
|
|||
|
|
if not entry:
|
|||
|
|
return None
|
|||
|
|
return {
|
|||
|
|
"text": entry["text"],
|
|||
|
|
"merge_count": entry["merge_count"],
|
|||
|
|
"metadata": entry["metadata"],
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
def _cleanup(self, now: float) -> None:
|
|||
|
|
expired = [k for k, v in self._pending.items() if now - v["timestamp"] > self._ttl * 2]
|
|||
|
|
for k in expired:
|
|||
|
|
self._pending.pop(k, None)
|
|||
|
|
|
|||
|
|
def clear(self) -> None:
|
|||
|
|
self._pending.clear()
|
|||
|
|
|
|||
|
|
@property
|
|||
|
|
def pending_count(self) -> int:
|
|||
|
|
return len(self._pending)
|