ForcePilot/backend/package/yuxi/channels/adapters/zalo_oa/session.py
Kris 30dd9e16f4 feat(zalo-oa): 实现完整的 Zalo OA 渠道适配器模块
新增 Zalo OA 官方账号完整集成能力,包含:
1. 基础通信能力:消息编解码、目标归一化、文本分块
2. 安全与校验:Webhook 签名验证、DM 策略管理、配对流程
3. 辅助工具:重复事件去重、请求限流、异常告警
4. 管理功能:账号多实例管理、配置验证、健康诊断
5. 扩展能力:媒体托管、视觉识别、TTS 语音合成
6. 运维支持:审计日志、状态监控、目录同步
2026-05-12 00:52:47 +08:00

172 lines
5.6 KiB
Python

from __future__ import annotations
import time
from typing import Any
from yuxi.channels.models import ChatType
DEFAULT_SESSION_TTL_SEC = 3600
DEFAULT_BIND_TTL_SEC = 86400
class SessionRouter:
def __init__(self, ttl_sec: int = DEFAULT_SESSION_TTL_SEC):
self._ttl_sec = ttl_sec
self._sessions: dict[str, dict[str, Any]] = {}
self._bindings: dict[str, dict[str, Any]] = {}
def resolve_thread_key(self, agent_id: str, follower_id: str, account_id: str = "default") -> str:
return f"agent:{agent_id}:zalo_oa:{account_id}:{ChatType.DIRECT.value}:{follower_id}"
def resolve_bind_key(self, agent_id: str, account_id: str = "default") -> str:
return f"agent:{agent_id}:zalo_oa:{account_id}"
def resolve_session_params(
self,
source_id: str,
sender_name: str = "",
avatar: str = "",
dm_policy: str = "",
account_id: str = "default",
) -> dict[str, Any]:
params: dict[str, Any] = {
"chat_type": ChatType.DIRECT,
"channel_chat_id": source_id,
}
if sender_name:
params["sender_name"] = sender_name
if avatar:
params["avatar"] = avatar
if dm_policy:
params["dm_policy"] = dm_policy
if account_id:
params["account_id"] = account_id
return params
def get_session(self, thread_key: str) -> dict[str, Any] | None:
entry = self._sessions.get(thread_key)
if entry is None:
return None
if time.time() - entry.get("created_at", 0) > self._ttl_sec:
del self._sessions[thread_key]
return None
return entry.get("data")
def set_session(self, thread_key: str, data: dict[str, Any]):
self._sessions[thread_key] = {
"data": data,
"created_at": time.time(),
}
def clear_session(self, thread_key: str):
self._sessions.pop(thread_key, None)
def bind_agent_account(
self,
agent_id: str,
account_id: str,
bind_data: dict[str, Any] | None = None,
):
bind_key = self.resolve_bind_key(agent_id, account_id)
self._bindings[bind_key] = {
"agent_id": agent_id,
"account_id": account_id,
"data": bind_data or {},
"created_at": time.time(),
"ttl_sec": DEFAULT_BIND_TTL_SEC,
}
def unbind_agent_account(self, agent_id: str, account_id: str):
bind_key = self.resolve_bind_key(agent_id, account_id)
self._bindings.pop(bind_key, None)
def is_bound(self, agent_id: str, account_id: str) -> bool:
bind_key = self.resolve_bind_key(agent_id, account_id)
entry = self._bindings.get(bind_key)
if entry is None:
return False
ttl = entry.get("ttl_sec", DEFAULT_BIND_TTL_SEC)
if time.time() - entry.get("created_at", 0) > ttl:
del self._bindings[bind_key]
return False
return True
def get_binding(self, agent_id: str, account_id: str) -> dict[str, Any] | None:
bind_key = self.resolve_bind_key(agent_id, account_id)
entry = self._bindings.get(bind_key)
if entry is None:
return None
ttl = entry.get("ttl_sec", DEFAULT_BIND_TTL_SEC)
if time.time() - entry.get("created_at", 0) > ttl:
del self._bindings[bind_key]
return None
return entry
def get_bound_sessions(self, agent_id: str, account_id: str) -> list[dict[str, Any]]:
bind_key = self.resolve_bind_key(agent_id, account_id)
prefix = f"{bind_key}:{ChatType.DIRECT.value}:"
sessions = []
now = time.time()
for key, entry in self._sessions.items():
if key.startswith(prefix):
if now - entry.get("created_at", 0) <= self._ttl_sec:
session_data = entry.get("data", {})
if session_data:
sessions.append(session_data)
return sessions
def cleanup_expired(self) -> int:
now = time.time()
expired_sessions = [k for k, v in self._sessions.items() if now - v.get("created_at", 0) > self._ttl_sec]
for k in expired_sessions:
del self._sessions[k]
expired_bindings = [
k
for k, v in self._bindings.items()
if now - v.get("created_at", 0) > v.get("ttl_sec", DEFAULT_BIND_TTL_SEC)
]
for k in expired_bindings:
del self._bindings[k]
return len(expired_sessions) + len(expired_bindings)
@property
def session_count(self) -> int:
return len(self._sessions)
@property
def binding_count(self) -> int:
return len(self._bindings)
_router = SessionRouter()
def resolve_thread_key(agent_id: str, follower_id: str, account_id: str = "default") -> str:
return _router.resolve_thread_key(agent_id, follower_id, account_id)
def resolve_session_params(
source_id: str,
sender_name: str = "",
avatar: str = "",
dm_policy: str = "",
account_id: str = "default",
) -> dict[str, Any]:
return _router.resolve_session_params(source_id, sender_name, avatar, dm_policy, account_id)
def bind_agent_account(agent_id: str, account_id: str, bind_data: dict[str, Any] | None = None):
_router.bind_agent_account(agent_id, account_id, bind_data)
def unbind_agent_account(agent_id: str, account_id: str):
_router.unbind_agent_account(agent_id, account_id)
def is_bound(agent_id: str, account_id: str) -> bool:
return _router.is_bound(agent_id, account_id)
def get_session_router() -> SessionRouter:
return _router