feat: 重构订单和物流信息展示格式

主要改动:
- 订单列表:使用 order_list 格式,展示 5 个订单(全部状态)
- 订单详情:使用 order_detail 格式,优化价格和时间显示
- 物流信息:使用 logistics 格式,根据 track id 动态生成步骤
- 商品图片:从 orderProduct.imageUrl 字段获取
- 时间格式:统一为 YYYY-MM-DD HH:MM:SS
- 多语言支持:amountLabel、orderTime 支持中英文
- 配置管理:新增 FRONTEND_URL 环境变量
- API 集成:改进 Mall API tracks 数据解析
- 认证优化:account_id 从 webhook 动态获取

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
wangliang
2026-01-23 18:49:40 +08:00
parent e8e89601a5
commit 0b5d0a8086
11 changed files with 1493 additions and 394 deletions

View File

@@ -28,6 +28,10 @@ class WebhookSender(BaseModel):
name: Optional[str] = None
email: Optional[str] = None
type: Optional[str] = None # "contact" or "user"
identifier: Optional[str] = None
jwt_token: Optional[str] = None # JWT token at sender root level
mall_token: Optional[str] = None # Mall token at sender root level
custom_attributes: Optional[dict] = None # May also contain tokens
class WebhookMessage(BaseModel):
@@ -156,13 +160,22 @@ async def handle_incoming_message(payload: ChatwootWebhookPayload, cookie_token:
sender_keys=list(payload_dict.get('sender', {}).keys()) if payload_dict.get('sender') else []
)
# 打印完整的 payload 内容用于调试
import json
logger.info(
"Full webhook payload JSON",
payload_json=json.dumps(payload_dict, indent=2, ensure_ascii=False, default=str)
)
# Get account_id from payload (top-level account object)
# Chatwoot webhook includes account info at the top level
account_obj = payload.account
# 从 webhook 中动态获取 account_id
account_id = str(account_obj.get("id")) if account_obj else "1"
# 优先使用 Cookie 中的 token
user_token = cookie_token
mall_token = None
# 如果 Cookie 中没有,尝试从多个来源提取 token
if not user_token:
@@ -173,14 +186,14 @@ async def handle_incoming_message(payload: ChatwootWebhookPayload, cookie_token:
contact_id=contact.id if contact else None,
contact_type=type(contact).__name__
)
# 只有 WebhookContact 才有 custom_attributes
if hasattr(contact, 'custom_attributes'):
logger.info(
"Checking contact custom_attributes",
has_custom_attributes=bool(contact.custom_attributes)
)
custom_attrs = contact.custom_attributes or {}
logger.info(
"Contact custom_attributes",
@@ -188,15 +201,25 @@ async def handle_incoming_message(payload: ChatwootWebhookPayload, cookie_token:
has_jwt_token='jwt_token' in custom_attrs if custom_attrs else False,
has_mall_token='mall_token' in custom_attrs if custom_attrs else False
)
contact_dict = {"custom_attributes": custom_attrs}
user_token = TokenManager.extract_token_from_contact(contact_dict)
logger.debug("Extracted token from contact", has_token=bool(user_token))
# 同时提取 jwt_token 和 mall_token
if custom_attrs.get('jwt_token'):
user_token = custom_attrs.get('jwt_token')
logger.info("JWT token found in contact.custom_attributes", token_prefix=user_token[:20] if user_token else None)
if custom_attrs.get('mall_token'):
mall_token = custom_attrs.get('mall_token')
logger.info("Mall token found in contact.custom_attributes", token_prefix=mall_token[:20] if mall_token else None)
# 如果没有找到 token尝试使用通用字段
if not user_token and not mall_token:
contact_dict = {"custom_attributes": custom_attrs}
user_token = TokenManager.extract_token_from_contact(contact_dict)
logger.debug("Extracted token from contact (generic)", has_token=bool(user_token))
else:
logger.debug("Contact type is WebhookSender, no custom_attributes available")
# 2. 尝试从 conversation.meta.sender.custom_attributes 获取Chatwoot SDK setUser 设置的位置)
if not user_token and conversation:
# 2. 尝试从 conversation.meta.sender 获取Chatwoot SDK setUser 设置的位置)
if (not user_token or not mall_token) and conversation:
logger.debug("Conversation object type", type=str(type(conversation)))
if hasattr(conversation, 'model_dump'):
conv_dict = conversation.model_dump()
@@ -210,11 +233,30 @@ async def handle_incoming_message(payload: ChatwootWebhookPayload, cookie_token:
sender_keys=list(meta_sender.keys()) if meta_sender else [],
has_custom_attributes=bool(meta_sender.get('custom_attributes')) if meta_sender else False
)
if meta_sender.get('custom_attributes'):
# 2.1. 优先从 meta.sender 根级别获取 token
if not user_token and meta_sender.get('jwt_token'):
user_token = meta_sender.get('jwt_token')
logger.info("JWT token found in conversation.meta.sender (root level)", token_prefix=user_token[:20] if user_token else None)
if not mall_token and meta_sender.get('mall_token'):
mall_token = meta_sender.get('mall_token')
logger.info("Mall token found in conversation.meta.sender (root level)", token_prefix=mall_token[:20] if mall_token else None)
# 2.2. 其次从 meta.sender.custom_attributes 获取
if (not user_token or not mall_token) and meta_sender.get('custom_attributes'):
logger.info("Found custom_attributes in meta.sender", keys=list(meta_sender['custom_attributes'].keys()))
user_token = TokenManager.extract_token_from_contact({'custom_attributes': meta_sender['custom_attributes']})
logger.info("Token found in conversation.meta.sender.custom_attributes", token_prefix=user_token[:20] if user_token else None)
custom_attrs = meta_sender['custom_attributes']
if not user_token and custom_attrs.get('jwt_token'):
user_token = custom_attrs.get('jwt_token')
logger.info("JWT token found in conversation.meta.sender.custom_attributes", token_prefix=user_token[:20] if user_token else None)
if not mall_token and custom_attrs.get('mall_token'):
mall_token = custom_attrs.get('mall_token')
logger.info("Mall token found in conversation.meta.sender.custom_attributes", token_prefix=mall_token[:20] if mall_token else None)
# 如果只有 jwt_token将它也用作 mall_token
if user_token and not mall_token:
mall_token = user_token
logger.debug("Using jwt_token as mall_token", token_prefix=mall_token[:20] if mall_token else None)
if user_token:
logger.info(
@@ -236,9 +278,22 @@ async def handle_incoming_message(payload: ChatwootWebhookPayload, cookie_token:
conversation_id=conversation_id,
user_id=user_id,
has_token=bool(user_token),
message_length=len(content)
message_length=len(content),
channel=conversation.channel if conversation else None
)
# 识别消息渠道(邮件、网站等)
message_channel = conversation.channel if conversation else "Channel"
is_email = message_channel == "Email"
# 邮件渠道特殊处理
if is_email:
logger.info(
"Email channel detected",
conversation_id=conversation_id,
sender_email=contact.email if contact else None
)
# Load conversation context from cache
cache = get_cache_manager()
await cache.connect()
@@ -249,6 +304,12 @@ async def handle_incoming_message(payload: ChatwootWebhookPayload, cookie_token:
# Add token to context if available
if user_token:
context["user_token"] = user_token
if mall_token:
context["mall_token"] = mall_token
# 添加渠道信息到 context让 Agent 知道是邮件还是网站)
context["channel"] = message_channel
context["is_email"] = is_email
try:
# Process message through agent workflow
@@ -259,24 +320,26 @@ async def handle_incoming_message(payload: ChatwootWebhookPayload, cookie_token:
message=content,
history=history,
context=context,
user_token=user_token
user_token=user_token,
mall_token=mall_token
)
# Get response
response = final_state.get("response")
if not response:
if response is None:
response = "抱歉,我暂时无法处理您的请求。请稍后重试或联系人工客服。"
# Send response to Chatwoot
# Create client with correct account_id from webhook
# Create Chatwoot client
from integrations.chatwoot import ChatwootClient
chatwoot = ChatwootClient(account_id=int(account_id))
await chatwoot.send_message(
conversation_id=conversation.id,
content=response
)
await chatwoot.close()
# Send response to Chatwoot (skip if empty - agent may have already sent rich content)
if response:
await chatwoot.send_message(
conversation_id=conversation.id,
content=response
)
# Handle human handoff
if final_state.get("requires_human"):
await chatwoot.update_conversation_status(
@@ -288,6 +351,8 @@ async def handle_incoming_message(payload: ChatwootWebhookPayload, cookie_token:
conversation_id=conversation.id,
labels=["needs_human", final_state.get("intent", "unknown")]
)
await chatwoot.close()
# Update cache
await cache.add_message(conversation_id, "user", content)
@@ -313,12 +378,12 @@ async def handle_incoming_message(payload: ChatwootWebhookPayload, cookie_token:
)
# Send error response
chatwoot = get_chatwoot_client()
chatwoot = get_chatwoot_client(account_id=int(account_id))
await chatwoot.send_message(
conversation_id=conversation.id,
content="抱歉,处理您的消息时遇到了问题。我们的客服团队将尽快为您服务。"
)
# Transfer to human
await chatwoot.update_conversation_status(
conversation_id=conversation.id,
@@ -328,30 +393,36 @@ async def handle_incoming_message(payload: ChatwootWebhookPayload, cookie_token:
async def handle_conversation_created(payload: ChatwootWebhookPayload) -> None:
"""Handle new conversation created
Args:
payload: Webhook payload
"""
conversation = payload.conversation
if not conversation:
return
conversation_id = str(conversation.id)
# Get account_id from payload
account_obj = payload.account
# 从 webhook 中动态获取 account_id
account_id = str(account_obj.get("id")) if account_obj else "1"
logger.info(
"New conversation created",
conversation_id=conversation_id
conversation_id=conversation_id,
account_id=account_id
)
# Initialize conversation context
cache = get_cache_manager()
await cache.connect()
context = {
"created": True,
"inbox_id": conversation.inbox_id
}
# Add contact info to context
contact = payload.contact
if contact:
@@ -359,15 +430,24 @@ async def handle_conversation_created(payload: ChatwootWebhookPayload) -> None:
context["contact_email"] = contact.email
if contact.custom_attributes:
context.update(contact.custom_attributes)
await cache.set_context(conversation_id, context)
# Send welcome message
chatwoot = get_chatwoot_client()
await chatwoot.send_message(
conversation_id=conversation.id,
content="您好!我是 AI 智能助手,很高兴为您服务。请问有什么可以帮您的?\n\n您可以询问我关于订单、商品、售后等问题,我会尽力为您解答。"
)
# 检查是否是邮件渠道
is_email = conversation.channel == "Email" if conversation else False
# 只对非邮件渠道发送欢迎消息
if not is_email:
chatwoot = get_chatwoot_client(account_id=int(account_id))
await chatwoot.send_message(
conversation_id=conversation.id,
content="您好!我是 AI 智能助手,很高兴为您服务。请问有什么可以帮您的?\n\n您可以询问我关于订单、商品、售后等问题,我会尽力为您解答。"
)
else:
logger.info(
"Email channel detected, skipping welcome message",
conversation_id=conversation_id
)
async def handle_conversation_status_changed(payload: ChatwootWebhookPayload) -> None: