新增了Alexa渠道的所有核心功能模块,包括配置管理、请求校验、会话管理、安全验证、去重、响应格式化、渐进式响应、主动推送、提醒功能、配对绑定以及Webhook端点,并完成了插件注册和配置项定义。
113 lines
4.0 KiB
Python
113 lines
4.0 KiB
Python
import logging
|
|
import os
|
|
import time
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_DEFAULT_TIMEOUT = 30
|
|
_CLEANUP_INTERVAL = 60
|
|
|
|
|
|
class AlexaSessionManager:
|
|
|
|
def __init__(self, account=None):
|
|
self._sessions: dict[str, dict] = {}
|
|
self._timeout = getattr(account, "session_timeout_seconds", _DEFAULT_TIMEOUT) if account else _DEFAULT_TIMEOUT
|
|
self._persistence_enabled = getattr(account, "session_persistence_enabled", False) if account else False
|
|
self._last_cleanup = time.time()
|
|
self._redis = None
|
|
if self._persistence_enabled:
|
|
self._init_redis()
|
|
|
|
def _init_redis(self):
|
|
redis_url = os.getenv("REDIS_URL", os.getenv("YUXI_REDIS_URL", ""))
|
|
if not redis_url:
|
|
logger.warning("Alexa session persistence enabled but no REDIS_URL configured")
|
|
self._persistence_enabled = False
|
|
return
|
|
try:
|
|
import redis.asyncio as aioredis
|
|
|
|
self._redis = aioredis.from_url(redis_url, decode_responses=True)
|
|
logger.info("Alexa session persistence: using Redis backend")
|
|
except Exception:
|
|
logger.warning("Alexa session persistence: Redis unavailable, falling back to in-memory")
|
|
self._persistence_enabled = False
|
|
|
|
def get(self, session_id: str) -> dict:
|
|
self._cleanup_expired()
|
|
session = self._sessions.get(session_id)
|
|
if session:
|
|
session["_last_access"] = time.time()
|
|
return session or {}
|
|
|
|
async def get_async(self, session_id: str) -> dict:
|
|
if self._redis:
|
|
try:
|
|
import json
|
|
|
|
data = await self._redis.get(f"alexa:session:{session_id}")
|
|
if data:
|
|
session = json.loads(data)
|
|
session["_last_access"] = time.time()
|
|
self._sessions[session_id] = session
|
|
await self._redis.expire(f"alexa:session:{session_id}", self._timeout)
|
|
return session
|
|
except Exception:
|
|
logger.debug("Redis session read failed, falling back to memory")
|
|
return self.get(session_id)
|
|
|
|
def set(self, session_id: str, attrs: dict):
|
|
attrs["_created_at"] = attrs.get("_created_at", time.time())
|
|
attrs["_last_access"] = time.time()
|
|
self._sessions[session_id] = attrs
|
|
|
|
async def set_async(self, session_id: str, attrs: dict):
|
|
attrs["_created_at"] = attrs.get("_created_at", time.time())
|
|
attrs["_last_access"] = time.time()
|
|
self._sessions[session_id] = attrs
|
|
if self._redis:
|
|
try:
|
|
import json
|
|
|
|
await self._redis.setex(
|
|
f"alexa:session:{session_id}",
|
|
self._timeout,
|
|
json.dumps(attrs, ensure_ascii=False, default=str),
|
|
)
|
|
except Exception:
|
|
logger.debug("Redis session write failed")
|
|
|
|
def delete(self, session_id: str):
|
|
self._sessions.pop(session_id, None)
|
|
|
|
async def delete_async(self, session_id: str):
|
|
self._sessions.pop(session_id, None)
|
|
if self._redis:
|
|
try:
|
|
await self._redis.delete(f"alexa:session:{session_id}")
|
|
except Exception:
|
|
logger.debug("Redis session delete failed")
|
|
|
|
def _cleanup_expired(self):
|
|
now = time.time()
|
|
if now - self._last_cleanup < _CLEANUP_INTERVAL:
|
|
return
|
|
self._last_cleanup = now
|
|
expired = [
|
|
sid for sid, s in self._sessions.items()
|
|
if now - s.get("_last_access", 0) > self._timeout
|
|
]
|
|
for sid in expired:
|
|
self._sessions.pop(sid, None)
|
|
if expired:
|
|
logger.debug("Alexa session: cleaned up %d expired sessions", len(expired))
|
|
|
|
async def close(self):
|
|
self._sessions.clear()
|
|
if self._redis:
|
|
try:
|
|
await self._redis.aclose()
|
|
except Exception:
|
|
pass
|
|
self._redis = None |