ForcePilot/backend/package/yuxi/channel/extensions/taobao/client.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

198 lines
6.2 KiB
Python

import hashlib
import hmac
import json
import logging
import time
import httpx
logger = logging.getLogger(__name__)
GATEWAY_URL = "https://eco.taobao.com/router/rest"
SANDBOX_GATEWAY_URL = "https://gw.api.tbsandbox.com/router/rest"
class TaobaoAPIError(Exception):
def __init__(self, code: str, msg: str, sub_code: str = "", sub_msg: str = ""):
self.code = code
self.msg = msg
self.sub_code = sub_code
self.sub_msg = sub_msg
super().__init__(f"[{code}] {msg}" + (f" ({sub_code}: {sub_msg})" if sub_code else ""))
class TaobaoClient:
def __init__(
self,
app_key: str,
app_secret: str,
sign_method: str = "md5",
sandbox: bool = False,
timeout: int = 10,
):
self.app_key = app_key
self.app_secret = app_secret
self.sign_method = sign_method
self.gateway = SANDBOX_GATEWAY_URL if sandbox else GATEWAY_URL
self.timeout = timeout
def _sign(self, params: dict) -> str:
sorted_keys = sorted(k for k in params if k != "sign" and params[k] is not None)
raw = "".join(f"{k}{params[k]}" for k in sorted_keys)
raw = f"{self.app_secret}{raw}{self.app_secret}"
if self.sign_method == "hmac-sha256":
return (
hmac.new(
self.app_secret.encode("utf-8"),
raw.encode("utf-8"),
hashlib.sha256,
)
.hexdigest()
.upper()
)
return hashlib.md5(raw.encode("utf-8")).hexdigest().upper()
def _build_payload(self, method: str, params: dict, session: str | None = None) -> dict:
payload = {
"method": method,
"app_key": self.app_key,
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
"format": "json",
"v": "2.0",
"sign_method": self.sign_method,
**params,
}
if session:
payload["session"] = session
payload["sign"] = self._sign(payload)
return payload
async def execute(
self,
method: str,
params: dict | None = None,
session: str | None = None,
) -> dict:
payload = self._build_payload(method, params or {}, session)
async with httpx.AsyncClient(timeout=self.timeout) as http:
resp = await http.post(self.gateway, data=payload)
resp.raise_for_status()
data = resp.json()
if "error_response" in data:
err = data["error_response"]
raise TaobaoAPIError(
code=err.get("code", "unknown"),
msg=err.get("msg", ""),
sub_code=err.get("sub_code", ""),
sub_msg=err.get("sub_msg", ""),
)
return data
async def get_token(self, code: str, redirect_uri: str) -> dict:
return await self.execute(
"taobao.top.auth.token.create",
{"code": code, "redirect_uri": redirect_uri},
)
async def refresh_token(self, refresh_token: str) -> dict:
return await self.execute(
"taobao.top.auth.token.refresh",
{"refresh_token": refresh_token},
)
async def send_customer_message(
self,
to_user: str,
content: str,
msg_type: int = 0,
media_id: str | None = None,
session: str | None = None,
) -> dict:
context = {"text": content} if msg_type == 0 else {"media_id": media_id or ""}
return await self.execute(
"taobao.openim.custmsg.push",
{
"to_user": to_user,
"msg_type": msg_type,
"context": json.dumps(context, ensure_ascii=False),
},
session=session,
)
async def get_trade_fullinfo(self, tid: int, session: str) -> dict:
return await self.execute(
"taobao.trade.fullinfo.get",
{
"tid": tid,
"fields": "tid,type,status,payment,orders,logistics",
},
session=session,
)
async def get_logistics_trace(self, tid: int, session: str) -> dict:
return await self.execute(
"taobao.logistics.trace.search",
{"tid": tid},
session=session,
)
async def get_userservice(self, user_id: str, session: str) -> dict:
return await self.execute(
"taobao.openim.userservice.get",
{"user_id": user_id},
session=session,
)
async def qianniu_sellerorder_query(self, buyer_nick: str, session: str) -> dict:
return await self.execute(
"taobao.qianniu.sellerorder.buyer.query",
{"buyer_nick": buyer_nick},
session=session,
)
async def qianniu_refund_get(self, refund_id: str, session: str) -> dict:
return await self.execute(
"taobao.qianniu.refund.get",
{"refund_id": refund_id},
session=session,
)
async def qianniu_buyer_tag_get(self, buyer_nick: str, session: str) -> dict:
return await self.execute(
"taobao.qianniu.buyer.tag.get",
{"buyer_nick": buyer_nick},
session=session,
)
async def qianniu_coupon_buyer_get(self, buyer_nick: str, session: str) -> dict:
return await self.execute(
"taobao.qianniu.coupon.buyer.get",
{"buyer_nick": buyer_nick},
session=session,
)
async def qianniu_cloudkefu_forward(self, buyer_nick: str, to_nick: str, session: str) -> dict:
return await self.execute(
"taobao.qianniu.cloudkefu.forward",
{"buyer_nick": buyer_nick, "to_nick": to_nick},
session=session,
)
async def get_item(self, item_id: int, session: str) -> dict:
return await self.execute(
"taobao.item.get",
{"item_id": item_id, "fields": "title,price,pic_url,desc"},
session=session,
)
async def get_chatlogs(self, user_id: str, begin: str, end: str, session: str) -> dict:
return await self.execute(
"taobao.openim.chatlogs.get",
{"user_id": user_id, "begin": begin, "end": end},
session=session,
)