2026-09-11 11:02:15 +08:00
|
|
|
"""Redis-backed anonymous customer-service session primitives."""
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import time
|
|
|
|
|
import uuid
|
|
|
|
|
from inspect import isawaitable
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SessionOwnershipError(Exception):
|
|
|
|
|
def __init__(self, code: int, message: str):
|
|
|
|
|
self.code = code
|
|
|
|
|
self.message = message
|
|
|
|
|
super().__init__(message)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _config(config_getter, key: str, default):
|
|
|
|
|
value = config_getter(key, str(default))
|
|
|
|
|
if isawaitable(value):
|
|
|
|
|
value = await value
|
|
|
|
|
return type(default)(value)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class AnonymousSessionService:
|
|
|
|
|
def __init__(self, redis, *, config_getter, clock=time.time):
|
|
|
|
|
self.redis = redis
|
|
|
|
|
self.config_getter = config_getter
|
|
|
|
|
self.clock = clock
|
|
|
|
|
|
|
|
|
|
async def create_session(self) -> str:
|
|
|
|
|
session_id = uuid.uuid4().hex
|
|
|
|
|
ttl = await _config(self.config_getter, "agent.customer.session.ttl", 1800)
|
|
|
|
|
await self.redis.set(f"session:{session_id}", "anonymous", ex=ttl)
|
|
|
|
|
# Redis lists cannot be created empty; the marker is ignored by readers.
|
|
|
|
|
await self.redis.rpush(f"session:{session_id}:messages", "")
|
|
|
|
|
await self.redis.expire(f"session:{session_id}:messages", ttl)
|
|
|
|
|
return session_id
|
|
|
|
|
|
|
|
|
|
async def verify_session_ownership(
|
|
|
|
|
self,
|
|
|
|
|
session_id: str,
|
|
|
|
|
*,
|
|
|
|
|
customer_id: str | None = None,
|
|
|
|
|
session_customer_id: str | None = None,
|
|
|
|
|
) -> None:
|
|
|
|
|
if not await self.redis.exists(f"session:{session_id}"):
|
|
|
|
|
raise SessionOwnershipError(404, "会话不存在或已过期")
|
|
|
|
|
if customer_id is not None and customer_id != session_customer_id:
|
|
|
|
|
raise SessionOwnershipError(403, "无权访问该会话")
|
|
|
|
|
|
|
|
|
|
async def consume_chat_quota(self, session_id: str) -> int | None:
|
|
|
|
|
window = await _config(
|
|
|
|
|
self.config_getter, "agent.customer.rate_limit.window_sec", 60
|
|
|
|
|
)
|
|
|
|
|
maximum = await _config(
|
|
|
|
|
self.config_getter, "agent.customer.rate_limit.max_requests", 20
|
|
|
|
|
)
|
|
|
|
|
key = f"rate:limit:anon:{session_id}:chat"
|
|
|
|
|
now = self.clock()
|
|
|
|
|
await self.redis.zremrangebyscore(key, 0, (now - window) * 1000)
|
|
|
|
|
count = await self.redis.zcard(key)
|
|
|
|
|
if count >= maximum:
|
2026-09-11 20:21:44 +08:00
|
|
|
entries = await self.redis.zrange(key, 0, 0, withscores=True)
|
|
|
|
|
oldest = entries[0][1] if entries else now * 1000
|
2026-09-11 11:02:15 +08:00
|
|
|
return max(1, int((oldest / 1000 + window) - now))
|
|
|
|
|
await self.redis.zadd(key, {uuid.uuid4().hex: now * 1000})
|
|
|
|
|
await self.redis.expire(key, window)
|
|
|
|
|
return None
|