新增淘宝(Taobao)渠道扩展,支持在 Yuxi 平台中集成淘宝电商客服渠道。 包含以下功能模块: - client: 淘宝 API 客户端封装 - config: 渠道配置管理 - gateway: SSE/WebSocket 网关接入 - webhook: Webhook 事件处理 - outbound: 外发消息管理 - message: 消息处理 - pairing: 用户配对与绑定 - security: 安全校验 - crypto: 加解密处理 - status: 会话状态管理 - tools: Agent 工具集成 - types: 类型定义
198 lines
6.2 KiB
Python
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,
|
|
)
|