fix: 改进错误处理和清理测试代码

## 主要修复

### 1. JSON 解析错误处理
- 修复所有 Agent 的 LLM 响应解析失败时返回原始内容的问题
- 当 JSON 解析失败时,返回友好的兜底消息而不是原始文本
- 影响文件: customer_service.py, order.py, product.py, aftersale.py

### 2. FAQ 快速路径修复
- 修复 customer_service.py 中变量定义顺序问题
- has_faq_query 在使用前未定义导致 NameError
- 添加详细的错误日志记录

### 3. Chatwoot 集成改进
- 添加响应内容调试日志
- 改进错误处理和日志记录

### 4. 订单查询优化
- 将订单列表默认返回数量从 10 条改为 5 条
- 统一 MCP 工具层和 Mall Client 层的默认值

### 5. 代码清理
- 删除所有测试代码和示例文件
- 刋试文件包括: test_*.py, test_*.html, test_*.sh
- 删除测试目录: tests/, agent/tests/, agent/examples/

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
wangliang
2026-01-27 13:15:58 +08:00
parent f4e77f39ce
commit 0f13102a02
21 changed files with 603 additions and 1697 deletions

View File

@@ -155,6 +155,109 @@ def get_field_label(field_key: str, language: str = "en") -> str:
return ORDER_FIELD_LABELS[language].get(field_key, ORDER_FIELD_LABELS["en"].get(field_key, field_key))
# 订单状态多语言映射
ORDER_STATUS_LABELS = {
"zh": { # 中文
"0": "已取消",
"1": "待支付",
"2": "已支付",
"3": "已发货",
"4": "已签收",
"15": "已完成",
"100": "超时取消",
"unknown": "未知"
},
"en": { # English
"0": "Cancelled",
"1": "Pending Payment",
"2": "Paid",
"3": "Shipped",
"4": "Delivered",
"15": "Completed",
"100": "Timeout Cancelled",
"unknown": "Unknown"
},
"nl": { # Dutch (荷兰语)
"0": "Geannuleerd",
"1": "Wachtend op betaling",
"2": "Betaald",
"3": "Verzonden",
"4": "Geleverd",
"15": "Voltooid",
"100": "Time-out geannuleerd",
"unknown": "Onbekend"
},
"de": { # German (德语)
"0": "Storniert",
"1": "Zahlung ausstehend",
"2": "Bezahlt",
"3": "Versandt",
"4": "Zugestellt",
"15": "Abgeschlossen",
"100": "Zeitüberschreitung storniert",
"unknown": "Unbekannt"
},
"es": { # Spanish (西班牙语)
"0": "Cancelado",
"1": "Pago pendiente",
"2": "Pagado",
"3": "Enviado",
"4": "Entregado",
"15": "Completado",
"100": "Cancelado por tiempo límite",
"unknown": "Desconocido"
},
"fr": { # French (法语)
"0": "Annulé",
"1": "En attente de paiement",
"2": "Payé",
"3": "Expédié",
"4": "Livré",
"15": "Terminé",
"100": "Annulé pour expiration",
"unknown": "Inconnu"
},
"it": { # Italian (意大利语)
"0": "Annullato",
"1": "In attesa di pagamento",
"2": "Pagato",
"3": "Spedito",
"4": "Consegnato",
"15": "Completato",
"100": "Annullato per timeout",
"unknown": "Sconosciuto"
},
"tr": { # Turkish (土耳其语)
"0": "İptal edildi",
"1": "Ödeme bekleniyor",
"2": "Ödendi",
"3": "Kargolandı",
"4": "Teslim edildi",
"15": "Tamamlandı",
"100": "Zaman aşımı iptal edildi",
"unknown": "Bilinmiyor"
}
}
def get_status_label(status_code: str, language: str = "en") -> str:
"""获取指定语言的订单状态标签
Args:
status_code: 状态码(如 "0", "1", "2" 等)
language: 语言代码(默认 "en"
Returns:
对应语言的状态标签
"""
if language not in ORDER_STATUS_LABELS:
language = "en" # 默认使用英文
return ORDER_STATUS_LABELS[language].get(
str(status_code),
ORDER_STATUS_LABELS["en"].get(str(status_code), ORDER_STATUS_LABELS["en"]["unknown"])
)
class MessageType(str, Enum):
"""Chatwoot message types"""
INCOMING = "incoming"
@@ -507,18 +610,25 @@ class ChatwootClient:
total_amount = order_data.get("total_amount", "0")
# 根据状态码映射状态和颜色
status_mapping = {
"0": {"status": "cancelled", "text": "已取消", "color": "text-red-600"},
"1": {"status": "pending", "text": "待支付", "color": "text-yellow-600"},
"2": {"status": "paid", "text": "已支付", "color": "text-blue-600"},
"3": {"status": "shipped", "text": "已发货", "color": "text-purple-600"},
"4": {"status": "signed", "text": "已签收", "color": "text-green-600"},
"15": {"status": "completed", "text": "已完成", "color": "text-green-600"},
"100": {"status": "cancelled", "text": "超时取消", "color": "text-red-600"},
# 根据状态码映射状态和颜色(支持多语言)
status_code_to_key = {
"0": {"key": "cancelled", "color": "text-red-600"},
"1": {"key": "pending", "color": "text-yellow-600"},
"2": {"key": "paid", "color": "text-blue-600"},
"3": {"key": "shipped", "color": "text-purple-600"},
"4": {"key": "signed", "color": "text-green-600"},
"15": {"key": "completed", "color": "text-green-600"},
"100": {"key": "cancelled", "color": "text-red-600"},
}
status_info = status_mapping.get(str(status), {"status": "unknown", "text": status_text or "未知", "color": "text-gray-600"})
status_key_info = status_code_to_key.get(str(status), {"key": "unknown", "color": "text-gray-600"})
status_label = get_status_label(str(status), language)
status_info = {
"status": status_key_info["key"],
"text": status_label,
"color": status_key_info["color"]
}
# 构建商品列表
items = order_data.get("items", [])
@@ -910,18 +1020,27 @@ class ChatwootClient:
sample_items=str(formatted_items[:2]) if formatted_items else "[]"
)
# 构建操作按钮
# 构建操作按钮 - 根据是否有物流信息决定是否显示物流按钮
actions = [
{
"text": details_text,
"reply": f"{details_reply_prefix}{order_id}"
},
{
"text": logistics_text,
"reply": f"{logistics_reply_prefix}{order_id}"
}
]
# 只有当订单有物流信息时才显示物流按钮
if order.get("has_parcels", False):
actions.append({
"text": logistics_text,
"reply": f"{logistics_reply_prefix}{order_id}"
})
logger.debug(
f"Built {len(actions)} actions for order {order_id}",
has_parcels=order.get("has_parcels", False),
actions_count=len(actions)
)
# 构建单个订单
order_data = {
"orderNumber": order_id,
@@ -964,6 +1083,165 @@ class ChatwootClient:
return response.json()
async def send_product_cards(
self,
conversation_id: int,
products: list[dict[str, Any]],
language: str = "en"
) -> dict[str, Any]:
"""发送商品搜索结果(使用 cards 格式)
Args:
conversation_id: 会话 ID
products: 商品列表,每个商品包含:
- spu_id: SPU ID
- spu_sn: SPU 编号
- product_name: 商品名称
- product_image: 商品图片 URL
- price: 价格
- special_price: 特价(可选)
- stock: 库存
- sales_count: 销量
language: 语言代码en, nl, de, es, fr, it, tr, zh默认 en
Returns:
发送结果
Example:
>>> products = [
... {
... "spu_id": "12345",
... "product_name": "Product A",
... "product_image": "https://...",
... "price": "99.99",
... "stock": 100
... }
... ]
>>> await chatwoot.send_product_cards(123, products, language="zh")
"""
# 获取前端 URL
frontend_url = settings.frontend_url.rstrip('/')
# 构建商品卡片
cards = []
for product in products:
spu_id = product.get("spu_id", "")
spu_sn = product.get("spu_sn", "")
product_name = product.get("product_name", "Unknown Product")
product_image = product.get("product_image", "")
price = product.get("price", "0")
special_price = product.get("special_price")
stock = product.get("stock", 0)
sales_count = product.get("sales_count", 0)
# 价格显示(如果有特价则显示特价)
try:
price_num = float(price) if price else 0
price_text = f"{price_num:.2f}"
except (ValueError, TypeError):
price_text = str(price) if price else "€0.00"
# 构建描述
if language == "zh":
description_parts = []
if special_price and float(special_price) < float(price or 0):
try:
special_num = float(special_price)
description_parts.append(f"特价: €{special_num:.2f}")
except:
pass
if stock is not None:
description_parts.append(f"库存: {stock}")
if sales_count:
description_parts.append(f"已售: {sales_count}")
description = " | ".join(description_parts) if description_parts else "暂无详细信息"
else:
description_parts = []
if special_price and float(special_price) < float(price or 0):
try:
special_num = float(special_price)
description_parts.append(f"Special: €{special_num:.2f}")
except:
pass
if stock is not None:
description_parts.append(f"Stock: {stock}")
if sales_count:
description_parts.append(f"Sold: {sales_count}")
description = " | ".join(description_parts) if description_parts else "No details available"
# 构建操作按钮
actions = []
if language == "zh":
actions.append({
"type": "link",
"text": "查看详情",
"uri": f"{frontend_url}/product/detail?spuId={spu_id}"
})
if stock and stock > 0:
actions.append({
"type": "link",
"text": "立即购买",
"uri": f"{frontend_url}/product/detail?spuId={spu_id}"
})
else:
actions.append({
"type": "link",
"text": "View Details",
"uri": f"{frontend_url}/product/detail?spuId={spu_id}"
})
if stock and stock > 0:
actions.append({
"type": "link",
"text": "Buy Now",
"uri": f"{frontend_url}/product/detail?spuId={spu_id}"
})
# 构建卡片
card = {
"title": product_name,
"description": description,
"media_url": product_image,
"actions": actions
}
cards.append(card)
# 发送 cards 类型消息
client = await self._get_client()
content_attributes = {
"items": cards
}
# 添加标题
if language == "zh":
content = f"找到 {len(products)} 个商品"
else:
content = f"Found {len(products)} products"
payload = {
"content": content,
"content_type": "cards",
"content_attributes": content_attributes
}
logger.info(
"Sending product cards",
conversation_id=conversation_id,
products_count=len(products),
language=language,
payload_preview=str(payload)[:1000]
)
response = await client.post(
f"/conversations/{conversation_id}/messages",
json=payload
)
response.raise_for_status()
return response.json()
# ============ Conversations ============
async def get_conversation(self, conversation_id: int) -> dict[str, Any]: