feat: 添加图片搜索功能和 Qwen 模型支持
图片搜索功能(以图搜图): - Chatwoot webhook 检测图片搜索消息 (content_type="search_image") - 从 content_attributes.url 提取图片 URL - 调用 Mall API 图片搜索接口 (/mall/api/spu?searchImageUrl=...) - 支持嵌套和顶层 URL 位置提取 - Product Agent 添加 fast path 直接调用图片搜索工具 - 防止无限循环(使用后清除 context.image_search_url) Qwen 模型支持: - 添加 LLM provider 选择(zhipu/qwen) - 实现 QwenLLMClient 类(基于 DashScope SDK) - 添加 dashscope>=1.14.0 依赖 - 修复 API key 设置(直接设置 dashscope.api_key) - 更新 .env.example 和 docker-compose.yml 配置 其他优化: - 重构 Chatwoot 集成代码(删除冗余) - 优化 Product Agent prompt - 增强 Customer Service Agent 多语言支持 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1089,7 +1089,7 @@ class ChatwootClient:
|
||||
products: list[dict[str, Any]],
|
||||
language: str = "en"
|
||||
) -> dict[str, Any]:
|
||||
"""发送商品搜索结果(使用 cards 格式)
|
||||
"""发送商品搜索结果(使用 product_list 格式)
|
||||
|
||||
Args:
|
||||
conversation_id: 会话 ID
|
||||
@@ -1100,8 +1100,7 @@ class ChatwootClient:
|
||||
- product_image: 商品图片 URL
|
||||
- price: 价格
|
||||
- special_price: 特价(可选)
|
||||
- stock: 库存
|
||||
- sales_count: 销量
|
||||
- href: 商品链接路径(可选)
|
||||
language: 语言代码(en, nl, de, es, fr, it, tr, zh),默认 en
|
||||
|
||||
Returns:
|
||||
@@ -1114,124 +1113,89 @@ class ChatwootClient:
|
||||
... "product_name": "Product A",
|
||||
... "product_image": "https://...",
|
||||
... "price": "99.99",
|
||||
... "stock": 100
|
||||
... "href": "/product/detail/12345"
|
||||
... }
|
||||
... ]
|
||||
>>> await chatwoot.send_product_cards(123, products, language="zh")
|
||||
"""
|
||||
# 获取前端 URL
|
||||
client = await self._get_client()
|
||||
|
||||
# 获取前端域名
|
||||
frontend_url = settings.frontend_url.rstrip('/')
|
||||
|
||||
# 构建商品卡片
|
||||
cards = []
|
||||
# 构建商品列表
|
||||
product_list = []
|
||||
|
||||
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)
|
||||
href = product.get("href", "")
|
||||
|
||||
# 价格显示(如果有特价则显示特价)
|
||||
# 价格显示(如果有特价则显示特价,否则显示原价)
|
||||
try:
|
||||
price_num = float(price) if price else 0
|
||||
price_text = f"€{price_num:.2f}"
|
||||
if special_price and float(special_price) > 0:
|
||||
price_num = float(special_price)
|
||||
else:
|
||||
price_num = float(price) if price else 0
|
||||
|
||||
# 根据语言选择货币符号
|
||||
if language == "zh":
|
||||
price_text = f"¥{price_num:.2f}"
|
||||
else:
|
||||
price_text = f"€{price_num:.2f}"
|
||||
except (ValueError, TypeError):
|
||||
price_text = str(price) if price else "€0.00"
|
||||
price_text = str(price) if price else ("¥0.00" if language == "zh" 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
|
||||
# 构建商品对象
|
||||
product_obj = {
|
||||
"image": product_image,
|
||||
"name": product_name,
|
||||
"price": price_text,
|
||||
"target": "_blank"
|
||||
}
|
||||
|
||||
cards.append(card)
|
||||
# 如果有 href,添加完整 URL
|
||||
if href:
|
||||
product_obj["url"] = f"{frontend_url}{href}"
|
||||
else:
|
||||
# 如果没有 href,使用 spu_id 构建默认链接
|
||||
spu_id = product.get("spu_id", "")
|
||||
if spu_id:
|
||||
product_obj["url"] = f"{frontend_url}/product/detail?spuId={spu_id}"
|
||||
|
||||
# 发送 cards 类型消息
|
||||
client = await self._get_client()
|
||||
product_list.append(product_obj)
|
||||
|
||||
# 构建标题
|
||||
if language == "zh":
|
||||
title = "找到以下商品"
|
||||
else:
|
||||
title = "Found following products"
|
||||
|
||||
# 构建 content_attributes
|
||||
content_attributes = {
|
||||
"items": cards
|
||||
"title": title,
|
||||
"products": product_list,
|
||||
"actions": []
|
||||
}
|
||||
|
||||
# 添加标题
|
||||
if language == "zh":
|
||||
content = f"找到 {len(products)} 个商品"
|
||||
else:
|
||||
content = f"Found {len(products)} products"
|
||||
|
||||
# 发送 product_list 类型消息
|
||||
payload = {
|
||||
"content": content,
|
||||
"content_type": "cards",
|
||||
"content": "",
|
||||
"content_type": "product_list",
|
||||
"message_type": 1,
|
||||
"content_attributes": content_attributes
|
||||
}
|
||||
|
||||
# 输出完整的 payload 用于调试
|
||||
import json as json_module
|
||||
logger.info(
|
||||
"Sending product cards",
|
||||
"Sending product list to Chatwoot",
|
||||
conversation_id=conversation_id,
|
||||
products_count=len(products),
|
||||
products_count=len(product_list),
|
||||
language=language,
|
||||
payload_preview=str(payload)[:1000]
|
||||
payload=json_module.dumps(payload, ensure_ascii=False, indent=2)
|
||||
)
|
||||
|
||||
response = await client.post(
|
||||
|
||||
Reference in New Issue
Block a user