新增拼多多(Pinduoduo)渠道扩展,支持在 Yuxi 平台中集成拼多多电商客服渠道。 包含以下功能模块: - client: 拼多多 API 客户端封装 - config: 渠道配置管理 - gateway: SSE/WebSocket 网关接入 - webhook: Webhook 事件处理 - outbound: 外发消息管理 - pairing: 用户配对与绑定 - security: 安全校验 - signature: 请求签名验证 - token: Token 管理 - dedupe: 消息去重 - monitor: 渠道状态监控 - status: 会话状态管理 - tools: Agent 工具集成 - window: 窗口管理 - types: 类型定义
356 lines
15 KiB
Python
356 lines
15 KiB
Python
from __future__ import annotations
|
||
|
||
import logging
|
||
|
||
from yuxi.channel.extensions.pinduoduo.client import (
|
||
PddGoodsClient,
|
||
PddLogisticsClient,
|
||
PddOrderClient,
|
||
PddRefundClient,
|
||
PddSessionClient,
|
||
)
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class PinduoduoTools:
|
||
def __init__(
|
||
self,
|
||
order_client: PddOrderClient,
|
||
logistics_client: PddLogisticsClient,
|
||
refund_client: PddRefundClient,
|
||
session_client: PddSessionClient | None = None,
|
||
goods_client: PddGoodsClient | None = None,
|
||
):
|
||
self._order = order_client
|
||
self._logistics = logistics_client
|
||
self._refund = refund_client
|
||
self._session = session_client
|
||
self._goods = goods_client
|
||
|
||
async def query_order(self, order_sn: str) -> str:
|
||
try:
|
||
data = await self._order.get_order_detail(order_sn)
|
||
order = data.get("order_info", data)
|
||
|
||
order_status_map = {
|
||
0: "待支付",
|
||
1: "已成团(已支付)",
|
||
2: "已发货",
|
||
3: "已签收",
|
||
4: "已取消",
|
||
5: "退款中/已退款",
|
||
}
|
||
status = order_status_map.get(order.get("order_status", -1), "未知")
|
||
|
||
return (
|
||
f"📦 订单 {order_sn}\n"
|
||
f"状态: {status}\n"
|
||
f"金额: ¥{order.get('order_amount', 0) / 100:.2f}\n"
|
||
f"商品: {order.get('goods_name', '未知商品')}\n"
|
||
f"下单时间: {order.get('created_time', '未知')}"
|
||
)
|
||
except Exception as e:
|
||
logger.exception("Query order failed: %s", order_sn)
|
||
return f"查询订单 {order_sn} 失败: {e}"
|
||
|
||
async def query_logistics(self, order_sn: str) -> str:
|
||
try:
|
||
data = await self._logistics.get_trace(order_sn)
|
||
traces = data.get("trace_list", data.get("traces", []))
|
||
|
||
if not traces:
|
||
return f"📦 订单 {order_sn} 暂无物流信息"
|
||
|
||
result = f"📦 订单 {order_sn} 物流轨迹:\n"
|
||
for t in traces[:5]:
|
||
result += f" [{t.get('time', '')}] {t.get('desc', t.get('status', ''))}\n"
|
||
return result.strip()
|
||
except Exception as e:
|
||
logger.exception("Query logistics failed: %s", order_sn)
|
||
return f"查询物流 {order_sn} 失败: {e}"
|
||
|
||
async def query_refund_status(self, order_sn: str) -> str:
|
||
try:
|
||
data = await self._refund.get_refund_info(order_sn)
|
||
|
||
refund_status_map = {
|
||
0: "买家申请退款",
|
||
1: "商家处理中",
|
||
2: "退款成功",
|
||
3: "退款关闭",
|
||
4: "商家拒绝",
|
||
}
|
||
status = refund_status_map.get(data.get("refund_status", -1), "未知")
|
||
|
||
return (
|
||
f"🔙 订单 {order_sn} 退款状态:\n"
|
||
f"状态: {status}\n"
|
||
f"退款金额: ¥{data.get('refund_amount', 0) / 100:.2f}\n"
|
||
f"申请原因: {data.get('refund_reason', '未知')}"
|
||
)
|
||
except Exception as e:
|
||
logger.exception("Query refund failed: %s", order_sn)
|
||
return f"查询退款 {order_sn} 失败: {e}"
|
||
|
||
async def transfer_human(self, session_id: str, mall_id: str, buyer_id: str, reason: str = "") -> str:
|
||
if not self._session:
|
||
return "转人工功能未初始化"
|
||
try:
|
||
await self._session.transfer_human(session_id=session_id, mall_id=mall_id, buyer_id=buyer_id, reason=reason)
|
||
return f"已成功将客服会话 {session_id} 转接人工客服。{reason and f'转接原因: {reason}' or ''}"
|
||
except Exception as e:
|
||
logger.exception("Transfer to human failed: %s", session_id)
|
||
return f"转接人工失败: {e}"
|
||
|
||
async def close_session(self, session_id: str, mall_id: str) -> str:
|
||
if not self._session:
|
||
return "关闭会话功能未初始化"
|
||
try:
|
||
await self._session.close_session(session_id=session_id, mall_id=mall_id)
|
||
return f"已成功关闭客服会话 {session_id}"
|
||
except Exception as e:
|
||
logger.exception("Close session failed: %s", session_id)
|
||
return f"关闭会话失败: {e}"
|
||
|
||
async def query_goods(self, goods_id: int) -> str:
|
||
if not self._goods:
|
||
return "商品查询功能未初始化"
|
||
try:
|
||
data = await self._goods.get_goods_detail(goods_id)
|
||
goods = data.get("goods_detail", data)
|
||
return (
|
||
f"🛒 商品 #{goods_id}\n"
|
||
f"名称: {goods.get('goods_name', '未知')}\n"
|
||
f"价格: ¥{goods.get('min_group_price', 0) / 100:.2f}\n"
|
||
f"库存: {goods.get('quantity', '未知')}\n"
|
||
f"状态: {'在售' if goods.get('is_onsale') else '下架'}"
|
||
)
|
||
except Exception as e:
|
||
logger.exception("Query goods failed: %s", goods_id)
|
||
return f"查询商品 {goods_id} 失败: {e}"
|
||
|
||
async def search_goods(self, keyword: str = "", page: int = 1) -> str:
|
||
if not self._goods:
|
||
return "商品搜索功能未初始化"
|
||
try:
|
||
data = await self._goods.get_goods_list(page=page, page_size=10)
|
||
goods_list = data.get("goods_list", [])
|
||
if not goods_list:
|
||
return f"未搜索到关键词 '{keyword}' 相关的商品"
|
||
result = f"搜索 '{keyword}':\n"
|
||
for g in goods_list[:5]:
|
||
result += (
|
||
f" #{g.get('goods_id')} {g.get('goods_name', '未知')} "
|
||
f"¥{g.get('min_group_price', 0) / 100:.2f}\n"
|
||
)
|
||
return result.strip()
|
||
except Exception as e:
|
||
logger.exception("Search goods failed: %s", keyword)
|
||
return f"搜索商品 '{keyword}' 失败: {e}"
|
||
|
||
async def agree_refund(self, order_sn: str, remark: str = "") -> str:
|
||
if not self._refund:
|
||
return "退款操作功能未初始化"
|
||
try:
|
||
await self._refund.agree_refund(order_sn, remark)
|
||
return f"✅ 已同意退款订单 {order_sn}。{remark}"
|
||
except Exception as e:
|
||
logger.exception("Agree refund failed: %s", order_sn)
|
||
return f"同意退款 {order_sn} 失败: {e}"
|
||
|
||
async def reject_refund(self, order_sn: str, reason: str, remark: str = "") -> str:
|
||
if not self._refund:
|
||
return "退款操作功能未初始化"
|
||
try:
|
||
await self._refund.reject_refund(order_sn, reason, remark)
|
||
return f"❌ 已拒绝退款订单 {order_sn}。原因: {reason}"
|
||
except Exception as e:
|
||
logger.exception("Reject refund failed: %s", order_sn)
|
||
return f"拒绝退款 {order_sn} 失败: {e}"
|
||
|
||
async def refund_operate(self, order_sn: str, action: str, reason: str = "", remark: str = "") -> str:
|
||
if action == "agree":
|
||
return await self.agree_refund(order_sn, remark)
|
||
elif action == "reject":
|
||
if not reason:
|
||
return "拒绝退款必须提供原因(reason 参数不能为空)"
|
||
return await self.reject_refund(order_sn, reason, remark)
|
||
else:
|
||
return f"未知操作 '{action}',仅支持 agree/reject"
|
||
|
||
def get_agent_tools(self) -> list[dict]:
|
||
tools = [
|
||
{
|
||
"name": "pdd_query_order",
|
||
"description": "查询拼多多订单详情。参数 order_sn: 订单编号",
|
||
"parameters": {
|
||
"type": "object",
|
||
"properties": {
|
||
"order_sn": {
|
||
"type": "string",
|
||
"description": "订单编号",
|
||
}
|
||
},
|
||
"required": ["order_sn"],
|
||
},
|
||
"handler": self.query_order,
|
||
},
|
||
{
|
||
"name": "pdd_query_logistics",
|
||
"description": "查询拼多多物流轨迹。参数 order_sn: 订单编号",
|
||
"parameters": {
|
||
"type": "object",
|
||
"properties": {
|
||
"order_sn": {
|
||
"type": "string",
|
||
"description": "订单编号",
|
||
}
|
||
},
|
||
"required": ["order_sn"],
|
||
},
|
||
"handler": self.query_logistics,
|
||
},
|
||
{
|
||
"name": "pdd_query_refund",
|
||
"description": "查询拼多多退款状态。参数 order_sn: 订单编号",
|
||
"parameters": {
|
||
"type": "object",
|
||
"properties": {
|
||
"order_sn": {
|
||
"type": "string",
|
||
"description": "订单编号",
|
||
}
|
||
},
|
||
"required": ["order_sn"],
|
||
},
|
||
"handler": self.query_refund_status,
|
||
},
|
||
{
|
||
"name": "pdd_refund_operate",
|
||
"description": "处理退款申请:同意或拒绝退款。参数 order_sn: 订单编号, action: 'agree'(同意)或'reject'(拒绝), reason: 拒绝原因(action='reject'时必需), remark: 备注(可选)",
|
||
"parameters": {
|
||
"type": "object",
|
||
"properties": {
|
||
"order_sn": {
|
||
"type": "string",
|
||
"description": "订单编号",
|
||
},
|
||
"action": {
|
||
"type": "string",
|
||
"enum": ["agree", "reject"],
|
||
"description": "操作类型:agree=同意退款,reject=拒绝退款",
|
||
},
|
||
"reason": {
|
||
"type": "string",
|
||
"description": "拒绝原因(action='reject'时必填)",
|
||
},
|
||
"remark": {
|
||
"type": "string",
|
||
"description": "备注说明(可选)",
|
||
},
|
||
},
|
||
"required": ["order_sn", "action"],
|
||
},
|
||
"handler": self.refund_operate,
|
||
},
|
||
]
|
||
if self._goods:
|
||
tools.extend([
|
||
{
|
||
"name": "pdd_query_goods",
|
||
"description": "查询拼多多商品详情。参数 goods_id: 商品数字ID",
|
||
"parameters": {
|
||
"type": "object",
|
||
"properties": {
|
||
"goods_id": {
|
||
"type": "integer",
|
||
"description": "商品数字ID",
|
||
}
|
||
},
|
||
"required": ["goods_id"],
|
||
},
|
||
"handler": self.query_goods,
|
||
},
|
||
{
|
||
"name": "pdd_search_goods",
|
||
"description": "搜索店铺商品列表。参数 keyword: 搜索关键词",
|
||
"parameters": {
|
||
"type": "object",
|
||
"properties": {
|
||
"keyword": {
|
||
"type": "string",
|
||
"description": "搜索关键词",
|
||
}
|
||
},
|
||
"required": ["keyword"],
|
||
},
|
||
"handler": self.search_goods,
|
||
},
|
||
])
|
||
if self._session:
|
||
tools.extend([
|
||
{
|
||
"name": "pdd_transfer_human",
|
||
"description": "将当前客服会话转接人工。当 AI 无法解决问题、买家明确要求转人工、或涉及投诉/纠纷/法律风险时使用。参数 reason: 转接原因(可选)",
|
||
"parameters": {
|
||
"type": "object",
|
||
"properties": {
|
||
"reason": {
|
||
"type": "string",
|
||
"description": "转接人工的原因说明",
|
||
}
|
||
},
|
||
"required": [],
|
||
},
|
||
"handler": self.transfer_human,
|
||
},
|
||
{
|
||
"name": "pdd_close_session",
|
||
"description": "关闭当前客服会话。当买家问题已解决、对话可结束时使用。",
|
||
"parameters": {
|
||
"type": "object",
|
||
"properties": {},
|
||
"required": [],
|
||
},
|
||
"handler": self.close_session,
|
||
},
|
||
])
|
||
return tools
|
||
|
||
async def execute_agent_tool(self, tool_name: str, params: dict, context: dict) -> dict:
|
||
tool_map = {
|
||
"pdd_query_order": self.query_order,
|
||
"pdd_query_logistics": self.query_logistics,
|
||
"pdd_query_refund": self.query_refund_status,
|
||
"pdd_refund_operate": self.refund_operate,
|
||
}
|
||
if self._goods:
|
||
tool_map["pdd_query_goods"] = self.query_goods
|
||
tool_map["pdd_search_goods"] = self.search_goods
|
||
if self._session:
|
||
tool_map["pdd_transfer_human"] = self.transfer_human
|
||
tool_map["pdd_close_session"] = self.close_session
|
||
handler = tool_map.get(tool_name)
|
||
if handler is None:
|
||
return {"success": False, "error": f"Unknown tool: {tool_name}"}
|
||
try:
|
||
if tool_name in ("pdd_transfer_human", "pdd_close_session"):
|
||
session_id = context.get("session_id", "")
|
||
mall_id = context.get("mall_id", "")
|
||
buyer_id = context.get("buyer_id", "")
|
||
if not session_id or not mall_id:
|
||
return {"success": False, "error": "会话上下文信息缺失,无法执行操作"}
|
||
if tool_name == "pdd_transfer_human":
|
||
result = await self.transfer_human(
|
||
session_id=session_id, mall_id=mall_id, buyer_id=buyer_id,
|
||
reason=params.get("reason", ""),
|
||
)
|
||
else:
|
||
result = await self.close_session(session_id=session_id, mall_id=mall_id)
|
||
else:
|
||
result = await handler(**params)
|
||
return {"success": True, "result": result}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|