ForcePilot/backend/package/yuxi/channels/adapters/whatsapp/inbound_cache.py
Kris 5c3611ff19 refactor(whatsapp): 整理WhatsApp适配器代码结构并修复多线程安全问题
主要变更:
1. 重构导入顺序,统一模块导入规范
2. 提取通用方法到session模块,减少代码重复
3. 为缓存类添加线程/异步锁,修复并发安全问题
4. 新增入站处理器和发送管理器模块,拆分业务逻辑
5. 优化凭证队列,改为异步实现
6. 移除废弃的SSE_POLLING能力标识
7. 修复轮询投票解析逻辑
8. 优化Markdown转换规则,避免格式冲突
9. 完善连接控制器的异常处理
10. 新增发送静默消息的API支持
2026-05-13 16:17:30 +08:00

113 lines
3.7 KiB
Python

from __future__ import annotations
import time
from collections import OrderedDict
from threading import Lock
from typing import Any
from yuxi.utils.logging_config import logger
def _normalize_jid(jid: str) -> str:
return jid.split("@")[0].replace("+", "").replace(" ", "").replace("-", "")
def _is_group_jid(jid: str) -> bool:
return "@g.us" in jid
class InboundMessageCache:
def __init__(self, max_size: int = 500, ttl_seconds: int = 600):
self._cache: OrderedDict[str, dict[str, Any]] = OrderedDict()
self._max_size = max_size
self._ttl = ttl_seconds
self._lock = Lock()
def put(self, msg_id: str, jid: str, content: str, metadata: dict[str, Any] | None = None) -> None:
if not msg_id:
return
with self._lock:
self._cache[msg_id] = {
"jid": jid,
"content": content[:500],
"timestamp": time.time(),
"metadata": metadata or {},
}
self._cache.move_to_end(msg_id)
self._evict()
def get(self, msg_id: str) -> dict[str, Any] | None:
with self._lock:
entry = self._cache.get(msg_id)
if entry is None:
return None
if time.time() - entry["timestamp"] > self._ttl:
self._cache.pop(msg_id, None)
return None
return entry
def lookup_inbound_meta(self, target_jid: str, target_msg_id: str | None = None) -> dict[str, Any] | None:
with self._lock:
if target_msg_id:
exact = self._cache.get(target_msg_id)
if exact and time.time() - exact["timestamp"] <= self._ttl:
return exact
target_is_group = _is_group_jid(target_jid)
candidates: list[tuple[str, dict[str, Any], float]] = []
now = time.time()
for msg_id, entry in self._cache.items():
if now - entry["timestamp"] > self._ttl:
continue
entry_jid = entry.get("jid", "")
if target_is_group != _is_group_jid(entry_jid):
continue
candidates.append((msg_id, entry, entry["timestamp"]))
if len(candidates) >= 10:
break
if not candidates:
return None
candidates.sort(key=lambda x: x[2], reverse=True)
best_msg_id, best_entry, _ = candidates[0]
logger.debug(
f"InboundMessageCache: fuzzy match for {target_jid} "
f"-> msg_id={best_msg_id}, candidates={len(candidates)}"
)
return best_entry
def resolve_quoted_message_key(self, target_jid: str, quoted_msg_id: str | None = None) -> dict[str, Any] | None:
meta = None
if quoted_msg_id:
meta = self.get(quoted_msg_id)
if meta is None:
meta = self.lookup_inbound_meta(target_jid, None)
if meta is None:
return None
msg_jid = meta.get("jid", target_jid)
return {
"remoteJid": msg_jid,
"fromMe": False,
"id": quoted_msg_id or "",
}
def _evict(self) -> None:
now = time.time()
expired = [k for k, v in self._cache.items() if now - v["timestamp"] > self._ttl]
for k in expired:
del self._cache[k]
while len(self._cache) > self._max_size:
oldest = next(iter(self._cache))
del self._cache[oldest]
def clear(self) -> None:
with self._lock:
self._cache.clear()
def __len__(self) -> int:
with self._lock:
return len(self._cache)