Compare commits
12 Commits
2dd46a8626
...
754804219f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
754804219f | ||
|
|
c8f26b6f9f | ||
|
|
0f13102a02 | ||
|
|
f4e77f39ce | ||
|
|
54eefba6f8 | ||
|
|
74c28eb838 | ||
|
|
ad7f30d54c | ||
|
|
7b676f8015 | ||
|
|
1aeb17fcce | ||
|
|
e58c3f0caf | ||
|
|
15a4bdeb75 | ||
|
|
fa2c8f8102 |
81
.env.production.example
Normal file
81
.env.production.example
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
# B2B Shopping AI Assistant - Production Environment Variables
|
||||||
|
# 生产环境配置示例 - 请复制为 .env.production 并填写真实值
|
||||||
|
|
||||||
|
# ============ AI Model ============
|
||||||
|
# 智谱 AI API Key
|
||||||
|
ZHIPU_API_KEY=your_zhipu_api_key_here
|
||||||
|
# 模型名称
|
||||||
|
ZHIPU_MODEL=GLM-4-Flash-250414
|
||||||
|
# 推理模式(开启会消耗更多 token,但更智能)
|
||||||
|
ENABLE_REASONING_MODE=false
|
||||||
|
# 复杂查询启用推理模式
|
||||||
|
REASONING_MODE_FOR_COMPLEX=true
|
||||||
|
|
||||||
|
# ============ Redis ============
|
||||||
|
# Redis 密码(必须设置强密码)
|
||||||
|
REDIS_PASSWORD=your_secure_redis_password_here
|
||||||
|
# Redis 主机
|
||||||
|
REDIS_HOST=redis
|
||||||
|
# Redis 端口
|
||||||
|
REDIS_PORT=6379
|
||||||
|
# Redis 数据库编号
|
||||||
|
REDIS_DB=0
|
||||||
|
|
||||||
|
# ============ Chatwoot (生产环境) ============
|
||||||
|
# Chatwoot API URL(生产环境地址)
|
||||||
|
CHATWOOT_API_URL=https://chatwoot.yourdomain.com
|
||||||
|
# Chatwoot API Token(从 Chatwoot 后台生成)
|
||||||
|
CHATWOOT_API_TOKEN=your_chatwoot_api_token_here
|
||||||
|
# Chatwoot Webhook Secret(配置在 Chatwoot webhook 设置中)
|
||||||
|
CHATWOOT_WEBHOOK_SECRET=your_webhook_secret_here
|
||||||
|
# Chatwoot Account ID
|
||||||
|
CHATWOOT_ACCOUNT_ID=1
|
||||||
|
|
||||||
|
# ============ Strapi CMS (FAQ/Knowledge Base) ============
|
||||||
|
# Strapi API URL
|
||||||
|
STRAPI_API_URL=https://cms.yourdomain.com
|
||||||
|
# Strapi API Token
|
||||||
|
STRAPI_API_TOKEN=your_strapi_api_token_here
|
||||||
|
|
||||||
|
# ============ Hyperf PHP API ============
|
||||||
|
# Hyperf API URL(生产环境)
|
||||||
|
HYPERF_API_URL=https://api.gaia888.com
|
||||||
|
# Hyperf API Token
|
||||||
|
HYPERF_API_TOKEN=your_hyperf_api_token_here
|
||||||
|
|
||||||
|
# ============ Mall API ============
|
||||||
|
# Mall API URL(生产环境)
|
||||||
|
MALL_API_URL=https://apicn.gaia888.com
|
||||||
|
# 租户 ID
|
||||||
|
MALL_TENANT_ID=2
|
||||||
|
# 货币代码
|
||||||
|
MALL_CURRENCY_CODE=EUR
|
||||||
|
# 语言 ID
|
||||||
|
MALL_LANGUAGE_ID=1
|
||||||
|
# 来源域名
|
||||||
|
MALL_SOURCE=www.gaia888.com
|
||||||
|
|
||||||
|
# ============ Frontend URLs ============
|
||||||
|
# 前端 URL(用于生成订单详情链接)
|
||||||
|
FRONTEND_URL=https://www.gaia888.com
|
||||||
|
|
||||||
|
# ============ Monitoring ============
|
||||||
|
# Sentry DSN(错误监控,可选)
|
||||||
|
SENTRY_DSN=
|
||||||
|
# Grafana 管理员密码
|
||||||
|
GRAFANA_ADMIN_PASSWORD=your_grafana_password_here
|
||||||
|
|
||||||
|
# ============ Application Config ============
|
||||||
|
# 日志级别(WARNING 或 ERROR)
|
||||||
|
LOG_LEVEL=WARNING
|
||||||
|
# 最大对话步数
|
||||||
|
MAX_CONVERSATION_STEPS=10
|
||||||
|
# 对话超时时间(秒)
|
||||||
|
CONVERSATION_TIMEOUT=3600
|
||||||
|
|
||||||
|
# ============ Security Notes ============
|
||||||
|
# 1. 所有密钥和密码都必须使用强密码
|
||||||
|
# 2. 生产环境不要使用默认密码
|
||||||
|
# 3. 定期轮换 API Token 和密钥
|
||||||
|
# 4. 使用环境变量管理工具(如 AWS Secrets Manager、Vault 等)
|
||||||
|
# 5. 不要将 .env.production 文件提交到 Git 仓库
|
||||||
381
DEPLOYMENT.md
Normal file
381
DEPLOYMENT.md
Normal file
@@ -0,0 +1,381 @@
|
|||||||
|
# B2B Shopping AI Assistant - 生产环境部署指南
|
||||||
|
|
||||||
|
## 📋 目录
|
||||||
|
|
||||||
|
- [部署前准备](#部署前准备)
|
||||||
|
- [快速部署](#快速部署)
|
||||||
|
- [详细配置](#详细配置)
|
||||||
|
- [监控与维护](#监控与维护)
|
||||||
|
- [故障排查](#故障排查)
|
||||||
|
- [升级策略](#升级策略)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 部署前准备
|
||||||
|
|
||||||
|
### 系统要求
|
||||||
|
|
||||||
|
- **操作系统**: Linux (推荐 Ubuntu 20.04+)
|
||||||
|
- **Docker**: 20.10+
|
||||||
|
- **Docker Compose**: 2.0+
|
||||||
|
- **CPU**: 4 核心以上
|
||||||
|
- **内存**: 8GB 以上
|
||||||
|
- **磁盘**: 50GB 以上
|
||||||
|
|
||||||
|
### 网络要求
|
||||||
|
|
||||||
|
- **开放端口**:
|
||||||
|
- `8000`: Agent 服务
|
||||||
|
- `8001-8004`: MCP 服务
|
||||||
|
- `3000-3001`: Chatwoot (如果部署在同一服务器)
|
||||||
|
- `9090`: Prometheus (可选)
|
||||||
|
- `3001`: Grafana (可选)
|
||||||
|
|
||||||
|
### 外部依赖
|
||||||
|
|
||||||
|
1. **Chatwoot**: 需要提前部署并配置好
|
||||||
|
2. **Strapi CMS**: 用于 FAQ 和知识库管理
|
||||||
|
3. **Hyperf API**: 后端业务 API
|
||||||
|
4. **Redis**: 使用 Docker Compose 内置,或外部 Redis 实例
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚡ 快速部署
|
||||||
|
|
||||||
|
### 1. 准备配置文件
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 复制环境变量模板
|
||||||
|
cp .env.production.example .env.production
|
||||||
|
|
||||||
|
# 编辑配置文件
|
||||||
|
vim .env.production
|
||||||
|
```
|
||||||
|
|
||||||
|
**必须配置的关键参数**:
|
||||||
|
|
||||||
|
```env
|
||||||
|
# AI 模型
|
||||||
|
ZHIPU_API_KEY=your_actual_api_key
|
||||||
|
|
||||||
|
# Redis 密码(必须修改)
|
||||||
|
REDIS_PASSWORD=your_strong_password_here
|
||||||
|
|
||||||
|
# Chatwoot
|
||||||
|
CHATWOOT_API_URL=https://your-chatwoot.com
|
||||||
|
CHATWOOT_API_TOKEN=your_chatwoot_token
|
||||||
|
CHATWOOT_WEBHOOK_SECRET=your_webhook_secret
|
||||||
|
|
||||||
|
# API 地址
|
||||||
|
HYPERF_API_URL=https://api.yourdomain.com
|
||||||
|
MALL_API_URL=https://apicn.yourdomain.com
|
||||||
|
STRAPI_API_URL=https://cms.yourdomain.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 执行部署
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 使用部署脚本(推荐)
|
||||||
|
./scripts/deploy-production.sh
|
||||||
|
|
||||||
|
# 或手动部署
|
||||||
|
docker-compose -f docker-compose.prod.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 验证部署
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检查服务状态
|
||||||
|
docker-compose -f docker-compose.prod.yml ps
|
||||||
|
|
||||||
|
# 健康检查
|
||||||
|
curl http://localhost:8000/health
|
||||||
|
|
||||||
|
# 查看日志
|
||||||
|
docker-compose -f docker-compose.prod.yml logs -f agent
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 详细配置
|
||||||
|
|
||||||
|
### 生产环境与开发环境差异
|
||||||
|
|
||||||
|
| 配置项 | 开发环境 | 生产环境 |
|
||||||
|
|--------|---------|---------|
|
||||||
|
| 日志级别 | INFO | WARNING |
|
||||||
|
| 代码挂载 | 是(支持热更新) | 否(使用镜像) |
|
||||||
|
| 资源限制 | 无 | 有限制 |
|
||||||
|
| 健康检查 | 基础 | 完整 |
|
||||||
|
| 日志轮转 | 无 | 有(10MB x 3) |
|
||||||
|
| 重启策略 | unless-stopped | always |
|
||||||
|
| Redis 密码 | 无 | 必须 |
|
||||||
|
|
||||||
|
### 资源配置
|
||||||
|
|
||||||
|
生产环境默认资源配置(可在 `docker-compose.prod.yml` 中调整):
|
||||||
|
|
||||||
|
| 服务 | CPU 限制 | 内存限制 | CPU 预留 | 内存预留 |
|
||||||
|
|------|---------|---------|---------|---------|
|
||||||
|
| Agent | 2 核 | 2GB | 0.5 核 | 512MB |
|
||||||
|
| MCP 服务 | 0.5 核 | 512MB | 0.25 核 | 256MB |
|
||||||
|
|
||||||
|
### 健康检查
|
||||||
|
|
||||||
|
所有服务都配置了健康检查:
|
||||||
|
|
||||||
|
- **Agent**: 每 30 秒检查 `/health` 端点
|
||||||
|
- **MCP 服务**: 每 30 秒检查 `/health` 端点
|
||||||
|
- **Redis**: 每 10 秒 ping 检查
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 监控与维护
|
||||||
|
|
||||||
|
### 启用监控(可选)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 启动 Prometheus + Grafana
|
||||||
|
docker-compose -f docker-compose.prod.yml --profile monitoring up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
监控服务地址:
|
||||||
|
- **Prometheus**: http://localhost:9090
|
||||||
|
- **Grafana**: http://localhost:3001 (默认用户名/密码: admin/admin)
|
||||||
|
|
||||||
|
### 日志管理
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 查看实时日志
|
||||||
|
docker-compose -f docker-compose.prod.yml logs -f
|
||||||
|
|
||||||
|
# 查看特定服务日志
|
||||||
|
docker-compose -f docker-compose.prod.yml logs -f agent
|
||||||
|
|
||||||
|
# 查看最近 100 行日志
|
||||||
|
docker-compose -f docker-compose.prod.yml logs --tail=100 agent
|
||||||
|
|
||||||
|
# 导出日志
|
||||||
|
docker-compose -f docker-compose.prod.yml logs agent > agent.log
|
||||||
|
```
|
||||||
|
|
||||||
|
日志文件位置:
|
||||||
|
- **Agent 日志**: `agent_logs_prod` volume
|
||||||
|
- **日志轮转**: 每个日志文件最大 10MB,保留 3 个文件
|
||||||
|
|
||||||
|
### 数据备份
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 备份 Redis 数据
|
||||||
|
docker run --rm \
|
||||||
|
-v ai_redis_data_prod:/data \
|
||||||
|
-v $(pwd)/backups:/backup \
|
||||||
|
alpine tar czf /backup/redis-$(date +%Y%m%d).tar.gz /data
|
||||||
|
|
||||||
|
# 备份 Grafana 配置
|
||||||
|
docker run --rm \
|
||||||
|
-v ai_grafana_data:/data \
|
||||||
|
-v $(pwd)/backups:/backup \
|
||||||
|
alpine tar czf /backup/grafana-$(date +%Y%m%d).tar.gz /data
|
||||||
|
```
|
||||||
|
|
||||||
|
### 性能监控
|
||||||
|
|
||||||
|
使用 Docker 命令监控资源使用:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 查看容器资源使用情况
|
||||||
|
docker stats
|
||||||
|
|
||||||
|
# 查看特定容器
|
||||||
|
docker stats ai_agent_prod
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔍 故障排查
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
|
||||||
|
#### 1. 服务无法启动
|
||||||
|
|
||||||
|
**检查日志**:
|
||||||
|
```bash
|
||||||
|
docker-compose -f docker-compose.prod.yml logs agent
|
||||||
|
```
|
||||||
|
|
||||||
|
**常见原因**:
|
||||||
|
- 环境变量未配置或配置错误
|
||||||
|
- 端口被占用
|
||||||
|
- 依赖服务未启动
|
||||||
|
|
||||||
|
#### 2. API 调用失败
|
||||||
|
|
||||||
|
**检查步骤**:
|
||||||
|
1. 验证 API Token 是否正确
|
||||||
|
2. 检查网络连接
|
||||||
|
3. 查看 API 服务状态
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 测试 API 连接
|
||||||
|
docker exec ai_agent_prod curl https://api.yourdomain.com/health
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Redis 连接失败
|
||||||
|
|
||||||
|
**检查 Redis 容器**:
|
||||||
|
```bash
|
||||||
|
docker exec ai_redis_prod redis-cli -a YOUR_PASSWORD ping
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. 内存不足
|
||||||
|
|
||||||
|
**解决方案**:
|
||||||
|
- 增加 Docker 内存限制
|
||||||
|
- 减少并发数
|
||||||
|
- 优化模型调用
|
||||||
|
|
||||||
|
### 服务重启
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 重启所有服务
|
||||||
|
docker-compose -f docker-compose.prod.yml restart
|
||||||
|
|
||||||
|
# 重启特定服务
|
||||||
|
docker-compose -f docker-compose.prod.yml restart agent
|
||||||
|
|
||||||
|
# 强制重新创建(更新环境变量后)
|
||||||
|
docker-compose -f docker-compose.prod.yml up -d --force-recreate agent
|
||||||
|
```
|
||||||
|
|
||||||
|
### 完全重置
|
||||||
|
|
||||||
|
⚠️ **警告**: 此操作会删除所有容器和数据卷
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 停止并删除所有容器
|
||||||
|
docker-compose -f docker-compose.prod.yml down -v
|
||||||
|
|
||||||
|
# 重新部署
|
||||||
|
./scripts/deploy-production.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 升级策略
|
||||||
|
|
||||||
|
### 滚动升级(零停机)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 拉取最新代码
|
||||||
|
git pull origin main
|
||||||
|
|
||||||
|
# 2. 构建新镜像
|
||||||
|
docker-compose -f docker-compose.prod.yml build
|
||||||
|
|
||||||
|
# 3. 逐个重启服务
|
||||||
|
docker-compose -f docker-compose.prod.yml up -d --no-deps agent
|
||||||
|
|
||||||
|
# 4. 验证新版本
|
||||||
|
curl http://localhost:8000/health
|
||||||
|
```
|
||||||
|
|
||||||
|
### 蓝绿部署
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 部署新版本到不同端口
|
||||||
|
docker-compose -f docker-compose.prod.yml up -d
|
||||||
|
|
||||||
|
# 2. 切换流量(修改 Nginx 配置)
|
||||||
|
|
||||||
|
# 3. 停止旧版本
|
||||||
|
docker-compose -f docker-compose.prod.yml down
|
||||||
|
```
|
||||||
|
|
||||||
|
### 回滚
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 回滚到上一个版本
|
||||||
|
git checkout PREVIOUS_VERSION_TAG
|
||||||
|
|
||||||
|
# 重新构建
|
||||||
|
docker-compose -f docker-compose.prod.yml build
|
||||||
|
|
||||||
|
# 重启服务
|
||||||
|
docker-compose -f docker-compose.prod.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 安全建议
|
||||||
|
|
||||||
|
### 环境变量管理
|
||||||
|
|
||||||
|
1. **使用密钥管理服务**:
|
||||||
|
- AWS Secrets Manager
|
||||||
|
- HashiCorp Vault
|
||||||
|
- Azure Key Vault
|
||||||
|
|
||||||
|
2. **文件权限**:
|
||||||
|
```bash
|
||||||
|
chmod 600 .env.production
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **不要提交敏感信息**:
|
||||||
|
```bash
|
||||||
|
# .gitignore
|
||||||
|
.env.production
|
||||||
|
.env
|
||||||
|
```
|
||||||
|
|
||||||
|
### 网络安全
|
||||||
|
|
||||||
|
1. **使用防火墙限制端口访问**
|
||||||
|
2. **配置 HTTPS/TLS**
|
||||||
|
3. **使用 VPN 或专线连接服务**
|
||||||
|
4. **定期更新 Docker 镜像**
|
||||||
|
|
||||||
|
### API 安全
|
||||||
|
|
||||||
|
1. **定期轮换 API Token**
|
||||||
|
2. **限制 API Token 权限**
|
||||||
|
3. **监控异常调用**
|
||||||
|
4. **设置速率限制**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 部署检查清单
|
||||||
|
|
||||||
|
部署前确认:
|
||||||
|
|
||||||
|
- [ ] 所有环境变量已配置
|
||||||
|
- [ ] Redis 密码已设置
|
||||||
|
- [ ] API Token 已验证
|
||||||
|
- [ ] 端口未被占用
|
||||||
|
- [ ] DNS 解析已配置
|
||||||
|
- [ ] SSL 证书已安装
|
||||||
|
- [ ] 监控已配置
|
||||||
|
- [ ] 备份策略已制定
|
||||||
|
- [ ] 回滚方案已准备
|
||||||
|
|
||||||
|
部署后验证:
|
||||||
|
|
||||||
|
- [ ] 所有容器正常运行
|
||||||
|
- [ ] 健康检查通过
|
||||||
|
- [ ] API 调用成功
|
||||||
|
- [ ] 日志正常输出
|
||||||
|
- [ ] 资源使用正常
|
||||||
|
- [ ] 监控数据正常
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🆘 获取帮助
|
||||||
|
|
||||||
|
- **文档**: `/docs` 目录
|
||||||
|
- **Issues**: GitHub Issues
|
||||||
|
- **日志**: `docker-compose logs`
|
||||||
|
- **监控**: Grafana Dashboard (如果启用)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**最后更新**: 2026-01-26
|
||||||
@@ -141,8 +141,15 @@ async def aftersale_agent(state: AgentState) -> AgentState:
|
|||||||
|
|
||||||
return state
|
return state
|
||||||
|
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError as e:
|
||||||
state = set_response(state, response.content)
|
logger.error(
|
||||||
|
"Failed to parse aftersale agent LLM response as JSON",
|
||||||
|
error=str(e),
|
||||||
|
conversation_id=state.get("conversation_id"),
|
||||||
|
raw_content=response.content[:500] if response.content else "EMPTY"
|
||||||
|
)
|
||||||
|
# Don't use raw content as response - use fallback instead
|
||||||
|
state = set_response(state, "抱歉,我无法理解您的请求。请尝试重新表述或联系人工客服。")
|
||||||
return state
|
return state
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -66,7 +66,47 @@ async def customer_service_agent(state: AgentState) -> AgentState:
|
|||||||
# Get detected language
|
# Get detected language
|
||||||
locale = state.get("detected_language", "en")
|
locale = state.get("detected_language", "en")
|
||||||
|
|
||||||
# Auto-detect category and query FAQ
|
# Check if we have already queried FAQ
|
||||||
|
tool_calls = state.get("tool_calls", [])
|
||||||
|
has_faq_query = any(tc.get("tool_name") in ["query_faq", "search_knowledge_base"] for tc in tool_calls)
|
||||||
|
|
||||||
|
# ========== ROUTING: Use sub_intent from router if available ==========
|
||||||
|
# Router already classified the intent, use it for direct FAQ query
|
||||||
|
sub_intent = state.get("sub_intent")
|
||||||
|
|
||||||
|
# Map sub_intent to FAQ category
|
||||||
|
sub_intent_to_category = {
|
||||||
|
"register_inquiry": "register",
|
||||||
|
"order_inquiry": "order",
|
||||||
|
"payment_inquiry": "payment",
|
||||||
|
"shipment_inquiry": "shipment",
|
||||||
|
"return_inquiry": "return",
|
||||||
|
"policy_inquiry": "return", # Policy queries use return FAQ
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if we should auto-query FAQ based on sub_intent
|
||||||
|
if sub_intent in sub_intent_to_category and not has_faq_query:
|
||||||
|
category = sub_intent_to_category[sub_intent]
|
||||||
|
logger.info(
|
||||||
|
f"Auto-querying FAQ based on sub_intent: {sub_intent} -> category: {category}",
|
||||||
|
conversation_id=state["conversation_id"]
|
||||||
|
)
|
||||||
|
|
||||||
|
state = add_tool_call(
|
||||||
|
state,
|
||||||
|
tool_name="query_faq",
|
||||||
|
arguments={
|
||||||
|
"category": category,
|
||||||
|
"locale": locale,
|
||||||
|
"limit": 5
|
||||||
|
},
|
||||||
|
server="strapi"
|
||||||
|
)
|
||||||
|
state["state"] = ConversationState.TOOL_CALLING.value
|
||||||
|
return state
|
||||||
|
# ========================================================================
|
||||||
|
|
||||||
|
# Auto-detect category and query FAQ (fallback if sub_intent not available)
|
||||||
message_lower = state["current_message"].lower()
|
message_lower = state["current_message"].lower()
|
||||||
|
|
||||||
# 定义分类关键词(支持多语言:en, nl, de, es, fr, it, tr, zh)
|
# 定义分类关键词(支持多语言:en, nl, de, es, fr, it, tr, zh)
|
||||||
@@ -163,17 +203,13 @@ async def customer_service_agent(state: AgentState) -> AgentState:
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
# 检测分类
|
# 检测分类(仅在未通过 sub_intent 匹配时使用)
|
||||||
detected_category = None
|
detected_category = None
|
||||||
for category, keywords in category_keywords.items():
|
for category, keywords in category_keywords.items():
|
||||||
if any(keyword in message_lower for keyword in keywords):
|
if any(keyword in message_lower for keyword in keywords):
|
||||||
detected_category = category
|
detected_category = category
|
||||||
break
|
break
|
||||||
|
|
||||||
# 检查是否已经查询过 FAQ
|
|
||||||
tool_calls = state.get("tool_calls", [])
|
|
||||||
has_faq_query = any(tc.get("tool_name") in ["query_faq", "search_knowledge_base"] for tc in tool_calls)
|
|
||||||
|
|
||||||
# 如果检测到分类且未查询过 FAQ,自动查询
|
# 如果检测到分类且未查询过 FAQ,自动查询
|
||||||
if detected_category and not has_faq_query:
|
if detected_category and not has_faq_query:
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -233,43 +269,72 @@ async def customer_service_agent(state: AgentState) -> AgentState:
|
|||||||
llm = get_llm_client()
|
llm = get_llm_client()
|
||||||
response = await llm.chat(messages, temperature=0.7)
|
response = await llm.chat(messages, temperature=0.7)
|
||||||
|
|
||||||
|
# Log raw response for debugging
|
||||||
|
logger.info(
|
||||||
|
"Customer service LLM response",
|
||||||
|
conversation_id=state["conversation_id"],
|
||||||
|
response_preview=response.content[:300] if response.content else "EMPTY",
|
||||||
|
response_length=len(response.content) if response.content else 0
|
||||||
|
)
|
||||||
|
|
||||||
# Parse response
|
# Parse response
|
||||||
content = response.content.strip()
|
content = response.content.strip()
|
||||||
|
|
||||||
|
# Handle markdown code blocks
|
||||||
if content.startswith("```"):
|
if content.startswith("```"):
|
||||||
content = content.split("```")[1]
|
parts = content.split("```")
|
||||||
if content.startswith("json"):
|
if len(parts) >= 2:
|
||||||
content = content[4:]
|
content = parts[1]
|
||||||
|
if content.startswith("json"):
|
||||||
|
content = content[4:]
|
||||||
|
content = content.strip()
|
||||||
|
|
||||||
result = json.loads(content)
|
try:
|
||||||
action = result.get("action")
|
result = json.loads(content)
|
||||||
|
action = result.get("action")
|
||||||
|
|
||||||
if action == "call_tool":
|
if action == "call_tool":
|
||||||
# Add tool call to state
|
# Add tool call to state
|
||||||
state = add_tool_call(
|
state = add_tool_call(
|
||||||
state,
|
state,
|
||||||
tool_name=result["tool_name"],
|
tool_name=result["tool_name"],
|
||||||
arguments=result.get("arguments", {}),
|
arguments=result.get("arguments", {}),
|
||||||
server="strapi"
|
server="strapi"
|
||||||
|
)
|
||||||
|
state["state"] = ConversationState.TOOL_CALLING.value
|
||||||
|
|
||||||
|
elif action == "respond":
|
||||||
|
state = set_response(state, result["response"])
|
||||||
|
state["state"] = ConversationState.GENERATING.value
|
||||||
|
|
||||||
|
elif action == "handoff":
|
||||||
|
state["requires_human"] = True
|
||||||
|
state["handoff_reason"] = result.get("reason", "User request")
|
||||||
|
else:
|
||||||
|
# Unknown action, treat as plain text response
|
||||||
|
logger.warning(
|
||||||
|
"Unknown action in LLM response",
|
||||||
|
action=action,
|
||||||
|
conversation_id=state["conversation_id"]
|
||||||
|
)
|
||||||
|
state = set_response(state, response.content)
|
||||||
|
|
||||||
|
return state
|
||||||
|
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
# JSON parsing failed
|
||||||
|
logger.error(
|
||||||
|
"Failed to parse LLM response as JSON",
|
||||||
|
error=str(e),
|
||||||
|
raw_content=content[:500],
|
||||||
|
conversation_id=state["conversation_id"]
|
||||||
)
|
)
|
||||||
state["state"] = ConversationState.TOOL_CALLING.value
|
# Don't use raw content as response - use fallback instead
|
||||||
|
state = set_response(state, "抱歉,我无法理解您的请求。请尝试重新表述或联系人工客服。")
|
||||||
elif action == "respond":
|
return state
|
||||||
state = set_response(state, result["response"])
|
|
||||||
state["state"] = ConversationState.GENERATING.value
|
|
||||||
|
|
||||||
elif action == "handoff":
|
|
||||||
state["requires_human"] = True
|
|
||||||
state["handoff_reason"] = result.get("reason", "User request")
|
|
||||||
|
|
||||||
return state
|
|
||||||
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
# LLM returned plain text, use as response
|
|
||||||
state = set_response(state, response.content)
|
|
||||||
return state
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Customer service agent failed", error=str(e))
|
logger.error("Customer service agent failed", error=str(e), exc_info=True)
|
||||||
state["error"] = str(e)
|
state["error"] = str(e)
|
||||||
return state
|
return state
|
||||||
|
|
||||||
|
|||||||
@@ -64,8 +64,8 @@ ORDER_AGENT_PROMPT = """你是一个专业的 B2B 订单服务助手。
|
|||||||
2. **get_mall_order_list** - 从商城 API 查询订单列表(推荐使用)
|
2. **get_mall_order_list** - 从商城 API 查询订单列表(推荐使用)
|
||||||
- user_token: 用户 token(自动注入)
|
- user_token: 用户 token(自动注入)
|
||||||
- page: 页码(可选,默认 1)
|
- page: 页码(可选,默认 1)
|
||||||
- limit: 每页数量(可选,默认 10)
|
- limit: 每页数量(可选,默认 5)
|
||||||
- 说明:查询用户的所有订单,按时间倒序排列
|
- 说明:查询用户的所有订单,按时间倒序排列,返回最近的 5 个订单
|
||||||
|
|
||||||
3. **get_logistics** - 从商城 API 查询物流信息
|
3. **get_logistics** - 从商城 API 查询物流信息
|
||||||
- order_id: 订单号(必需)
|
- order_id: 订单号(必需)
|
||||||
@@ -339,8 +339,8 @@ async def order_agent(state: AgentState) -> AgentState:
|
|||||||
error=str(e),
|
error=str(e),
|
||||||
content_preview=content[:500]
|
content_preview=content[:500]
|
||||||
)
|
)
|
||||||
# 如果解析失败,尝试将原始内容作为直接回复
|
# Don't use raw content as response - use fallback instead
|
||||||
state = set_response(state, response.content)
|
state = set_response(state, "抱歉,我无法理解您的请求。请尝试重新表述或联系人工客服。")
|
||||||
return state
|
return state
|
||||||
|
|
||||||
action = result.get("action")
|
action = result.get("action")
|
||||||
@@ -381,6 +381,14 @@ async def order_agent(state: AgentState) -> AgentState:
|
|||||||
if tool_name in ["get_mall_order", "get_logistics", "query_order"]:
|
if tool_name in ["get_mall_order", "get_logistics", "query_order"]:
|
||||||
arguments["order_id"] = state["entities"]["order_id"]
|
arguments["order_id"] = state["entities"]["order_id"]
|
||||||
|
|
||||||
|
# Force limit=5 for order list queries (unless explicitly set)
|
||||||
|
if tool_name == "get_mall_order_list" and "limit" not in arguments:
|
||||||
|
arguments["limit"] = 5
|
||||||
|
logger.debug(
|
||||||
|
"Forced limit=5 for order list query",
|
||||||
|
conversation_id=state["conversation_id"]
|
||||||
|
)
|
||||||
|
|
||||||
state = add_tool_call(
|
state = add_tool_call(
|
||||||
state,
|
state,
|
||||||
tool_name=result["tool_name"],
|
tool_name=result["tool_name"],
|
||||||
@@ -730,8 +738,11 @@ def _parse_mall_order_data(data: dict[str, Any]) -> dict[str, Any]:
|
|||||||
if actual_order_data.get("remark") or actual_order_data.get("user_remark"):
|
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", ""))
|
order_data["remark"] = actual_order_data.get("remark", actual_order_data.get("user_remark", ""))
|
||||||
|
|
||||||
# 物流信息(如果有)
|
# 物流信息(如果有)- 添加 has_parcels 标记用于判断是否显示物流按钮
|
||||||
if actual_order_data.get("parcels") and len(actual_order_data.get("parcels", [])) > 0:
|
has_parcels = actual_order_data.get("parcels") and len(actual_order_data.get("parcels", [])) > 0
|
||||||
|
order_data["has_parcels"] = has_parcels
|
||||||
|
|
||||||
|
if has_parcels:
|
||||||
# parcels 是一个数组,包含物流信息
|
# parcels 是一个数组,包含物流信息
|
||||||
first_parcel = actual_order_data["parcels"][0] if isinstance(actual_order_data["parcels"], list) else actual_order_data["parcels"]
|
first_parcel = actual_order_data["parcels"][0] if isinstance(actual_order_data["parcels"], list) else actual_order_data["parcels"]
|
||||||
if isinstance(first_parcel, dict):
|
if isinstance(first_parcel, dict):
|
||||||
|
|||||||
@@ -22,11 +22,10 @@ PRODUCT_AGENT_PROMPT = """你是一个专业的 B2B 商品顾问助手。
|
|||||||
## 可用工具
|
## 可用工具
|
||||||
|
|
||||||
1. **search_products** - 搜索商品
|
1. **search_products** - 搜索商品
|
||||||
- query: 搜索关键词
|
- keyword: 搜索关键词(商品名称、编号等)
|
||||||
- filters: 过滤条件(category, price_range, brand 等)
|
- page_size: 每页数量(默认 5,最大 100)
|
||||||
- sort: 排序方式(price_asc/price_desc/sales/latest)
|
- page: 页码(默认 1)
|
||||||
- page: 页码
|
- 说明:此工具使用 Mall API 搜索商品 SPU,支持用户 token 认证,返回卡片格式展示
|
||||||
- page_size: 每页数量
|
|
||||||
|
|
||||||
2. **get_product_detail** - 获取商品详情
|
2. **get_product_detail** - 获取商品详情
|
||||||
- product_id: 商品ID
|
- product_id: 商品ID
|
||||||
@@ -57,6 +56,31 @@ PRODUCT_AGENT_PROMPT = """你是一个专业的 B2B 商品顾问助手。
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**示例**:
|
||||||
|
用户说:"搜索 ring"
|
||||||
|
返回:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"action": "call_tool",
|
||||||
|
"tool_name": "search_products",
|
||||||
|
"arguments": {
|
||||||
|
"keyword": "ring"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
用户说:"查找手机"
|
||||||
|
返回:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"action": "call_tool",
|
||||||
|
"tool_name": "search_products",
|
||||||
|
"arguments": {
|
||||||
|
"keyword": "手机"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
当需要向用户询问更多信息时:
|
当需要向用户询问更多信息时:
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -148,24 +172,88 @@ async def product_agent(state: AgentState) -> AgentState:
|
|||||||
|
|
||||||
# Parse response
|
# Parse response
|
||||||
content = response.content.strip()
|
content = response.content.strip()
|
||||||
|
|
||||||
|
# Log raw LLM response for debugging
|
||||||
|
logger.info(
|
||||||
|
"Product agent LLM response",
|
||||||
|
response_length=len(content),
|
||||||
|
response_preview=content[:200],
|
||||||
|
conversation_id=state["conversation_id"]
|
||||||
|
)
|
||||||
|
|
||||||
if content.startswith("```"):
|
if content.startswith("```"):
|
||||||
content = content.split("```")[1]
|
content = content.split("```")[1]
|
||||||
if content.startswith("json"):
|
if content.startswith("json"):
|
||||||
content = content[4:]
|
content = content[4:]
|
||||||
|
# Remove leading/trailing whitespace after removing code block markers
|
||||||
|
content = content.strip()
|
||||||
|
|
||||||
|
# Handle non-JSON format: "tool_name\n{args}"
|
||||||
|
if '\n' in content and not content.startswith('{'):
|
||||||
|
lines = content.split('\n', 1)
|
||||||
|
tool_name = lines[0].strip()
|
||||||
|
args_json = lines[1].strip() if len(lines) > 1 else '{}'
|
||||||
|
|
||||||
|
try:
|
||||||
|
arguments = json.loads(args_json) if args_json else {}
|
||||||
|
result = {
|
||||||
|
"action": "call_tool",
|
||||||
|
"tool_name": tool_name,
|
||||||
|
"arguments": arguments
|
||||||
|
}
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
# If args parsing fails, use empty dict
|
||||||
|
result = {
|
||||||
|
"action": "call_tool",
|
||||||
|
"tool_name": tool_name,
|
||||||
|
"arguments": {}
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
# Standard JSON format
|
||||||
|
result = json.loads(content)
|
||||||
|
|
||||||
result = json.loads(content)
|
|
||||||
action = result.get("action")
|
action = result.get("action")
|
||||||
|
|
||||||
if action == "call_tool":
|
if action == "call_tool":
|
||||||
arguments = result.get("arguments", {})
|
arguments = result.get("arguments", {})
|
||||||
|
tool_name = result.get("tool_name", "")
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"Product agent calling tool",
|
||||||
|
tool_name=tool_name,
|
||||||
|
arguments=arguments,
|
||||||
|
conversation_id=state["conversation_id"]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Inject context for product search (Mall API)
|
||||||
|
if tool_name == "search_products":
|
||||||
|
arguments["user_token"] = state.get("user_token")
|
||||||
|
arguments["user_id"] = state["user_id"]
|
||||||
|
arguments["account_id"] = state["account_id"]
|
||||||
|
|
||||||
|
# Set default page_size if not provided
|
||||||
|
if "page_size" not in arguments:
|
||||||
|
arguments["page_size"] = 5
|
||||||
|
|
||||||
|
# Set default page if not provided
|
||||||
|
if "page" not in arguments:
|
||||||
|
arguments["page"] = 1
|
||||||
|
|
||||||
|
# Map "query" parameter to "keyword" for compatibility
|
||||||
|
if "query" in arguments and "keyword" not in arguments:
|
||||||
|
arguments["keyword"] = arguments.pop("query")
|
||||||
|
logger.info(
|
||||||
|
"Parameter mapped: query -> keyword",
|
||||||
|
conversation_id=state["conversation_id"]
|
||||||
|
)
|
||||||
|
|
||||||
# Inject context for recommendation
|
# Inject context for recommendation
|
||||||
if result["tool_name"] == "recommend_products":
|
if tool_name == "recommend_products":
|
||||||
arguments["user_id"] = state["user_id"]
|
arguments["user_id"] = state["user_id"]
|
||||||
arguments["account_id"] = state["account_id"]
|
arguments["account_id"] = state["account_id"]
|
||||||
|
|
||||||
# Inject context for quote
|
# Inject context for quote
|
||||||
if result["tool_name"] == "get_quote":
|
if tool_name == "get_quote":
|
||||||
arguments["account_id"] = state["account_id"]
|
arguments["account_id"] = state["account_id"]
|
||||||
|
|
||||||
# Use entity if available
|
# Use entity if available
|
||||||
@@ -177,7 +265,7 @@ async def product_agent(state: AgentState) -> AgentState:
|
|||||||
|
|
||||||
state = add_tool_call(
|
state = add_tool_call(
|
||||||
state,
|
state,
|
||||||
tool_name=result["tool_name"],
|
tool_name=tool_name,
|
||||||
arguments=arguments,
|
arguments=arguments,
|
||||||
server="product"
|
server="product"
|
||||||
)
|
)
|
||||||
@@ -193,8 +281,15 @@ async def product_agent(state: AgentState) -> AgentState:
|
|||||||
|
|
||||||
return state
|
return state
|
||||||
|
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError as e:
|
||||||
state = set_response(state, response.content)
|
logger.error(
|
||||||
|
"Failed to parse product agent LLM response as JSON",
|
||||||
|
error=str(e),
|
||||||
|
conversation_id=state.get("conversation_id"),
|
||||||
|
raw_content=response.content[:500] if response.content else "EMPTY"
|
||||||
|
)
|
||||||
|
# Don't use raw content as response - use fallback instead
|
||||||
|
state = set_response(state, "抱歉,我无法理解您的请求。请尝试重新表述或联系人工客服。")
|
||||||
return state
|
return state
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -206,6 +301,63 @@ async def product_agent(state: AgentState) -> AgentState:
|
|||||||
async def _generate_product_response(state: AgentState) -> AgentState:
|
async def _generate_product_response(state: AgentState) -> AgentState:
|
||||||
"""Generate response based on product tool results"""
|
"""Generate response based on product tool results"""
|
||||||
|
|
||||||
|
# 特殊处理:如果是 search_products 工具返回,直接发送商品卡片
|
||||||
|
has_product_search_result = False
|
||||||
|
products = []
|
||||||
|
|
||||||
|
for result in state["tool_results"]:
|
||||||
|
if result["success"] and result["tool_name"] == "search_products":
|
||||||
|
data = result["data"]
|
||||||
|
if isinstance(data, dict) and data.get("success"):
|
||||||
|
products = data.get("products", [])
|
||||||
|
has_product_search_result = True
|
||||||
|
logger.info(
|
||||||
|
"Product search results found",
|
||||||
|
products_count=len(products),
|
||||||
|
keyword=data.get("keyword", "")
|
||||||
|
)
|
||||||
|
break
|
||||||
|
|
||||||
|
# 如果有商品搜索结果,直接发送商品卡片
|
||||||
|
if has_product_search_result and products:
|
||||||
|
try:
|
||||||
|
from integrations.chatwoot import ChatwootClient
|
||||||
|
from core.language_detector import detect_language
|
||||||
|
|
||||||
|
# 检测语言
|
||||||
|
detected_language = state.get("detected_language", "en")
|
||||||
|
|
||||||
|
# 发送商品卡片
|
||||||
|
chatwoot = ChatwootClient(account_id=int(state.get("account_id", 1)))
|
||||||
|
conversation_id = state.get("conversation_id")
|
||||||
|
|
||||||
|
if conversation_id:
|
||||||
|
await chatwoot.send_product_cards(
|
||||||
|
conversation_id=int(conversation_id),
|
||||||
|
products=products,
|
||||||
|
language=detected_language
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"Product cards sent successfully",
|
||||||
|
conversation_id=conversation_id,
|
||||||
|
products_count=len(products),
|
||||||
|
language=detected_language
|
||||||
|
)
|
||||||
|
|
||||||
|
# 清空响应,避免重复发送
|
||||||
|
state = set_response(state, "")
|
||||||
|
state["state"] = ConversationState.GENERATING.value
|
||||||
|
return state
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(
|
||||||
|
"Failed to send product cards, falling back to text response",
|
||||||
|
error=str(e),
|
||||||
|
products_count=len(products)
|
||||||
|
)
|
||||||
|
|
||||||
|
# 常规处理:生成文本响应
|
||||||
tool_context = []
|
tool_context = []
|
||||||
for result in state["tool_results"]:
|
for result in state["tool_results"]:
|
||||||
if result["success"]:
|
if result["success"]:
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ async def classify_intent(state: AgentState) -> AgentState:
|
|||||||
content = response.content.strip()
|
content = response.content.strip()
|
||||||
|
|
||||||
# Log raw response for debugging
|
# Log raw response for debugging
|
||||||
logger.debug(
|
logger.info(
|
||||||
"LLM response for intent classification",
|
"LLM response for intent classification",
|
||||||
response_preview=content[:500] if content else "EMPTY",
|
response_preview=content[:500] if content else "EMPTY",
|
||||||
content_length=len(content) if content else 0
|
content_length=len(content) if content else 0
|
||||||
|
|||||||
@@ -154,7 +154,8 @@ class ZhipuLLMClient:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Determine if reasoning mode should be used
|
# Determine if reasoning mode should be used
|
||||||
use_reasoning = enable_reasoning if enable_reasoning is not None else self._should_use_reasoning(formatted_messages)
|
# 强制禁用深度思考模式以提升响应速度(2026-01-26)
|
||||||
|
use_reasoning = False # Override all settings to disable thinking mode
|
||||||
|
|
||||||
if use_reasoning:
|
if use_reasoning:
|
||||||
logger.info("Reasoning mode enabled for this request")
|
logger.info("Reasoning mode enabled for this request")
|
||||||
|
|||||||
@@ -155,6 +155,109 @@ def get_field_label(field_key: str, language: str = "en") -> str:
|
|||||||
return ORDER_FIELD_LABELS[language].get(field_key, ORDER_FIELD_LABELS["en"].get(field_key, field_key))
|
return ORDER_FIELD_LABELS[language].get(field_key, ORDER_FIELD_LABELS["en"].get(field_key, field_key))
|
||||||
|
|
||||||
|
|
||||||
|
# 订单状态多语言映射
|
||||||
|
ORDER_STATUS_LABELS = {
|
||||||
|
"zh": { # 中文
|
||||||
|
"0": "已取消",
|
||||||
|
"1": "待支付",
|
||||||
|
"2": "已支付",
|
||||||
|
"3": "已发货",
|
||||||
|
"4": "已签收",
|
||||||
|
"15": "已完成",
|
||||||
|
"100": "超时取消",
|
||||||
|
"unknown": "未知"
|
||||||
|
},
|
||||||
|
"en": { # English
|
||||||
|
"0": "Cancelled",
|
||||||
|
"1": "Pending Payment",
|
||||||
|
"2": "Paid",
|
||||||
|
"3": "Shipped",
|
||||||
|
"4": "Delivered",
|
||||||
|
"15": "Completed",
|
||||||
|
"100": "Timeout Cancelled",
|
||||||
|
"unknown": "Unknown"
|
||||||
|
},
|
||||||
|
"nl": { # Dutch (荷兰语)
|
||||||
|
"0": "Geannuleerd",
|
||||||
|
"1": "Wachtend op betaling",
|
||||||
|
"2": "Betaald",
|
||||||
|
"3": "Verzonden",
|
||||||
|
"4": "Geleverd",
|
||||||
|
"15": "Voltooid",
|
||||||
|
"100": "Time-out geannuleerd",
|
||||||
|
"unknown": "Onbekend"
|
||||||
|
},
|
||||||
|
"de": { # German (德语)
|
||||||
|
"0": "Storniert",
|
||||||
|
"1": "Zahlung ausstehend",
|
||||||
|
"2": "Bezahlt",
|
||||||
|
"3": "Versandt",
|
||||||
|
"4": "Zugestellt",
|
||||||
|
"15": "Abgeschlossen",
|
||||||
|
"100": "Zeitüberschreitung storniert",
|
||||||
|
"unknown": "Unbekannt"
|
||||||
|
},
|
||||||
|
"es": { # Spanish (西班牙语)
|
||||||
|
"0": "Cancelado",
|
||||||
|
"1": "Pago pendiente",
|
||||||
|
"2": "Pagado",
|
||||||
|
"3": "Enviado",
|
||||||
|
"4": "Entregado",
|
||||||
|
"15": "Completado",
|
||||||
|
"100": "Cancelado por tiempo límite",
|
||||||
|
"unknown": "Desconocido"
|
||||||
|
},
|
||||||
|
"fr": { # French (法语)
|
||||||
|
"0": "Annulé",
|
||||||
|
"1": "En attente de paiement",
|
||||||
|
"2": "Payé",
|
||||||
|
"3": "Expédié",
|
||||||
|
"4": "Livré",
|
||||||
|
"15": "Terminé",
|
||||||
|
"100": "Annulé pour expiration",
|
||||||
|
"unknown": "Inconnu"
|
||||||
|
},
|
||||||
|
"it": { # Italian (意大利语)
|
||||||
|
"0": "Annullato",
|
||||||
|
"1": "In attesa di pagamento",
|
||||||
|
"2": "Pagato",
|
||||||
|
"3": "Spedito",
|
||||||
|
"4": "Consegnato",
|
||||||
|
"15": "Completato",
|
||||||
|
"100": "Annullato per timeout",
|
||||||
|
"unknown": "Sconosciuto"
|
||||||
|
},
|
||||||
|
"tr": { # Turkish (土耳其语)
|
||||||
|
"0": "İptal edildi",
|
||||||
|
"1": "Ödeme bekleniyor",
|
||||||
|
"2": "Ödendi",
|
||||||
|
"3": "Kargolandı",
|
||||||
|
"4": "Teslim edildi",
|
||||||
|
"15": "Tamamlandı",
|
||||||
|
"100": "Zaman aşımı iptal edildi",
|
||||||
|
"unknown": "Bilinmiyor"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_status_label(status_code: str, language: str = "en") -> str:
|
||||||
|
"""获取指定语言的订单状态标签
|
||||||
|
|
||||||
|
Args:
|
||||||
|
status_code: 状态码(如 "0", "1", "2" 等)
|
||||||
|
language: 语言代码(默认 "en")
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
对应语言的状态标签
|
||||||
|
"""
|
||||||
|
if language not in ORDER_STATUS_LABELS:
|
||||||
|
language = "en" # 默认使用英文
|
||||||
|
return ORDER_STATUS_LABELS[language].get(
|
||||||
|
str(status_code),
|
||||||
|
ORDER_STATUS_LABELS["en"].get(str(status_code), ORDER_STATUS_LABELS["en"]["unknown"])
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class MessageType(str, Enum):
|
class MessageType(str, Enum):
|
||||||
"""Chatwoot message types"""
|
"""Chatwoot message types"""
|
||||||
INCOMING = "incoming"
|
INCOMING = "incoming"
|
||||||
@@ -507,18 +610,25 @@ class ChatwootClient:
|
|||||||
|
|
||||||
total_amount = order_data.get("total_amount", "0")
|
total_amount = order_data.get("total_amount", "0")
|
||||||
|
|
||||||
# 根据状态码映射状态和颜色
|
# 根据状态码映射状态和颜色(支持多语言)
|
||||||
status_mapping = {
|
status_code_to_key = {
|
||||||
"0": {"status": "cancelled", "text": "已取消", "color": "text-red-600"},
|
"0": {"key": "cancelled", "color": "text-red-600"},
|
||||||
"1": {"status": "pending", "text": "待支付", "color": "text-yellow-600"},
|
"1": {"key": "pending", "color": "text-yellow-600"},
|
||||||
"2": {"status": "paid", "text": "已支付", "color": "text-blue-600"},
|
"2": {"key": "paid", "color": "text-blue-600"},
|
||||||
"3": {"status": "shipped", "text": "已发货", "color": "text-purple-600"},
|
"3": {"key": "shipped", "color": "text-purple-600"},
|
||||||
"4": {"status": "signed", "text": "已签收", "color": "text-green-600"},
|
"4": {"key": "signed", "color": "text-green-600"},
|
||||||
"15": {"status": "completed", "text": "已完成", "color": "text-green-600"},
|
"15": {"key": "completed", "color": "text-green-600"},
|
||||||
"100": {"status": "cancelled", "text": "超时取消", "color": "text-red-600"},
|
"100": {"key": "cancelled", "color": "text-red-600"},
|
||||||
}
|
}
|
||||||
|
|
||||||
status_info = status_mapping.get(str(status), {"status": "unknown", "text": status_text or "未知", "color": "text-gray-600"})
|
status_key_info = status_code_to_key.get(str(status), {"key": "unknown", "color": "text-gray-600"})
|
||||||
|
status_label = get_status_label(str(status), language)
|
||||||
|
|
||||||
|
status_info = {
|
||||||
|
"status": status_key_info["key"],
|
||||||
|
"text": status_label,
|
||||||
|
"color": status_key_info["color"]
|
||||||
|
}
|
||||||
|
|
||||||
# 构建商品列表
|
# 构建商品列表
|
||||||
items = order_data.get("items", [])
|
items = order_data.get("items", [])
|
||||||
@@ -910,18 +1020,27 @@ class ChatwootClient:
|
|||||||
sample_items=str(formatted_items[:2]) if formatted_items else "[]"
|
sample_items=str(formatted_items[:2]) if formatted_items else "[]"
|
||||||
)
|
)
|
||||||
|
|
||||||
# 构建操作按钮
|
# 构建操作按钮 - 根据是否有物流信息决定是否显示物流按钮
|
||||||
actions = [
|
actions = [
|
||||||
{
|
{
|
||||||
"text": details_text,
|
"text": details_text,
|
||||||
"reply": f"{details_reply_prefix}{order_id}"
|
"reply": f"{details_reply_prefix}{order_id}"
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": logistics_text,
|
|
||||||
"reply": f"{logistics_reply_prefix}{order_id}"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# 只有当订单有物流信息时才显示物流按钮
|
||||||
|
if order.get("has_parcels", False):
|
||||||
|
actions.append({
|
||||||
|
"text": logistics_text,
|
||||||
|
"reply": f"{logistics_reply_prefix}{order_id}"
|
||||||
|
})
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
f"Built {len(actions)} actions for order {order_id}",
|
||||||
|
has_parcels=order.get("has_parcels", False),
|
||||||
|
actions_count=len(actions)
|
||||||
|
)
|
||||||
|
|
||||||
# 构建单个订单
|
# 构建单个订单
|
||||||
order_data = {
|
order_data = {
|
||||||
"orderNumber": order_id,
|
"orderNumber": order_id,
|
||||||
@@ -964,6 +1083,165 @@ class ChatwootClient:
|
|||||||
|
|
||||||
return response.json()
|
return response.json()
|
||||||
|
|
||||||
|
async def send_product_cards(
|
||||||
|
self,
|
||||||
|
conversation_id: int,
|
||||||
|
products: list[dict[str, Any]],
|
||||||
|
language: str = "en"
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""发送商品搜索结果(使用 cards 格式)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
conversation_id: 会话 ID
|
||||||
|
products: 商品列表,每个商品包含:
|
||||||
|
- spu_id: SPU ID
|
||||||
|
- spu_sn: SPU 编号
|
||||||
|
- product_name: 商品名称
|
||||||
|
- product_image: 商品图片 URL
|
||||||
|
- price: 价格
|
||||||
|
- special_price: 特价(可选)
|
||||||
|
- stock: 库存
|
||||||
|
- sales_count: 销量
|
||||||
|
language: 语言代码(en, nl, de, es, fr, it, tr, zh),默认 en
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
发送结果
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> products = [
|
||||||
|
... {
|
||||||
|
... "spu_id": "12345",
|
||||||
|
... "product_name": "Product A",
|
||||||
|
... "product_image": "https://...",
|
||||||
|
... "price": "99.99",
|
||||||
|
... "stock": 100
|
||||||
|
... }
|
||||||
|
... ]
|
||||||
|
>>> await chatwoot.send_product_cards(123, products, language="zh")
|
||||||
|
"""
|
||||||
|
# 获取前端 URL
|
||||||
|
frontend_url = settings.frontend_url.rstrip('/')
|
||||||
|
|
||||||
|
# 构建商品卡片
|
||||||
|
cards = []
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
# 价格显示(如果有特价则显示特价)
|
||||||
|
try:
|
||||||
|
price_num = float(price) if price else 0
|
||||||
|
price_text = f"€{price_num:.2f}"
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
price_text = str(price) if price 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
|
||||||
|
}
|
||||||
|
|
||||||
|
cards.append(card)
|
||||||
|
|
||||||
|
# 发送 cards 类型消息
|
||||||
|
client = await self._get_client()
|
||||||
|
|
||||||
|
content_attributes = {
|
||||||
|
"items": cards
|
||||||
|
}
|
||||||
|
|
||||||
|
# 添加标题
|
||||||
|
if language == "zh":
|
||||||
|
content = f"找到 {len(products)} 个商品"
|
||||||
|
else:
|
||||||
|
content = f"Found {len(products)} products"
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"content": content,
|
||||||
|
"content_type": "cards",
|
||||||
|
"content_attributes": content_attributes
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"Sending product cards",
|
||||||
|
conversation_id=conversation_id,
|
||||||
|
products_count=len(products),
|
||||||
|
language=language,
|
||||||
|
payload_preview=str(payload)[:1000]
|
||||||
|
)
|
||||||
|
|
||||||
|
response = await client.post(
|
||||||
|
f"/conversations/{conversation_id}/messages",
|
||||||
|
json=payload
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
return response.json()
|
||||||
|
|
||||||
# ============ Conversations ============
|
# ============ Conversations ============
|
||||||
|
|
||||||
async def get_conversation(self, conversation_id: int) -> dict[str, Any]:
|
async def get_conversation(self, conversation_id: int) -> dict[str, Any]:
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ system_prompt: |
|
|||||||
|
|
||||||
## Output Format
|
## Output Format
|
||||||
|
|
||||||
|
⚠️ **CRITICAL**: You MUST return a valid JSON object. Do NOT chat with the user. Do NOT provide explanations outside the JSON.
|
||||||
|
|
||||||
Please return in JSON format with the following fields:
|
Please return in JSON format with the following fields:
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -64,10 +66,34 @@ system_prompt: |
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
**Example 1**:
|
||||||
|
User: "Where is my order 123456?"
|
||||||
|
Response:
|
||||||
|
```json
|
||||||
|
{"intent": "order", "confidence": 0.95, "sub_intent": "order_query", "entities": {"order_id": "123456"}, "reasoning": "User asking about order status"}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example 2**:
|
||||||
|
User: "退货政策是什么"
|
||||||
|
Response:
|
||||||
|
```json
|
||||||
|
{"intent": "customer_service", "confidence": 0.90, "sub_intent": "return_policy", "entities": {}, "reasoning": "User asking about return policy"}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example 3**:
|
||||||
|
User: "I want to return this item"
|
||||||
|
Response:
|
||||||
|
```json
|
||||||
|
{"intent": "aftersale", "confidence": 0.85, "sub_intent": "return_request", "entities": {}, "reasoning": "User wants to return an item"}
|
||||||
|
```
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
- If intent is unclear, confidence should be lower
|
- If intent is unclear, confidence should be lower
|
||||||
- If unable to determine intent, return "unknown"
|
- If unable to determine intent, return "unknown"
|
||||||
- Entity extraction should be accurate, don't fill in fields that don't exist
|
- Entity extraction should be accurate, don't fill in fields that don't exist
|
||||||
|
- **ALWAYS return JSON, NEVER return plain text**
|
||||||
|
|
||||||
tool_descriptions:
|
tool_descriptions:
|
||||||
classify: "Classify user intent and extract entities"
|
classify: "Classify user intent and extract entities"
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
"""
|
|
||||||
测试端点 - 用于测试退货 FAQ
|
|
||||||
"""
|
|
||||||
from fastapi import APIRouter, HTTPException
|
|
||||||
from pydantic import BaseModel
|
|
||||||
|
|
||||||
from core.graph import process_message
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/test", tags=["test"])
|
|
||||||
|
|
||||||
|
|
||||||
class TestRequest(BaseModel):
|
|
||||||
"""测试请求"""
|
|
||||||
conversation_id: str
|
|
||||||
user_id: str
|
|
||||||
account_id: str
|
|
||||||
message: str
|
|
||||||
history: list = []
|
|
||||||
context: dict = {}
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/faq")
|
|
||||||
async def test_faq(request: TestRequest):
|
|
||||||
"""测试 FAQ 回答
|
|
||||||
|
|
||||||
简化的测试端点,用于测试退货相关 FAQ
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# 调用处理流程
|
|
||||||
result = await process_message(
|
|
||||||
conversation_id=request.conversation_id,
|
|
||||||
user_id=request.user_id,
|
|
||||||
account_id=request.account_id,
|
|
||||||
message=request.message,
|
|
||||||
history=request.history,
|
|
||||||
context=request.context
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"success": True,
|
|
||||||
"response": result.get("response"),
|
|
||||||
"intent": result.get("intent"),
|
|
||||||
"tool_calls": result.get("tool_calls", []),
|
|
||||||
"step_count": result.get("step_count", 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
import traceback
|
|
||||||
traceback.print_exc()
|
|
||||||
|
|
||||||
return {
|
|
||||||
"success": False,
|
|
||||||
"error": str(e),
|
|
||||||
"response": None
|
|
||||||
}
|
|
||||||
@@ -350,6 +350,15 @@ async def handle_incoming_message(payload: ChatwootWebhookPayload, cookie_token:
|
|||||||
if response is None:
|
if response is None:
|
||||||
response = "抱歉,我暂时无法处理您的请求。请稍后重试或联系人工客服。"
|
response = "抱歉,我暂时无法处理您的请求。请稍后重试或联系人工客服。"
|
||||||
|
|
||||||
|
# Log the response content for debugging
|
||||||
|
logger.info(
|
||||||
|
"Preparing to send response to Chatwoot",
|
||||||
|
conversation_id=conversation_id,
|
||||||
|
response_length=len(response) if response else 0,
|
||||||
|
response_preview=response[:200] if response else None,
|
||||||
|
has_response=bool(response)
|
||||||
|
)
|
||||||
|
|
||||||
# Create Chatwoot client(已在前面创建,这里不需要再次创建)
|
# Create Chatwoot client(已在前面创建,这里不需要再次创建)
|
||||||
# chatwoot 已在 try 块之前创建
|
# chatwoot 已在 try 块之前创建
|
||||||
|
|
||||||
@@ -359,6 +368,10 @@ async def handle_incoming_message(payload: ChatwootWebhookPayload, cookie_token:
|
|||||||
conversation_id=conversation.id,
|
conversation_id=conversation.id,
|
||||||
content=response
|
content=response
|
||||||
)
|
)
|
||||||
|
logger.info(
|
||||||
|
"Response sent to Chatwoot successfully",
|
||||||
|
conversation_id=conversation_id
|
||||||
|
)
|
||||||
|
|
||||||
# 关闭 typing status(隐藏"正在输入...")
|
# 关闭 typing status(隐藏"正在输入...")
|
||||||
try:
|
try:
|
||||||
|
|||||||
336
docker-compose.prod.yml
Normal file
336
docker-compose.prod.yml
Normal file
@@ -0,0 +1,336 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
# ============ Infrastructure ============
|
||||||
|
|
||||||
|
# Redis (Cache & Queue) - 生产环境配置
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
container_name: ai_redis_prod
|
||||||
|
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD:-prod_redis_password_2024}
|
||||||
|
volumes:
|
||||||
|
- redis_data_prod:/data
|
||||||
|
networks:
|
||||||
|
- ai_network_prod
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD:-prod_redis_password_2024}", "ping"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 5
|
||||||
|
restart: always
|
||||||
|
logging:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
|
|
||||||
|
# ============ AI Agent Layer ============
|
||||||
|
|
||||||
|
# LangGraph Agent Main Service - 生产环境
|
||||||
|
agent:
|
||||||
|
build:
|
||||||
|
context: ./agent
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
args:
|
||||||
|
- ENVIRONMENT=production
|
||||||
|
image: ai-agent:latest
|
||||||
|
container_name: ai_agent_prod
|
||||||
|
environment:
|
||||||
|
# AI Model
|
||||||
|
ZHIPU_API_KEY: ${ZHIPU_API_KEY}
|
||||||
|
ZHIPU_MODEL: ${ZHIPU_MODEL:-GLM-4-Flash-250414}
|
||||||
|
ENABLE_REASONING_MODE: ${ENABLE_REASONING_MODE:-false}
|
||||||
|
REASONING_MODE_FOR_COMPLEX: ${REASONING_MODE_FOR_COMPLEX:-true}
|
||||||
|
|
||||||
|
# Redis
|
||||||
|
REDIS_HOST: redis
|
||||||
|
REDIS_PORT: 6379
|
||||||
|
REDIS_PASSWORD: ${REDIS_PASSWORD:-prod_redis_password_2024}
|
||||||
|
REDIS_DB: 0
|
||||||
|
|
||||||
|
# Chatwoot (生产环境)
|
||||||
|
CHATWOOT_API_URL: ${CHATWOOT_API_URL}
|
||||||
|
CHATWOOT_API_TOKEN: ${CHATWOOT_API_TOKEN}
|
||||||
|
CHATWOOT_WEBHOOK_SECRET: ${CHATWOOT_WEBHOOK_SECRET}
|
||||||
|
CHATWOOT_ACCOUNT_ID: ${CHATWOOT_ACCOUNT_ID:-1}
|
||||||
|
|
||||||
|
# External APIs
|
||||||
|
STRAPI_API_URL: ${STRAPI_API_URL}
|
||||||
|
STRAPI_API_TOKEN: ${STRAPI_API_TOKEN}
|
||||||
|
HYPERF_API_URL: ${HYPERF_API_URL}
|
||||||
|
HYPERF_API_TOKEN: ${HYPERF_API_TOKEN}
|
||||||
|
|
||||||
|
# Mall API
|
||||||
|
MALL_API_URL: ${MALL_API_URL}
|
||||||
|
MALL_TENANT_ID: ${MALL_TENANT_ID:-2}
|
||||||
|
MALL_CURRENCY_CODE: ${MALL_CURRENCY_CODE:-EUR}
|
||||||
|
MALL_LANGUAGE_ID: ${MALL_LANGUAGE_ID:-1}
|
||||||
|
MALL_SOURCE: ${MALL_SOURCE:-www.gaia888.com}
|
||||||
|
|
||||||
|
# Frontend URLs
|
||||||
|
FRONTEND_URL: ${FRONTEND_URL:-https://www.gaia888.com}
|
||||||
|
|
||||||
|
# MCP Servers
|
||||||
|
STRAPI_MCP_URL: http://strapi_mcp:8001
|
||||||
|
ORDER_MCP_URL: http://order_mcp:8002
|
||||||
|
AFTERSALE_MCP_URL: http://aftersale_mcp:8003
|
||||||
|
PRODUCT_MCP_URL: http://product_mcp:8004
|
||||||
|
|
||||||
|
# Config
|
||||||
|
LOG_LEVEL: ${LOG_LEVEL:-WARNING}
|
||||||
|
MAX_CONVERSATION_STEPS: ${MAX_CONVERSATION_STEPS:-10}
|
||||||
|
CONVERSATION_TIMEOUT: ${CONVERSATION_TIMEOUT:-3600}
|
||||||
|
|
||||||
|
# Production specific
|
||||||
|
ENVIRONMENT: production
|
||||||
|
SENTRY_DSN: ${SENTRY_DSN}
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
volumes:
|
||||||
|
- agent_logs_prod:/app/logs
|
||||||
|
depends_on:
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
strapi_mcp:
|
||||||
|
condition: service_started
|
||||||
|
order_mcp:
|
||||||
|
condition: service_started
|
||||||
|
aftersale_mcp:
|
||||||
|
condition: service_started
|
||||||
|
product_mcp:
|
||||||
|
condition: service_started
|
||||||
|
networks:
|
||||||
|
- ai_network_prod
|
||||||
|
restart: always
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 40s
|
||||||
|
logging:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: '2'
|
||||||
|
memory: 2G
|
||||||
|
reservations:
|
||||||
|
cpus: '0.5'
|
||||||
|
memory: 512M
|
||||||
|
|
||||||
|
# ============ MCP Servers ============
|
||||||
|
|
||||||
|
# Strapi MCP (FAQ/Knowledge Base) - 生产环境
|
||||||
|
strapi_mcp:
|
||||||
|
build:
|
||||||
|
context: ./mcp_servers/strapi_mcp
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
image: ai-strapi-mcp:latest
|
||||||
|
container_name: ai_strapi_mcp_prod
|
||||||
|
environment:
|
||||||
|
STRAPI_API_URL: ${STRAPI_API_URL}
|
||||||
|
STRAPI_API_TOKEN: ${STRAPI_API_TOKEN}
|
||||||
|
LOG_LEVEL: ${LOG_LEVEL:-WARNING}
|
||||||
|
ENVIRONMENT: production
|
||||||
|
ports:
|
||||||
|
- "8001:8001"
|
||||||
|
volumes:
|
||||||
|
- ./mcp_servers/shared:/app/shared:ro
|
||||||
|
networks:
|
||||||
|
- ai_network_prod
|
||||||
|
restart: always
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:8001/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
logging:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: '0.5'
|
||||||
|
memory: 512M
|
||||||
|
reservations:
|
||||||
|
cpus: '0.25'
|
||||||
|
memory: 256M
|
||||||
|
|
||||||
|
# Order MCP - 生产环境
|
||||||
|
order_mcp:
|
||||||
|
build:
|
||||||
|
context: ./mcp_servers/order_mcp
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
image: ai-order-mcp:latest
|
||||||
|
container_name: ai_order_mcp_prod
|
||||||
|
environment:
|
||||||
|
HYPERF_API_URL: ${HYPERF_API_URL}
|
||||||
|
HYPERF_API_TOKEN: ${HYPERF_API_TOKEN}
|
||||||
|
MALL_API_URL: ${MALL_API_URL}
|
||||||
|
MALL_TENANT_ID: ${MALL_TENANT_ID:-2}
|
||||||
|
MALL_CURRENCY_CODE: ${MALL_CURRENCY_CODE:-EUR}
|
||||||
|
MALL_LANGUAGE_ID: ${MALL_LANGUAGE_ID:-1}
|
||||||
|
MALL_SOURCE: ${MALL_SOURCE:-www.gaia888.com}
|
||||||
|
LOG_LEVEL: ${LOG_LEVEL:-WARNING}
|
||||||
|
ENVIRONMENT: production
|
||||||
|
ports:
|
||||||
|
- "8002:8002"
|
||||||
|
volumes:
|
||||||
|
- ./mcp_servers/shared:/app/shared:ro
|
||||||
|
networks:
|
||||||
|
- ai_network_prod
|
||||||
|
restart: always
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:8002/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
logging:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: '0.5'
|
||||||
|
memory: 512M
|
||||||
|
reservations:
|
||||||
|
cpus: '0.25'
|
||||||
|
memory: 256M
|
||||||
|
|
||||||
|
# Aftersale MCP - 生产环境
|
||||||
|
aftersale_mcp:
|
||||||
|
build:
|
||||||
|
context: ./mcp_servers/aftersale_mcp
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
image: ai-aftersale-mcp:latest
|
||||||
|
container_name: ai_aftersale_mcp_prod
|
||||||
|
environment:
|
||||||
|
HYPERF_API_URL: ${HYPERF_API_URL}
|
||||||
|
HYPERF_API_TOKEN: ${HYPERF_API_TOKEN}
|
||||||
|
LOG_LEVEL: ${LOG_LEVEL:-WARNING}
|
||||||
|
ENVIRONMENT: production
|
||||||
|
ports:
|
||||||
|
- "8003:8003"
|
||||||
|
volumes:
|
||||||
|
- ./mcp_servers/shared:/app/shared:ro
|
||||||
|
networks:
|
||||||
|
- ai_network_prod
|
||||||
|
restart: always
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:8003/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
logging:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: '0.5'
|
||||||
|
memory: 512M
|
||||||
|
reservations:
|
||||||
|
cpus: '0.25'
|
||||||
|
memory: 256M
|
||||||
|
|
||||||
|
# Product MCP - 生产环境
|
||||||
|
product_mcp:
|
||||||
|
build:
|
||||||
|
context: ./mcp_servers/product_mcp
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
image: ai-product-mcp:latest
|
||||||
|
container_name: ai_product_mcp_prod
|
||||||
|
environment:
|
||||||
|
HYPERF_API_URL: ${HYPERF_API_URL}
|
||||||
|
HYPERF_API_TOKEN: ${HYPERF_API_TOKEN}
|
||||||
|
LOG_LEVEL: ${LOG_LEVEL:-WARNING}
|
||||||
|
ENVIRONMENT: production
|
||||||
|
ports:
|
||||||
|
- "8004:8004"
|
||||||
|
volumes:
|
||||||
|
- ./mcp_servers/shared:/app/shared:ro
|
||||||
|
networks:
|
||||||
|
- ai_network_prod
|
||||||
|
restart: always
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:8004/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
logging:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: '0.5'
|
||||||
|
memory: 512M
|
||||||
|
reservations:
|
||||||
|
cpus: '0.25'
|
||||||
|
memory: 256M
|
||||||
|
|
||||||
|
# ============ Monitoring (Optional) ============
|
||||||
|
|
||||||
|
# Prometheus - 指标收集
|
||||||
|
prometheus:
|
||||||
|
image: prom/prometheus:latest
|
||||||
|
container_name: ai_prometheus
|
||||||
|
command:
|
||||||
|
- '--config.file=/etc/prometheus/prometheus.yml'
|
||||||
|
- '--storage.tsdb.path=/prometheus'
|
||||||
|
- '--web.console.libraries=/usr/share/prometheus/console_libraries'
|
||||||
|
- '--web.console.templates=/usr/share/prometheus/consoles'
|
||||||
|
ports:
|
||||||
|
- "9090:9090"
|
||||||
|
volumes:
|
||||||
|
- ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml:ro
|
||||||
|
- prometheus_data:/prometheus
|
||||||
|
networks:
|
||||||
|
- ai_network_prod
|
||||||
|
restart: always
|
||||||
|
profiles:
|
||||||
|
- monitoring
|
||||||
|
|
||||||
|
# Grafana - 可视化监控
|
||||||
|
grafana:
|
||||||
|
image: grafana/grafana:latest
|
||||||
|
container_name: ai_grafana
|
||||||
|
environment:
|
||||||
|
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD:-admin}
|
||||||
|
- GF_USERS_ALLOW_SIGN_UP=false
|
||||||
|
ports:
|
||||||
|
- "3001:3000"
|
||||||
|
volumes:
|
||||||
|
- grafana_data:/var/lib/grafana
|
||||||
|
- ./monitoring/grafana/dashboards:/etc/grafana/provisioning/dashboards:ro
|
||||||
|
- ./monitoring/grafana/datasources:/etc/grafana/provisioning/datasources:ro
|
||||||
|
networks:
|
||||||
|
- ai_network_prod
|
||||||
|
restart: always
|
||||||
|
profiles:
|
||||||
|
- monitoring
|
||||||
|
|
||||||
|
networks:
|
||||||
|
ai_network_prod:
|
||||||
|
driver: bridge
|
||||||
|
ipam:
|
||||||
|
config:
|
||||||
|
- subnet: 172.20.0.0/16
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
redis_data_prod:
|
||||||
|
agent_logs_prod:
|
||||||
|
prometheus_data:
|
||||||
|
grafana_data:
|
||||||
@@ -149,9 +149,16 @@ services:
|
|||||||
context: ./mcp_servers/product_mcp
|
context: ./mcp_servers/product_mcp
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
container_name: ai_product_mcp
|
container_name: ai_product_mcp
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
environment:
|
environment:
|
||||||
HYPERF_API_URL: ${HYPERF_API_URL}
|
HYPERF_API_URL: ${HYPERF_API_URL}
|
||||||
HYPERF_API_TOKEN: ${HYPERF_API_TOKEN}
|
HYPERF_API_TOKEN: ${HYPERF_API_TOKEN}
|
||||||
|
MALL_API_URL: ${MALL_API_URL}
|
||||||
|
MALL_TENANT_ID: ${MALL_TENANT_ID:-2}
|
||||||
|
MALL_CURRENCY_CODE: ${MALL_CURRENCY_CODE:-EUR}
|
||||||
|
MALL_LANGUAGE_ID: ${MALL_LANGUAGE_ID:-1}
|
||||||
|
MALL_SOURCE: ${MALL_SOURCE:-us.qa1.gaia888.com}
|
||||||
LOG_LEVEL: ${LOG_LEVEL:-INFO}
|
LOG_LEVEL: ${LOG_LEVEL:-INFO}
|
||||||
ports:
|
ports:
|
||||||
- "8004:8004"
|
- "8004:8004"
|
||||||
|
|||||||
408
docs/PRODUCT_SEARCH_SERVICE.md
Normal file
408
docs/PRODUCT_SEARCH_SERVICE.md
Normal file
@@ -0,0 +1,408 @@
|
|||||||
|
# 商品搜索服务实现文档
|
||||||
|
|
||||||
|
## 功能概述
|
||||||
|
|
||||||
|
添加了基于 Mall API 的商品搜索服务,支持根据关键词搜索 SPU 商品,并以 Chatwoot cards 格式展示搜索结果。
|
||||||
|
|
||||||
|
## 技术架构
|
||||||
|
|
||||||
|
```
|
||||||
|
用户发送搜索请求
|
||||||
|
↓
|
||||||
|
Router Agent 识别商品意图
|
||||||
|
↓
|
||||||
|
Product Agent 处理
|
||||||
|
↓
|
||||||
|
调用 Product MCP 工具: search_spu_products
|
||||||
|
↓
|
||||||
|
MallClient 调用 Mall API: /mall/api/spu
|
||||||
|
↓
|
||||||
|
返回商品列表
|
||||||
|
↓
|
||||||
|
发送 Chatwoot Cards 展示商品
|
||||||
|
```
|
||||||
|
|
||||||
|
## 修改的文件
|
||||||
|
|
||||||
|
### 1. MallClient - SPU 搜索 API
|
||||||
|
**文件**: `mcp_servers/shared/mall_client.py:267-306`
|
||||||
|
|
||||||
|
**新增方法**:
|
||||||
|
```python
|
||||||
|
async def search_spu_products(
|
||||||
|
self,
|
||||||
|
keyword: str,
|
||||||
|
page_size: int = 60,
|
||||||
|
page: int = 1
|
||||||
|
) -> dict[str, Any]
|
||||||
|
```
|
||||||
|
|
||||||
|
**功能**: 调用 Mall API `/mall/api/spu` 接口搜索商品
|
||||||
|
|
||||||
|
### 2. Product MCP - SPU 搜索工具
|
||||||
|
**文件**: `mcp_servers/product_mcp/server.py:291-378`
|
||||||
|
|
||||||
|
**新增工具**: `search_spu_products`
|
||||||
|
|
||||||
|
**参数**:
|
||||||
|
- `keyword` (必需): 搜索关键词
|
||||||
|
- `page_size`: 每页数量(默认 60,最大 100)
|
||||||
|
- `page`: 页码(默认 1)
|
||||||
|
- `user_token` (必需): 用户 JWT token,用于 Mall API 认证
|
||||||
|
- `user_id`: 用户 ID(自动注入)
|
||||||
|
- `account_id`: 账户 ID(自动注入)
|
||||||
|
|
||||||
|
**返回数据格式**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"products": [
|
||||||
|
{
|
||||||
|
"spu_id": "12345",
|
||||||
|
"spu_sn": "61607",
|
||||||
|
"product_name": "Product Name",
|
||||||
|
"product_image": "https://...",
|
||||||
|
"price": "99.99",
|
||||||
|
"special_price": "89.99",
|
||||||
|
"stock": 100,
|
||||||
|
"sales_count": 50
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total": 156,
|
||||||
|
"keyword": "61607"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Chatwoot 集成 - 商品卡片发送
|
||||||
|
**文件**: `agent/integrations/chatwoot.py:1086-1243`
|
||||||
|
|
||||||
|
**新增方法**:
|
||||||
|
```python
|
||||||
|
async def send_product_cards(
|
||||||
|
self,
|
||||||
|
conversation_id: int,
|
||||||
|
products: list[dict[str, Any]],
|
||||||
|
language: str = "en"
|
||||||
|
) -> dict[str, Any]
|
||||||
|
```
|
||||||
|
|
||||||
|
**功能**: 发送商品搜索结果卡片到 Chatwoot
|
||||||
|
|
||||||
|
**卡片数据结构**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"content": "Found 3 products",
|
||||||
|
"content_type": "cards",
|
||||||
|
"content_attributes": {
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"title": "Product Name",
|
||||||
|
"description": "Special: €89.99 | Stock: 100 | Sold: 50",
|
||||||
|
"media_url": "https://...",
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"type": "link",
|
||||||
|
"text": "View Details",
|
||||||
|
"uri": "https://www.gaia888.com/product/detail?spuId=12345"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "link",
|
||||||
|
"text": "Buy Now",
|
||||||
|
"uri": "https://www.gaia888.com/product/detail?spuId=12345"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Product Agent - 搜索结果处理
|
||||||
|
**文件**: `agent/agents/product.py:206-313`
|
||||||
|
|
||||||
|
**修改内容**: 在 `_generate_product_response` 方法中添加特殊处理逻辑
|
||||||
|
|
||||||
|
**逻辑**:
|
||||||
|
1. 检测是否为 `search_spu_products` 工具返回
|
||||||
|
2. 如果是,直接调用 `send_product_cards` 发送商品卡片
|
||||||
|
3. 如果失败,降级到文本响应
|
||||||
|
|
||||||
|
### 5. Product Agent - Prompt 更新
|
||||||
|
**文件**: `agent/agents/product.py:22-52`
|
||||||
|
|
||||||
|
**修改内容**: 更新 PRODUCT_AGENT_PROMPT 可用工具列表
|
||||||
|
|
||||||
|
**更新**:
|
||||||
|
- 将 `search_spu_products` 设为第一个工具(推荐使用)
|
||||||
|
- 说明此工具使用 Mall API 搜索商品 SPU,支持用户 token 认证,返回卡片格式展示
|
||||||
|
- 原有的 `search_products` 标记为高级搜索工具(使用 Hyperf API)
|
||||||
|
|
||||||
|
### 6. Docker Compose - 环境变量配置
|
||||||
|
**文件**: `docker-compose.yml:146-170`
|
||||||
|
|
||||||
|
**修改内容**: 为 Product MCP 添加 Mall API 相关环境变量和 env_file
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
product_mcp:
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
MALL_API_URL: ${MALL_API_URL}
|
||||||
|
MALL_TENANT_ID: ${MALL_TENANT_ID:-2}
|
||||||
|
MALL_CURRENCY_CODE: ${MALL_CURRENCY_CODE:-EUR}
|
||||||
|
MALL_LANGUAGE_ID: ${MALL_LANGUAGE_ID:-1}
|
||||||
|
MALL_SOURCE: ${MALL_SOURCE:-us.qa1.gaia888.com}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用方式
|
||||||
|
|
||||||
|
### 用户在 Chatwoot 中搜索商品
|
||||||
|
|
||||||
|
**示例对话**:
|
||||||
|
```
|
||||||
|
用户: 搜索 61607
|
||||||
|
用户: 我想找手机
|
||||||
|
用户: 查找电脑产品
|
||||||
|
```
|
||||||
|
|
||||||
|
### Agent 调用流程
|
||||||
|
|
||||||
|
1. **Router Agent** 识别商品搜索意图
|
||||||
|
2. **Product Agent** 接收请求
|
||||||
|
3. **LLM** 决定调用 `search_spu_products` 工具
|
||||||
|
4. **Product MCP** 执行工具调用:
|
||||||
|
- 从 state 获取 `user_token`(用户的 JWT token)
|
||||||
|
- 创建 MallClient 实例
|
||||||
|
- 调用 Mall API `/mall/api/spu?keyword=xxx&pageSize=60&page=1`
|
||||||
|
- 解析返回结果
|
||||||
|
5. **Product Agent** 接收工具结果
|
||||||
|
6. **Chatwoot 集成** 发送商品卡片
|
||||||
|
|
||||||
|
## 商品卡片展示
|
||||||
|
|
||||||
|
### 中文界面
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────┐
|
||||||
|
│ 找到 3 个商品 │
|
||||||
|
├─────────────────────────────────┤
|
||||||
|
│ ┌─────────────────────────────┐ │
|
||||||
|
│ │ [图片] │ │
|
||||||
|
│ │ Product Name │ │
|
||||||
|
│ │ 特价: €89.99 | 库存: 100 │ │
|
||||||
|
│ │ [查看详情] [立即购买] │ │
|
||||||
|
│ └─────────────────────────────┘ │
|
||||||
|
│ ┌─────────────────────────────┐ │
|
||||||
|
│ │ [图片] │ │
|
||||||
|
│ │ Product Name 2 │ │
|
||||||
|
│ │ €99.99 | 库存: 50 │ │
|
||||||
|
│ │ [查看详情] [立即购买] │ │
|
||||||
|
│ └─────────────────────────────┘ │
|
||||||
|
└─────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 英文界面
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────┐
|
||||||
|
│ Found 3 products │
|
||||||
|
├─────────────────────────────────┤
|
||||||
|
│ ┌─────────────────────────────┐ │
|
||||||
|
│ │ [Image] │ │
|
||||||
|
│ │ Product Name │ │
|
||||||
|
│ │ Special: €89.99 | Stock: 100 │ │
|
||||||
|
│ │ [View Details] [Buy Now] │ │
|
||||||
|
│ └─────────────────────────────┘ │
|
||||||
|
└─────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## 多语言支持
|
||||||
|
|
||||||
|
商品卡片支持以下语言:
|
||||||
|
|
||||||
|
| 语言 | 代码 | 示例描述 |
|
||||||
|
|------|------|----------|
|
||||||
|
| 中文 | zh | 特价: €89.99 \| 库存: 100 |
|
||||||
|
| 英语 | en | Special: €89.99 \| Stock: 100 |
|
||||||
|
| 荷兰语 | nl | Aanbieding: €89.99 \| Voorraad: 100 |
|
||||||
|
| 德语 | de | Angebot: €89.99 \| Lager: 100 |
|
||||||
|
| 西班牙语 | es | Oferta: €89.99 \| Stock: 100 |
|
||||||
|
| 法语 | fr | Spécial: €89.99 \| Stock: 100 |
|
||||||
|
| 意大利语 | it | Offerta: €89.99 \| Stock: 100 |
|
||||||
|
| 土耳其语 | tr | Özel: €89.99 \| Stok: 100 |
|
||||||
|
|
||||||
|
## API 接口说明
|
||||||
|
|
||||||
|
### Mall API: 搜索 SPU 商品
|
||||||
|
|
||||||
|
**端点**: `GET /mall/api/spu`
|
||||||
|
|
||||||
|
**请求参数**:
|
||||||
|
```
|
||||||
|
keyword: 搜索关键词(商品名称、编号等)
|
||||||
|
pageSize: 每页数量(默认 60,最大 100)
|
||||||
|
page: 页码(默认 1)
|
||||||
|
```
|
||||||
|
|
||||||
|
**请求头**:
|
||||||
|
```
|
||||||
|
Authorization: Bearer {user_token}
|
||||||
|
Content-Type: application/json
|
||||||
|
tenant-Id: 2
|
||||||
|
currency-code: EUR
|
||||||
|
language-id: 1
|
||||||
|
source: us.qa1.gaia888.com
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应格式**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "success",
|
||||||
|
"result": {
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"spuId": "12345",
|
||||||
|
"spuSn": "61607",
|
||||||
|
"productName": "Product Name",
|
||||||
|
"productImage": "https://...",
|
||||||
|
"price": "99.99",
|
||||||
|
"specialPrice": "89.99",
|
||||||
|
"stock": 100,
|
||||||
|
"salesCount": 50
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total": 156
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 配置要求
|
||||||
|
|
||||||
|
### 环境变量
|
||||||
|
|
||||||
|
在 `.env` 文件中配置:
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Mall API
|
||||||
|
MALL_API_URL=https://apicn.qa1.gaia888.com
|
||||||
|
MALL_TENANT_ID=2
|
||||||
|
MALL_CURRENCY_CODE=EUR
|
||||||
|
MALL_LANGUAGE_ID=1
|
||||||
|
MALL_SOURCE=us.qa1.gaia888.com
|
||||||
|
|
||||||
|
# 前端 URL(用于生成商品详情链接)
|
||||||
|
FRONTEND_URL=https://www.gaia888.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### 必需条件
|
||||||
|
|
||||||
|
1. **用户认证**: 商品搜索需要用户登录,获取 JWT token
|
||||||
|
2. **Token 注入**: Agent 会自动从 Chatwoot webhook 中提取 `user_token`
|
||||||
|
3. **网络访问**: Agent 需要能够访问 Mall API(`apicn.qa1.gaia888.com`)
|
||||||
|
|
||||||
|
## 测试
|
||||||
|
|
||||||
|
### 1. 测试脚本
|
||||||
|
|
||||||
|
运行测试脚本(需要提供有效的 user token):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python test_product_search.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 在 Chatwoot 中测试
|
||||||
|
|
||||||
|
1. 打开 Chatwoot 对话框
|
||||||
|
2. 发送搜索请求,例如:
|
||||||
|
- "搜索 61607"
|
||||||
|
- "我想找手机"
|
||||||
|
- "查找商品:电脑"
|
||||||
|
|
||||||
|
### 3. 查看 MCP 工具
|
||||||
|
|
||||||
|
访问 Product MCP 健康检查:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8004/health
|
||||||
|
```
|
||||||
|
|
||||||
|
预期响应:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "healthy",
|
||||||
|
"service": "product_mcp",
|
||||||
|
"version": "1.0.0"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 故障排查
|
||||||
|
|
||||||
|
### 问题 1: 返回 "用户未登录"
|
||||||
|
|
||||||
|
**原因**: 缺少有效的 `user_token`
|
||||||
|
|
||||||
|
**解决方案**:
|
||||||
|
1. 确保用户已在 Chatwoot 中登录
|
||||||
|
2. 检查 webhook 是否正确提取 `user_token`
|
||||||
|
3. 查看日志:`docker-compose logs -f agent`
|
||||||
|
|
||||||
|
### 问题 2: 返回空商品列表
|
||||||
|
|
||||||
|
**原因**:
|
||||||
|
- 关键词不匹配
|
||||||
|
- Mall API 返回空结果
|
||||||
|
|
||||||
|
**解决方案**:
|
||||||
|
1. 尝试不同的关键词
|
||||||
|
2. 检查 Mall API 是否可访问
|
||||||
|
3. 查看 Mall API 响应日志
|
||||||
|
|
||||||
|
### 问题 3: 卡片无法显示
|
||||||
|
|
||||||
|
**原因**:
|
||||||
|
- 商品图片 URL 无效
|
||||||
|
- Chatwoot 不支持 cards 格式
|
||||||
|
|
||||||
|
**解决方案**:
|
||||||
|
1. 检查 `product_image` 字段是否为有效 URL
|
||||||
|
2. 验证 Chatwoot API 版本是否支持 cards
|
||||||
|
3. 查看 Chatwoot 集成日志
|
||||||
|
|
||||||
|
## 性能优化
|
||||||
|
|
||||||
|
### 已实现的优化
|
||||||
|
|
||||||
|
1. **分页限制**: 默认返回 60 个商品,避免数据过大
|
||||||
|
2. **用户认证**: 使用用户 token 而不是全局 API token,更安全
|
||||||
|
3. **错误处理**: 优雅降级到文本响应
|
||||||
|
|
||||||
|
### 未来可优化
|
||||||
|
|
||||||
|
1. **缓存热门搜索**: 缓存常见关键词的搜索结果
|
||||||
|
2. **并行搜索**: 支持多关键词并行搜索
|
||||||
|
3. **智能推荐**: 基于搜索历史智能推荐
|
||||||
|
|
||||||
|
## 相关文件清单
|
||||||
|
|
||||||
|
| 文件 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `mcp_servers/shared/mall_client.py` | Mall API 客户端(新增 SPU 搜索方法) |
|
||||||
|
| `mcp_servers/product_mcp/server.py` | Product MCP(新增 SPU 搜索工具) |
|
||||||
|
| `agent/integrations/chatwoot.py` | Chatwoot 集成(新增商品卡片方法) |
|
||||||
|
| `agent/agents/product.py` | Product Agent(新增卡片处理逻辑) |
|
||||||
|
| `docker-compose.yml` | 容器配置(Product MCP 环境变量) |
|
||||||
|
|
||||||
|
## 版本历史
|
||||||
|
|
||||||
|
- **2026-01-26**: 初始版本
|
||||||
|
- 添加 Mall API SPU 搜索支持
|
||||||
|
- 添加 Chatwoot cards 商品展示
|
||||||
|
- 支持多语言商品卡片
|
||||||
|
- 集成用户认证
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**文档版本**: 1.0
|
||||||
|
**最后更新**: 2026-01-26
|
||||||
|
**维护者**: Claude Code
|
||||||
@@ -1,337 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="zh-CN">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>B2B AI 助手 - 调试版本</title>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
||||||
max-width: 1400px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 20px;
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
.container {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
.left-panel, .right-panel {
|
|
||||||
background: white;
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 20px;
|
|
||||||
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
|
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
color: #333;
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
grid-column: 1 / -1;
|
|
||||||
}
|
|
||||||
.subtitle {
|
|
||||||
text-align: center;
|
|
||||||
color: #666;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
grid-column: 1 / -1;
|
|
||||||
}
|
|
||||||
h2 {
|
|
||||||
color: #667eea;
|
|
||||||
border-bottom: 2px solid #667eea;
|
|
||||||
padding-bottom: 10px;
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
.log-container {
|
|
||||||
background: #1e1e1e;
|
|
||||||
color: #00ff00;
|
|
||||||
padding: 15px;
|
|
||||||
border-radius: 5px;
|
|
||||||
height: 400px;
|
|
||||||
overflow-y: auto;
|
|
||||||
font-family: 'Courier New', monospace;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
.log-entry {
|
|
||||||
margin: 5px 0;
|
|
||||||
padding: 3px 0;
|
|
||||||
}
|
|
||||||
.log-info { color: #00ff00; }
|
|
||||||
.log-warn { color: #ffaa00; }
|
|
||||||
.log-error { color: #ff4444; }
|
|
||||||
.log-success { color: #44ff44; }
|
|
||||||
.status-box {
|
|
||||||
background: #f8f9fa;
|
|
||||||
border-left: 4px solid #667eea;
|
|
||||||
padding: 15px;
|
|
||||||
margin: 10px 0;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
.status-item {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 8px 0;
|
|
||||||
border-bottom: 1px solid #ddd;
|
|
||||||
}
|
|
||||||
.status-item:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
.status-label {
|
|
||||||
font-weight: bold;
|
|
||||||
color: #667eea;
|
|
||||||
}
|
|
||||||
.status-value {
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
.test-buttons {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
margin: 15px 0;
|
|
||||||
}
|
|
||||||
.test-btn {
|
|
||||||
background: #667eea;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 10px 20px;
|
|
||||||
border-radius: 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 14px;
|
|
||||||
transition: background 0.3s;
|
|
||||||
}
|
|
||||||
.test-btn:hover {
|
|
||||||
background: #5568d3;
|
|
||||||
}
|
|
||||||
.test-btn:disabled {
|
|
||||||
background: #ccc;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
.clear-btn {
|
|
||||||
background: #ff4444;
|
|
||||||
}
|
|
||||||
.clear-btn:hover {
|
|
||||||
background: #dd3333;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>🤖 B2B AI 智能客服助手 - 调试面板</h1>
|
|
||||||
<p class="subtitle">实时监控 Widget 状态和消息流</p>
|
|
||||||
|
|
||||||
<div class="container">
|
|
||||||
<div class="left-panel">
|
|
||||||
<h2>📊 连接状态</h2>
|
|
||||||
<div class="status-box">
|
|
||||||
<div class="status-item">
|
|
||||||
<span class="status-label">Chatwoot 服务:</span>
|
|
||||||
<span class="status-value" id="chatwootStatus">检查中...</span>
|
|
||||||
</div>
|
|
||||||
<div class="status-item">
|
|
||||||
<span class="status-label">Widget SDK:</span>
|
|
||||||
<span class="status-value" id="widgetStatus">未加载</span>
|
|
||||||
</div>
|
|
||||||
<div class="status-item">
|
|
||||||
<span class="status-label">WebSocket:</span>
|
|
||||||
<span class="status-value" id="wsStatus">未连接</span>
|
|
||||||
</div>
|
|
||||||
<div class="status-item">
|
|
||||||
<span class="status-label">当前会话:</span>
|
|
||||||
<span class="status-value" id="conversationId">无</span>
|
|
||||||
</div>
|
|
||||||
<div class="status-item">
|
|
||||||
<span class="status-label">Website Token:</span>
|
|
||||||
<span class="status-value" id="websiteToken">39PNCMvbMk3NvB7uaDNucc6o</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2>🧪 测试操作</h2>
|
|
||||||
<div class="test-buttons">
|
|
||||||
<button class="test-btn" onclick="checkChatwootService()">检查服务</button>
|
|
||||||
<button class="test-btn" onclick="refreshWidget()">刷新 Widget</button>
|
|
||||||
<button class="test-btn" onclick="getConversationInfo()">获取会话信息</button>
|
|
||||||
<button class="test-btn clear-btn" onclick="clearLogs()">清除日志</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2>📝 快速测试问题(点击复制到剪贴板)</h2>
|
|
||||||
<div class="test-buttons">
|
|
||||||
<button class="test-btn" onclick="sendTestMessage('你好')">👋 你好</button>
|
|
||||||
<button class="test-btn" onclick="sendTestMessage('查询订单 202071324')">📦 查询订单</button>
|
|
||||||
<button class="test-btn" onclick="sendTestMessage('如何退货?')">❓ 如何退货</button>
|
|
||||||
<button class="test-btn" onclick="sendTestMessage('营业时间')">🕐 营业时间</button>
|
|
||||||
</div>
|
|
||||||
<p style="color: #666; font-size: 14px; margin-top: 10px;">
|
|
||||||
💡 提示:点击按钮后,在右下角聊天窗口中按 Ctrl+V 粘贴并发送
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="right-panel">
|
|
||||||
<h2>📋 实时日志</h2>
|
|
||||||
<div class="log-container" id="logContainer">
|
|
||||||
<div class="log-entry log-info">[系统] 日志系统已启动...</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const logContainer = document.getElementById('logContainer');
|
|
||||||
|
|
||||||
function addLog(message, type = 'info') {
|
|
||||||
const timestamp = new Date().toLocaleTimeString();
|
|
||||||
const logEntry = document.createElement('div');
|
|
||||||
logEntry.className = `log-entry log-${type}`;
|
|
||||||
logEntry.textContent = `[${timestamp}] ${message}`;
|
|
||||||
logContainer.appendChild(logEntry);
|
|
||||||
logContainer.scrollTop = logContainer.scrollHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearLogs() {
|
|
||||||
logContainer.innerHTML = '<div class="log-entry log-info">[系统] 日志已清除</div>';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查 Chatwoot 服务
|
|
||||||
async function checkChatwootService() {
|
|
||||||
addLog('检查 Chatwoot 服务状态...', 'info');
|
|
||||||
const statusEl = document.getElementById('chatwootStatus');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch('http://localhost:3000', { mode: 'no-cors' });
|
|
||||||
statusEl.textContent = '✅ 运行中';
|
|
||||||
statusEl.style.color = '#28a745';
|
|
||||||
addLog('✅ Chatwoot 服务运行正常', 'success');
|
|
||||||
} catch (error) {
|
|
||||||
statusEl.textContent = '❌ 无法访问';
|
|
||||||
statusEl.style.color = '#dc3545';
|
|
||||||
addLog(`❌ 无法连接到 Chatwoot: ${error.message}`, 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 发送测试消息 - 直接复制到剪贴板
|
|
||||||
function sendTestMessage(message) {
|
|
||||||
addLog(`📋 已复制消息到剪贴板: "${message}"`, 'info');
|
|
||||||
addLog('→ 请在右下角聊天窗口中粘贴并发送', 'warn');
|
|
||||||
|
|
||||||
// 复制到剪贴板
|
|
||||||
navigator.clipboard.writeText(message).then(() => {
|
|
||||||
// 可选:自动打开 Widget
|
|
||||||
if (window.$chatwoot && window.$chatwoot.toggle) {
|
|
||||||
try {
|
|
||||||
window.$chatwoot.toggle('open');
|
|
||||||
addLog('✅ 聊天窗口已打开', 'success');
|
|
||||||
} catch (e) {
|
|
||||||
addLog('⚠️ 无法自动打开聊天窗口', 'warn');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}).catch(err => {
|
|
||||||
addLog(`❌ 复制失败: ${err.message}`, 'error');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 刷新 Widget
|
|
||||||
function refreshWidget() {
|
|
||||||
addLog('刷新 Widget...', 'info');
|
|
||||||
location.reload();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取会话信息
|
|
||||||
function getConversationInfo() {
|
|
||||||
if (window.$chatwoot) {
|
|
||||||
try {
|
|
||||||
const info = window.$chatwoot.getConversationInfo();
|
|
||||||
addLog(`会话信息: ${JSON.stringify(info)}`, 'info');
|
|
||||||
} catch (error) {
|
|
||||||
addLog(`无法获取会话信息: ${error.message}`, 'warn');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 页面加载时检查服务
|
|
||||||
window.addEventListener('load', function() {
|
|
||||||
setTimeout(checkChatwootService, 1000);
|
|
||||||
});
|
|
||||||
|
|
||||||
// ==================== Chatwoot Widget 配置 ====================
|
|
||||||
|
|
||||||
window.chatwootSettings = {
|
|
||||||
"position": "right",
|
|
||||||
"type": "expanded_bubble",
|
|
||||||
"launcherTitle": "Chat with us"
|
|
||||||
};
|
|
||||||
|
|
||||||
(function(d,t) {
|
|
||||||
var BASE_URL = "http://localhost:3000";
|
|
||||||
var g = d.createElement(t), s = d.getElementsByTagName(t)[0];
|
|
||||||
g.src = BASE_URL + "/packs/js/sdk.js";
|
|
||||||
g.async = true;
|
|
||||||
g.onload = function() {
|
|
||||||
addLog('Chatwoot SDK 文件已加载', 'success');
|
|
||||||
document.getElementById('widgetStatus').textContent = '✅ 已加载';
|
|
||||||
|
|
||||||
window.chatwootSDK.run({
|
|
||||||
websiteToken: '39PNCMvbMk3NvB7uaDNucc6o',
|
|
||||||
baseUrl: BASE_URL
|
|
||||||
});
|
|
||||||
|
|
||||||
addLog('Website Token: 39PNCMvbMk3NvB7uaDNucc6o', 'info');
|
|
||||||
addLog('Base URL: ' + BASE_URL, 'info');
|
|
||||||
|
|
||||||
// 监听 Widget 就绪事件
|
|
||||||
setTimeout(function() {
|
|
||||||
if (window.$chatwoot) {
|
|
||||||
addLog('✅ Chatwoot Widget 已初始化', 'success');
|
|
||||||
document.getElementById('wsStatus').textContent = '✅ 已连接';
|
|
||||||
|
|
||||||
// 设置用户信息(可选)
|
|
||||||
window.$chatwoot.setUser('debug_user_' + Date.now(), {
|
|
||||||
email: 'debug@example.com',
|
|
||||||
name: 'Debug User'
|
|
||||||
});
|
|
||||||
|
|
||||||
addLog('用户信息已设置', 'info');
|
|
||||||
} else {
|
|
||||||
addLog('❌ Widget 初始化失败', 'error');
|
|
||||||
document.getElementById('widgetStatus').textContent = '❌ 初始化失败';
|
|
||||||
}
|
|
||||||
}, 2000);
|
|
||||||
};
|
|
||||||
|
|
||||||
g.onerror = function() {
|
|
||||||
addLog('❌ Chatwoot SDK 加载失败', 'error');
|
|
||||||
document.getElementById('widgetStatus').textContent = '❌ 加载失败';
|
|
||||||
};
|
|
||||||
|
|
||||||
s.parentNode.insertBefore(g, s);
|
|
||||||
})(document, "script");
|
|
||||||
|
|
||||||
// 监听网络错误
|
|
||||||
window.addEventListener('error', function(e) {
|
|
||||||
if (e.message.includes('404')) {
|
|
||||||
addLog(`⚠️ 404 错误: ${e.filename}`, 'warn');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 拦截 fetch 请求
|
|
||||||
const originalFetch = window.fetch;
|
|
||||||
window.fetch = function(...args) {
|
|
||||||
const url = args[0];
|
|
||||||
|
|
||||||
// 记录发送到 Chatwoot API 的请求
|
|
||||||
if (typeof url === 'string' && url.includes('localhost:3000')) {
|
|
||||||
const method = args[1]?.method || 'GET';
|
|
||||||
addLog(`API 请求: ${method} ${url}`, 'info');
|
|
||||||
}
|
|
||||||
|
|
||||||
return originalFetch.apply(this, args).then(response => {
|
|
||||||
// 记录错误响应
|
|
||||||
if (!response.ok && url.includes('localhost:3000')) {
|
|
||||||
addLog(`API 响应: ${response.status} ${response.statusText} - ${url}`, 'error');
|
|
||||||
}
|
|
||||||
return response;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
addLog('调试系统已初始化', 'success');
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- Chatwoot Widget 会自动加载 -->
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,262 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="zh-CN">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>B2B AI 助手 - 测试页面</title>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 20px;
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
.container {
|
|
||||||
background: white;
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 40px;
|
|
||||||
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
|
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
color: #333;
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
.subtitle {
|
|
||||||
text-align: center;
|
|
||||||
color: #666;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
}
|
|
||||||
.info-box {
|
|
||||||
background: #f8f9fa;
|
|
||||||
border-left: 4px solid #667eea;
|
|
||||||
padding: 15px 20px;
|
|
||||||
margin: 20px 0;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
.info-box h3 {
|
|
||||||
margin-top: 0;
|
|
||||||
color: #667eea;
|
|
||||||
}
|
|
||||||
.feature-list {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
||||||
gap: 20px;
|
|
||||||
margin: 30px 0;
|
|
||||||
}
|
|
||||||
.feature-card {
|
|
||||||
background: #f8f9fa;
|
|
||||||
padding: 20px;
|
|
||||||
border-radius: 8px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.feature-card h4 {
|
|
||||||
color: #667eea;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
.test-questions {
|
|
||||||
background: #fff3cd;
|
|
||||||
border: 1px solid #ffc107;
|
|
||||||
padding: 20px;
|
|
||||||
border-radius: 5px;
|
|
||||||
margin: 20px 0;
|
|
||||||
}
|
|
||||||
.test-questions h3 {
|
|
||||||
margin-top: 0;
|
|
||||||
color: #856404;
|
|
||||||
}
|
|
||||||
.question-list {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
.question-list li {
|
|
||||||
background: white;
|
|
||||||
margin: 10px 0;
|
|
||||||
padding: 12px 15px;
|
|
||||||
border-radius: 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
}
|
|
||||||
.question-list li:hover {
|
|
||||||
background: #667eea;
|
|
||||||
color: white;
|
|
||||||
transform: translateX(5px);
|
|
||||||
}
|
|
||||||
.status {
|
|
||||||
text-align: center;
|
|
||||||
padding: 15px;
|
|
||||||
border-radius: 5px;
|
|
||||||
margin: 20px 0;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
.status.online {
|
|
||||||
background: #d4edda;
|
|
||||||
color: #155724;
|
|
||||||
}
|
|
||||||
.status.testing {
|
|
||||||
background: #fff3cd;
|
|
||||||
color: #856404;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<h1>🤖 B2B AI 智能客服助手</h1>
|
|
||||||
<p class="subtitle">基于 LangGraph + MCP 的智能客服系统</p>
|
|
||||||
|
|
||||||
<div class="status online">
|
|
||||||
✅ 系统状态:所有服务运行正常
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="tokenStatus" class="status testing" style="display: none;">
|
|
||||||
🍪 Token 状态:检测中...
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info-box">
|
|
||||||
<h3>📝 如何测试</h3>
|
|
||||||
<ol>
|
|
||||||
<li>点击右下角的聊天图标打开对话窗口</li>
|
|
||||||
<li>输入你的名字开始对话</li>
|
|
||||||
<li>尝试下面的问题测试 AI 能力</li>
|
|
||||||
<li>查看 AI 如何理解并回答你的问题</li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="test-questions">
|
|
||||||
<h3>💬 推荐测试问题</h3>
|
|
||||||
<p style="color: #666; margin-bottom: 15px;">点击以下问题直接复制到聊天窗口:</p>
|
|
||||||
<ul class="question-list">
|
|
||||||
<li onclick="copyQuestion(this.textContent)">🕐 你们的营业时间是什么?</li>
|
|
||||||
<li onclick="copyQuestion(this.textContent)">📦 我的订单 202071324 怎么样了?</li>
|
|
||||||
<li onclick="copyQuestion(this.textContent)">🔍 查询订单 202071324</li>
|
|
||||||
<li onclick="copyQuestion(this.textContent)">📞 如何联系客服?</li>
|
|
||||||
<li onclick="copyQuestion(this.textContent)">🛍️ 我想退换货</li>
|
|
||||||
<li onclick="copyQuestion(this.textContent)">📦 订单 202071324 的物流信息</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="feature-list">
|
|
||||||
<div class="feature-card">
|
|
||||||
<h4>🎯 智能意图识别</h4>
|
|
||||||
<p>自动识别客户需求并分类</p>
|
|
||||||
</div>
|
|
||||||
<div class="feature-card">
|
|
||||||
<h4>📚 知识库查询</h4>
|
|
||||||
<p>快速检索 FAQ 和政策文档</p>
|
|
||||||
</div>
|
|
||||||
<div class="feature-card">
|
|
||||||
<h4>📦 订单管理</h4>
|
|
||||||
<p>查询订单、售后等服务</p>
|
|
||||||
</div>
|
|
||||||
<div class="feature-card">
|
|
||||||
<h4>🔄 多轮对话</h4>
|
|
||||||
<p>支持上下文理解的连续对话</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info-box">
|
|
||||||
<h3>🔧 技术栈</h3>
|
|
||||||
<ul>
|
|
||||||
<li><strong>前端:</strong>Chatwoot 客户支持平台</li>
|
|
||||||
<li><strong>AI 引擎:</strong>LangGraph + 智谱 AI (GLM-4.5)</li>
|
|
||||||
<li><strong>知识库:</strong>Strapi CMS + MCP</li>
|
|
||||||
<li><strong>业务系统:</strong>Hyperf PHP API</li>
|
|
||||||
<li><strong>缓存:</strong>Redis</li>
|
|
||||||
<li><strong>容器:</strong>Docker Compose</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
function copyQuestion(text) {
|
|
||||||
// 移除表情符号
|
|
||||||
const cleanText = text.replace(/^[^\s]+\s*/, '');
|
|
||||||
navigator.clipboard.writeText(cleanText).then(() => {
|
|
||||||
alert('问题已复制!请粘贴到聊天窗口中发送。');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== Cookie Token 读取 ====================
|
|
||||||
|
|
||||||
function getCookie(name) {
|
|
||||||
const value = `; ${document.cookie}`;
|
|
||||||
const parts = value.split(`; ${name}=`);
|
|
||||||
if (parts.length === 2) return parts.pop().split(";").shift();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkToken() {
|
|
||||||
const token = getCookie('token');
|
|
||||||
const statusDiv = document.getElementById('tokenStatus');
|
|
||||||
|
|
||||||
if (token) {
|
|
||||||
statusDiv.style.display = 'block';
|
|
||||||
statusDiv.className = 'status online';
|
|
||||||
statusDiv.innerHTML = `✅ Token 已找到 | 长度: ${token.length} 字符 | 前缀: ${token.substring(0, 20)}...`;
|
|
||||||
// 存储到 window 供后续使用
|
|
||||||
window._chatwootUserToken = token;
|
|
||||||
console.log('✅ Token 已从 Cookie 读取');
|
|
||||||
} else {
|
|
||||||
statusDiv.style.display = 'block';
|
|
||||||
statusDiv.className = 'status testing';
|
|
||||||
statusDiv.innerHTML = '⚠️ 未找到 Token | 请确保已登录商城 | Cookie 名称: token';
|
|
||||||
console.warn('⚠️ 未找到 Token,订单查询功能可能无法使用');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 页面加载时检查 Token
|
|
||||||
window.addEventListener('load', function() {
|
|
||||||
setTimeout(checkToken, 1000);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- Chatwoot Widget - 官方集成方式 -->
|
|
||||||
<script>
|
|
||||||
(function(d,t) {
|
|
||||||
var BASE_URL="http://localhost:3000";
|
|
||||||
var g=d.createElement(t),s=d.getElementsByTagName(t)[0];
|
|
||||||
g.src=BASE_URL+"/packs/js/sdk.js";
|
|
||||||
g.async = true;
|
|
||||||
s.parentNode.insertBefore(g,s);
|
|
||||||
g.onload=function(){
|
|
||||||
window.chatwootSDK.run({
|
|
||||||
websiteToken: '39PNCMvbMk3NvB7uaDNucc6o',
|
|
||||||
baseUrl: BASE_URL,
|
|
||||||
locale: 'zh_CN',
|
|
||||||
userIdentifier: getCookie('token') || 'web_user_' + Date.now()
|
|
||||||
});
|
|
||||||
|
|
||||||
const userToken = getCookie('token');
|
|
||||||
console.log('✅ Chatwoot Widget 已加载 (官方集成方式)');
|
|
||||||
console.log('Base URL:', BASE_URL);
|
|
||||||
console.log('Website Token: 39PNCMvbMk3NvB7uaDNucc6o');
|
|
||||||
console.log('Locale: zh_CN');
|
|
||||||
console.log('User Identifier:', userToken || 'web_user_' + Date.now());
|
|
||||||
|
|
||||||
// 设置用户信息(可选)
|
|
||||||
setTimeout(function() {
|
|
||||||
const token = getCookie('token');
|
|
||||||
if (token && window.$chatwoot) {
|
|
||||||
window.$chatwoot.setUser('user_' + Date.now(), {
|
|
||||||
email: 'user@example.com',
|
|
||||||
name: 'Website User',
|
|
||||||
phone_number: ''
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('✅ 用户信息已设置');
|
|
||||||
} else if (!token) {
|
|
||||||
console.warn('⚠️ 未找到 Token');
|
|
||||||
}
|
|
||||||
}, 2000);
|
|
||||||
}
|
|
||||||
g.onerror=function(){
|
|
||||||
console.error('❌ Chatwoot SDK 加载失败');
|
|
||||||
console.error('请确保 Chatwoot 运行在: ' + BASE_URL);
|
|
||||||
}
|
|
||||||
})(document,"script");
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,425 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="zh-CN">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>会话 ID 检查工具</title>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
||||||
max-width: 800px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 20px;
|
|
||||||
background: #f5f5f5;
|
|
||||||
}
|
|
||||||
.container {
|
|
||||||
background: white;
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 30px;
|
|
||||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
color: #333;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.info-box {
|
|
||||||
background: #e7f3ff;
|
|
||||||
border-left: 4px solid #2196F3;
|
|
||||||
padding: 15px 20px;
|
|
||||||
margin: 20px 0;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
.info-box h3 {
|
|
||||||
margin-top: 0;
|
|
||||||
color: #2196F3;
|
|
||||||
}
|
|
||||||
.data-display {
|
|
||||||
background: #f8f9fa;
|
|
||||||
border: 1px solid #dee2e6;
|
|
||||||
border-radius: 5px;
|
|
||||||
padding: 15px;
|
|
||||||
margin: 15px 0;
|
|
||||||
font-family: 'Courier New', monospace;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
.data-label {
|
|
||||||
font-weight: bold;
|
|
||||||
color: #495057;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
.data-value {
|
|
||||||
color: #212529;
|
|
||||||
background: white;
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 3px;
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
button {
|
|
||||||
background: #2196F3;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 10px 20px;
|
|
||||||
border-radius: 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 14px;
|
|
||||||
margin: 5px;
|
|
||||||
}
|
|
||||||
button:hover {
|
|
||||||
background: #0b7dda;
|
|
||||||
}
|
|
||||||
button.danger {
|
|
||||||
background: #dc3545;
|
|
||||||
}
|
|
||||||
button.danger:hover {
|
|
||||||
background: #c82333;
|
|
||||||
}
|
|
||||||
.instructions {
|
|
||||||
background: #fff3cd;
|
|
||||||
border-left: 4px solid #ffc107;
|
|
||||||
padding: 15px 20px;
|
|
||||||
margin: 20px 0;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
.instructions ol {
|
|
||||||
margin: 10px 0;
|
|
||||||
padding-left: 20px;
|
|
||||||
}
|
|
||||||
.instructions li {
|
|
||||||
margin: 8px 0;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<h1>🔍 Chatwoot 会话 ID 检查工具</h1>
|
|
||||||
|
|
||||||
<div class="instructions">
|
|
||||||
<h3>📝 使用说明</h3>
|
|
||||||
<ol>
|
|
||||||
<li>打开浏览器开发者工具(按 F12)</li>
|
|
||||||
<li>切换到 Console(控制台)标签</li>
|
|
||||||
<li>点击下面的"显示会话信息"按钮</li>
|
|
||||||
<li>在 Console 中查看当前的 conversation_id</li>
|
|
||||||
<li>将这个 ID 与 Agent 日志中的 conversation_id 对比</li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info-box">
|
|
||||||
<h3>🎯 操作按钮</h3>
|
|
||||||
<button onclick="showConversationInfo()">显示会话信息</button>
|
|
||||||
<button onclick="checkWidgetStatus()">检查 Widget 状态</button>
|
|
||||||
<button onclick="checkToken()">检查 Token</button>
|
|
||||||
<button onclick="testOrderAPI()">测试订单 API</button>
|
|
||||||
<button onclick="clearLocalStorage()" class="danger">清除本地存储(重新开始)</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info-box">
|
|
||||||
<h3>📊 信息显示</h3>
|
|
||||||
<div class="data-display">
|
|
||||||
<div class="data-label">Widget SDK 状态:</div>
|
|
||||||
<div class="data-value" id="widgetStatus">未初始化</div>
|
|
||||||
</div>
|
|
||||||
<div class="data-display">
|
|
||||||
<div class="data-label">当前会话 ID:</div>
|
|
||||||
<div class="data-value" id="conversationId">未知</div>
|
|
||||||
</div>
|
|
||||||
<div class="data-display">
|
|
||||||
<div class="data-label">Token 状态:</div>
|
|
||||||
<div class="data-value" id="tokenStatus">未检查</div>
|
|
||||||
</div>
|
|
||||||
<div class="data-display">
|
|
||||||
<div class="data-label">订单 API 测试结果:</div>
|
|
||||||
<div class="data-value" id="orderApiResult">未测试</div>
|
|
||||||
</div>
|
|
||||||
<div class="data-display">
|
|
||||||
<div class="data-label">本地存储数据:</div>
|
|
||||||
<div class="data-value" id="localStorageData">无</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="instructions">
|
|
||||||
<h3>💡 问题排查</h3>
|
|
||||||
<p><strong>如果看不到 AI 回复:</strong></p>
|
|
||||||
<ol>
|
|
||||||
<li>点击"清除本地存储"按钮</li>
|
|
||||||
<li>刷新页面(Ctrl+Shift+R)</li>
|
|
||||||
<li>在右下角聊天窗口重新发送消息</li>
|
|
||||||
<li>查看 Agent 日志: <code>docker logs ai_agent --tail 50</code></li>
|
|
||||||
<li>对比 Console 中的 conversation_id 与日志中的是否一致</li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// 获取 Cookie 中的 token
|
|
||||||
function getCookie(name) {
|
|
||||||
const value = `; ${document.cookie}`;
|
|
||||||
const parts = value.split(`; ${name}=`);
|
|
||||||
if (parts.length === 2) return parts.pop().split(";").shift();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查 Token
|
|
||||||
function checkToken() {
|
|
||||||
console.log('======================================');
|
|
||||||
console.log('Token 检查');
|
|
||||||
console.log('======================================');
|
|
||||||
|
|
||||||
const token = getCookie('token');
|
|
||||||
const tokenStatusDiv = document.getElementById('tokenStatus');
|
|
||||||
|
|
||||||
if (token) {
|
|
||||||
console.log('✅ Token 已找到');
|
|
||||||
console.log('Token 长度:', token.length);
|
|
||||||
console.log('Token 前缀:', token.substring(0, 50) + '...');
|
|
||||||
tokenStatusDiv.textContent = `✅ 已找到 | 长度: ${token.length} | 前缀: ${token.substring(0, 30)}...`;
|
|
||||||
tokenStatusDiv.style.color = '#28a745';
|
|
||||||
} else {
|
|
||||||
console.log('❌ 未找到 Token');
|
|
||||||
console.log('Cookie 名称: token');
|
|
||||||
tokenStatusDiv.textContent = '❌ 未找到 | Cookie 名称: token';
|
|
||||||
tokenStatusDiv.style.color = '#dc3545';
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('所有 Cookie:', document.cookie);
|
|
||||||
console.log('======================================');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 测试订单 API
|
|
||||||
async function testOrderAPI() {
|
|
||||||
console.log('======================================');
|
|
||||||
console.log('测试订单 API');
|
|
||||||
console.log('======================================');
|
|
||||||
|
|
||||||
const token = getCookie('token');
|
|
||||||
const resultDiv = document.getElementById('orderApiResult');
|
|
||||||
|
|
||||||
if (!token) {
|
|
||||||
console.error('❌ 未找到 Token,无法调用 API');
|
|
||||||
resultDiv.textContent = '❌ 未找到 Token';
|
|
||||||
resultDiv.style.color = '#dc3545';
|
|
||||||
alert('❌ 未找到 Token,请先确保已登录商城');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const orderId = '202071324';
|
|
||||||
const apiUrl = `https://apicn.qa1.gaia888.com/mall/api/order/show?orderId=${orderId}`;
|
|
||||||
|
|
||||||
console.log('API URL:', apiUrl);
|
|
||||||
console.log('Authorization:', `Bearer ${token.substring(0, 30)}...`);
|
|
||||||
|
|
||||||
resultDiv.textContent = '🔄 请求中...';
|
|
||||||
resultDiv.style.color = '#ffc107';
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(apiUrl, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'accept': 'application/json, text/plain, */*',
|
|
||||||
'accept-language': 'zh-CN,zh;q=0.9',
|
|
||||||
'authorization': `Bearer ${token}`,
|
|
||||||
'currency-code': 'EUR',
|
|
||||||
'device-type': 'pc',
|
|
||||||
'language-id': '1',
|
|
||||||
'origin': 'https://www.qa1.gaia888.com',
|
|
||||||
'referer': 'https://www.qa1.gaia888.com/',
|
|
||||||
'sec-fetch-dest': 'empty',
|
|
||||||
'sec-fetch-mode': 'cors',
|
|
||||||
'sec-fetch-site': 'same-site',
|
|
||||||
'source': 'us.qa1.gaia888.com',
|
|
||||||
'tenant-id': '2'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('响应状态:', response.status);
|
|
||||||
console.log('响应头:', Object.fromEntries(response.headers.entries()));
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json();
|
|
||||||
console.log('✅ API 调用成功');
|
|
||||||
console.log('响应数据:', data);
|
|
||||||
|
|
||||||
resultDiv.textContent = `✅ 成功 (HTTP ${response.status}) | 订单 ${orderId}`;
|
|
||||||
resultDiv.style.color = '#28a745';
|
|
||||||
|
|
||||||
alert(`✅ 订单 API 调用成功!\n\n订单 ID: ${orderId}\n状态码: ${response.status}\n\n详细数据请查看控制台`);
|
|
||||||
} else {
|
|
||||||
const errorText = await response.text();
|
|
||||||
console.error('❌ API 调用失败');
|
|
||||||
console.error('状态码:', response.status);
|
|
||||||
console.error('响应内容:', errorText);
|
|
||||||
|
|
||||||
resultDiv.textContent = `❌ 失败 (HTTP ${response.status})`;
|
|
||||||
resultDiv.style.color = '#dc3545';
|
|
||||||
|
|
||||||
alert(`❌ 订单 API 调用失败\n\n状态码: ${response.status}\n错误: ${errorText}`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ 网络错误:', error);
|
|
||||||
resultDiv.textContent = `❌ 网络错误: ${error.message}`;
|
|
||||||
resultDiv.style.color = '#dc3545';
|
|
||||||
alert(`❌ 网络错误\n\n${error.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('======================================');
|
|
||||||
}
|
|
||||||
|
|
||||||
function showConversationInfo() {
|
|
||||||
console.log('======================================');
|
|
||||||
console.log('Chatwoot Widget 会话信息');
|
|
||||||
console.log('======================================');
|
|
||||||
|
|
||||||
if (window.$chatwoot) {
|
|
||||||
try {
|
|
||||||
// 尝试获取会话信息
|
|
||||||
const info = window.$chatwoot.getConversationInfo();
|
|
||||||
console.log('✅ 会话信息:', info);
|
|
||||||
document.getElementById('conversationId').textContent =
|
|
||||||
info && info.conversationId ? info.conversationId : '无法获取';
|
|
||||||
} catch (e) {
|
|
||||||
console.log('⚠️ 无法获取会话信息:', e.message);
|
|
||||||
document.getElementById('conversationId').textContent = '无法获取';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log('❌ Widget 未初始化');
|
|
||||||
document.getElementById('conversationId').textContent = 'Widget 未初始化';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示本地存储
|
|
||||||
const storage = {};
|
|
||||||
for (let i = 0; i < localStorage.length; i++) {
|
|
||||||
const key = localStorage.key(i);
|
|
||||||
if (key.includes('chatwoot') || key.includes('widget')) {
|
|
||||||
storage[key] = localStorage.getItem(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
console.log('本地存储 (Chatwoot 相关):', storage);
|
|
||||||
document.getElementById('localStorageData').textContent =
|
|
||||||
Object.keys(storage).length > 0 ? JSON.stringify(storage, null, 2) : '无';
|
|
||||||
|
|
||||||
console.log('======================================');
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkWidgetStatus() {
|
|
||||||
console.log('======================================');
|
|
||||||
console.log('Widget 状态检查');
|
|
||||||
console.log('======================================');
|
|
||||||
console.log('window.$chatwoot:', window.$chatwoot);
|
|
||||||
console.log('window.chatwootSDK:', window.chatwootSDK);
|
|
||||||
|
|
||||||
if (window.$chatwoot) {
|
|
||||||
console.log('✅ Widget 已加载');
|
|
||||||
console.log('可用方法:', Object.getOwnPropertyNames(window.$chatwoot));
|
|
||||||
document.getElementById('widgetStatus').textContent = '✅ 已加载';
|
|
||||||
document.getElementById('widgetStatus').style.color = '#28a745';
|
|
||||||
} else {
|
|
||||||
console.log('❌ Widget 未加载');
|
|
||||||
document.getElementById('widgetStatus').textContent = '❌ 未加载';
|
|
||||||
document.getElementById('widgetStatus').style.color = '#dc3545';
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('======================================');
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearLocalStorage() {
|
|
||||||
if (confirm('确定要清除所有本地存储吗?这将重置会话。')) {
|
|
||||||
// 清除 Chatwoot 相关的本地存储
|
|
||||||
const keysToRemove = [];
|
|
||||||
for (let i = 0; i < localStorage.length; i++) {
|
|
||||||
const key = localStorage.key(i);
|
|
||||||
if (key.includes('chatwoot') || key.includes('widget')) {
|
|
||||||
keysToRemove.push(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
keysToRemove.forEach(key => localStorage.removeItem(key));
|
|
||||||
|
|
||||||
console.log(`✅ 已清除 ${keysToRemove.length} 个本地存储项`);
|
|
||||||
console.log('清除的键:', keysToRemove);
|
|
||||||
|
|
||||||
alert(`✅ 已清除 ${keysToRemove.length} 个本地存储项\n\n页面将重新加载以创建新的会话。`);
|
|
||||||
location.reload();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 页面加载时显示本地存储和检查 Token
|
|
||||||
window.addEventListener('load', function() {
|
|
||||||
// 显示本地存储
|
|
||||||
const storage = {};
|
|
||||||
for (let i = 0; i < localStorage.length; i++) {
|
|
||||||
const key = localStorage.key(i);
|
|
||||||
if (key.includes('chatwoot') || key.includes('widget')) {
|
|
||||||
storage[key] = localStorage.getItem(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (Object.keys(storage).length > 0) {
|
|
||||||
document.getElementById('localStorageData').textContent = JSON.stringify(storage, null, 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 自动检查 Token
|
|
||||||
setTimeout(checkToken, 500);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- Chatwoot Widget -->
|
|
||||||
<script>
|
|
||||||
(function(d,t) {
|
|
||||||
var BASE_URL="http://localhost:3000";
|
|
||||||
var g=d.createElement(t),s=d.getElementsByTagName(t)[0];
|
|
||||||
g.src=BASE_URL+"/packs/js/sdk.js";
|
|
||||||
g.async = true;
|
|
||||||
s.parentNode.insertBefore(g,s);
|
|
||||||
g.onload=function(){
|
|
||||||
// 获取 token 函数
|
|
||||||
function getCookie(name) {
|
|
||||||
const value = `; ${document.cookie}`;
|
|
||||||
const parts = value.split(`; ${name}=`);
|
|
||||||
if (parts.length === 2) return parts.pop().split(";").shift();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const token = getCookie('token');
|
|
||||||
|
|
||||||
// 初始化配置
|
|
||||||
const widgetConfig = {
|
|
||||||
websiteToken: '39PNCMvbMk3NvB7uaDNucc6o',
|
|
||||||
baseUrl: BASE_URL,
|
|
||||||
locale: 'zh_CN',
|
|
||||||
userIdentifier: token || 'web_user_' + Date.now()
|
|
||||||
};
|
|
||||||
|
|
||||||
window.chatwootSDK.run(widgetConfig);
|
|
||||||
|
|
||||||
// 等待 widget 加载完成后设置用户属性
|
|
||||||
setTimeout(() => {
|
|
||||||
if (token && window.chatwootSDK.setUser) {
|
|
||||||
window.chatwootSDK.setUser(
|
|
||||||
token || 'web_user_' + Date.now(),
|
|
||||||
{
|
|
||||||
jwt_token: token,
|
|
||||||
mall_token: token
|
|
||||||
}
|
|
||||||
);
|
|
||||||
console.log('✅ 已通过 setUser 设置用户属性');
|
|
||||||
} else if (token && window.$chatwoot) {
|
|
||||||
// 备用方案:使用 $chatwoot.setCustomAttributes
|
|
||||||
window.$chatwoot.setCustomAttributes({
|
|
||||||
jwt_token: token,
|
|
||||||
mall_token: token
|
|
||||||
});
|
|
||||||
console.log('✅ 已通过 $chatwoot.setCustomAttributes 设置用户属性');
|
|
||||||
}
|
|
||||||
}, 1000);
|
|
||||||
|
|
||||||
console.log('✅ Chatwoot Widget 已加载');
|
|
||||||
console.log('Locale: zh_CN');
|
|
||||||
console.log('User Identifier:', token || 'web_user_' + Date.now());
|
|
||||||
|
|
||||||
document.getElementById('widgetStatus').textContent = '✅ 已加载';
|
|
||||||
document.getElementById('widgetStatus').style.color = '#28a745';
|
|
||||||
}
|
|
||||||
})(document,"script");
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,261 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="zh-CN">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>B2B AI 助手 - 简化测试</title>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 20px;
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
.container {
|
|
||||||
background: white;
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 40px;
|
|
||||||
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
|
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
color: #333;
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
.subtitle {
|
|
||||||
text-align: center;
|
|
||||||
color: #666;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
}
|
|
||||||
.info-box {
|
|
||||||
background: #f8f9fa;
|
|
||||||
border-left: 4px solid #667eea;
|
|
||||||
padding: 15px 20px;
|
|
||||||
margin: 20px 0;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
.info-box h3 {
|
|
||||||
margin-top: 0;
|
|
||||||
color: #667eea;
|
|
||||||
}
|
|
||||||
.test-questions {
|
|
||||||
background: #fff3cd;
|
|
||||||
border: 1px solid #ffc107;
|
|
||||||
padding: 20px;
|
|
||||||
border-radius: 5px;
|
|
||||||
margin: 20px 0;
|
|
||||||
}
|
|
||||||
.test-questions h3 {
|
|
||||||
margin-top: 0;
|
|
||||||
color: #856404;
|
|
||||||
}
|
|
||||||
.question-list {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
.question-list li {
|
|
||||||
background: white;
|
|
||||||
margin: 10px 0;
|
|
||||||
padding: 12px 15px;
|
|
||||||
border-radius: 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
}
|
|
||||||
.question-list li:hover {
|
|
||||||
background: #667eea;
|
|
||||||
color: white;
|
|
||||||
transform: translateX(5px);
|
|
||||||
}
|
|
||||||
.status {
|
|
||||||
text-align: center;
|
|
||||||
padding: 15px;
|
|
||||||
border-radius: 5px;
|
|
||||||
margin: 20px 0;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
.status.online {
|
|
||||||
background: #d4edda;
|
|
||||||
color: #155724;
|
|
||||||
}
|
|
||||||
.instructions {
|
|
||||||
background: #e7f3ff;
|
|
||||||
border-left: 4px solid #2196F3;
|
|
||||||
padding: 15px 20px;
|
|
||||||
margin: 20px 0;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
.instructions ol {
|
|
||||||
margin: 10px 0;
|
|
||||||
padding-left: 20px;
|
|
||||||
}
|
|
||||||
.instructions li {
|
|
||||||
margin: 8px 0;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
.feature-list {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
||||||
gap: 20px;
|
|
||||||
margin: 30px 0;
|
|
||||||
}
|
|
||||||
.feature-card {
|
|
||||||
background: #f8f9fa;
|
|
||||||
padding: 20px;
|
|
||||||
border-radius: 8px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.feature-card h4 {
|
|
||||||
color: #667eea;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<h1>🤖 B2B AI 智能客服助手</h1>
|
|
||||||
<p class="subtitle">简化测试页面 - Chatwoot 官方集成方式</p>
|
|
||||||
|
|
||||||
<div class="status online">
|
|
||||||
✅ 系统状态:使用官方标准集成
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="instructions">
|
|
||||||
<h3>📝 使用说明</h3>
|
|
||||||
<ol>
|
|
||||||
<li><strong>点击右下角的聊天图标</strong>打开 Chatwoot 对话窗口</li>
|
|
||||||
<li><strong>输入消息</strong>开始与 AI 对话</li>
|
|
||||||
<li><strong>或者</strong>点击下面的测试问题,复制后在聊天窗口粘贴发送</li>
|
|
||||||
<li><strong>查看 AI 如何理解和回答</strong>你的问题</li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="test-questions">
|
|
||||||
<h3>💬 推荐测试问题</h3>
|
|
||||||
<p style="color: #666; margin-bottom: 15px;">点击以下问题复制到剪贴板,然后在聊天窗口粘贴(Ctrl+V)并发送:</p>
|
|
||||||
<ul class="question-list">
|
|
||||||
<li onclick="copyQuestion(this.textContent)">🕐 你们的营业时间是什么?</li>
|
|
||||||
<li onclick="copyQuestion(this.textContent)">📦 我的订单 202071324 怎么样了?</li>
|
|
||||||
<li onclick="copyQuestion(this.textContent)">🔍 查询订单 202071324</li>
|
|
||||||
<li onclick="copyQuestion(this.textContent)">📞 如何联系客服?</li>
|
|
||||||
<li onclick="copyQuestion(this.textContent)">🛍️ 我想退换货</li>
|
|
||||||
<li onclick="copyQuestion(this.textContent)">📦 订单 202071324 的物流信息</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info-box">
|
|
||||||
<h3>🔧 技术栈</h3>
|
|
||||||
<ul>
|
|
||||||
<li><strong>前端:</strong>Chatwoot 客户支持平台(官方 Widget SDK)</li>
|
|
||||||
<li><strong>AI 引擎:</strong>LangGraph + 智谱 AI (GLM-4.5)</li>
|
|
||||||
<li><strong>知识库:</strong>Strapi CMS + MCP</li>
|
|
||||||
<li><strong>业务系统:</strong>Hyperf PHP API</li>
|
|
||||||
<li><strong>缓存:</strong>Redis</li>
|
|
||||||
<li><strong>容器:</strong>Docker Compose</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="feature-list">
|
|
||||||
<div class="feature-card">
|
|
||||||
<h4>🎯 智能意图识别</h4>
|
|
||||||
<p>自动识别客户需求并分类</p>
|
|
||||||
</div>
|
|
||||||
<div class="feature-card">
|
|
||||||
<h4>📚 知识库查询</h4>
|
|
||||||
<p>快速检索 FAQ 和政策文档</p>
|
|
||||||
</div>
|
|
||||||
<div class="feature-card">
|
|
||||||
<h4>📦 订单管理</h4>
|
|
||||||
<p>查询订单、售后等服务</p>
|
|
||||||
</div>
|
|
||||||
<div class="feature-card">
|
|
||||||
<h4>🔄 多轮对话</h4>
|
|
||||||
<p>支持上下文理解的连续对话</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info-box">
|
|
||||||
<h3>📊 系统信息</h3>
|
|
||||||
<p><strong>Chatwoot 服务:</strong>http://localhost:3000</p>
|
|
||||||
<p><strong>Website Token:</strong>39PNCMvbMk3NvB7uaDNucc6o</p>
|
|
||||||
<p><strong>集成方式:</strong>Chatwoot 官方 SDK</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
function copyQuestion(text) {
|
|
||||||
// 移除表情符号
|
|
||||||
const cleanText = text.replace(/^[^\s]+\s*/, '');
|
|
||||||
navigator.clipboard.writeText(cleanText).then(() => {
|
|
||||||
alert('✅ 问题已复制到剪贴板!\n\n请在聊天窗口中按 Ctrl+V 粘贴并发送。');
|
|
||||||
}).catch(err => {
|
|
||||||
console.error('复制失败:', err);
|
|
||||||
alert('❌ 复制失败,请手动复制问题文本。');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== Cookie Token 读取 ====================
|
|
||||||
|
|
||||||
function getCookie(name) {
|
|
||||||
const value = `; ${document.cookie}`;
|
|
||||||
const parts = value.split(`; ${name}=`);
|
|
||||||
if (parts.length === 2) return parts.pop().split(";").shift();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 页面加载时检查 Token
|
|
||||||
window.addEventListener('load', function() {
|
|
||||||
const token = getCookie('token');
|
|
||||||
if (token) {
|
|
||||||
console.log('✅ Token 已从 Cookie 读取');
|
|
||||||
console.log('Token 长度:', token.length);
|
|
||||||
} else {
|
|
||||||
console.warn('⚠️ 未找到 Token,订单查询功能可能无法使用');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- Chatwoot Widget - 官方集成方式 -->
|
|
||||||
<script>
|
|
||||||
(function(d,t) {
|
|
||||||
var BASE_URL="http://localhost:3000";
|
|
||||||
var g=d.createElement(t),s=d.getElementsByTagName(t)[0];
|
|
||||||
g.src=BASE_URL+"/packs/js/sdk.js";
|
|
||||||
g.async = true;
|
|
||||||
s.parentNode.insertBefore(g,s);
|
|
||||||
g.onload=function(){
|
|
||||||
window.chatwootSDK.run({
|
|
||||||
websiteToken: '39PNCMvbMk3NvB7uaDNucc6o',
|
|
||||||
baseUrl: BASE_URL
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('✅ Chatwoot Widget 已加载 (官方集成方式)');
|
|
||||||
console.log('Base URL:', BASE_URL);
|
|
||||||
console.log('Website Token: 39PNCMvbMk3NvB7uaDNucc6o');
|
|
||||||
|
|
||||||
// 设置用户信息(可选)
|
|
||||||
setTimeout(function() {
|
|
||||||
const token = getCookie('token');
|
|
||||||
if (token && window.$chatwoot) {
|
|
||||||
window.$chatwoot.setUser('user_' + Date.now(), {
|
|
||||||
email: 'user@example.com',
|
|
||||||
name: 'Website User',
|
|
||||||
phone_number: ''
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('✅ 用户信息已设置');
|
|
||||||
} else if (!token) {
|
|
||||||
console.warn('⚠️ 未找到 Token');
|
|
||||||
}
|
|
||||||
}, 2000);
|
|
||||||
}
|
|
||||||
g.onerror=function(){
|
|
||||||
console.error('❌ Chatwoot SDK 加载失败');
|
|
||||||
console.error('请确保 Chatwoot 运行在: ' + BASE_URL);
|
|
||||||
}
|
|
||||||
})(document,"script");
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -494,7 +494,7 @@ async def get_mall_order_list(
|
|||||||
date_added: Optional[str] = None,
|
date_added: Optional[str] = None,
|
||||||
date_end: Optional[str] = None,
|
date_end: Optional[str] = None,
|
||||||
no: Optional[str] = None,
|
no: Optional[str] = None,
|
||||||
status: Optional[int] = None,
|
status: int = 10000,
|
||||||
is_drop_shopping: int = 0
|
is_drop_shopping: int = 0
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Query order list from Mall API with filters
|
"""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_added: 开始日期,格式 YYYY-MM-DD (default: None)
|
||||||
date_end: 结束日期,格式 YYYY-MM-DD (default: None)
|
date_end: 结束日期,格式 YYYY-MM-DD (default: None)
|
||||||
no: 订单号筛选 (default: None)
|
no: 订单号筛选 (default: None)
|
||||||
status: 订单状态筛选 (default: None, None表示全部状态)
|
status: 订单状态筛选 (default: 10000, 10000表示全部状态)
|
||||||
is_drop_shopping: 是否代发货 (default: 0)
|
is_drop_shopping: 是否代发货 (default: 0)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -673,9 +673,14 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
tool_obj = _tools[tool_name]
|
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
|
# 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
|
# Extract content from ToolResult
|
||||||
# ToolResult.content is a list of TextContent objects with a 'text' attribute
|
# ToolResult.content is a list of TextContent objects with a 'text' attribute
|
||||||
|
|||||||
@@ -3,15 +3,13 @@ Product MCP Server - Product search, recommendations, and quotes
|
|||||||
"""
|
"""
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
from typing import Optional, List
|
from typing import Optional, List, Dict, Any
|
||||||
|
|
||||||
# Add shared module to path
|
# Add shared module to path
|
||||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
from fastmcp import FastMCP
|
from fastmcp import FastMCP
|
||||||
from pydantic_settings import BaseSettings
|
from pydantic_settings import BaseSettings
|
||||||
|
|
||||||
|
|
||||||
from pydantic import ConfigDict
|
from pydantic import ConfigDict
|
||||||
|
|
||||||
|
|
||||||
@@ -19,6 +17,11 @@ class Settings(BaseSettings):
|
|||||||
"""Server configuration"""
|
"""Server configuration"""
|
||||||
hyperf_api_url: str
|
hyperf_api_url: str
|
||||||
hyperf_api_token: str
|
hyperf_api_token: str
|
||||||
|
mall_api_url: str
|
||||||
|
mall_tenant_id: str = "2"
|
||||||
|
mall_currency_code: str = "EUR"
|
||||||
|
mall_language_id: str = "1"
|
||||||
|
mall_source: str = "us.qa1.gaia888.com"
|
||||||
log_level: str = "INFO"
|
log_level: str = "INFO"
|
||||||
|
|
||||||
model_config = ConfigDict(env_file=".env")
|
model_config = ConfigDict(env_file=".env")
|
||||||
@@ -31,74 +34,24 @@ mcp = FastMCP(
|
|||||||
"Product Service"
|
"Product Service"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Tool registry for HTTP access
|
||||||
|
_tools: Dict[str, Any] = {}
|
||||||
|
|
||||||
|
|
||||||
|
def register_tool(name: str):
|
||||||
|
"""Decorator to register tool in _tools dict"""
|
||||||
|
def decorator(func):
|
||||||
|
_tools[name] = func
|
||||||
|
return func
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
# Hyperf client for this server
|
# Hyperf client for this server
|
||||||
from shared.hyperf_client import HyperfClient
|
from shared.hyperf_client import HyperfClient
|
||||||
hyperf = HyperfClient(settings.hyperf_api_url, settings.hyperf_api_token)
|
hyperf = HyperfClient(settings.hyperf_api_url, settings.hyperf_api_token)
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
@register_tool("get_product_detail")
|
||||||
async def search_products(
|
|
||||||
query: str,
|
|
||||||
category: Optional[str] = None,
|
|
||||||
brand: Optional[str] = None,
|
|
||||||
price_min: Optional[float] = None,
|
|
||||||
price_max: Optional[float] = None,
|
|
||||||
sort: str = "relevance",
|
|
||||||
page: int = 1,
|
|
||||||
page_size: int = 20
|
|
||||||
) -> dict:
|
|
||||||
"""Search products
|
|
||||||
|
|
||||||
Args:
|
|
||||||
query: Search keywords
|
|
||||||
category: Category filter
|
|
||||||
brand: Brand filter
|
|
||||||
price_min: Minimum price filter
|
|
||||||
price_max: Maximum price filter
|
|
||||||
sort: Sort order (relevance, price_asc, price_desc, sales, latest)
|
|
||||||
page: Page number (default: 1)
|
|
||||||
page_size: Items per page (default: 20)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List of matching products
|
|
||||||
"""
|
|
||||||
payload = {
|
|
||||||
"query": query,
|
|
||||||
"sort": sort,
|
|
||||||
"page": page,
|
|
||||||
"page_size": page_size,
|
|
||||||
"filters": {}
|
|
||||||
}
|
|
||||||
|
|
||||||
if category:
|
|
||||||
payload["filters"]["category"] = category
|
|
||||||
if brand:
|
|
||||||
payload["filters"]["brand"] = brand
|
|
||||||
if price_min is not None or price_max is not None:
|
|
||||||
payload["filters"]["price_range"] = {}
|
|
||||||
if price_min is not None:
|
|
||||||
payload["filters"]["price_range"]["min"] = price_min
|
|
||||||
if price_max is not None:
|
|
||||||
payload["filters"]["price_range"]["max"] = price_max
|
|
||||||
|
|
||||||
try:
|
|
||||||
result = await hyperf.post("/products/search", json=payload)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"success": True,
|
|
||||||
"products": result.get("products", []),
|
|
||||||
"total": result.get("total", 0),
|
|
||||||
"pagination": result.get("pagination", {})
|
|
||||||
}
|
|
||||||
except Exception as e:
|
|
||||||
return {
|
|
||||||
"success": False,
|
|
||||||
"error": str(e),
|
|
||||||
"products": []
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def get_product_detail(
|
async def get_product_detail(
|
||||||
product_id: str
|
product_id: str
|
||||||
@@ -126,6 +79,7 @@ async def get_product_detail(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@register_tool("recommend_products")
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def recommend_products(
|
async def recommend_products(
|
||||||
user_id: str,
|
user_id: str,
|
||||||
@@ -174,6 +128,7 @@ async def recommend_products(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@register_tool("get_quote")
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def get_quote(
|
async def get_quote(
|
||||||
product_id: str,
|
product_id: str,
|
||||||
@@ -233,6 +188,7 @@ async def get_quote(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@register_tool("check_inventory")
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def check_inventory(
|
async def check_inventory(
|
||||||
product_ids: List[str],
|
product_ids: List[str],
|
||||||
@@ -266,6 +222,7 @@ async def check_inventory(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@register_tool("get_categories")
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def get_categories() -> dict:
|
async def get_categories() -> dict:
|
||||||
"""Get product category tree
|
"""Get product category tree
|
||||||
@@ -288,7 +245,112 @@ async def get_categories() -> dict:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@register_tool("search_products")
|
||||||
|
@mcp.tool()
|
||||||
|
async def search_products(
|
||||||
|
keyword: str,
|
||||||
|
page_size: int = 5,
|
||||||
|
page: int = 1
|
||||||
|
) -> dict:
|
||||||
|
"""Search products from Mall API
|
||||||
|
|
||||||
|
从 Mall API 搜索商品 SPU(根据关键词)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
keyword: 搜索关键词(商品名称、编号等)
|
||||||
|
page_size: 每页数量 (default: 5, max 100)
|
||||||
|
page: 页码 (default: 1)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
商品列表,包含 SPU 信息、商品图片、价格等
|
||||||
|
Product list including SPU ID, name, image, price, etc.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from shared.mall_client import MallClient
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
logger.info(f"search_products called with keyword={keyword}")
|
||||||
|
print(f"[DEBUG] search_products called: keyword={keyword}")
|
||||||
|
|
||||||
|
# 创建 Mall 客户端(无需 token)
|
||||||
|
mall = MallClient(
|
||||||
|
api_url=settings.mall_api_url,
|
||||||
|
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,
|
||||||
|
page=page
|
||||||
|
)
|
||||||
|
|
||||||
|
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"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: 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": [...], ...}}
|
||||||
|
if "data" in result and isinstance(result["data"], dict):
|
||||||
|
products = result["data"].get("data", [])
|
||||||
|
else:
|
||||||
|
products = result.get("list", [])
|
||||||
|
total = result.get("total", 0)
|
||||||
|
|
||||||
|
# 格式化商品数据
|
||||||
|
formatted_products = []
|
||||||
|
for product in products:
|
||||||
|
formatted_products.append({
|
||||||
|
"spu_id": product.get("spuId"),
|
||||||
|
"spu_sn": product.get("spuSn"),
|
||||||
|
"product_name": product.get("spuName"), # 修正字段名
|
||||||
|
"product_image": product.get("masterImage"), # 修正字段名
|
||||||
|
"price": product.get("price"),
|
||||||
|
"special_price": product.get("specialPrice"),
|
||||||
|
"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 {
|
||||||
|
"success": True,
|
||||||
|
"products": formatted_products,
|
||||||
|
"total": total,
|
||||||
|
"keyword": keyword
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": str(e),
|
||||||
|
"products": [],
|
||||||
|
"total": 0
|
||||||
|
}
|
||||||
|
finally:
|
||||||
|
# 关闭客户端
|
||||||
|
if 'client' in dir() and 'mall' in dir():
|
||||||
|
await mall.close()
|
||||||
|
|
||||||
|
|
||||||
# Health check endpoint
|
# Health check endpoint
|
||||||
|
@register_tool("health_check")
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def health_check() -> dict:
|
async def health_check() -> dict:
|
||||||
"""Check server health status"""
|
"""Check server health status"""
|
||||||
@@ -301,17 +363,96 @@ async def health_check() -> dict:
|
|||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import uvicorn
|
import uvicorn
|
||||||
|
|
||||||
# Create FastAPI app from MCP
|
|
||||||
app = mcp.http_app()
|
|
||||||
|
|
||||||
# Add health endpoint
|
|
||||||
from starlette.responses import JSONResponse
|
from starlette.responses import JSONResponse
|
||||||
|
from starlette.routing import Route
|
||||||
|
from starlette.requests import Request
|
||||||
|
|
||||||
|
# Custom tool execution endpoint
|
||||||
|
async def execute_tool(request: Request):
|
||||||
|
"""Execute an MCP tool via HTTP"""
|
||||||
|
tool_name = request.path_params["tool_name"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 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)}")
|
||||||
|
|
||||||
|
# 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
|
||||||
|
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
|
||||||
|
if tool_result.content and len(tool_result.content) > 0:
|
||||||
|
content = tool_result.content[0].text
|
||||||
|
# Try to parse as JSON if possible
|
||||||
|
try:
|
||||||
|
import json
|
||||||
|
result = json.loads(content)
|
||||||
|
except:
|
||||||
|
result = content
|
||||||
|
else:
|
||||||
|
result = None
|
||||||
|
|
||||||
|
return JSONResponse({
|
||||||
|
"success": True,
|
||||||
|
"result": result
|
||||||
|
})
|
||||||
|
|
||||||
|
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)
|
||||||
|
}, status_code=500)
|
||||||
|
|
||||||
|
# Health check endpoint
|
||||||
async def health_check(request):
|
async def health_check(request):
|
||||||
return JSONResponse({"status": "healthy"})
|
return JSONResponse({"status": "healthy"})
|
||||||
|
|
||||||
# Add the route to the app
|
# Create routes list
|
||||||
from starlette.routing import Route
|
routes = [
|
||||||
app.router.routes.append(Route('/health', health_check, methods=['GET']))
|
Route('/health', health_check, methods=['GET']),
|
||||||
|
Route('/tools/{tool_name}', execute_tool, methods=['POST'])
|
||||||
|
]
|
||||||
|
|
||||||
|
# Create app from MCP with custom routes
|
||||||
|
app = mcp.http_app()
|
||||||
|
|
||||||
|
# Add our custom routes to the existing app
|
||||||
|
for route in routes:
|
||||||
|
app.router.routes.append(route)
|
||||||
|
|
||||||
uvicorn.run(app, host="0.0.0.0", port=8004)
|
uvicorn.run(app, host="0.0.0.0", port=8004)
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ class MallClient:
|
|||||||
"""
|
"""
|
||||||
if self._client is None:
|
if self._client is None:
|
||||||
default_headers = {
|
default_headers = {
|
||||||
"Authorization": f"Bearer {self.api_token}",
|
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"Accept": "application/json, text/plain, */*",
|
"Accept": "application/json, text/plain, */*",
|
||||||
"Device-Type": "pc",
|
"Device-Type": "pc",
|
||||||
@@ -70,6 +69,10 @@ class MallClient:
|
|||||||
"DNT": "1",
|
"DNT": "1",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# 只有在有 token 时才添加 Authorization header
|
||||||
|
if self.api_token:
|
||||||
|
default_headers["Authorization"] = f"Bearer {self.api_token}"
|
||||||
|
|
||||||
# 合并额外的 headers(用于 Authorization2 等)
|
# 合并额外的 headers(用于 Authorization2 等)
|
||||||
if extra_headers:
|
if extra_headers:
|
||||||
default_headers.update(extra_headers)
|
default_headers.update(extra_headers)
|
||||||
@@ -131,6 +134,14 @@ class MallClient:
|
|||||||
json=json,
|
json=json,
|
||||||
headers=request_headers
|
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()
|
response.raise_for_status()
|
||||||
|
|
||||||
data = response.json()
|
data = response.json()
|
||||||
@@ -197,14 +208,14 @@ class MallClient:
|
|||||||
async def get_order_list(
|
async def get_order_list(
|
||||||
self,
|
self,
|
||||||
page: int = 1,
|
page: int = 1,
|
||||||
limit: int = 10,
|
limit: int = 5,
|
||||||
customer_id: int = 0,
|
customer_id: int = 0,
|
||||||
order_types: Optional[list[int]] = None,
|
order_types: Optional[list[int]] = None,
|
||||||
shipping_status: int = 10000,
|
shipping_status: int = 10000,
|
||||||
date_added: Optional[str] = None,
|
date_added: Optional[str] = None,
|
||||||
date_end: Optional[str] = None,
|
date_end: Optional[str] = None,
|
||||||
no: Optional[str] = None,
|
no: Optional[str] = None,
|
||||||
status: Optional[int] = None,
|
status: int = 10000,
|
||||||
is_drop_shopping: int = 0
|
is_drop_shopping: int = 0
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Query order list with filters
|
"""Query order list with filters
|
||||||
@@ -213,14 +224,14 @@ class MallClient:
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
page: 页码 (default: 1)
|
page: 页码 (default: 1)
|
||||||
limit: 每页数量 (default: 10)
|
limit: 每页数量 (default: 5)
|
||||||
customer_id: 客户ID (default: 0)
|
customer_id: 客户ID (default: 0)
|
||||||
order_types: 订单类型数组,如 [1, 2] (default: None)
|
order_types: 订单类型数组,如 [1, 2] (default: None)
|
||||||
shipping_status: 物流状态 (default: 10000)
|
shipping_status: 物流状态 (default: 10000, 10000表示全部状态)
|
||||||
date_added: 开始日期,格式 YYYY-MM-DD (default: None)
|
date_added: 开始日期,格式 YYYY-MM-DD (default: None)
|
||||||
date_end: 结束日期,格式 YYYY-MM-DD (default: None)
|
date_end: 结束日期,格式 YYYY-MM-DD (default: None)
|
||||||
no: 订单号 (default: None)
|
no: 订单号 (default: None)
|
||||||
status: 订单状态 (default: None)
|
status: 订单状态 (default: 10000, 10000表示全部状态)
|
||||||
is_drop_shopping: 是否代发货 (default: 0)
|
is_drop_shopping: 是否代发货 (default: 0)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -239,6 +250,7 @@ class MallClient:
|
|||||||
"limit": limit,
|
"limit": limit,
|
||||||
"customerId": customer_id,
|
"customerId": customer_id,
|
||||||
"shippingStatus": shipping_status,
|
"shippingStatus": shipping_status,
|
||||||
|
"status": status,
|
||||||
"isDropShopping": is_drop_shopping
|
"isDropShopping": is_drop_shopping
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,8 +265,6 @@ class MallClient:
|
|||||||
params["dateEnd"] = date_end
|
params["dateEnd"] = date_end
|
||||||
if no:
|
if no:
|
||||||
params["no"] = no
|
params["no"] = no
|
||||||
if status is not None:
|
|
||||||
params["status"] = status
|
|
||||||
|
|
||||||
result = await self.get(
|
result = await self.get(
|
||||||
"/mall/api/order/list",
|
"/mall/api/order/list",
|
||||||
@@ -265,6 +275,47 @@ class MallClient:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise Exception(f"查询订单列表失败 (Query order list failed): {str(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
|
# Global Mall client instance
|
||||||
mall_client: Optional[MallClient] = None
|
mall_client: Optional[MallClient] = None
|
||||||
|
|||||||
95
scripts/backup-production.sh
Executable file
95
scripts/backup-production.sh
Executable file
@@ -0,0 +1,95 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# B2B Shopping AI Assistant - Production Backup Script
|
||||||
|
# 生产环境备份脚本
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# 配置
|
||||||
|
BACKUP_DIR="./backups"
|
||||||
|
DATE=$(date +%Y%m%d_%H%M%S)
|
||||||
|
RETENTION_DAYS=7
|
||||||
|
|
||||||
|
# 颜色输出
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
log_info() {
|
||||||
|
echo -e "${GREEN}[INFO]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_warn() {
|
||||||
|
echo -e "${YELLOW}[WARN]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 创建备份目录
|
||||||
|
mkdir -p "$BACKUP_DIR"
|
||||||
|
|
||||||
|
log_info "========================================"
|
||||||
|
log_info "生产环境备份 - $DATE"
|
||||||
|
log_info "========================================"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 1. 备份 Redis 数据
|
||||||
|
log_info "1. 备份 Redis 数据..."
|
||||||
|
docker run --rm \
|
||||||
|
-v ai_redis_data_prod:/data \
|
||||||
|
-v "$(pwd)/$BACKUP_DIR":/backup \
|
||||||
|
alpine tar czf /backup/redis-$DATE.tar.gz -C /data .
|
||||||
|
|
||||||
|
log_info "✅ Redis 数据备份完成: redis-$DATE.tar.gz"
|
||||||
|
|
||||||
|
# 2. 备份 Agent 日志
|
||||||
|
log_info "2. 备份 Agent 日志..."
|
||||||
|
docker run --rm \
|
||||||
|
-v ai_agent_logs_prod:/data \
|
||||||
|
-v "$(pwd)/$BACKUP_DIR":/backup \
|
||||||
|
alpine tar czf /backup/agent-logs-$DATE.tar.gz -C /data .
|
||||||
|
|
||||||
|
log_info "✅ Agent 日志备份完成: agent-logs-$DATE.tar.gz"
|
||||||
|
|
||||||
|
# 3. 备份 Grafana 配置(如果存在)
|
||||||
|
if docker volume inspect ai_grafana_data &> /dev/null; then
|
||||||
|
log_info "3. 备份 Grafana 配置..."
|
||||||
|
docker run --rm \
|
||||||
|
-v ai_grafana_data:/data \
|
||||||
|
-v "$(pwd)/$BACKUP_DIR":/backup \
|
||||||
|
alpine tar czf /backup/grafana-$DATE.tar.gz -C /data .
|
||||||
|
|
||||||
|
log_info "✅ Grafana 配置备份完成: grafana-$DATE.tar.gz"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 4. 备份环境变量文件
|
||||||
|
log_info "4. 备份环境变量文件..."
|
||||||
|
cp .env.production "$BACKUP_DIR/env-$DATE.backup"
|
||||||
|
chmod 600 "$BACKUP_DIR/env-$DATE.backup"
|
||||||
|
|
||||||
|
log_info "✅ 环境变量备份完成: env-$DATE.backup"
|
||||||
|
|
||||||
|
# 5. 清理旧备份
|
||||||
|
log_info "5. 清理 $RETENTION_DAYS 天前的旧备份..."
|
||||||
|
find "$BACKUP_DIR" -name "*.tar.gz" -mtime +$RETENTION_DAYS -delete
|
||||||
|
find "$BACKUP_DIR" -name "env-*.backup" -mtime +$RETENTION_DAYS -delete
|
||||||
|
|
||||||
|
log_info "✅ 旧备份清理完成"
|
||||||
|
|
||||||
|
# 6. 生成备份清单
|
||||||
|
log_info "6. 生成备份清单..."
|
||||||
|
cat > "$BACKUP_DIR/manifest-$DATE.txt" << EOF
|
||||||
|
备份时间: $DATE
|
||||||
|
备份内容:
|
||||||
|
- Redis 数据: redis-$DATE.tar.gz
|
||||||
|
- Agent 日志: agent-logs-$DATE.tar.gz
|
||||||
|
- 环境变量: env-$DATE.backup
|
||||||
|
EOF
|
||||||
|
|
||||||
|
log_info "✅ 备份清单生成完成: manifest-$DATE.txt"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
log_info "========================================"
|
||||||
|
log_info "✅ 备份完成!"
|
||||||
|
log_info "========================================"
|
||||||
|
echo ""
|
||||||
|
log_info "备份文件位置: $BACKUP_DIR"
|
||||||
|
log_info ""
|
||||||
|
ls -lh "$BACKUP_DIR" | grep "$DATE"
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Chatwoot 配置诊断工具
|
|
||||||
|
|
||||||
echo "======================================"
|
|
||||||
echo "Chatwoot 配置诊断工具"
|
|
||||||
echo "======================================"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# 检查是否提供了 API Token
|
|
||||||
if [ -z "$CHATWOOT_API_TOKEN" ]; then
|
|
||||||
echo "❌ 请先设置 CHATWOOT_API_TOKEN 环境变量"
|
|
||||||
echo ""
|
|
||||||
echo "获取方式:"
|
|
||||||
echo "1. 访问 http://localhost:3000"
|
|
||||||
echo "2. 登录后进入 Settings → Profile → Access Tokens"
|
|
||||||
echo "3. 创建一个新的 Access Token"
|
|
||||||
echo ""
|
|
||||||
echo "然后运行:"
|
|
||||||
echo " CHATWOOT_API_TOKEN=your_token $0"
|
|
||||||
echo ""
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
CHATWOOT_BASE_URL="http://localhost:3000"
|
|
||||||
ACCOUNT_ID="2"
|
|
||||||
|
|
||||||
echo "🔍 正在检查 Chatwoot 配置..."
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# 1. 检查服务是否运行
|
|
||||||
echo "1️⃣ 检查 Chatwoot 服务状态..."
|
|
||||||
if curl -s "$CHATWOOT_BASE_URL" > /dev/null; then
|
|
||||||
echo " ✅ Chatwoot 服务正常运行"
|
|
||||||
else
|
|
||||||
echo " ❌ Chatwoot 服务无法访问"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# 2. 获取所有收件箱
|
|
||||||
echo "2️⃣ 获取所有收件箱..."
|
|
||||||
INBOXES=$(curl -s \
|
|
||||||
-H "Authorization: Bearer $CHATWOOT_API_TOKEN" \
|
|
||||||
"$CHATWOOT_BASE_URL/api/v1/accounts/$ACCOUNT_ID/inboxes")
|
|
||||||
|
|
||||||
echo "$INBOXES" | grep -o '"id":[0-9]*' | wc -l | xargs echo " 找到收件箱数量:"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# 3. 解析并显示每个收件箱的详细信息
|
|
||||||
echo "3️⃣ 收件箱详细信息:"
|
|
||||||
echo "======================================"
|
|
||||||
|
|
||||||
# 提取所有收件箱的 ID
|
|
||||||
INBOX_IDS=$(echo "$INBOXES" | grep -o '"id":[0-9]*' | grep -o '[0-9]*' | sort -u)
|
|
||||||
|
|
||||||
for INBOX_ID in $INBOX_IDS; do
|
|
||||||
echo ""
|
|
||||||
echo "📬 收件箱 ID: $INBOX_ID"
|
|
||||||
echo "--------------------------------------"
|
|
||||||
|
|
||||||
# 获取收件箱详情
|
|
||||||
INBOX_DETAIL=$(curl -s \
|
|
||||||
-H "Authorization: Bearer $CHATWOOT_API_TOKEN" \
|
|
||||||
"$CHATWOOT_BASE_URL/api/v1/accounts/$ACCOUNT_ID/inboxes/$INBOX_ID")
|
|
||||||
|
|
||||||
# 提取收件箱名称
|
|
||||||
NAME=$(echo "$INBOX_DETAIL" | grep -o '"name":"[^"]*"' | head -1 | cut -d'"' -f4)
|
|
||||||
echo " 名称: $NAME"
|
|
||||||
|
|
||||||
# 提取收件箱类型
|
|
||||||
TYPE=$(echo "$INBOX_DETAIL" | grep -o '"inbox_type":"[^"]*"' | head -1 | cut -d'"' -f4)
|
|
||||||
echo " 类型: $TYPE"
|
|
||||||
|
|
||||||
# 提取 Website Token(如果有)
|
|
||||||
WEBSITE_TOKEN=$(echo "$INBOX_DETAIL" | grep -o '"website_token":"[^"]*"' | head -1 | cut -d'"' -f4)
|
|
||||||
if [ -n "$WEBSITE_TOKEN" ]; then
|
|
||||||
echo " Website Token: $WEBSITE_TOKEN"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 提取 Webhook URL
|
|
||||||
WEBHOOK_URL=$(echo "$INBOX_DETAIL" | grep -o '"webhook_url":"[^"]*"' | head -1 | cut -d'"' -f4)
|
|
||||||
if [ -n "$WEBHOOK_URL" ]; then
|
|
||||||
echo " Webhook URL: $WEBHOOK_URL"
|
|
||||||
else
|
|
||||||
echo " Webhook URL: ❌ 未配置"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 检查是否是测试页面使用的 token
|
|
||||||
if [ "$WEBSITE_TOKEN" = "39PNCMvbMk3NvB7uaDNucc6o" ]; then
|
|
||||||
echo ""
|
|
||||||
echo " ⭐ 这是测试页面使用的收件箱!"
|
|
||||||
echo " Webhook 应该配置为: http://agent:8000/webhooks/chatwoot"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "======================================"
|
|
||||||
echo ""
|
|
||||||
echo "📋 下一步操作:"
|
|
||||||
echo ""
|
|
||||||
echo "1. 找到 Website Token 为 '39PNCMvbMk3NvB7uaDNucc6o' 的收件箱"
|
|
||||||
echo "2. 记录该收件箱的 ID"
|
|
||||||
echo "3. 确保该收件箱的 Webhook URL 配置为:"
|
|
||||||
echo " http://agent:8000/webhooks/chatwoot"
|
|
||||||
echo ""
|
|
||||||
echo "💡 提示:可以通过 Chatwoot 界面更新配置:"
|
|
||||||
echo " Settings → Inboxes → 选择收件箱 → Configuration → Webhook URL"
|
|
||||||
echo ""
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# 检查 Chatwoot 会话和消息
|
|
||||||
|
|
||||||
echo "======================================"
|
|
||||||
echo "Chatwoot 会话检查工具"
|
|
||||||
echo "======================================"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# 需要设置环境变量
|
|
||||||
if [ -z "$CHATWOOT_API_TOKEN" ]; then
|
|
||||||
echo "❌ 请先设置 CHATWOOT_API_TOKEN 环境变量"
|
|
||||||
echo ""
|
|
||||||
echo "获取方式:"
|
|
||||||
echo "1. 访问 http://localhost:3000"
|
|
||||||
echo "2. 登录后进入 Settings → Profile → Access Tokens"
|
|
||||||
echo "3. 创建一个新的 Access Token"
|
|
||||||
echo ""
|
|
||||||
echo "然后运行:"
|
|
||||||
echo " CHATWOOT_API_TOKEN=your_token $0"
|
|
||||||
echo ""
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
CHATWOOT_BASE_URL="http://localhost:3000"
|
|
||||||
ACCOUNT_ID="2"
|
|
||||||
|
|
||||||
echo "🔍 正在检查 Chatwoot 会话..."
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# 1. 获取所有收件箱
|
|
||||||
echo "1️⃣ 获取所有收件箱..."
|
|
||||||
INBOXES=$(curl -s \
|
|
||||||
-H "Authorization: Bearer $CHATWOOT_API_TOKEN" \
|
|
||||||
"$CHATWOOT_BASE_URL/api/v1/accounts/$ACCOUNT_ID/inboxes")
|
|
||||||
|
|
||||||
INBOX_IDS=$(echo "$INBOXES" | grep -o '"id":[0-9]*' | grep -o '[0-9]*' | sort -u | head -5)
|
|
||||||
|
|
||||||
echo " 找到收件箱: $(echo "$INBOX_IDS" | wc -l) 个"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# 2. 检查每个收件箱的会话
|
|
||||||
echo "2️⃣ 检查最近的会话..."
|
|
||||||
echo "======================================"
|
|
||||||
|
|
||||||
for INBOX_ID in $INBOX_IDS; do
|
|
||||||
echo ""
|
|
||||||
echo "📬 收件箱 ID: $INBOX_ID"
|
|
||||||
echo "--------------------------------------"
|
|
||||||
|
|
||||||
# 获取收件箱名称
|
|
||||||
INBOX_NAME=$(echo "$INBOXES" | grep -o "\"id\":$INBOX_ID" -A 20 | grep '"name":"' | head -1 | cut -d'"' -f4)
|
|
||||||
echo " 名称: $INBOX_NAME"
|
|
||||||
|
|
||||||
# 获取最近5个会话
|
|
||||||
CONVERSATIONS=$(curl -s \
|
|
||||||
-H "Authorization: Bearer $CHATWOOT_API_TOKEN" \
|
|
||||||
"$CHATWOOT_BASE_URL/api/v1/accounts/$ACCOUNT_ID/conversations?inbox_id=$INBOX_ID&sort=-created_at" | head -100)
|
|
||||||
|
|
||||||
CONV_IDS=$(echo "$CONVERSATIONS" | grep -o '"id":[0-9]*' | grep -o '[0-9]*' | head -5)
|
|
||||||
|
|
||||||
if [ -z "$CONV_IDS" ]; then
|
|
||||||
echo " 没有会话"
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo " 最近的会话:"
|
|
||||||
echo "$CONV_IDS" | while read CONV_ID; do
|
|
||||||
# 获取会话详情
|
|
||||||
CONV_DETAIL=$(curl -s \
|
|
||||||
-H "Authorization: Bearer $CHATWOOT_API_TOKEN" \
|
|
||||||
"$CHATWOOT_BASE_URL/api/v1/accounts/$ACCOUNT_ID/conversations/$CONV_ID")
|
|
||||||
|
|
||||||
# 提取会话信息
|
|
||||||
STATUS=$(echo "$CONV_DETAIL" | grep -o '"status":"[^"]*"' | head -1 | cut -d'"' -f4)
|
|
||||||
CREATED_AT=$(echo "$CONV_DETAIL" | grep -o '"created_at":[^,}]*' | head -1 | cut -d'"' -f2)
|
|
||||||
|
|
||||||
# 获取消息数量
|
|
||||||
MESSAGES=$(curl -s \
|
|
||||||
-H "Authorization: Bearer $CHATWOOT_API_TOKEN" \
|
|
||||||
"$CHATWOOT_BASE_URL/api/v1/accounts/$ACCOUNT_ID/conversations/$CONV_ID/messages")
|
|
||||||
|
|
||||||
MSG_COUNT=$(echo "$MESSAGES" | grep -o '"content":' | wc -l)
|
|
||||||
|
|
||||||
echo " • 会话 #$CONV_ID - 状态: $Status - 消息数: $MSG_COUNT"
|
|
||||||
|
|
||||||
# 获取最后几条消息
|
|
||||||
echo "$MESSAGES" | grep -o '"content":"[^"]*"' | tail -3 | while read MSG; do
|
|
||||||
CONTENT=$(echo "$MSG" | cut -d'"' -f4 | sed 's/"/"/g' | head -c 50)
|
|
||||||
echo " - $CONTENT..."
|
|
||||||
done
|
|
||||||
done
|
|
||||||
done
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "======================================"
|
|
||||||
echo ""
|
|
||||||
echo "💡 提示:"
|
|
||||||
echo "1. 查看上面的会话列表"
|
|
||||||
echo "2. 记录你正在测试的会话 ID"
|
|
||||||
echo "3. 在 Agent 日志中查找相同的 conversation_id"
|
|
||||||
echo "4. 如果会话 ID 不匹配,说明 Widget 连接到了错误的会话"
|
|
||||||
echo ""
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# 实时监控 Chatwoot 和 Agent 日志
|
|
||||||
|
|
||||||
echo "======================================"
|
|
||||||
echo "Chatwoot 消息流程实时监控"
|
|
||||||
echo "======================================"
|
|
||||||
echo ""
|
|
||||||
echo "📋 使用说明:"
|
|
||||||
echo "1. 在测试页面 http://localhost:8080/test-chat.html 发送消息"
|
|
||||||
echo "2. 观察下面的日志输出"
|
|
||||||
echo "3. 按 Ctrl+C 停止监控"
|
|
||||||
echo ""
|
|
||||||
echo "======================================"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# 检查 Docker 容器是否运行
|
|
||||||
if ! docker ps | grep -q "ai_agent"; then
|
|
||||||
echo "❌ Agent 容器未运行"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! docker ps | grep -q "ai_chatwoot"; then
|
|
||||||
echo "❌ Chatwoot 容器未运行"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "✅ 所有容器运行正常"
|
|
||||||
echo ""
|
|
||||||
echo "🔍 开始监控日志..."
|
|
||||||
echo "======================================"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# 使用多 tail 监控多个容器
|
|
||||||
docker logs ai_agent -f 2>&1 &
|
|
||||||
AGENT_PID=$!
|
|
||||||
|
|
||||||
docker logs ai_chatwoot -f 2>&1 &
|
|
||||||
CHATWOOT_PID=$!
|
|
||||||
|
|
||||||
# 清理函数
|
|
||||||
cleanup() {
|
|
||||||
echo ""
|
|
||||||
echo "======================================"
|
|
||||||
echo "停止监控..."
|
|
||||||
kill $AGENT_PID $CHATWOOT_PID 2>/dev/null
|
|
||||||
exit 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# 捕获 Ctrl+C
|
|
||||||
trap cleanup INT TERM
|
|
||||||
|
|
||||||
# 等待
|
|
||||||
wait
|
|
||||||
195
scripts/deploy-production.sh
Executable file
195
scripts/deploy-production.sh
Executable file
@@ -0,0 +1,195 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# B2B Shopping AI Assistant - Production Deployment Script
|
||||||
|
# 生产环境部署脚本
|
||||||
|
|
||||||
|
set -e # 遇到错误立即退出
|
||||||
|
|
||||||
|
# 颜色输出
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# 日志函数
|
||||||
|
log_info() {
|
||||||
|
echo -e "${GREEN}[INFO]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_warn() {
|
||||||
|
echo -e "${YELLOW}[WARN]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_error() {
|
||||||
|
echo -e "${RED}[ERROR]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 检查必要的命令
|
||||||
|
check_requirements() {
|
||||||
|
log_info "检查系统依赖..."
|
||||||
|
|
||||||
|
if ! command -v docker &> /dev/null; then
|
||||||
|
log_error "Docker 未安装,请先安装 Docker"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v docker-compose &> /dev/null; then
|
||||||
|
log_error "Docker Compose 未安装,请先安装 Docker Compose"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "系统依赖检查完成"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 检查环境变量文件
|
||||||
|
check_env_file() {
|
||||||
|
log_info "检查环境变量文件..."
|
||||||
|
|
||||||
|
if [ ! -f .env.production ]; then
|
||||||
|
log_error ".env.production 文件不存在"
|
||||||
|
log_info "请复制 .env.production.example 并填写真实值:"
|
||||||
|
log_info " cp .env.production.example .env.production"
|
||||||
|
log_info " vim .env.production"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 检查必要的环境变量
|
||||||
|
source .env.production
|
||||||
|
|
||||||
|
required_vars=(
|
||||||
|
"ZHIPU_API_KEY"
|
||||||
|
"CHATWOOT_API_TOKEN"
|
||||||
|
"HYPERF_API_URL"
|
||||||
|
"MALL_API_URL"
|
||||||
|
"REDIS_PASSWORD"
|
||||||
|
)
|
||||||
|
|
||||||
|
missing_vars=()
|
||||||
|
for var in "${required_vars[@]}"; do
|
||||||
|
if [ -z "${!var}" ] || [[ "${!var}" == *"your_"*"_here" ]]; then
|
||||||
|
missing_vars+=("$var")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ ${#missing_vars[@]} -ne 0 ]; then
|
||||||
|
log_error "以下环境变量未设置或使用默认值:"
|
||||||
|
for var in "${missing_vars[@]}"; do
|
||||||
|
log_error " - $var"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
log_info "环境变量检查完成"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 构建镜像
|
||||||
|
build_images() {
|
||||||
|
log_info "开始构建 Docker 镜像..."
|
||||||
|
|
||||||
|
docker-compose -f docker-compose.prod.yml build --no-cache
|
||||||
|
|
||||||
|
log_info "Docker 镜像构建完成"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 停止现有服务
|
||||||
|
stop_services() {
|
||||||
|
log_info "停止现有服务..."
|
||||||
|
|
||||||
|
docker-compose -f docker-compose.prod.yml down
|
||||||
|
|
||||||
|
log_info "现有服务已停止"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 启动服务
|
||||||
|
start_services() {
|
||||||
|
log_info "启动生产环境服务..."
|
||||||
|
|
||||||
|
docker-compose -f docker-compose.prod.yml up -d
|
||||||
|
|
||||||
|
log_info "服务启动完成"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 健康检查
|
||||||
|
health_check() {
|
||||||
|
log_info "等待服务启动..."
|
||||||
|
|
||||||
|
sleep 10
|
||||||
|
|
||||||
|
# 检查 Agent 服务
|
||||||
|
if curl -f http://localhost:8000/health &> /dev/null; then
|
||||||
|
log_info "✅ Agent 服务健康检查通过"
|
||||||
|
else
|
||||||
|
log_error "❌ Agent 服务健康检查失败"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 检查 MCP 服务
|
||||||
|
mcp_ports=(8001 8002 8003 8004)
|
||||||
|
for port in "${mcp_ports[@]}"; do
|
||||||
|
if curl -f http://localhost:$port/health &> /dev/null; then
|
||||||
|
log_info "✅ MCP 服务 (端口 $port) 健康检查通过"
|
||||||
|
else
|
||||||
|
log_warn "⚠️ MCP 服务 (端口 $port) 健康检查失败"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# 查看服务状态
|
||||||
|
show_status() {
|
||||||
|
log_info "服务状态:"
|
||||||
|
docker-compose -f docker-compose.prod.yml ps
|
||||||
|
}
|
||||||
|
|
||||||
|
# 查看日志
|
||||||
|
show_logs() {
|
||||||
|
log_info "最近的日志:"
|
||||||
|
docker-compose -f docker-compose.prod.yml logs --tail=50
|
||||||
|
}
|
||||||
|
|
||||||
|
# 主函数
|
||||||
|
main() {
|
||||||
|
log_info "========================================"
|
||||||
|
log_info "B2B Shopping AI Assistant - 生产环境部署"
|
||||||
|
log_info "========================================"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 检查参数
|
||||||
|
if [ "$1" == "--skip-build" ]; then
|
||||||
|
log_warn "跳过镜像构建步骤"
|
||||||
|
else
|
||||||
|
check_requirements
|
||||||
|
check_env_file
|
||||||
|
build_images
|
||||||
|
fi
|
||||||
|
|
||||||
|
stop_services
|
||||||
|
start_services
|
||||||
|
|
||||||
|
log_info "等待服务就绪..."
|
||||||
|
sleep 15
|
||||||
|
|
||||||
|
if health_check; then
|
||||||
|
log_info "========================================"
|
||||||
|
log_info "✅ 部署成功!"
|
||||||
|
log_info "========================================"
|
||||||
|
echo ""
|
||||||
|
log_info "服务地址:"
|
||||||
|
log_info " - Agent: http://localhost:8000"
|
||||||
|
log_info " - Strapi MCP: http://localhost:8001"
|
||||||
|
log_info " - Order MCP: http://localhost:8002"
|
||||||
|
log_info " - After MCP: http://localhost:8003"
|
||||||
|
log_info " - Product MCP: http://localhost:8004"
|
||||||
|
echo ""
|
||||||
|
log_info "查看日志:"
|
||||||
|
log_info " docker-compose -f docker-compose.prod.yml logs -f"
|
||||||
|
echo ""
|
||||||
|
log_info "查看状态:"
|
||||||
|
log_info " docker-compose -f docker-compose.prod.yml ps"
|
||||||
|
else
|
||||||
|
log_error "部署失败,请检查日志"
|
||||||
|
show_logs
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# 执行主函数
|
||||||
|
main "$@"
|
||||||
41
scripts/set-contact-token.sh
Executable file
41
scripts/set-contact-token.sh
Executable file
@@ -0,0 +1,41 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# 为 Chatwoot Contact 设置 JWT Token
|
||||||
|
#
|
||||||
|
# 使用方法:
|
||||||
|
# ./set-contact-token.sh <contact_id> <jwt_token>
|
||||||
|
#
|
||||||
|
# 示例:
|
||||||
|
# ./set-contact-token.sh 4 "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||||||
|
|
||||||
|
CHATWOOT_BASE_URL="http://192.168.15.34:3000"
|
||||||
|
ACCOUNT_ID="2"
|
||||||
|
|
||||||
|
# 从环境变量或参数获取 token
|
||||||
|
CONTACT_ID=${1:-"4"}
|
||||||
|
JWT_TOKEN=${2:-"your_jwt_token_here"}
|
||||||
|
MALL_TOKEN=${3:-"$JWT_TOKEN"} # 默认使用相同的 token
|
||||||
|
|
||||||
|
# Chatwoot API Token(需要在管理界面创建)
|
||||||
|
CHATWOOT_API_TOKEN="fnWaEeAyC1gw1FYQq6YJMWSj"
|
||||||
|
|
||||||
|
echo "📝 为 Contact #$CONTACT_ID 设置 token..."
|
||||||
|
echo "JWT Token: ${JWT_TOKEN:0:30}..."
|
||||||
|
echo "Mall Token: ${MALL_TOKEN:0:30}..."
|
||||||
|
|
||||||
|
# 更新 contact 的 custom_attributes
|
||||||
|
curl -X PUT "$CHATWOOT_BASE_URL/api/v1/accounts/$ACCOUNT_ID/contacts/$CONTACT_ID" \
|
||||||
|
-H "Authorization: Bearer $CHATWOOT_API_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{
|
||||||
|
\"custom_attributes\": {
|
||||||
|
\"jwt_token\": \"$JWT_TOKEN\",
|
||||||
|
\"mall_token\": \"$MALL_TOKEN\"
|
||||||
|
}
|
||||||
|
}" | python3 -m json.tool
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✅ Token 设置完成!"
|
||||||
|
echo ""
|
||||||
|
echo "验证:"
|
||||||
|
echo " curl -H \"Authorization: Bearer $CHATWOOT_API_TOKEN\" \\"
|
||||||
|
echo " $CHATWOOT_BASE_URL/api/v1/accounts/$ACCOUNT_ID/contacts/$CONTACT_ID"
|
||||||
31
scripts/set-remote-contact-token.sh
Executable file
31
scripts/set-remote-contact-token.sh
Executable file
@@ -0,0 +1,31 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# 为远程 Chatwoot 的 Contact 设置 JWT Token
|
||||||
|
|
||||||
|
CHATWOOT_BASE_URL="http://192.168.15.28:3000"
|
||||||
|
ACCOUNT_ID="2"
|
||||||
|
CONTACT_ID=${1:-"4"} # Contact ID (从日志中看到是 4)
|
||||||
|
|
||||||
|
# 从参数或使用测试 token
|
||||||
|
JWT_TOKEN=${2:-"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"}
|
||||||
|
|
||||||
|
echo "📝 为 Contact #$CONTACT_ID 设置 token..."
|
||||||
|
echo "JWT Token: ${JWT_TOKEN:0:30}..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 更新 contact 的 custom_attributes
|
||||||
|
curl -X PUT "$CHATWOOT_BASE_URL/api/v1/accounts/$ACCOUNT_ID/contacts/$CONTACT_ID" \
|
||||||
|
-H "Authorization: Bearer wFc2Dpi3wcf9eT5Cibckd68z" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{
|
||||||
|
\"custom_attributes\": {
|
||||||
|
\"jwt_token\": \"$JWT_TOKEN\",
|
||||||
|
\"mall_token\": \"$JWT_TOKEN\"
|
||||||
|
}
|
||||||
|
}" | python3 -m json.tool
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✅ Token 设置完成!"
|
||||||
|
echo ""
|
||||||
|
echo "验证:"
|
||||||
|
echo " curl -H \"Authorization: Bearer wFc2Dpi3wcf9eT5Cibckd68z\" \\"
|
||||||
|
echo " $CHATWOOT_BASE_URL/api/v1/accounts/$ACCOUNT_ID/contacts/$CONTACT_ID"
|
||||||
73
scripts/start.sh
Executable file
73
scripts/start.sh
Executable file
@@ -0,0 +1,73 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# 启动脚本 - B2B AI Assistant
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "========================================"
|
||||||
|
echo "🚀 启动 B2B AI Assistant 服务"
|
||||||
|
echo "========================================"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 检查 Docker 是否运行
|
||||||
|
if ! docker info > /dev/null 2>&1; then
|
||||||
|
echo "❌ Docker 未运行,请先启动 Docker"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ Docker 运行正常"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 检查远程 Chatwoot 连接
|
||||||
|
echo "🔍 检查远程 Chatwoot 连接..."
|
||||||
|
CHATWOOT_URL="http://192.168.15.28:3000"
|
||||||
|
if curl -s --connect-timeout 5 "$CHATWOOT_URL" > /dev/null 2>&1; then
|
||||||
|
echo "✅ 远程 Chatwoot 连接正常 ($CHATWOOT_URL)"
|
||||||
|
else
|
||||||
|
echo "⚠️ 警告:无法连接到远程 Chatwoot ($CHATWOOT_URL)"
|
||||||
|
echo " 请确保 Chatwoot 正在运行"
|
||||||
|
read -p "是否继续启动?(y/n) " -n 1 -r
|
||||||
|
echo
|
||||||
|
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 获取本机 IP
|
||||||
|
LOCAL_IP=$(hostname -I | awk '{print $1}')
|
||||||
|
echo "📡 本机 IP: $LOCAL_IP"
|
||||||
|
echo " Webhook URL: http://$LOCAL_IP:8000/webhooks/chatwoot"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 启动服务
|
||||||
|
echo "🐳 启动 Docker 服务..."
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "⏳ 等待服务启动..."
|
||||||
|
sleep 5
|
||||||
|
|
||||||
|
# 检查服务状态
|
||||||
|
echo ""
|
||||||
|
echo "📊 服务状态:"
|
||||||
|
docker-compose ps
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "========================================"
|
||||||
|
echo "✅ 服务启动完成!"
|
||||||
|
echo "========================================"
|
||||||
|
echo ""
|
||||||
|
echo "📝 常用命令:"
|
||||||
|
echo " 查看日志: docker-compose logs -f agent"
|
||||||
|
echo " 查看状态: docker-compose ps"
|
||||||
|
echo " 停止服务: docker-compose down"
|
||||||
|
echo " 重启服务: docker-compose restart [service_name]"
|
||||||
|
echo ""
|
||||||
|
echo "📖 测试页面:"
|
||||||
|
echo " http://localhost:8080/docs/test-chat.html"
|
||||||
|
echo " http://localhost:8080/docs/test-conversation-id.html"
|
||||||
|
echo ""
|
||||||
|
echo "⚠️ 重要:"
|
||||||
|
echo " 请确保远程 Chatwoot (192.168.15.28:3000) 已配置 Webhook:"
|
||||||
|
echo " http://$LOCAL_IP:8000/webhooks/chatwoot"
|
||||||
|
echo ""
|
||||||
21
scripts/stop.sh
Executable file
21
scripts/stop.sh
Executable file
@@ -0,0 +1,21 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# 停止脚本 - B2B AI Assistant
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "========================================"
|
||||||
|
echo "🛑 停止 B2B AI Assistant 服务"
|
||||||
|
echo "========================================"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 停止服务
|
||||||
|
echo "🐳 停止 Docker 服务..."
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✅ 所有服务已停止"
|
||||||
|
echo ""
|
||||||
|
echo "💡 提示:"
|
||||||
|
echo " 如需删除数据卷,运行: docker-compose down -v"
|
||||||
|
echo " 如需重新启动,运行: ./scripts/start.sh"
|
||||||
|
echo ""
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# 更新 Chatwoot Webhook 配置脚本
|
|
||||||
|
|
||||||
# 配置
|
|
||||||
CHATWOOT_BASE_URL="http://localhost:3000"
|
|
||||||
ACCOUNT_ID="2" # 你的账户 ID
|
|
||||||
INBOX_ID="" # 需要填入你的收件箱 ID
|
|
||||||
API_TOKEN="" # 需要填入你的 Chatwoot API Token
|
|
||||||
NEW_WEBHOOK_URL="http://agent:8000/webhooks/chatwoot"
|
|
||||||
WEBHOOK_SECRET="b7a12b9c9173718596f02fd912fb59f97891a0e7abb1a5e457b4c8858b2d21b5"
|
|
||||||
|
|
||||||
# 使用说明
|
|
||||||
echo "======================================"
|
|
||||||
echo "Chatwoot Webhook 配置更新工具"
|
|
||||||
echo "======================================"
|
|
||||||
echo ""
|
|
||||||
echo "请先设置以下变量:"
|
|
||||||
echo "1. INBOX_ID - 你的收件箱 ID"
|
|
||||||
echo "2. API_TOKEN - Chatwoot API Token(从 Settings → Profile → Access Tokens 获取)"
|
|
||||||
echo ""
|
|
||||||
echo "然后运行:"
|
|
||||||
echo " INBOX_ID=<收件箱ID> API_TOKEN=<API Token> $0"
|
|
||||||
echo ""
|
|
||||||
echo "或者直接编辑此脚本设置变量。"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# 检查参数
|
|
||||||
if [ -z "$INBOX_ID" ] || [ -z "$API_TOKEN" ]; then
|
|
||||||
echo "❌ 缺少必要参数"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 获取当前 webhook 配置
|
|
||||||
echo "📋 获取当前 webhook 配置..."
|
|
||||||
CURRENT_CONFIG=$(curl -s \
|
|
||||||
-H "Authorization: Bearer $API_TOKEN" \
|
|
||||||
"$CHATWOOT_BASE_URL/api/v1/accounts/$ACCOUNT_ID/inboxes/$INBOX_ID")
|
|
||||||
|
|
||||||
echo "当前配置:"
|
|
||||||
echo "$CURRENT_CONFIG" | grep -o '"webhook_url":"[^"]*"' || echo "未找到 webhook_url"
|
|
||||||
|
|
||||||
# 更新 webhook
|
|
||||||
echo ""
|
|
||||||
echo "🔄 更新 webhook URL 为: $NEW_WEBHOOK_URL"
|
|
||||||
|
|
||||||
UPDATE_RESPONSE=$(curl -s -X PUT \
|
|
||||||
-H "Authorization: Bearer $API_TOKEN" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "{
|
|
||||||
\"inbox\": {
|
|
||||||
\"webhook_url\": \"$NEW_WEBHOOK_URL\"
|
|
||||||
}
|
|
||||||
}" \
|
|
||||||
"$CHATWOOT_BASE_URL/api/v1/accounts/$ACCOUNT_ID/inboxes/$INBOX_ID")
|
|
||||||
|
|
||||||
echo "更新响应:"
|
|
||||||
echo "$UPDATE_RESPONSE"
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "✅ 配置更新完成!"
|
|
||||||
echo ""
|
|
||||||
echo "现在可以在 Chatwoot 中测试发送消息了。"
|
|
||||||
17
scripts/verify-contact-token.sh
Executable file
17
scripts/verify-contact-token.sh
Executable file
@@ -0,0 +1,17 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# 验证 Contact 的 token 设置
|
||||||
|
|
||||||
|
CHATWOOT_BASE_URL="http://192.168.15.34:3000"
|
||||||
|
ACCOUNT_ID="2"
|
||||||
|
CONTACT_ID=${1:-"4"}
|
||||||
|
CHATWOOT_API_TOKEN="fnWaEeAyC1gw1FYQq6YJMWSj"
|
||||||
|
|
||||||
|
echo "🔍 查询 Contact #$CONTACT_ID 的信息..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
curl -s "$CHATWOOT_BASE_URL/api/v1/accounts/$ACCOUNT_ID/contacts/$CONTACT_ID" \
|
||||||
|
-H "Authorization: Bearer $CHATWOOT_API_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" | python3 -m json.tool | grep -A 20 "custom_attributes"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✅ 查询完成!"
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# 验证 Chatwoot Webhook 配置
|
|
||||||
|
|
||||||
echo "======================================"
|
|
||||||
echo "Chatwoot Webhook 配置验证工具"
|
|
||||||
echo "======================================"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# 检查 Agent 服务
|
|
||||||
echo "1️⃣ 检查 Agent 服务..."
|
|
||||||
if curl -s http://localhost:8000/health | grep -q "healthy"; then
|
|
||||||
echo " ✅ Agent 服务运行正常 (http://localhost:8000)"
|
|
||||||
else
|
|
||||||
echo " ❌ Agent 服务未运行"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# 检查 Chatwoot 服务
|
|
||||||
echo "2️⃣ 检查 Chatwoot 服务..."
|
|
||||||
if curl -s http://localhost:3000 > /dev/null; then
|
|
||||||
echo " ✅ Chatwoot 服务运行正常 (http://localhost:3000)"
|
|
||||||
else
|
|
||||||
echo " ❌ Chatwoot 服务未运行"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# 检查网络连通性(从 Chatwoot 容器访问 Agent)
|
|
||||||
echo "3️⃣ 检查容器间网络连通性..."
|
|
||||||
if docker exec ai_chatwoot wget -q -O - http://agent:8000/health | grep -q "healthy"; then
|
|
||||||
echo " ✅ Chatwoot 可以访问 Agent (http://agent:8000)"
|
|
||||||
else
|
|
||||||
echo " ❌ Chatwoot 无法访问 Agent"
|
|
||||||
echo " 请检查两个容器是否在同一 Docker 网络中"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# 检查环境变量配置
|
|
||||||
echo "4️⃣ 检查环境变量配置..."
|
|
||||||
if [ -f .env ]; then
|
|
||||||
if grep -q "CHATWOOT_WEBHOOK_SECRET" .env; then
|
|
||||||
echo " ✅ CHATWOOT_WEBHOOK_SECRET 已配置"
|
|
||||||
else
|
|
||||||
echo " ⚠️ CHATWOOT_WEBHOOK_SECRET 未配置(可选)"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo " ⚠️ .env 文件不存在"
|
|
||||||
fi
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# 显示配置摘要
|
|
||||||
echo "======================================"
|
|
||||||
echo "📋 配置摘要"
|
|
||||||
echo "======================================"
|
|
||||||
echo ""
|
|
||||||
echo "Agent 服务:"
|
|
||||||
echo " • 容器名称: ai_agent"
|
|
||||||
echo " • 内部地址: http://agent:8000"
|
|
||||||
echo " • Webhook 端点: http://agent:8000/webhooks/chatwoot"
|
|
||||||
echo " • 外部访问: http://localhost:8000"
|
|
||||||
echo ""
|
|
||||||
echo "Chatwoot 服务:"
|
|
||||||
echo " • 容器名称: ai_chatwoot"
|
|
||||||
echo " • 内部地址: http://chatwoot:3000"
|
|
||||||
echo " • 外部访问: http://localhost:3000"
|
|
||||||
echo ""
|
|
||||||
echo "📝 在 Chatwoot 界面中配置:"
|
|
||||||
echo " 1. 访问: http://localhost:3000"
|
|
||||||
echo " 2. 进入: Settings → Inboxes → 选择 Website 收件箱"
|
|
||||||
echo " 3. 点击: Configuration 标签"
|
|
||||||
echo " 4. 设置 Webhook URL 为: http://agent:8000/webhooks/chatwoot"
|
|
||||||
echo " 5. 点击 Save 保存"
|
|
||||||
echo ""
|
|
||||||
echo "⚠️ 注意事项:"
|
|
||||||
echo " • 不要在 Chatwoot 中启用内置机器人(Bot)"
|
|
||||||
echo " • 只配置 Webhook 即可"
|
|
||||||
echo " • Webhook URL 使用 'agent' 而不是 'localhost'"
|
|
||||||
echo ""
|
|
||||||
echo "======================================"
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# 测试所有 FAQ 分类
|
|
||||||
|
|
||||||
echo "=========================================="
|
|
||||||
echo "🧪 测试所有 FAQ 分类"
|
|
||||||
echo "=========================================="
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# 定义测试用例
|
|
||||||
declare -A TEST_CASES=(
|
|
||||||
["订单相关"]="How do I place an order?"
|
|
||||||
["支付相关"]="What payment methods do you accept?"
|
|
||||||
["运输相关"]="What are the shipping options?"
|
|
||||||
["退货相关"]="I received a defective item, what should I do?"
|
|
||||||
["账号相关"]="I forgot my password, now what?"
|
|
||||||
["营业时间"]="What are your opening hours?"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 测试每个分类
|
|
||||||
for category in "${!TEST_CASES[@]}"; do
|
|
||||||
question="${TEST_CASES[$category]}"
|
|
||||||
conv_id="test_${category}___$(date +%s)"
|
|
||||||
|
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
||||||
echo "📋 分类: $category"
|
|
||||||
echo "📝 问题: $question"
|
|
||||||
echo "⏳ 处理中..."
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# 调用 API
|
|
||||||
RESPONSE=$(docker exec ai_agent curl -s -X POST 'http://localhost:8000/api/agent/query' \
|
|
||||||
-H 'Content-Type: application/json' \
|
|
||||||
-d "{\"conversation_id\":\"$conv_id\",\"user_id\":\"test_user\",\"account_id\":\"2\",\"message\":\"$question\"}")
|
|
||||||
|
|
||||||
# 解析并显示结果
|
|
||||||
echo "$RESPONSE" | python3 << PYTHON
|
|
||||||
import json
|
|
||||||
import sys
|
|
||||||
|
|
||||||
try:
|
|
||||||
data = json.load(sys.stdin)
|
|
||||||
|
|
||||||
# 提取响应
|
|
||||||
response = data.get("response", "")
|
|
||||||
intent = data.get("intent", "")
|
|
||||||
|
|
||||||
if response:
|
|
||||||
# 清理 HTML 标签(如果有)
|
|
||||||
import re
|
|
||||||
clean_response = re.sub(r'<[^<]+?>', '', response)
|
|
||||||
clean_response = clean_response.strip()
|
|
||||||
|
|
||||||
# 截断过长响应
|
|
||||||
if len(clean_response) > 300:
|
|
||||||
clean_response = clean_response[:300] + "..."
|
|
||||||
|
|
||||||
print(f"🎯 意图: {intent}")
|
|
||||||
print(f"🤖 回答: {clean_response}")
|
|
||||||
else:
|
|
||||||
print("❌ 未获得回答")
|
|
||||||
print(f"调试信息: {json.dumps(data, indent=2, ensure_ascii=False)}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ 解析错误: {e}")
|
|
||||||
print(f"原始响应: {sys.stdin.read()}")
|
|
||||||
PYTHON
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
sleep 2 # 间隔 2 秒
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "=========================================="
|
|
||||||
echo "✅ 所有测试完成"
|
|
||||||
echo "=========================================="
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
"""
|
|
||||||
测试商城订单查询接口
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
python test_mall_order_query.py <order_id>
|
|
||||||
|
|
||||||
Example:
|
|
||||||
python test_mall_order_query.py 202071324
|
|
||||||
"""
|
|
||||||
import asyncio
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
|
|
||||||
# Add mcp_servers to path
|
|
||||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "mcp_servers"))
|
|
||||||
|
|
||||||
from shared.mall_client import MallClient
|
|
||||||
|
|
||||||
|
|
||||||
async def test_order_query(order_id: str, token: str):
|
|
||||||
"""测试订单查询
|
|
||||||
|
|
||||||
Args:
|
|
||||||
order_id: 订单号
|
|
||||||
token: JWT Token
|
|
||||||
"""
|
|
||||||
print(f"\n{'='*60}")
|
|
||||||
print(f"测试商城订单查询接口")
|
|
||||||
print(f"{'='*60}")
|
|
||||||
print(f"订单号 (Order ID): {order_id}")
|
|
||||||
print(f"API URL: https://apicn.qa1.gaia888.com")
|
|
||||||
print(f"{'='*60}\n")
|
|
||||||
|
|
||||||
# 创建客户端
|
|
||||||
client = MallClient(
|
|
||||||
api_url="https://apicn.qa1.gaia888.com",
|
|
||||||
api_token=token,
|
|
||||||
tenant_id="2",
|
|
||||||
currency_code="EUR",
|
|
||||||
language_id="1",
|
|
||||||
source="us.qa1.gaia888.com"
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# 调用订单查询接口
|
|
||||||
result = await client.get_order_by_id(order_id)
|
|
||||||
|
|
||||||
# 打印结果
|
|
||||||
print("✅ 查询成功 (Query Success)!")
|
|
||||||
print(f"\n返回数据 (Response Data):")
|
|
||||||
print("-" * 60)
|
|
||||||
import json
|
|
||||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
||||||
print("-" * 60)
|
|
||||||
|
|
||||||
# 提取关键信息
|
|
||||||
if isinstance(result, dict):
|
|
||||||
print(f"\n关键信息 (Key Information):")
|
|
||||||
print(f" 订单号 (Order ID): {result.get('order_id') or result.get('orderId') or order_id}")
|
|
||||||
print(f" 订单状态 (Status): {result.get('status') or result.get('order_status') or 'N/A'}")
|
|
||||||
print(f" 订单金额 (Amount): {result.get('total_amount') or result.get('amount') or 'N/A'}")
|
|
||||||
|
|
||||||
# 商品信息
|
|
||||||
items = result.get('items') or result.get('order_items') or result.get('products')
|
|
||||||
if items:
|
|
||||||
print(f" 商品数量 (Items): {len(items)}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ 查询失败 (Query Failed): {str(e)}")
|
|
||||||
import traceback
|
|
||||||
traceback.print_exc()
|
|
||||||
|
|
||||||
finally:
|
|
||||||
await client.close()
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""主函数"""
|
|
||||||
if len(sys.argv) < 2:
|
|
||||||
print("Usage: python test_mall_order_query.py <order_id> [token]")
|
|
||||||
print("\nExample:")
|
|
||||||
print(' python test_mall_order_query.py 202071324')
|
|
||||||
print(' python test_mall_order_query.py 202071324 "your_jwt_token_here"')
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
order_id = sys.argv[1]
|
|
||||||
|
|
||||||
# 从命令行获取 token,如果没有提供则使用默认的测试 token
|
|
||||||
if len(sys.argv) >= 3:
|
|
||||||
token = sys.argv[2]
|
|
||||||
else:
|
|
||||||
# 使用用户提供的示例 token
|
|
||||||
token = "eyJ0eXAiOiJqd3QifQ.eyJzdWIiOiIxIiwiaXNzIjoiaHR0cDpcL1wvOiIsImV4cCI6MTc3MDUyMDY2MSwiaWF0IjoxNzY3OTI4NjYxLCJuYmYiOjE3Njc5Mjg2NjEsInVzZXJJZCI6MTAxNDMyLCJ0eXBlIjoyLCJ0ZW5hbnRJZCI6MiwidWlkIjoxMDE0MzIsInMiOiJkM0tZMjMiLCJqdGkiOiI3YjcwYTI2MzYwYjJmMzA3YmQ4YTYzNDAxOGVlNjlmZSJ9.dwiqln19-yAQSJd1w5bxZFrRgyohdAkHa1zW3W7Ov2I"
|
|
||||||
print("⚠️ 使用默认的测试 token(可能已过期)")
|
|
||||||
print(" 如需测试,请提供有效的 token:")
|
|
||||||
print(f' python {sys.argv[0]} {order_id} "your_jwt_token_here"\n')
|
|
||||||
|
|
||||||
# 运行异步测试
|
|
||||||
asyncio.run(test_order_query(order_id, token))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
"""
|
|
||||||
测试退货相关 FAQ 回答
|
|
||||||
"""
|
|
||||||
import asyncio
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
|
|
||||||
# 添加 agent 目录到路径
|
|
||||||
sys.path.insert(0, '/app')
|
|
||||||
|
|
||||||
from agents.customer_service import customer_service_agent
|
|
||||||
from core.state import AgentState
|
|
||||||
|
|
||||||
|
|
||||||
async def test_return_faq():
|
|
||||||
"""测试退货相关 FAQ"""
|
|
||||||
|
|
||||||
# 测试问题列表
|
|
||||||
test_questions = [
|
|
||||||
"I received a defective item, what should I do?",
|
|
||||||
"How do I return a product?",
|
|
||||||
"What is your return policy?",
|
|
||||||
"I want to get a refund for my order",
|
|
||||||
]
|
|
||||||
|
|
||||||
for question in test_questions:
|
|
||||||
print(f"\n{'='*60}")
|
|
||||||
print(f"📝 问题: {question}")
|
|
||||||
print(f"{'='*60}")
|
|
||||||
|
|
||||||
# 初始化状态
|
|
||||||
state = AgentState(
|
|
||||||
conversation_id="test_return_001",
|
|
||||||
user_id="test_user",
|
|
||||||
account_id="2",
|
|
||||||
message=question,
|
|
||||||
history=[],
|
|
||||||
context={}
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# 调用客服 Agent
|
|
||||||
final_state = await customer_service_agent(state)
|
|
||||||
|
|
||||||
# 获取响应
|
|
||||||
response = final_state.get("response", "无响应")
|
|
||||||
tool_calls = final_state.get("tool_calls", [])
|
|
||||||
intent = final_state.get("intent")
|
|
||||||
|
|
||||||
print(f"\n🎯 意图识别: {intent}")
|
|
||||||
print(f"\n🤖 AI 回答:")
|
|
||||||
print(response)
|
|
||||||
print(f"\n📊 调用的工具: {tool_calls}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"\n❌ 错误: {e}")
|
|
||||||
import traceback
|
|
||||||
traceback.print_exc()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
print("🧪 测试退货相关 FAQ 回答\n")
|
|
||||||
asyncio.run(test_return_faq())
|
|
||||||
Reference in New Issue
Block a user