ForcePilot/backend/package/yuxi/channel/extensions/taobao/webhook.py
Kris 904ab9a6c2 feat(channel): 添加淘宝渠道扩展
新增淘宝(Taobao)渠道扩展,支持在 Yuxi 平台中集成淘宝电商客服渠道。

包含以下功能模块:
- client: 淘宝 API 客户端封装
- config: 渠道配置管理
- gateway: SSE/WebSocket 网关接入
- webhook: Webhook 事件处理
- outbound: 外发消息管理
- message: 消息处理
- pairing: 用户配对与绑定
- security: 安全校验
- crypto: 加解密处理
- status: 会话状态管理
- tools: Agent 工具集成
- types: 类型定义
2026-05-21 11:46:45 +08:00

128 lines
4.5 KiB
Python

import json
import logging
import time
from collections import defaultdict
from fastapi import APIRouter, Request, Response
from yuxi.channel.extensions.taobao.config import TaobaoConfigAdapter
from yuxi.channel.extensions.taobao.crypto import decrypt_message, verify_top_sign
from yuxi.channel.extensions.taobao.gateway import get_current_gateway
from yuxi.channel.extensions.taobao.message import parse_to_unified_message
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/webhook/taobao", tags=["taobao"])
_config_adapter = TaobaoConfigAdapter()
_dedupe_sets: dict[str, dict[str, float]] = defaultdict(dict)
DEDUPE_TTL_SECONDS = 300
def _get_config(account_id: str = "default"):
config = _config_adapter.resolve_account(account_id)
return _config_adapter.build_account(account_id, config)
def _dedupe_check(account_id: str, msg_id: str) -> bool:
now = time.monotonic()
dedupe = _dedupe_sets[account_id]
if msg_id in dedupe and now - dedupe[msg_id] < DEDUPE_TTL_SECONDS:
return True
dedupe[msg_id] = now
cutoff = now - DEDUPE_TTL_SECONDS
_dedupe_sets[account_id] = {k: v for k, v in dedupe.items() if v >= cutoff}
return False
@router.get("/callback")
async def verify_callback_default(request: Request):
return await _verify_callback(request, "default")
@router.get("/account/{account_id}/callback")
async def verify_callback_account(request: Request, account_id: str):
return await _verify_callback(request, account_id)
async def _verify_callback(request: Request, account_id: str):
config = _get_config(account_id)
if not config.is_configured():
return Response(status_code=503, content="not configured")
params = dict(request.query_params)
sign = params.pop("sign", "")
computed_ok = verify_top_sign(params, config.app_secret, sign, config.sign_method)
if computed_ok:
return Response(content="success")
return Response(status_code=403, content="sign error")
@router.post("/callback")
async def handle_callback_default(request: Request):
return await _handle_callback(request, "default")
@router.post("/account/{account_id}/callback")
async def handle_callback_account(request: Request, account_id: str):
return await _handle_callback(request, account_id)
async def _handle_callback(request: Request, account_id: str):
gateway = get_current_gateway()
if gateway is None:
return Response(status_code=503, content="gateway not started")
queue = gateway.get_message_queue()
if queue is None:
return Response(status_code=503, content="gateway not started")
config = _get_config(account_id)
if not config.is_configured():
return Response(status_code=503, content="not configured")
try:
body = await request.body()
content_type = request.headers.get("content-type", "")
if "application/x-www-form-urlencoded" in content_type or "multipart/form-data" in content_type:
form = await request.form()
params = {k: v for k, v in form.items()}
else:
params = json.loads(body.decode("utf-8"))
sign = params.pop("sign", "")
if not verify_top_sign(params, config.app_secret, sign, config.sign_method):
return Response(status_code=403, content="sign verification failed")
message_data = params
if config.encrypt_messages and "encrypt_data" in params:
decrypted = decrypt_message(
encrypt_data=params.get("encrypt_data", ""),
encrypt_key=params.get("encrypt_key", ""),
sign=params.get("encrypt_sign", ""),
timestamp=params.get("timestamp", ""),
app_secret=config.app_secret,
rsa_private_key_pem=config.rsa_private_key,
)
if decrypted is None:
return Response(status_code=400, content="decrypt failed")
message_data = json.loads(decrypted)
msg_id = message_data.get("msg_id", "") or message_data.get("id", "")
if msg_id and _dedupe_check(account_id, msg_id):
logger.debug("Duplicate message ignored: %s", msg_id)
return Response(content="ok")
unified = parse_to_unified_message(message_data)
if unified:
await gateway.enqueue_message(unified)
return Response(content="ok")
except Exception:
logger.exception("Taobao webhook processing error")
return Response(status_code=500, content="internal error")