新增拼多多(Pinduoduo)渠道扩展,支持在 Yuxi 平台中集成拼多多电商客服渠道。 包含以下功能模块: - client: 拼多多 API 客户端封装 - config: 渠道配置管理 - gateway: SSE/WebSocket 网关接入 - webhook: Webhook 事件处理 - outbound: 外发消息管理 - pairing: 用户配对与绑定 - security: 安全校验 - signature: 请求签名验证 - token: Token 管理 - dedupe: 消息去重 - monitor: 渠道状态监控 - status: 会话状态管理 - tools: Agent 工具集成 - window: 窗口管理 - types: 类型定义
94 lines
3.1 KiB
Python
94 lines
3.1 KiB
Python
from __future__ import annotations
|
|
|
|
import logging
|
|
|
|
from fastapi import APIRouter, HTTPException, Request
|
|
|
|
from yuxi.channel.extensions.pinduoduo.dedupe import PddMessageDeduplicator
|
|
from yuxi.channel.extensions.pinduoduo.monitor import PinduoduoMonitor
|
|
from yuxi.channel.extensions.pinduoduo.signature import verify_push_sign
|
|
from yuxi.channel.extensions.pinduoduo.types import PinduoduoAccount
|
|
from yuxi.channel.extensions.pinduoduo.window import PinduoduoWindowTracker
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_handlers: dict[str, dict] = {}
|
|
|
|
|
|
def register_handler(
|
|
account_id: str,
|
|
account: PinduoduoAccount,
|
|
monitor: PinduoduoMonitor,
|
|
deduplicator: PddMessageDeduplicator,
|
|
put_fn,
|
|
window_tracker: PinduoduoWindowTracker | None = None,
|
|
) -> None:
|
|
_handlers[account_id] = {
|
|
"account": account,
|
|
"monitor": monitor,
|
|
"deduplicator": deduplicator,
|
|
"put_fn": put_fn,
|
|
"window_tracker": window_tracker,
|
|
}
|
|
logger.info("Webhook handler registered for account=%s", account_id)
|
|
|
|
|
|
def unregister_handler(account_id: str) -> None:
|
|
_handlers.pop(account_id, None)
|
|
logger.info("Webhook handler unregistered for account=%s", account_id)
|
|
|
|
|
|
def create_webhook_router() -> APIRouter:
|
|
router = APIRouter(prefix="/webhook/pinduoduo", tags=["pinduoduo"])
|
|
|
|
@router.post("/callback")
|
|
async def handle_push(request: Request):
|
|
try:
|
|
body = await request.json()
|
|
except Exception:
|
|
raise HTTPException(status_code=400, detail="Invalid JSON")
|
|
|
|
sign = request.headers.get("X-Pdd-Sign", "")
|
|
mall_id = str(body.get("mall_id", ""))
|
|
|
|
handler = None
|
|
for h in _handlers.values():
|
|
if h["account"].mall_id == mall_id:
|
|
handler = h
|
|
break
|
|
|
|
if handler is None:
|
|
logger.warning("No handler found for mall_id=%s", mall_id)
|
|
return {"status": "no_handler"}
|
|
|
|
if handler["account"].client_secret:
|
|
body_str = (await request.body()).decode("utf-8")
|
|
if not verify_push_sign(body_str, handler["account"].client_secret, sign, handler["account"].sign_method):
|
|
logger.warning("Push signature verification failed for mall_id=%s", mall_id)
|
|
raise HTTPException(status_code=403, detail="Invalid signature")
|
|
|
|
monitor: PinduoduoMonitor = handler["monitor"]
|
|
messages = monitor.parse_push_event(body)
|
|
|
|
for msg in messages:
|
|
if handler["deduplicator"].is_duplicate(msg.msg_id):
|
|
continue
|
|
|
|
if handler.get("window_tracker") and msg.buyer_id:
|
|
handler["window_tracker"].record(msg.buyer_id)
|
|
|
|
unified = monitor.to_unified_message(msg)
|
|
await handler["put_fn"](unified)
|
|
|
|
return {"status": "ok", "message_count": len(messages)}
|
|
|
|
@router.post("/callback/verify")
|
|
async def verify_push_url(request: Request):
|
|
params = request.query_params
|
|
echostr = params.get("echostr", "")
|
|
if echostr:
|
|
return {"echostr": echostr}
|
|
return {"status": "ok"}
|
|
|
|
return router
|