新增京东(JD)渠道扩展,支持在 Yuxi 平台中集成京东客服渠道。 包含以下功能模块: - client: 京东 API 客户端封装 - config: 渠道配置管理 - gateway: SSE/WebSocket 网关接入 - webhook: Webhook 事件处理 - outbound: 外发消息管理 - pairing: 用户配对与绑定 - security: 安全校验 - signature: 请求签名验证 - crypto: 加解密处理 - dedupe: 消息去重 - monitor: 渠道状态监控 - status: 会话状态管理 - session: 会话管理 - business: 业务逻辑处理 - types: 类型定义
129 lines
4.3 KiB
Python
129 lines
4.3 KiB
Python
import asyncio
|
|
import logging
|
|
from datetime import datetime, timedelta, UTC
|
|
|
|
from yuxi.channel.extensions.jd.client import JOSClient
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class SessionEntry:
|
|
__slots__ = ("chat_id", "user_id", "messages", "turn_count", "created_at", "updated_at", "status")
|
|
|
|
def __init__(self, chat_id: str, user_id: str):
|
|
self.chat_id = chat_id
|
|
self.user_id = user_id
|
|
self.messages: list[dict] = []
|
|
self.turn_count = 0
|
|
self.created_at = datetime.now(UTC)
|
|
self.updated_at = datetime.now(UTC)
|
|
self.status = "active"
|
|
|
|
|
|
class JDSession:
|
|
MAX_HISTORY = 50
|
|
SESSION_TIMEOUT_SECONDS = 1800
|
|
CLEANUP_INTERVAL = 600
|
|
|
|
def __init__(self, client: JOSClient | None = None):
|
|
self._sessions: dict[str, SessionEntry] = {}
|
|
self._client = client
|
|
self._cleanup_task: asyncio.Task | None = None
|
|
|
|
async def start(self):
|
|
self._cleanup_task = asyncio.create_task(self._cleanup_loop())
|
|
|
|
async def stop(self):
|
|
if self._cleanup_task:
|
|
self._cleanup_task.cancel()
|
|
try:
|
|
await self._cleanup_task
|
|
except asyncio.CancelledError:
|
|
pass
|
|
self._cleanup_task = None
|
|
|
|
def add_message(self, chat_id: str, user_id: str, msg: dict):
|
|
session = self._sessions.get(chat_id)
|
|
if session is None:
|
|
session = SessionEntry(chat_id, user_id)
|
|
self._sessions[chat_id] = session
|
|
|
|
session.messages.append({**msg, "_timestamp": datetime.now(UTC).isoformat()})
|
|
if len(session.messages) > self.MAX_HISTORY:
|
|
session.messages.pop(0)
|
|
|
|
if session.status != "active":
|
|
session.status = "active"
|
|
session.turn_count = 1
|
|
else:
|
|
session.turn_count += 1
|
|
|
|
session.updated_at = datetime.now(UTC)
|
|
|
|
def get_history(self, chat_id: str, limit: int = 20) -> list[dict]:
|
|
session = self._sessions.get(chat_id)
|
|
if session is None:
|
|
return []
|
|
return session.messages[-limit:]
|
|
|
|
def get_session_context(self, chat_id: str) -> dict | None:
|
|
session = self._sessions.get(chat_id)
|
|
if session is None:
|
|
return None
|
|
return {
|
|
"chat_id": session.chat_id,
|
|
"user_id": session.user_id,
|
|
"turn_count": session.turn_count,
|
|
"status": session.status,
|
|
"created_at": session.created_at.isoformat(),
|
|
"updated_at": session.updated_at.isoformat(),
|
|
"message_count": len(session.messages),
|
|
}
|
|
|
|
def close_session(self, chat_id: str):
|
|
session = self._sessions.get(chat_id)
|
|
if session:
|
|
session.status = "closed"
|
|
session.updated_at = datetime.now(UTC)
|
|
|
|
def clear_session(self, chat_id: str):
|
|
self._sessions.pop(chat_id, None)
|
|
|
|
async def fetch_remote_history(
|
|
self, chat_id: str, to_id: str, access_token: str, page: int = 1, page_size: int = 20
|
|
) -> list[dict]:
|
|
if self._client is None:
|
|
return []
|
|
|
|
biz_params = {
|
|
"chat_id": chat_id,
|
|
"to_id": to_id,
|
|
"page": str(page),
|
|
"page_size": str(page_size),
|
|
}
|
|
|
|
try:
|
|
data = await self._client.call("jingdong.im.getChatLog", biz_params, access_token)
|
|
messages = data.get("jingdong_im_getChatLog_responce", {}).get("result", {}).get("messages", [])
|
|
return messages if isinstance(messages, list) else []
|
|
except Exception:
|
|
logger.exception("Failed to fetch JD chat history for %s", chat_id)
|
|
return []
|
|
|
|
async def _cleanup_loop(self):
|
|
while True:
|
|
try:
|
|
await asyncio.sleep(self.CLEANUP_INTERVAL)
|
|
self._expire_sessions()
|
|
except asyncio.CancelledError:
|
|
break
|
|
except Exception:
|
|
logger.exception("JD session cleanup loop error")
|
|
|
|
def _expire_sessions(self):
|
|
cutoff = datetime.now(UTC) - timedelta(seconds=self.SESSION_TIMEOUT_SECONDS)
|
|
expired = [chat_id for chat_id, session in self._sessions.items() if session.updated_at < cutoff]
|
|
for chat_id in expired:
|
|
session = self._sessions.pop(chat_id)
|
|
logger.debug("JD session expired: chat_id=%s turns=%d", session.chat_id, session.turn_count)
|