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:
wangliang
2026-01-27 19:10:06 +08:00
parent 754804219f
commit 965b11316e
12 changed files with 937 additions and 199 deletions

View File

@@ -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(