ForcePilot/backend/package/yuxi/channel/extensions/pinduoduo/tools.py
Kris 1476e82ef0 feat(channel): 添加拼多多渠道扩展
新增拼多多(Pinduoduo)渠道扩展,支持在 Yuxi 平台中集成拼多多电商客服渠道。

包含以下功能模块:
- client: 拼多多 API 客户端封装
- config: 渠道配置管理
- gateway: SSE/WebSocket 网关接入
- webhook: Webhook 事件处理
- outbound: 外发消息管理
- pairing: 用户配对与绑定
- security: 安全校验
- signature: 请求签名验证
- token: Token 管理
- dedupe: 消息去重
- monitor: 渠道状态监控
- status: 会话状态管理
- tools: Agent 工具集成
- window: 窗口管理
- types: 类型定义
2026-05-21 11:33:31 +08:00

356 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)}