feat: 修复订单查询和物流查询功能
主要修改: 1. 订单数据解析修复 (agent/agents/order.py) - 修复 Mall API 返回数据的嵌套结构解析 - 更新字段映射:orderId→order_id, orderProduct→items, statusText→status_text - 支持多种商品图片字段:image, pic, thumb, productImg - 添加详细的调试日志 2. 物流查询修复 (mcp_servers/order_mcp/server.py) - 修复物流接口返回数据结构解析 (data[].trackingCode→tracking_number) - 添加 print() 日志用于调试 - 支持多种字段名映射 3. Chatwoot 集成优化 (agent/integrations/chatwoot.py) - 添加 json 模块导入 - 完善订单卡片和表单展示功能 4. API 请求头优化 (mcp_servers/shared/mall_client.py) - 更新 User-Agent 和 Accept 头 - 修正 Origin 和 Referer 大小写 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
556
CLAUDE.md
Normal file
556
CLAUDE.md
Normal file
@@ -0,0 +1,556 @@
|
||||
# B2B Shopping AI Assistant - Claude 开发上下文
|
||||
|
||||
> 本文件记录项目架构、技术决策、最近修改历史,便于 Claude Code 继续开发维护
|
||||
|
||||
---
|
||||
|
||||
## 项目概览
|
||||
|
||||
**项目名称**: B2B Shopping AI Assistant
|
||||
**技术栈**: Python + LangGraph + FastMCP + Chatwoot + Docker
|
||||
**AI 模型**: 智谱 AI GLM-4-Flash
|
||||
**主要功能**: 订单查询、产品咨询、售后服务、FAQ 智能问答
|
||||
|
||||
---
|
||||
|
||||
## 服务架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 客户端渠道 │
|
||||
│ (Website Widget / API / Chatwoot) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Chatwoot (消息平台) │
|
||||
│ http://localhost:3000 │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ LangGraph Agent (AI 代理层) │
|
||||
│ http://localhost:8000 │
|
||||
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
||||
│ │ Router │ │ Product │ │ Order │ │ Aftersale│ │
|
||||
│ │ Agent │ │ Agent │ │ Agent │ │ Agent │ │
|
||||
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ MCP Servers (工具服务) │
|
||||
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
||||
│ │ Strapi │ │ Order │ │ Aftersale│ │ Product │ │
|
||||
│ │ MCP │ │ MCP │ │ MCP │ │ MCP │ │
|
||||
│ │ :8001 │ │ :8002 │ │ :8003 │ │ :8004 │ │
|
||||
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 后端服务 (Hyperf PHP) │
|
||||
│ https://api.qa1.gaia888.com │
|
||||
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
||||
│ │ Orders │ │ Products │ │ Aftersales│ │
|
||||
│ └──────────┘ └──────────┘ └──────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
ai/
|
||||
├── agent/ # AI Agent 主服务
|
||||
│ ├── agents/ # 各业务 Agent 实现
|
||||
│ │ ├── router.py # 路由 Agent(意图识别)
|
||||
│ │ ├── customer_service.py # 客服 Agent(FAQ、公司信息)
|
||||
│ │ ├── order.py # 订单 Agent
|
||||
│ │ ├── product.py # 产品 Agent
|
||||
│ │ └── aftersale.py # 售后 Agent
|
||||
│ ├── core/ # 核心框架
|
||||
│ │ ├── state.py # AgentState 状态管理
|
||||
│ │ ├── graph.py # LangGraph 工作流
|
||||
│ │ ├── llm.py # LLM 客户端(智谱 AI)
|
||||
│ │ └── language_detector.py # 多语言检测
|
||||
│ ├── integrations/ # 第三方集成
|
||||
│ │ ├── chatwoot.py # Chatwoot API 客户端
|
||||
│ │ └── hyperf_client.py # Hyperf API 客户端
|
||||
│ ├── prompts/ # 提示词模板
|
||||
│ ├── utils/ # 工具函数
|
||||
│ │ ├── logger.py # 日志工具
|
||||
│ │ ├── faq_library.py # 本地 FAQ 库(仅社交问候)
|
||||
│ │ ├── response_cache.py # Redis 响应缓存
|
||||
│ │ ├── cache.py # Redis 缓存客户端
|
||||
│ │ └── token_manager.py # Token 管理
|
||||
│ ├── webhooks/ # Webhook 处理
|
||||
│ │ └── chatwoot_webhook.py # Chatwoot Webhook 接收
|
||||
│ ├── config.py # 配置管理
|
||||
│ └── main.py # FastAPI 应用入口
|
||||
│
|
||||
├── mcp_servers/ # MCP 服务集群
|
||||
│ ├── strapi_mcp/ # 知识库 MCP (端口 8001)
|
||||
│ ├── order_mcp/ # 订单 MCP (端口 8002)
|
||||
│ ├── aftersale_mcp/ # 售后 MCP (端口 8003)
|
||||
│ ├── product_mcp/ # 产品 MCP (端口 8004)
|
||||
│ └── shared/ # 共享模块
|
||||
│
|
||||
├── docker-compose.yml # Docker 编排配置
|
||||
├── nginx.conf # Nginx 配置
|
||||
├── .env # 环境变量
|
||||
└── postgres-with-pgvector.Dockerfile # PostgreSQL Dockerfile
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 重要配置文件
|
||||
|
||||
### 环境变量 (.env)
|
||||
|
||||
```env
|
||||
# AI 模型
|
||||
ZHIPU_API_KEY=87ecd9d88ef549bc817e1feeba226a3b.hfbTWh4Hhdw8Kulh
|
||||
ZHIPU_MODEL=GLM-4-Flash-250414
|
||||
ENABLE_REASONING_MODE=false
|
||||
REASONING_MODE_FOR_COMPLEX=true
|
||||
|
||||
# Redis
|
||||
REDIS_HOST=redis
|
||||
REDIS_PORT=6379
|
||||
|
||||
# Chatwoot
|
||||
CHATWOOT_API_URL=http://chatwoot:3000
|
||||
CHATWOOT_API_TOKEN=fnWaEeAyC1gw1FYQq6YJMWSj
|
||||
CHATWOOT_ACCOUNT_ID=2
|
||||
CHATWOOT_WEBHOOK_SECRET=b7a12b9c9173718596f02fd912fb59f97891a0e7abb1a5e457b4c8858b2d21b5
|
||||
|
||||
# Hyperf API
|
||||
HYPERF_API_URL=https://api.qa1.gaia888.com
|
||||
HYPERF_API_TOKEN=
|
||||
|
||||
# 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
|
||||
```
|
||||
|
||||
### 服务端口映射
|
||||
|
||||
| 服务 | 容器名 | 内部端口 | 外部端口 | 说明 |
|
||||
|------|--------|----------|----------|------|
|
||||
| Agent | ai_agent | 8000 | 8000 | AI 代理服务 |
|
||||
| Chatwoot | ai_chatwoot | 3000 | 3000 | 客服平台 |
|
||||
| Redis | ai_redis | 6379 | - | 缓存/队列 |
|
||||
| PostgreSQL | ai_postgres | 5432 | - | Chatwoot 数据库 |
|
||||
| Nginx | ai_nginx | 80 | 8080 | 文档静态服务 |
|
||||
| Strapi MCP | ai_strapi_mcp | 8001 | 8001 | 知识库服务 |
|
||||
| Order MCP | ai_order_mcp | 8002 | 8002 | 订单服务 |
|
||||
| Aftersale MCP | ai_aftersale_mcp | 8003 | 8003 | 售后服务 |
|
||||
| Product MCP | ai_product_mcp | 8004 | 8004 | 产品服务 |
|
||||
|
||||
---
|
||||
|
||||
## 关键代码文件
|
||||
|
||||
### 1. Agent 核心流程
|
||||
|
||||
**agent/core/graph.py:156-188**
|
||||
- LangGraph 状态图定义
|
||||
- Agent 路由逻辑
|
||||
- 条件边和工具调用流程
|
||||
|
||||
**agent/core/state.py:16-67**
|
||||
- AgentState 数据结构
|
||||
- ConversationState 枚举
|
||||
- 核心状态字段定义
|
||||
|
||||
### 2. Router - 意图识别
|
||||
|
||||
**agent/agents/router.py:38-57**
|
||||
- FAQ 快速路径优化(在 LLM 前检查本地 FAQ)
|
||||
- 多语言意图识别
|
||||
|
||||
**agent/agents/router.py:73-147**
|
||||
- LLM 意图分类 prompt
|
||||
- 支持意图:customer_service, order, product, aftersale
|
||||
|
||||
### 3. Customer Service Agent
|
||||
|
||||
**agent/agents/customer_service.py:40-64**
|
||||
- Router 级 FAQ 响应复用
|
||||
- 本地 FAQ 库备份检查
|
||||
|
||||
**agent/agents/customer_service.py:72-164**
|
||||
- 多语言分类关键词(8 种语言:en, nl, de, es, fr, it, tr, zh)
|
||||
- 自动查询 FAQ 分类:register, order, payment, shipment, return
|
||||
|
||||
### 4. Chatwoot 集成
|
||||
|
||||
**agent/integrations/chatwoot.py:228-384**
|
||||
- `send_order_form()` - 使用 content_type="form" 发送订单详情
|
||||
- `send_order_card()` - 使用 content_type="cards" 发送订单卡片
|
||||
- `send_rich_message()` - 通用富媒体消息发送
|
||||
|
||||
**agent/webhooks/chatwoot_webhook.py**
|
||||
- Webhook 接收处理
|
||||
- 消息格式转换
|
||||
- 并发会话管理
|
||||
|
||||
### 5. FAQ 系统
|
||||
|
||||
**agent/utils/faq_library.py:15-49**
|
||||
- 本地 FAQ 库(仅包含社交问候:你好、谢谢、再见等)
|
||||
- 业务 FAQ 由 Strapi MCP 处理
|
||||
- 支持精确匹配和模糊匹配
|
||||
|
||||
---
|
||||
|
||||
## Context7 MCP 集成
|
||||
|
||||
### 什么是 Context7?
|
||||
|
||||
**Context7 MCP** 是一个为 AI 编码助手和 LLM 提供实时、版本特定的代码文档和示例的服务。它解决了 AI 模型训练数据过时的问题,确保 AI 始终使用最新的官方文档。
|
||||
|
||||
**核心功能**:
|
||||
- **实时文档访问**: 从 GitHub 仓库获取最新的官方文档
|
||||
- **版本特定**: 支持查询特定版本(如 React 19、Next.js 15)的文档
|
||||
- **多语言支持**: 支持 100+ 主流库(前端、后端、数据库、AI/ML)
|
||||
- **向量搜索**: 使用语义搜索找到最相关的文档片段
|
||||
|
||||
### 配置方式
|
||||
|
||||
#### 本项目配置命令
|
||||
|
||||
```bash
|
||||
claude mcp add --transport http context7 https://mcp.context7.com/mcp \
|
||||
--header "CONTEXT7_API_KEY: ctx7sk-9be917f9-9086-4d85-a3d3-23555aaa2da6"
|
||||
```
|
||||
|
||||
#### API 密钥获取
|
||||
|
||||
1. 访问 [Upstash Console](https://console.upstash.com/)
|
||||
2. 创建新项目或选择现有项目
|
||||
3. 导航至 "Context7" 标签
|
||||
4. 复制 API 密钥(格式:`ctx7sk-<uuid>`)
|
||||
|
||||
### 可用工具
|
||||
|
||||
#### 1. `get-documentation-context`
|
||||
获取文档上下文(主要工具)
|
||||
|
||||
**参数**:
|
||||
```json
|
||||
{
|
||||
"library_id": "react", // 库 ID (必填)
|
||||
"query": "如何使用 useEffect", // 查询问题 (必填)
|
||||
"version": "19.0.0" // 指定版本 (可选)
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. `get-docs`
|
||||
获取库的完整文档列表
|
||||
|
||||
**参数**:
|
||||
```json
|
||||
{
|
||||
"library_id": "nextjs",
|
||||
"version": "15.0.0"
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. `resolve-library-id`
|
||||
从库名称/URL 获取标准化的 library_id
|
||||
|
||||
**参数**:
|
||||
```json
|
||||
{
|
||||
"identifier": "react" // 或 GitHub URL、npm 包名等
|
||||
}
|
||||
```
|
||||
|
||||
**返回**:
|
||||
```json
|
||||
{
|
||||
"library_id": "react",
|
||||
"available_versions": ["18.3.1", "19.0.0", "19.1.0"]
|
||||
}
|
||||
```
|
||||
|
||||
#### 4. `search-code`
|
||||
在文档中搜索代码示例
|
||||
|
||||
**参数**:
|
||||
```json
|
||||
{
|
||||
"query": "useEffect cleanup",
|
||||
"libraries": ["react", "nextjs"],
|
||||
"limit": 5
|
||||
}
|
||||
```
|
||||
|
||||
### 支持的库(部分)
|
||||
|
||||
| 类别 | 库 |
|
||||
|------|-----|
|
||||
| 前端框架 | React, Vue, Next.js, Nuxt, Svelte |
|
||||
| 后端框架 | FastAPI, Django, Express, Laravel |
|
||||
| 数据库 | PostgreSQL, MySQL, MongoDB, Redis |
|
||||
| 工具库 | Lodash, Axios, Moment.js |
|
||||
| AI/ML | LangChain, TensorFlow, PyTorch |
|
||||
|
||||
### 使用场景
|
||||
|
||||
**在开发中使用 Context7**:
|
||||
- 查询 LangGraph 最新 API 文档
|
||||
- 获取 FastAPI 特定版本的代码示例
|
||||
- 了解 Docker Compose 最新配置选项
|
||||
|
||||
**示例**:
|
||||
```
|
||||
用户: LangGraph 如何定义条件边?
|
||||
Claude: [调用 get-documentation-context]
|
||||
根据 LangGraph 官方文档,条件边使用 add_conditional_edges()...
|
||||
```
|
||||
|
||||
### 相关链接
|
||||
|
||||
- [Context7 GitHub 仓库](https://github.com/upstash/context7)
|
||||
- [Context7 API 文档](https://context7.mintlify.app/api-reference/context/get-documentation-context)
|
||||
- [Upstash Console](https://console.upstash.com/)
|
||||
|
||||
---
|
||||
|
||||
## 最近修改历史
|
||||
|
||||
### 2026-01-20 - Form 格式订单详情
|
||||
|
||||
**修改文件**: `agent/integrations/chatwoot.py`
|
||||
|
||||
**修改内容**:
|
||||
- 新增 `send_order_form()` 方法,使用 `content_type="form"` 展示订单详情
|
||||
- 支持字段类型:text, text_area, select
|
||||
- 订单字段映射:订单号、状态、下单时间、商品列表、总金额、运费、物流信息、备注
|
||||
|
||||
**API 参考**:
|
||||
- [Create New Message - Chatwoot API](https://developers.chatwoot.com/api-reference/messages/create-new-message)
|
||||
- [Interactive Messages - Chatwoot User Guide](https://www.chatwoot.com/hc/user-guide/articles/1677689344-how-to-use-interactive-messages)
|
||||
|
||||
**数据结构**:
|
||||
```json
|
||||
{
|
||||
"content": "订单详情",
|
||||
"content_type": "form",
|
||||
"content_attributes": {
|
||||
"items": [
|
||||
{
|
||||
"name": "order_id",
|
||||
"label": "订单号",
|
||||
"type": "text",
|
||||
"default": "123456789"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2026-01-20 - 多语言 FAQ 分类支持
|
||||
|
||||
**修改文件**: `agent/agents/customer_service.py:72-164`
|
||||
|
||||
**修改内容**:
|
||||
- 为 5 个业务分类添加 8 种语言关键词(en, nl, de, es, fr, it, tr, zh)
|
||||
- 分类:register, order, payment, shipment, return
|
||||
|
||||
**示例**:
|
||||
```python
|
||||
category_keywords = {
|
||||
"register": [
|
||||
"register", "sign up", "account", # English
|
||||
"注册", "账号", "登录", "密码", # Chinese
|
||||
# ... 其他语言
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 2026-01-20 - FAQ 架构优化
|
||||
|
||||
**修改文件**:
|
||||
- `agent/agents/router.py:38-57`
|
||||
- `agent/utils/faq_library.py:15-49`
|
||||
|
||||
**问题**: 本地 FAQ 库包含业务相关问答,导致返回硬编码数据而非 Strapi CMS 数据
|
||||
|
||||
**解决方案**:
|
||||
1. 本地 FAQ 库仅保留社交问候(18 个关键词)
|
||||
2. Router 层添加 FAQ 快速路径检查
|
||||
3. 业务查询自动调用 Strapi MCP 工具
|
||||
|
||||
**本地 FAQ 覆盖范围**:
|
||||
- 问候类:你好、hi、hello、hey
|
||||
- 感谢类:谢谢、thank you、thanks
|
||||
- 再见类:再见、bye、goodbye
|
||||
- 时间问候:早上好、下午好、晚上好、good morning
|
||||
|
||||
### 2026-01-20 - Chatwoot PID 文件修复
|
||||
|
||||
**修改文件**: `docker-compose.yml:64-68`
|
||||
|
||||
**问题**: Chatwoot 容器因残留 PID 文件陷入重启循环
|
||||
|
||||
**解决方案**: 在启动命令中自动清理 PID 文件
|
||||
```yaml
|
||||
chatwoot:
|
||||
command: sh -c "rm -f /app/tmp/pids/server.pid && bundle exec rails s -p 3000 -b 0.0.0.0"
|
||||
```
|
||||
|
||||
### 2026-01-20 - Docker 镜像拉取优化
|
||||
|
||||
**修改文件**: `/etc/docker/daemon.json`
|
||||
|
||||
**问题**: Docker Hub 超时导致镜像拉取失败
|
||||
|
||||
**解决方案**: 添加中国镜像源
|
||||
```json
|
||||
{
|
||||
"registry-mirrors": [
|
||||
"https://docker.m.daocloud.io",
|
||||
"https://docker.1panel.live",
|
||||
"https://dockerhub.icu"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 常见操作
|
||||
|
||||
### 启动服务
|
||||
|
||||
```bash
|
||||
# 启动所有服务
|
||||
docker-compose up -d
|
||||
|
||||
# 查看服务状态
|
||||
docker-compose ps
|
||||
|
||||
# 查看日志
|
||||
docker-compose logs -f agent
|
||||
|
||||
# 重启单个服务
|
||||
docker-compose restart agent
|
||||
```
|
||||
|
||||
### 环境变量生效
|
||||
|
||||
```bash
|
||||
# 修改 .env 后,需要重启相关服务
|
||||
# Agent 服务依赖的环境变量:ZHIPU_*, REDIS_*, CHATWOOT_*, STRAPI_*, HYPERF_*
|
||||
docker-compose restart agent
|
||||
|
||||
# Chatwoot 服务依赖的环境变量:CHATWOOT_*, POSTGRES_*, REDIS_*
|
||||
docker-compose restart chatwoot chatwoot_worker
|
||||
|
||||
# MCP 服务依赖的环境变量:HYPERF_*, STRAPI_*
|
||||
docker-compose restart strapi_mcp order_mcp aftersale_mcp product_mcp
|
||||
```
|
||||
|
||||
### Git 提交
|
||||
|
||||
```bash
|
||||
# 查看状态
|
||||
git status
|
||||
|
||||
# 添加文件(排除测试文件)
|
||||
git add agent/integrations/chatwoot.py
|
||||
|
||||
# 提交
|
||||
git commit -m "feat: 添加订单详情 form 格式展示支持"
|
||||
|
||||
# 推送
|
||||
git push origin main
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 技术决策记录
|
||||
|
||||
### 1. 为什么使用 LangGraph?
|
||||
|
||||
- **状态图工作流**: 支持 Agent 之间的条件跳转和循环
|
||||
- **内置检查点**: 支持 conversation history 的持久化
|
||||
- **可视化调试**: 可以导出 Mermaid 图表查看工作流
|
||||
|
||||
### 2. 为什么使用 MCP (Model Context Protocol)?
|
||||
|
||||
- **解耦**: Agent 与业务逻辑分离
|
||||
- **复用**: MCP 工具可被多个 Agent 共享
|
||||
- **标准化**: 统一的工具调用接口
|
||||
|
||||
### 3. 为什么 FAQ 分两级?
|
||||
|
||||
- **本地 FAQ 库**: 处理社交问候(你好、谢谢等),避免 LLM 调用
|
||||
- **Strapi MCP**: 处理业务 FAQ(注册、订单等),确保数据一致性
|
||||
|
||||
### 4. 为什么 Router 层检查 FAQ?
|
||||
|
||||
- **性能优化**: 跳过 LLM 意图识别,直接返回
|
||||
- **成本降低**: 减少 LLM API 调用
|
||||
- **用户体验**: 即时响应常见问候
|
||||
|
||||
### 5. 为什么集成 Context7 MCP?
|
||||
|
||||
- **解决文档过时**: AI 模型训练数据截止到 2023 年,无法获取最新 API
|
||||
- **版本特定**: 支持查询特定版本的文档(如 LangGraph 0.3 vs 0.2)
|
||||
- **代码示例**: 提供实际可运行的代码片段,减少 AI 幻觉
|
||||
- **开发效率**: 减少查文档时间,专注于业务逻辑
|
||||
|
||||
---
|
||||
|
||||
## 已知问题
|
||||
|
||||
### 1. Form 类型数据展示
|
||||
|
||||
**问题**: Chatwoot 的 form 类型主要用于用户输入,用于只读展示时字段可编辑
|
||||
|
||||
**当前方案**: 使用 `default` 属性预填值,用户可以看到但技术上可修改
|
||||
|
||||
**未来改进**:
|
||||
- 研究是否支持只读模式
|
||||
- 或考虑使用 cards + JSON 格式展示结构化数据
|
||||
|
||||
### 2. Token 传递
|
||||
|
||||
**问题**: 部分 MCP 工具调用可能未正确传递 Chatwoot conversation ID
|
||||
|
||||
**当前方案**: 在 AgentState 中维护 conversation_id
|
||||
|
||||
---
|
||||
|
||||
## 参考文档
|
||||
|
||||
### 项目相关
|
||||
- [LangGraph 文档](https://langchain-ai.github.io/langgraph/)
|
||||
- [FastMCP 文档](https://github.com/jlowin/fastmcp)
|
||||
- [Chatwoot API 文档](https://developers.chatwoot.com/)
|
||||
- [智谱 AI API 文档](https://open.bigmodel.cn/dev/api)
|
||||
|
||||
### Context7 相关
|
||||
- [Context7 GitHub 仓库](https://github.com/upstash/context7)
|
||||
- [Context7 API 文档](https://context7.mintlify.app/api-reference/context/get-documentation-context)
|
||||
- [Upstash Console](https://console.upstash.com/)
|
||||
- [Context7 MCP 教程](https://dev.to/mehmetakar/context7-mcp-tutorial-3he2)
|
||||
|
||||
### 外部参考
|
||||
- [热门 MCP Server 详解 - 火山引擎](https://www.volcengine.com/docs/86677/2165252)
|
||||
- [Context7 MCP 为 Cursor 提供实时文档上下文 - 知乎](https://zhuanlan.zhihu.com/p/1914959847792804088)
|
||||
- [Context7 MCP Server 介绍 - 二师兄的博客](https://www.cloudesx.com/article/Context7-mcp-server)
|
||||
- [AI 编程焕新:用 Context7 - 腾讯云](https://cloud.tencent.com/developer/article/2528436)
|
||||
|
||||
---
|
||||
|
||||
**最后更新**: 2026-01-20
|
||||
**维护者**: Claude Code
|
||||
@@ -7,6 +7,7 @@ from typing import Any
|
||||
from core.state import AgentState, ConversationState, add_tool_call, set_response, update_context
|
||||
from core.llm import get_llm_client, Message
|
||||
from utils.logger import get_logger
|
||||
from integrations.chatwoot import ChatwootClient
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -362,19 +363,267 @@ async def order_agent(state: AgentState) -> AgentState:
|
||||
async def _generate_order_response(state: AgentState) -> AgentState:
|
||||
"""Generate response based on order tool results"""
|
||||
|
||||
# 解析订单数据并尝试使用 form 格式发送
|
||||
order_data = None
|
||||
user_message = ""
|
||||
logistics_data = None
|
||||
|
||||
for result in state["tool_results"]:
|
||||
if result["success"]:
|
||||
data = result["data"]
|
||||
tool_name = result["tool_name"]
|
||||
|
||||
# 提取订单ID到上下文
|
||||
if isinstance(data, dict):
|
||||
if data.get("order_id"):
|
||||
state = update_context(state, {"order_id": data["order_id"]})
|
||||
elif data.get("orders") and len(data["orders"]) > 0:
|
||||
state = update_context(state, {"order_id": data["orders"][0].get("order_id")})
|
||||
|
||||
# 处理 get_mall_order 返回的订单数据
|
||||
if tool_name == "get_mall_order" and isinstance(data, dict):
|
||||
# MCP 返回结构: {"success": true, "result": {...}}
|
||||
# result 可能包含: {"success": bool, "order": {...}, "order_id": "...", "error": "..."}
|
||||
mcp_result = data.get("result", {})
|
||||
|
||||
# 检查是否有错误(如未登录)
|
||||
if mcp_result.get("error") or not mcp_result.get("success"):
|
||||
logger.warning(
|
||||
"get_mall_order returned error",
|
||||
error=mcp_result.get("error"),
|
||||
require_login=mcp_result.get("require_login")
|
||||
)
|
||||
# 设置错误消息到状态中
|
||||
if mcp_result.get("require_login"):
|
||||
user_message = mcp_result.get("error", "请先登录账户以查询订单信息")
|
||||
elif mcp_result.get("order"):
|
||||
# 有订单数据
|
||||
order_data = _parse_mall_order_data(mcp_result["order"])
|
||||
# 如果 order_data 中没有 order_id,从外层获取
|
||||
if not order_data.get("order_id") and mcp_result.get("order_id"):
|
||||
order_data["order_id"] = mcp_result["order_id"]
|
||||
else:
|
||||
logger.warning(
|
||||
"get_mall_order returned success but no order data",
|
||||
data_keys=list(data.keys()),
|
||||
result_keys=list(mcp_result.keys()) if isinstance(mcp_result, dict) else None
|
||||
)
|
||||
|
||||
# 处理 query_order 返回的订单数据
|
||||
elif tool_name == "query_order" and isinstance(data, dict):
|
||||
if data.get("orders") and len(data["orders"]) > 0:
|
||||
order_data = _parse_order_data(data["orders"][0])
|
||||
if len(data["orders"]) > 1:
|
||||
user_message = f"找到 {len(data['orders'])} 个订单,显示最新的一个:"
|
||||
|
||||
# 处理 get_logistics 返回的物流数据
|
||||
elif tool_name == "get_logistics" and isinstance(data, dict):
|
||||
logistics_data = _parse_logistics_data(data)
|
||||
# 如果之前有订单数据,添加物流信息
|
||||
if order_data:
|
||||
order_data["logistics"] = logistics_data
|
||||
|
||||
# 尝试使用 Chatwoot cards 格式发送
|
||||
if order_data:
|
||||
try:
|
||||
# 检查是否有有效的 order_id
|
||||
if not order_data.get("order_id"):
|
||||
logger.warning(
|
||||
"No valid order_id in order_data, falling back to text response",
|
||||
order_data=order_data
|
||||
)
|
||||
return await _generate_text_response(state)
|
||||
|
||||
chatwoot = ChatwootClient()
|
||||
conversation_id = state.get("conversation_id")
|
||||
|
||||
if conversation_id:
|
||||
# 记录订单数据(用于调试)
|
||||
logger.info(
|
||||
"Preparing to send order card",
|
||||
conversation_id=conversation_id,
|
||||
order_id=order_data.get("order_id"),
|
||||
items_count=len(order_data.get("items", []))
|
||||
)
|
||||
|
||||
# 发送订单卡片(使用默认的"查看订单详情"按钮)
|
||||
await chatwoot.send_order_card(
|
||||
conversation_id=conversation_id,
|
||||
order_data=order_data
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"Order card sent successfully",
|
||||
conversation_id=conversation_id,
|
||||
order_id=order_data.get("order_id")
|
||||
)
|
||||
|
||||
# 设置确认消息
|
||||
response_text = user_message or "订单详情如下"
|
||||
state = set_response(state, response_text)
|
||||
state["state"] = ConversationState.GENERATING.value
|
||||
return state
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Failed to send order card, falling back to text response",
|
||||
error=str(e),
|
||||
order_id=order_data.get("order_id")
|
||||
)
|
||||
|
||||
# 降级处理:使用原来的 LLM 生成逻辑
|
||||
return await _generate_text_response(state)
|
||||
|
||||
|
||||
def _parse_mall_order_data(data: dict[str, Any]) -> dict[str, Any]:
|
||||
"""解析商城 API 返回的订单数据"""
|
||||
# 记录原始数据结构(用于调试)
|
||||
logger.info(
|
||||
"Parsing mall order data",
|
||||
data_keys=list(data.keys()),
|
||||
has_order_id=bool(data.get("order_id")),
|
||||
has_order_sn=bool(data.get("order_sn")),
|
||||
has_nested_order=bool(data.get("order")),
|
||||
order_id_preview=data.get("order_id", data.get("order_sn", "")),
|
||||
# 如果有 order 字段,记录其内容类型和键
|
||||
nested_order_type=type(data.get("order")).__name__ if data.get("order") else None,
|
||||
nested_order_keys=list(data.get("order", {}).keys()) if isinstance(data.get("order"), dict) else None
|
||||
)
|
||||
|
||||
# Mall API 返回结构:外层包含 userId, reqContext 等,实际的订单数据在 order 字段中
|
||||
# 如果有嵌套的 order 字段,提取出来
|
||||
actual_order_data = data.get("order", data) if data.get("order") else data
|
||||
|
||||
# 记录提取的订单数据结构(用于调试)
|
||||
logger.info(
|
||||
"Extracted order data structure",
|
||||
actual_order_keys=list(actual_order_data.keys()) if isinstance(actual_order_data, dict) else type(actual_order_data).__name__,
|
||||
has_items=bool(actual_order_data.get("items")),
|
||||
has_order_items=bool(actual_order_data.get("order_items")),
|
||||
has_products=bool(actual_order_data.get("products")),
|
||||
has_orderProduct=bool(actual_order_data.get("orderProduct")),
|
||||
has_orderGoods=bool(actual_order_data.get("orderGoods")),
|
||||
has_goods=bool(actual_order_data.get("goods"))
|
||||
)
|
||||
|
||||
order_data = {
|
||||
"order_id": actual_order_data.get("orderId", actual_order_data.get("order_id", actual_order_data.get("order_sn", ""))),
|
||||
"status": actual_order_data.get("orderStatusId", actual_order_data.get("status", "unknown")),
|
||||
"status_text": actual_order_data.get("statusText", actual_order_data.get("status_text", actual_order_data.get("status", ""))),
|
||||
"total_amount": actual_order_data.get("total", actual_order_data.get("total_amount", actual_order_data.get("order_amount", "0.00"))),
|
||||
"shipping_fee": actual_order_data.get("shipping_fee", actual_order_data.get("freight_amount", "0")),
|
||||
}
|
||||
|
||||
# 下单时间
|
||||
if actual_order_data.get("created_at"):
|
||||
order_data["created_at"] = actual_order_data["created_at"]
|
||||
elif actual_order_data.get("add_time"):
|
||||
order_data["created_at"] = actual_order_data["add_time"]
|
||||
elif actual_order_data.get("dateAdded"):
|
||||
order_data["created_at"] = actual_order_data["dateAdded"]
|
||||
|
||||
# 商品列表 - 尝试多种可能的字段名(优先 orderProduct)
|
||||
items = (
|
||||
actual_order_data.get("orderProduct") or
|
||||
actual_order_data.get("items") or
|
||||
actual_order_data.get("order_items") or
|
||||
actual_order_data.get("products") or
|
||||
actual_order_data.get("orderGoods") or
|
||||
actual_order_data.get("goods") or
|
||||
[]
|
||||
)
|
||||
|
||||
# 记录商品列表数据结构(用于调试)
|
||||
if items and len(items) > 0:
|
||||
first_item = items[0]
|
||||
logger.info(
|
||||
"First item structure",
|
||||
first_item_keys=list(first_item.keys()) if isinstance(first_item, dict) else type(first_item).__name__,
|
||||
has_image_url=bool(first_item.get("image_url")) if isinstance(first_item, dict) else False,
|
||||
has_image=bool(first_item.get("image")) if isinstance(first_item, dict) else False,
|
||||
has_pic=bool(first_item.get("pic")) if isinstance(first_item, dict) else False,
|
||||
sample_item_data=str(first_item)[:500] if isinstance(first_item, dict) else str(first_item)
|
||||
)
|
||||
|
||||
if items:
|
||||
order_data["items"] = []
|
||||
for item in items:
|
||||
item_data = {
|
||||
"name": item.get("name", item.get("productName", item.get("product_name", "未知商品"))),
|
||||
"quantity": item.get("quantity", item.get("num", item.get("productNum", 1))),
|
||||
"price": item.get("price", item.get("total", item.get("productPrice", item.get("product_price", "0.00"))))
|
||||
}
|
||||
# 添加商品图片(支持多种可能的字段名)
|
||||
image_url = (
|
||||
item.get("image") or
|
||||
item.get("image_url") or
|
||||
item.get("pic") or
|
||||
item.get("thumb") or
|
||||
item.get("product_image") or
|
||||
item.get("pic_url") or
|
||||
item.get("thumb_url") or
|
||||
item.get("img") or
|
||||
item.get("productImg") or
|
||||
item.get("thumb")
|
||||
)
|
||||
if image_url:
|
||||
item_data["image_url"] = image_url
|
||||
else:
|
||||
# 记录没有图片的商品(用于调试)
|
||||
logger.debug(
|
||||
"No image found for product",
|
||||
product_name=item_data.get("name"),
|
||||
available_keys=list(item.keys())
|
||||
)
|
||||
order_data["items"].append(item_data)
|
||||
|
||||
# 备注
|
||||
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", ""))
|
||||
|
||||
return order_data
|
||||
|
||||
|
||||
def _parse_order_data(data: dict[str, Any]) -> dict[str, Any]:
|
||||
"""解析历史订单数据"""
|
||||
return _parse_mall_order_data(data)
|
||||
|
||||
|
||||
def _parse_logistics_data(data: dict[str, Any]) -> dict[str, Any]:
|
||||
"""解析物流数据"""
|
||||
# MCP 返回结构: {"success": true, "result": {...物流数据...}}
|
||||
mcp_result = data.get("result", data) if data.get("result") else data
|
||||
|
||||
logger.info(
|
||||
"Parsing logistics data",
|
||||
data_keys=list(data.keys()) if isinstance(data, dict) else None,
|
||||
has_result_in_data=bool(data.get("result")),
|
||||
mcp_result_keys=list(mcp_result.keys()) if isinstance(mcp_result, dict) else None,
|
||||
raw_tracking_number_value=repr(mcp_result.get("tracking_number")) if mcp_result.get("tracking_number") is not None else None,
|
||||
raw_courier_value=repr(mcp_result.get("courier")) if mcp_result.get("courier") is not None else None,
|
||||
has_tracking_number=bool(mcp_result.get("tracking_number")),
|
||||
has_courier=bool(mcp_result.get("courier")),
|
||||
has_timeline=bool(mcp_result.get("timeline"))
|
||||
)
|
||||
|
||||
return {
|
||||
"carrier": mcp_result.get("courier", mcp_result.get("carrier", mcp_result.get("express_name", "未知"))),
|
||||
"tracking_number": mcp_result.get("tracking_number") or "",
|
||||
"status": mcp_result.get("status"),
|
||||
"estimated_delivery": mcp_result.get("estimatedDelivery"),
|
||||
"timeline": mcp_result.get("timeline", [])
|
||||
}
|
||||
|
||||
|
||||
async def _generate_text_response(state: AgentState) -> AgentState:
|
||||
"""生成纯文本回复(降级方案)"""
|
||||
|
||||
# Build context from tool results
|
||||
tool_context = []
|
||||
for result in state["tool_results"]:
|
||||
if result["success"]:
|
||||
data = result["data"]
|
||||
tool_context.append(f"工具 {result['tool_name']} 返回:\n{json.dumps(data, ensure_ascii=False, indent=2)}")
|
||||
|
||||
# Extract order_id for context
|
||||
if isinstance(data, dict):
|
||||
if data.get("order_id"):
|
||||
state = update_context(state, {"order_id": data["order_id"]})
|
||||
elif data.get("orders") and len(data["orders"]) > 0:
|
||||
state = update_context(state, {"order_id": data["orders"][0].get("order_id")})
|
||||
else:
|
||||
tool_context.append(f"工具 {result['tool_name']} 执行失败: {result['error']}")
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
Chatwoot API Client for B2B Shopping AI Assistant
|
||||
"""
|
||||
import json
|
||||
from typing import Any, Optional
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
@@ -172,9 +173,122 @@ class ChatwootClient:
|
||||
self,
|
||||
conversation_id: int,
|
||||
order_data: dict[str, Any],
|
||||
actions: list[dict[str, Any]]
|
||||
actions: Optional[list[dict[str, Any]]] = None
|
||||
) -> dict[str, Any]:
|
||||
"""发送订单卡片消息(Markdown 文本 + 操作按钮)
|
||||
"""发送订单卡片消息(使用 Chatwoot cards 格式)
|
||||
|
||||
卡片结构:
|
||||
- 图片:第一件商品的图片
|
||||
- 标题:订单号 + 状态
|
||||
- 描述:汇总信息
|
||||
- 按钮:跳转到订单详情页面
|
||||
|
||||
Args:
|
||||
conversation_id: 会话 ID
|
||||
order_data: 订单数据,包含:
|
||||
- order_id: 订单号
|
||||
- status: 订单状态
|
||||
- status_text: 状态文本
|
||||
- items: 商品列表
|
||||
- total_amount: 总金额
|
||||
actions: 操作按钮配置列表(可选)
|
||||
|
||||
Returns:
|
||||
发送结果
|
||||
|
||||
Example:
|
||||
>>> order_data = {
|
||||
... "order_id": "202071324",
|
||||
... "status_text": "已发货",
|
||||
... "items": [{"name": "商品A", "quantity": 2}],
|
||||
... "total_amount": "599.00"
|
||||
... }
|
||||
>>> await chatwoot.send_order_card(123, order_data)
|
||||
"""
|
||||
order_id = order_data.get("order_id", "")
|
||||
status_text = order_data.get("status_text", order_data.get("status", ""))
|
||||
|
||||
# 获取第一件商品的图片
|
||||
items = order_data.get("items", [])
|
||||
media_url = None
|
||||
if items and len(items) > 0:
|
||||
first_item = items[0]
|
||||
media_url = first_item.get("image_url")
|
||||
|
||||
# 构建标题
|
||||
title = f"订单 #{order_id} {status_text}"
|
||||
|
||||
# 构建描述
|
||||
if items and len(items) > 0:
|
||||
if len(items) == 1:
|
||||
items_desc = items[0].get("name", "商品")
|
||||
else:
|
||||
items_desc = f"{items[0].get('name', '商品A')} 等共计 {len(items)} 件商品"
|
||||
description = f"包含 {items_desc},实付 ¥{order_data.get('total_amount', '0.00')}"
|
||||
else:
|
||||
description = f"实付 ¥{order_data.get('total_amount', '0.00')}"
|
||||
|
||||
# 构建操作按钮
|
||||
card_actions = []
|
||||
if actions:
|
||||
card_actions = actions
|
||||
else:
|
||||
# 默认按钮:跳转到订单详情页面
|
||||
card_actions = [
|
||||
{
|
||||
"type": "link",
|
||||
"text": "查看订单详情",
|
||||
"uri": f"https://www.qa1.gaia888.com/customer/order/detail?orderId={order_id}"
|
||||
}
|
||||
]
|
||||
|
||||
# 构建单个卡片
|
||||
card = {
|
||||
"title": title,
|
||||
"description": description,
|
||||
"actions": card_actions
|
||||
}
|
||||
|
||||
# 如果有图片,添加 media_url
|
||||
if media_url:
|
||||
card["media_url"] = media_url
|
||||
|
||||
# 构建 content_attributes
|
||||
content_attributes = {
|
||||
"items": [card]
|
||||
}
|
||||
|
||||
# 记录发送的数据(用于调试)
|
||||
logger.info(
|
||||
"Sending order card",
|
||||
conversation_id=conversation_id,
|
||||
order_id=order_id,
|
||||
has_media=bool(media_url),
|
||||
payload_preview=json.dumps({
|
||||
"content": "订单详情",
|
||||
"content_type": "cards",
|
||||
"content_attributes": content_attributes
|
||||
}, ensure_ascii=False, indent=2)[:1000]
|
||||
)
|
||||
|
||||
# 发送富媒体消息
|
||||
return await self.send_rich_message(
|
||||
conversation_id=conversation_id,
|
||||
content="订单详情",
|
||||
content_type="cards",
|
||||
content_attributes=content_attributes
|
||||
)
|
||||
|
||||
async def send_order_form(
|
||||
self,
|
||||
conversation_id: int,
|
||||
order_data: dict[str, Any],
|
||||
actions: Optional[list[dict[str, Any]]] = None
|
||||
) -> dict[str, Any]:
|
||||
"""发送订单详情表单消息(使用 content_type=form)
|
||||
|
||||
根据 Chatwoot API 文档实现的 form 格式订单详情展示。
|
||||
form 类型支持的字段类型:text, text_area, email, select
|
||||
|
||||
Args:
|
||||
conversation_id: 会话 ID
|
||||
@@ -188,11 +302,9 @@ class ChatwootClient:
|
||||
- shipping_fee: 运费(可选)
|
||||
- logistics: 物流信息(可选)
|
||||
- remark: 备注(可选)
|
||||
actions: 操作按钮配置列表,每个按钮包含:
|
||||
- type: "link" 或 "postback"
|
||||
- text: 按钮文字
|
||||
- uri: 链接地址(type=link 时必需)
|
||||
- payload: 回传数据(type=postback 时必需)
|
||||
actions: 操作按钮配置列表(可选),每个按钮包含:
|
||||
- label: 按钮文字(用于 select 选项的显示)
|
||||
- value: 按钮值(用于 select 选项的值)
|
||||
|
||||
Returns:
|
||||
发送结果
|
||||
@@ -202,27 +314,127 @@ class ChatwootClient:
|
||||
... "order_id": "123456789",
|
||||
... "status": "shipped",
|
||||
... "status_text": "已发货",
|
||||
... "created_at": "2023-10-27 14:30",
|
||||
... "total_amount": "1058.00",
|
||||
... "items": [...]
|
||||
... "items": [{"name": "商品A", "quantity": 2, "price": "100.00"}]
|
||||
... }
|
||||
>>> actions = [
|
||||
... {"type": "link", "text": "查看详情", "uri": "https://..."},
|
||||
... {"type": "postback", "text": "联系客服", "payload": "CONTACT_SUPPORT"}
|
||||
... {"label": "查看详情", "value": "VIEW_DETAILS"},
|
||||
... {"label": "联系客服", "value": "CONTACT_SUPPORT"}
|
||||
... ]
|
||||
>>> await chatwoot.send_order_card(123, order_data, actions)
|
||||
>>> await chatwoot.send_order_form(123, order_data, actions)
|
||||
"""
|
||||
# 生成 Markdown 内容
|
||||
markdown_content = format_order_card_markdown(order_data)
|
||||
# 构建表单字段
|
||||
form_items = []
|
||||
|
||||
# 生成按钮卡片
|
||||
buttons = create_action_buttons(actions)
|
||||
# 订单号(只读文本)
|
||||
form_items.append({
|
||||
"name": "order_id",
|
||||
"label": "订单号",
|
||||
"type": "text",
|
||||
"placeholder": "订单号",
|
||||
"default": order_data.get("order_id", "")
|
||||
})
|
||||
|
||||
# 发送富媒体消息
|
||||
# 订单状态(只读文本)
|
||||
status_text = order_data.get("status_text", order_data.get("status", "unknown"))
|
||||
form_items.append({
|
||||
"name": "status",
|
||||
"label": "订单状态",
|
||||
"type": "text",
|
||||
"placeholder": "订单状态",
|
||||
"default": status_text
|
||||
})
|
||||
|
||||
# 下单时间(只读文本)
|
||||
if order_data.get("created_at"):
|
||||
form_items.append({
|
||||
"name": "created_at",
|
||||
"label": "下单时间",
|
||||
"type": "text",
|
||||
"placeholder": "下单时间",
|
||||
"default": order_data["created_at"]
|
||||
})
|
||||
|
||||
# 商品列表(多行文本)
|
||||
items = order_data.get("items", [])
|
||||
if items:
|
||||
items_text = "\n".join([
|
||||
f"▫️ {item.get('name', '未知商品')} × {item.get('quantity', 1)} - ¥{item.get('price', '0.00')}"
|
||||
for item in items
|
||||
])
|
||||
form_items.append({
|
||||
"name": "items",
|
||||
"label": "商品详情",
|
||||
"type": "text_area",
|
||||
"placeholder": "商品列表",
|
||||
"default": items_text
|
||||
})
|
||||
|
||||
# 总金额(只读文本)
|
||||
form_items.append({
|
||||
"name": "total_amount",
|
||||
"label": "总金额",
|
||||
"type": "text",
|
||||
"placeholder": "总金额",
|
||||
"default": f"¥{order_data.get('total_amount', '0.00')}"
|
||||
})
|
||||
|
||||
# 运费(只读文本)
|
||||
if order_data.get("shipping_fee") is not None:
|
||||
form_items.append({
|
||||
"name": "shipping_fee",
|
||||
"label": "运费",
|
||||
"type": "text",
|
||||
"placeholder": "运费",
|
||||
"default": f"¥{order_data['shipping_fee']}"
|
||||
})
|
||||
|
||||
# 物流信息(多行文本)
|
||||
logistics = order_data.get("logistics")
|
||||
if logistics:
|
||||
logistics_text = (
|
||||
f"承运商: {logistics.get('carrier', '未知')}\n"
|
||||
f"单号: {logistics.get('tracking_number', '未知')}"
|
||||
)
|
||||
form_items.append({
|
||||
"name": "logistics",
|
||||
"label": "物流信息",
|
||||
"type": "text_area",
|
||||
"placeholder": "物流信息",
|
||||
"default": logistics_text
|
||||
})
|
||||
|
||||
# 备注(多行文本)
|
||||
if order_data.get("remark"):
|
||||
form_items.append({
|
||||
"name": "remark",
|
||||
"label": "备注",
|
||||
"type": "text_area",
|
||||
"placeholder": "备注",
|
||||
"default": order_data["remark"]
|
||||
})
|
||||
|
||||
# 操作选项(下拉选择,如果提供了 actions)
|
||||
if actions:
|
||||
form_items.append({
|
||||
"name": "action_select",
|
||||
"label": "操作",
|
||||
"type": "select",
|
||||
"options": actions
|
||||
})
|
||||
|
||||
# 构建 content_attributes
|
||||
content_attributes = {
|
||||
"items": form_items
|
||||
}
|
||||
|
||||
# 发送 form 类型消息
|
||||
return await self.send_rich_message(
|
||||
conversation_id=conversation_id,
|
||||
content=markdown_content,
|
||||
content_type="cards",
|
||||
content_attributes=buttons
|
||||
content="订单详情",
|
||||
content_type="form",
|
||||
content_attributes=content_attributes
|
||||
)
|
||||
|
||||
# ============ Conversations ============
|
||||
|
||||
@@ -317,10 +317,8 @@ async def get_mall_order(
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
logger.info(
|
||||
"get_mall_order called",
|
||||
order_id=order_id,
|
||||
has_user_token=bool(user_token),
|
||||
user_token_prefix=user_token[:20] if user_token else None
|
||||
f"get_mall_order called: order_id={order_id}, has_user_token={bool(user_token)}, "
|
||||
f"token_prefix={user_token[:20] if user_token else None}"
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -347,9 +345,8 @@ async def get_mall_order(
|
||||
result = await client.get_order_by_id(order_id)
|
||||
|
||||
logger.info(
|
||||
"Mall API request successful",
|
||||
order_id=order_id,
|
||||
result_keys=list(result.keys()) if isinstance(result, dict) else None
|
||||
f"Mall API request successful: order_id={order_id}, "
|
||||
f"result_keys={list(result.keys()) if isinstance(result, dict) else None}"
|
||||
)
|
||||
|
||||
return {
|
||||
@@ -359,9 +356,7 @@ async def get_mall_order(
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Mall API request failed",
|
||||
order_id=order_id,
|
||||
error=str(e)
|
||||
f"Mall API request failed: order_id={order_id}, error={str(e)}"
|
||||
)
|
||||
return {
|
||||
"success": False,
|
||||
@@ -395,18 +390,11 @@ async def get_logistics(
|
||||
Returns:
|
||||
物流信息,包含快递公司、状态、预计送达时间、物流轨迹等
|
||||
"""
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
logger.info(
|
||||
"get_logistics called",
|
||||
order_id=order_id,
|
||||
has_user_token=bool(user_token)
|
||||
)
|
||||
print(f"[get_logistics] Called with order_id={order_id}, has_user_token={bool(user_token)}")
|
||||
|
||||
# 必须提供 user_token
|
||||
if not user_token:
|
||||
logger.error("No user_token provided for logistics query")
|
||||
print("[get_logistics] ERROR: No user_token provided")
|
||||
return {
|
||||
"success": False,
|
||||
"error": "用户未登录,请先登录账户以查询物流信息",
|
||||
@@ -424,32 +412,50 @@ async def get_logistics(
|
||||
source=settings.mall_source
|
||||
)
|
||||
|
||||
print(f"[get_logistics] Calling Mall API: /mall/api/order/parcel?orderId={order_id}")
|
||||
|
||||
result = await client.get(
|
||||
"/mall/api/order/parcel",
|
||||
params={"orderId": order_id}
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"Logistics query successful",
|
||||
order_id=order_id,
|
||||
has_tracking=bool(result.get("trackingNumber"))
|
||||
)
|
||||
print(f"[get_logistics] SUCCESS: result_keys={list(result.keys()) if isinstance(result, dict) else type(result).__name__}")
|
||||
print(f"[get_logistics] Sample data: {str(result)[:500]}")
|
||||
|
||||
# Mall API 返回结构:{ "total": 1, "data": [{ "trackingCode": "...", "carrier": "...", ... }] }
|
||||
logistics_list = result.get("data", [])
|
||||
|
||||
if logistics_list and len(logistics_list) > 0:
|
||||
first_logistics = logistics_list[0]
|
||||
tracking_number = first_logistics.get("trackingCode", "")
|
||||
carrier = first_logistics.get("carrier", "未知")
|
||||
|
||||
print(f"[get_logistics] Extracted: tracking_number={tracking_number}, carrier={carrier}")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"order_id": order_id,
|
||||
"tracking_number": result.get("trackingNumber"),
|
||||
"courier": result.get("courier"),
|
||||
"status": result.get("status"),
|
||||
"estimated_delivery": result.get("estimatedDelivery"),
|
||||
"timeline": result.get("timeline", [])
|
||||
"tracking_number": tracking_number,
|
||||
"courier": carrier,
|
||||
"tracking_url": first_logistics.get("trackingUrl", ""),
|
||||
"status": first_logistics.get("status", ""),
|
||||
"timeline": [] # 如果 API 返回轨迹信息,可以在这里添加
|
||||
}
|
||||
else:
|
||||
print(f"[get_logistics] WARNING: No logistics data found in response")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"order_id": order_id,
|
||||
"tracking_number": "",
|
||||
"courier": "暂无物流信息",
|
||||
"status": "",
|
||||
"timeline": []
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Logistics query failed",
|
||||
order_id=order_id,
|
||||
error=str(e)
|
||||
)
|
||||
import traceback
|
||||
print(f"[get_logistics] ERROR: {type(e).__name__}: {str(e)}")
|
||||
print(f"[get_logistics] TRACEBACK:\n{traceback.format_exc()}")
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
|
||||
@@ -55,14 +55,16 @@ class MallClient:
|
||||
headers={
|
||||
"Authorization": f"Bearer {self.api_token}",
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
"Accept": "application/json, text/plain, */*",
|
||||
"Device-Type": "pc",
|
||||
"tenant-Id": self.tenant_id,
|
||||
"currency-code": self.currency_code,
|
||||
"language-id": self.language_id,
|
||||
"source": self.source,
|
||||
"origin": "https://www.qa1.gaia888.com",
|
||||
"referer": "https://www.qa1.gaia888.com/",
|
||||
"Origin": "https://www.qa1.gaia888.com",
|
||||
"Referer": "https://www.qa1.gaia888.com/",
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36",
|
||||
"DNT": "1",
|
||||
},
|
||||
timeout=30.0
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user