ForcePilot/backend/package/yuxi/channel/extensions/jd/session.py
Kris bb8ade9e2b feat(channel): 添加京东渠道扩展
新增京东(JD)渠道扩展,支持在 Yuxi 平台中集成京东客服渠道。

包含以下功能模块:
- client: 京东 API 客户端封装
- config: 渠道配置管理
- gateway: SSE/WebSocket 网关接入
- webhook: Webhook 事件处理
- outbound: 外发消息管理
- pairing: 用户配对与绑定
- security: 安全校验
- signature: 请求签名验证
- crypto: 加解密处理
- dedupe: 消息去重
- monitor: 渠道状态监控
- status: 会话状态管理
- session: 会话管理
- business: 业务逻辑处理
- types: 类型定义
2026-05-21 11:05:40 +08:00

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)