feat: 修复订单查询和物流查询功能

主要修改:

1. 订单数据解析修复 (agent/agents/order.py)
   - 修复 Mall API 返回数据的嵌套结构解析
   - 更新字段映射:orderId→order_id, orderProduct→items, statusText→status_text
   - 支持多种商品图片字段:image, pic, thumb, productImg
   - 添加详细的调试日志

2. 物流查询修复 (mcp_servers/order_mcp/server.py)
   - 修复物流接口返回数据结构解析 (data[].trackingCode→tracking_number)
   - 添加 print() 日志用于调试
   - 支持多种字段名映射

3. Chatwoot 集成优化 (agent/integrations/chatwoot.py)
   - 添加 json 模块导入
   - 完善订单卡片和表单展示功能

4. API 请求头优化 (mcp_servers/shared/mall_client.py)
   - 更新 User-Agent 和 Accept 头
   - 修正 Origin 和 Referer 大小写

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
wangliang
2026-01-20 19:10:21 +08:00
parent 6b6172d8f0
commit e8e89601a5
5 changed files with 1098 additions and 73 deletions

View File

@@ -7,6 +7,7 @@ from typing import Any
from core.state import AgentState, ConversationState, add_tool_call, set_response, update_context
from core.llm import get_llm_client, Message
from utils.logger import get_logger
from integrations.chatwoot import ChatwootClient
logger = get_logger(__name__)
@@ -361,23 +362,271 @@ async def order_agent(state: AgentState) -> AgentState:
async def _generate_order_response(state: AgentState) -> AgentState:
"""Generate response based on order tool results"""
# 解析订单数据并尝试使用 form 格式发送
order_data = None
user_message = ""
logistics_data = None
for result in state["tool_results"]:
if result["success"]:
data = result["data"]
tool_name = result["tool_name"]
# 提取订单ID到上下文
if isinstance(data, dict):
if data.get("order_id"):
state = update_context(state, {"order_id": data["order_id"]})
elif data.get("orders") and len(data["orders"]) > 0:
state = update_context(state, {"order_id": data["orders"][0].get("order_id")})
# 处理 get_mall_order 返回的订单数据
if tool_name == "get_mall_order" and isinstance(data, dict):
# MCP 返回结构: {"success": true, "result": {...}}
# result 可能包含: {"success": bool, "order": {...}, "order_id": "...", "error": "..."}
mcp_result = data.get("result", {})
# 检查是否有错误(如未登录)
if mcp_result.get("error") or not mcp_result.get("success"):
logger.warning(
"get_mall_order returned error",
error=mcp_result.get("error"),
require_login=mcp_result.get("require_login")
)
# 设置错误消息到状态中
if mcp_result.get("require_login"):
user_message = mcp_result.get("error", "请先登录账户以查询订单信息")
elif mcp_result.get("order"):
# 有订单数据
order_data = _parse_mall_order_data(mcp_result["order"])
# 如果 order_data 中没有 order_id从外层获取
if not order_data.get("order_id") and mcp_result.get("order_id"):
order_data["order_id"] = mcp_result["order_id"]
else:
logger.warning(
"get_mall_order returned success but no order data",
data_keys=list(data.keys()),
result_keys=list(mcp_result.keys()) if isinstance(mcp_result, dict) else None
)
# 处理 query_order 返回的订单数据
elif tool_name == "query_order" and isinstance(data, dict):
if data.get("orders") and len(data["orders"]) > 0:
order_data = _parse_order_data(data["orders"][0])
if len(data["orders"]) > 1:
user_message = f"找到 {len(data['orders'])} 个订单,显示最新的一个:"
# 处理 get_logistics 返回的物流数据
elif tool_name == "get_logistics" and isinstance(data, dict):
logistics_data = _parse_logistics_data(data)
# 如果之前有订单数据,添加物流信息
if order_data:
order_data["logistics"] = logistics_data
# 尝试使用 Chatwoot cards 格式发送
if order_data:
try:
# 检查是否有有效的 order_id
if not order_data.get("order_id"):
logger.warning(
"No valid order_id in order_data, falling back to text response",
order_data=order_data
)
return await _generate_text_response(state)
chatwoot = ChatwootClient()
conversation_id = state.get("conversation_id")
if conversation_id:
# 记录订单数据(用于调试)
logger.info(
"Preparing to send order card",
conversation_id=conversation_id,
order_id=order_data.get("order_id"),
items_count=len(order_data.get("items", []))
)
# 发送订单卡片(使用默认的"查看订单详情"按钮)
await chatwoot.send_order_card(
conversation_id=conversation_id,
order_data=order_data
)
logger.info(
"Order card sent successfully",
conversation_id=conversation_id,
order_id=order_data.get("order_id")
)
# 设置确认消息
response_text = user_message or "订单详情如下"
state = set_response(state, response_text)
state["state"] = ConversationState.GENERATING.value
return state
except Exception as e:
logger.error(
"Failed to send order card, falling back to text response",
error=str(e),
order_id=order_data.get("order_id")
)
# 降级处理:使用原来的 LLM 生成逻辑
return await _generate_text_response(state)
def _parse_mall_order_data(data: dict[str, Any]) -> dict[str, Any]:
"""解析商城 API 返回的订单数据"""
# 记录原始数据结构(用于调试)
logger.info(
"Parsing mall order data",
data_keys=list(data.keys()),
has_order_id=bool(data.get("order_id")),
has_order_sn=bool(data.get("order_sn")),
has_nested_order=bool(data.get("order")),
order_id_preview=data.get("order_id", data.get("order_sn", "")),
# 如果有 order 字段,记录其内容类型和键
nested_order_type=type(data.get("order")).__name__ if data.get("order") else None,
nested_order_keys=list(data.get("order", {}).keys()) if isinstance(data.get("order"), dict) else None
)
# Mall API 返回结构:外层包含 userId, reqContext 等,实际的订单数据在 order 字段中
# 如果有嵌套的 order 字段,提取出来
actual_order_data = data.get("order", data) if data.get("order") else data
# 记录提取的订单数据结构(用于调试)
logger.info(
"Extracted order data structure",
actual_order_keys=list(actual_order_data.keys()) if isinstance(actual_order_data, dict) else type(actual_order_data).__name__,
has_items=bool(actual_order_data.get("items")),
has_order_items=bool(actual_order_data.get("order_items")),
has_products=bool(actual_order_data.get("products")),
has_orderProduct=bool(actual_order_data.get("orderProduct")),
has_orderGoods=bool(actual_order_data.get("orderGoods")),
has_goods=bool(actual_order_data.get("goods"))
)
order_data = {
"order_id": actual_order_data.get("orderId", actual_order_data.get("order_id", actual_order_data.get("order_sn", ""))),
"status": actual_order_data.get("orderStatusId", actual_order_data.get("status", "unknown")),
"status_text": actual_order_data.get("statusText", actual_order_data.get("status_text", actual_order_data.get("status", ""))),
"total_amount": actual_order_data.get("total", actual_order_data.get("total_amount", actual_order_data.get("order_amount", "0.00"))),
"shipping_fee": actual_order_data.get("shipping_fee", actual_order_data.get("freight_amount", "0")),
}
# 下单时间
if actual_order_data.get("created_at"):
order_data["created_at"] = actual_order_data["created_at"]
elif actual_order_data.get("add_time"):
order_data["created_at"] = actual_order_data["add_time"]
elif actual_order_data.get("dateAdded"):
order_data["created_at"] = actual_order_data["dateAdded"]
# 商品列表 - 尝试多种可能的字段名(优先 orderProduct
items = (
actual_order_data.get("orderProduct") or
actual_order_data.get("items") or
actual_order_data.get("order_items") or
actual_order_data.get("products") or
actual_order_data.get("orderGoods") or
actual_order_data.get("goods") or
[]
)
# 记录商品列表数据结构(用于调试)
if items and len(items) > 0:
first_item = items[0]
logger.info(
"First item structure",
first_item_keys=list(first_item.keys()) if isinstance(first_item, dict) else type(first_item).__name__,
has_image_url=bool(first_item.get("image_url")) if isinstance(first_item, dict) else False,
has_image=bool(first_item.get("image")) if isinstance(first_item, dict) else False,
has_pic=bool(first_item.get("pic")) if isinstance(first_item, dict) else False,
sample_item_data=str(first_item)[:500] if isinstance(first_item, dict) else str(first_item)
)
if items:
order_data["items"] = []
for item in items:
item_data = {
"name": item.get("name", item.get("productName", item.get("product_name", "未知商品"))),
"quantity": item.get("quantity", item.get("num", item.get("productNum", 1))),
"price": item.get("price", item.get("total", item.get("productPrice", item.get("product_price", "0.00"))))
}
# 添加商品图片(支持多种可能的字段名)
image_url = (
item.get("image") or
item.get("image_url") or
item.get("pic") or
item.get("thumb") or
item.get("product_image") or
item.get("pic_url") or
item.get("thumb_url") or
item.get("img") or
item.get("productImg") or
item.get("thumb")
)
if image_url:
item_data["image_url"] = image_url
else:
# 记录没有图片的商品(用于调试)
logger.debug(
"No image found for product",
product_name=item_data.get("name"),
available_keys=list(item.keys())
)
order_data["items"].append(item_data)
# 备注
if actual_order_data.get("remark") or actual_order_data.get("user_remark"):
order_data["remark"] = actual_order_data.get("remark", actual_order_data.get("user_remark", ""))
return order_data
def _parse_order_data(data: dict[str, Any]) -> dict[str, Any]:
"""解析历史订单数据"""
return _parse_mall_order_data(data)
def _parse_logistics_data(data: dict[str, Any]) -> dict[str, Any]:
"""解析物流数据"""
# MCP 返回结构: {"success": true, "result": {...物流数据...}}
mcp_result = data.get("result", data) if data.get("result") else data
logger.info(
"Parsing logistics data",
data_keys=list(data.keys()) if isinstance(data, dict) else None,
has_result_in_data=bool(data.get("result")),
mcp_result_keys=list(mcp_result.keys()) if isinstance(mcp_result, dict) else None,
raw_tracking_number_value=repr(mcp_result.get("tracking_number")) if mcp_result.get("tracking_number") is not None else None,
raw_courier_value=repr(mcp_result.get("courier")) if mcp_result.get("courier") is not None else None,
has_tracking_number=bool(mcp_result.get("tracking_number")),
has_courier=bool(mcp_result.get("courier")),
has_timeline=bool(mcp_result.get("timeline"))
)
return {
"carrier": mcp_result.get("courier", mcp_result.get("carrier", mcp_result.get("express_name", "未知"))),
"tracking_number": mcp_result.get("tracking_number") or "",
"status": mcp_result.get("status"),
"estimated_delivery": mcp_result.get("estimatedDelivery"),
"timeline": mcp_result.get("timeline", [])
}
async def _generate_text_response(state: AgentState) -> AgentState:
"""生成纯文本回复(降级方案)"""
# Build context from tool results
tool_context = []
for result in state["tool_results"]:
if result["success"]:
data = result["data"]
tool_context.append(f"工具 {result['tool_name']} 返回:\n{json.dumps(data, ensure_ascii=False, indent=2)}")
# Extract order_id for context
if isinstance(data, dict):
if data.get("order_id"):
state = update_context(state, {"order_id": data["order_id"]})
elif data.get("orders") and len(data["orders"]) > 0:
state = update_context(state, {"order_id": data["orders"][0].get("order_id")})
else:
tool_context.append(f"工具 {result['tool_name']} 执行失败: {result['error']}")
prompt = f"""基于以下订单系统返回的信息,生成对用户的回复。
用户问题: {state["current_message"]}
@@ -388,18 +637,18 @@ async def _generate_order_response(state: AgentState) -> AgentState:
请生成一个清晰、友好的回复,包含订单的关键信息(订单号、状态、金额、物流等)。
如果是物流信息,请按时间线整理展示。
只返回回复内容,不要返回 JSON。"""
messages = [
Message(role="system", content="你是一个专业的订单客服助手,请根据系统返回的信息回答用户的订单问题。"),
Message(role="user", content=prompt)
]
try:
llm = get_llm_client()
response = await llm.chat(messages, temperature=0.7)
state = set_response(state, response.content)
return state
except Exception as e:
logger.error("Order response generation failed", error=str(e))
state = set_response(state, "抱歉,处理订单信息时遇到问题。请稍后重试或联系人工客服。")

View File

@@ -1,6 +1,7 @@
"""
Chatwoot API Client for B2B Shopping AI Assistant
"""
import json
from typing import Any, Optional
from dataclasses import dataclass
from enum import Enum
@@ -172,9 +173,122 @@ class ChatwootClient:
self,
conversation_id: int,
order_data: dict[str, Any],
actions: list[dict[str, Any]]
actions: Optional[list[dict[str, Any]]] = None
) -> dict[str, Any]:
"""发送订单卡片消息(Markdown 文本 + 操作按钮
"""发送订单卡片消息(使用 Chatwoot cards 格式
卡片结构:
- 图片:第一件商品的图片
- 标题:订单号 + 状态
- 描述:汇总信息
- 按钮:跳转到订单详情页面
Args:
conversation_id: 会话 ID
order_data: 订单数据,包含:
- order_id: 订单号
- status: 订单状态
- status_text: 状态文本
- items: 商品列表
- total_amount: 总金额
actions: 操作按钮配置列表(可选)
Returns:
发送结果
Example:
>>> order_data = {
... "order_id": "202071324",
... "status_text": "已发货",
... "items": [{"name": "商品A", "quantity": 2}],
... "total_amount": "599.00"
... }
>>> await chatwoot.send_order_card(123, order_data)
"""
order_id = order_data.get("order_id", "")
status_text = order_data.get("status_text", order_data.get("status", ""))
# 获取第一件商品的图片
items = order_data.get("items", [])
media_url = None
if items and len(items) > 0:
first_item = items[0]
media_url = first_item.get("image_url")
# 构建标题
title = f"订单 #{order_id} {status_text}"
# 构建描述
if items and len(items) > 0:
if len(items) == 1:
items_desc = items[0].get("name", "商品")
else:
items_desc = f"{items[0].get('name', '商品A')} 等共计 {len(items)} 件商品"
description = f"包含 {items_desc},实付 ¥{order_data.get('total_amount', '0.00')}"
else:
description = f"实付 ¥{order_data.get('total_amount', '0.00')}"
# 构建操作按钮
card_actions = []
if actions:
card_actions = actions
else:
# 默认按钮:跳转到订单详情页面
card_actions = [
{
"type": "link",
"text": "查看订单详情",
"uri": f"https://www.qa1.gaia888.com/customer/order/detail?orderId={order_id}"
}
]
# 构建单个卡片
card = {
"title": title,
"description": description,
"actions": card_actions
}
# 如果有图片,添加 media_url
if media_url:
card["media_url"] = media_url
# 构建 content_attributes
content_attributes = {
"items": [card]
}
# 记录发送的数据(用于调试)
logger.info(
"Sending order card",
conversation_id=conversation_id,
order_id=order_id,
has_media=bool(media_url),
payload_preview=json.dumps({
"content": "订单详情",
"content_type": "cards",
"content_attributes": content_attributes
}, ensure_ascii=False, indent=2)[:1000]
)
# 发送富媒体消息
return await self.send_rich_message(
conversation_id=conversation_id,
content="订单详情",
content_type="cards",
content_attributes=content_attributes
)
async def send_order_form(
self,
conversation_id: int,
order_data: dict[str, Any],
actions: Optional[list[dict[str, Any]]] = None
) -> dict[str, Any]:
"""发送订单详情表单消息(使用 content_type=form
根据 Chatwoot API 文档实现的 form 格式订单详情展示。
form 类型支持的字段类型text, text_area, email, select
Args:
conversation_id: 会话 ID
@@ -188,11 +302,9 @@ class ChatwootClient:
- shipping_fee: 运费(可选)
- logistics: 物流信息(可选)
- remark: 备注(可选)
actions: 操作按钮配置列表,每个按钮包含:
- type: "link""postback"
- text: 按钮文字
- uri: 链接地址type=link 时必需)
- payload: 回传数据type=postback 时必需)
actions: 操作按钮配置列表(可选),每个按钮包含:
- label: 按钮文字(用于 select 选项的显示)
- value: 按钮值(用于 select 选项的值)
Returns:
发送结果
@@ -202,27 +314,127 @@ class ChatwootClient:
... "order_id": "123456789",
... "status": "shipped",
... "status_text": "已发货",
... "created_at": "2023-10-27 14:30",
... "total_amount": "1058.00",
... "items": [...]
... "items": [{"name": "商品A", "quantity": 2, "price": "100.00"}]
... }
>>> actions = [
... {"type": "link", "text": "查看详情", "uri": "https://..."},
... {"type": "postback", "text": "联系客服", "payload": "CONTACT_SUPPORT"}
... {"label": "查看详情", "value": "VIEW_DETAILS"},
... {"label": "联系客服", "value": "CONTACT_SUPPORT"}
... ]
>>> await chatwoot.send_order_card(123, order_data, actions)
>>> await chatwoot.send_order_form(123, order_data, actions)
"""
# 生成 Markdown 内容
markdown_content = format_order_card_markdown(order_data)
# 构建表单字段
form_items = []
# 生成按钮卡片
buttons = create_action_buttons(actions)
# 订单号(只读文本)
form_items.append({
"name": "order_id",
"label": "订单号",
"type": "text",
"placeholder": "订单号",
"default": order_data.get("order_id", "")
})
# 发送富媒体消息
# 订单状态(只读文本)
status_text = order_data.get("status_text", order_data.get("status", "unknown"))
form_items.append({
"name": "status",
"label": "订单状态",
"type": "text",
"placeholder": "订单状态",
"default": status_text
})
# 下单时间(只读文本)
if order_data.get("created_at"):
form_items.append({
"name": "created_at",
"label": "下单时间",
"type": "text",
"placeholder": "下单时间",
"default": order_data["created_at"]
})
# 商品列表(多行文本)
items = order_data.get("items", [])
if items:
items_text = "\n".join([
f"▫️ {item.get('name', '未知商品')} × {item.get('quantity', 1)} - ¥{item.get('price', '0.00')}"
for item in items
])
form_items.append({
"name": "items",
"label": "商品详情",
"type": "text_area",
"placeholder": "商品列表",
"default": items_text
})
# 总金额(只读文本)
form_items.append({
"name": "total_amount",
"label": "总金额",
"type": "text",
"placeholder": "总金额",
"default": f"¥{order_data.get('total_amount', '0.00')}"
})
# 运费(只读文本)
if order_data.get("shipping_fee") is not None:
form_items.append({
"name": "shipping_fee",
"label": "运费",
"type": "text",
"placeholder": "运费",
"default": f"¥{order_data['shipping_fee']}"
})
# 物流信息(多行文本)
logistics = order_data.get("logistics")
if logistics:
logistics_text = (
f"承运商: {logistics.get('carrier', '未知')}\n"
f"单号: {logistics.get('tracking_number', '未知')}"
)
form_items.append({
"name": "logistics",
"label": "物流信息",
"type": "text_area",
"placeholder": "物流信息",
"default": logistics_text
})
# 备注(多行文本)
if order_data.get("remark"):
form_items.append({
"name": "remark",
"label": "备注",
"type": "text_area",
"placeholder": "备注",
"default": order_data["remark"]
})
# 操作选项(下拉选择,如果提供了 actions
if actions:
form_items.append({
"name": "action_select",
"label": "操作",
"type": "select",
"options": actions
})
# 构建 content_attributes
content_attributes = {
"items": form_items
}
# 发送 form 类型消息
return await self.send_rich_message(
conversation_id=conversation_id,
content=markdown_content,
content_type="cards",
content_attributes=buttons
content="订单详情",
content_type="form",
content_attributes=content_attributes
)
# ============ Conversations ============