From e093995368e2f82490cea209aee0fc61e05f3103 Mon Sep 17 00:00:00 2001
From: wangliang
Date: Fri, 16 Jan 2026 16:28:47 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=BC=BA=20Agent=20=E7=B3=BB?=
=?UTF-8?q?=E7=BB=9F=E5=92=8C=E5=AE=8C=E5=96=84=E9=A1=B9=E7=9B=AE=E7=BB=93?=
=?UTF-8?q?=E6=9E=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
主要改进:
- Agent 增强: 订单查询、售后支持、客服路由等功能优化
- 新增语言检测和 Token 管理模块
- 改进 Chatwoot webhook 处理和用户标识
- MCP 服务器增强: 订单 MCP 和 Strapi MCP 功能扩展
- 新增商城客户端、知识库、缓存和同步模块
- 添加多语言提示词系统 (YAML)
- 完善项目结构: 整理文档、脚本和测试文件
- 新增调试和测试工具脚本
Co-Authored-By: Claude Sonnet 4.5
---
agent/agents/aftersale.py | 195 ++++-------
agent/agents/customer_service.py | 167 ++++-----
agent/agents/order.py | 214 ++++++++++--
agent/agents/router.py | 110 ++----
agent/core/graph.py | 54 ++-
agent/core/language_detector.py | 150 ++++++++
agent/core/state.py | 36 +-
agent/integrations/chatwoot.py | 6 +-
agent/prompts/__init__.py | 9 +
agent/prompts/aftersale/en.yaml | 152 +++++++++
agent/prompts/base.py | 110 ++++++
agent/prompts/customer_service/en.yaml | 123 +++++++
agent/prompts/order/en.yaml | 82 +++++
agent/prompts/product/en.yaml | 83 +++++
agent/prompts/router/en.yaml | 76 +++++
agent/requirements.txt | 6 +
agent/test_endpoint.py | 55 +++
agent/utils/token_manager.py | 105 ++++++
agent/webhooks/chatwoot_webhook.py | 68 +++-
docker-compose.yml | 30 +-
docs/PORT_SCHEME.md | 108 ++++++
docs/RETURN_FAQ_TEST.md | 130 +++++++
docs/TEST_STEPS.md | 110 ++++++
docs/chatwoot-widget-integration.js | 155 +++++++++
docs/test-chat-debug.html | 337 ++++++++++++++++++
docs/test-chat.html | 100 +++++-
docs/test-conversation-id.html | 401 ++++++++++++++++++++++
docs/test-simple.html | 261 ++++++++++++++
mcp_servers/order_mcp/requirements.txt | 4 +
mcp_servers/order_mcp/server.py | 179 +++++++++-
mcp_servers/shared/mall_client.py | 180 ++++++++++
mcp_servers/shared/strapi_client.py | 4 +-
mcp_servers/strapi_mcp/cache.py | 161 +++++++++
mcp_servers/strapi_mcp/http_routes.py | 86 ++++-
mcp_servers/strapi_mcp/knowledge_base.py | 418 +++++++++++++++++++++++
mcp_servers/strapi_mcp/requirements.txt | 6 +
mcp_servers/strapi_mcp/server.py | 72 +++-
mcp_servers/strapi_mcp/sync.py | 252 ++++++++++++++
nginx.conf | 52 +++
plans/order-mcp-implementation.md | 165 +++++++++
scripts/check-chatwoot-config.sh | 108 ++++++
scripts/check-conversations.sh | 102 ++++++
scripts/debug-webhook.sh | 53 +++
scripts/update-chatwoot-webhook.sh | 62 ++++
scripts/verify-webhook.sh | 81 +++++
tests/test_all_faq.sh | 74 ++++
tests/test_mall_order_query.py | 103 ++++++
tests/test_return_faq.py | 63 ++++
48 files changed, 5263 insertions(+), 395 deletions(-)
create mode 100644 agent/core/language_detector.py
create mode 100644 agent/prompts/__init__.py
create mode 100644 agent/prompts/aftersale/en.yaml
create mode 100644 agent/prompts/base.py
create mode 100644 agent/prompts/customer_service/en.yaml
create mode 100644 agent/prompts/order/en.yaml
create mode 100644 agent/prompts/product/en.yaml
create mode 100644 agent/prompts/router/en.yaml
create mode 100644 agent/test_endpoint.py
create mode 100644 agent/utils/token_manager.py
create mode 100644 docs/PORT_SCHEME.md
create mode 100644 docs/RETURN_FAQ_TEST.md
create mode 100644 docs/TEST_STEPS.md
create mode 100644 docs/chatwoot-widget-integration.js
create mode 100644 docs/test-chat-debug.html
create mode 100644 docs/test-conversation-id.html
create mode 100644 docs/test-simple.html
create mode 100644 mcp_servers/shared/mall_client.py
create mode 100644 mcp_servers/strapi_mcp/cache.py
create mode 100644 mcp_servers/strapi_mcp/knowledge_base.py
create mode 100644 mcp_servers/strapi_mcp/sync.py
create mode 100644 nginx.conf
create mode 100644 plans/order-mcp-implementation.md
create mode 100755 scripts/check-chatwoot-config.sh
create mode 100755 scripts/check-conversations.sh
create mode 100755 scripts/debug-webhook.sh
create mode 100644 scripts/update-chatwoot-webhook.sh
create mode 100755 scripts/verify-webhook.sh
create mode 100755 tests/test_all_faq.sh
create mode 100644 tests/test_mall_order_query.py
create mode 100644 tests/test_return_faq.py
diff --git a/agent/agents/aftersale.py b/agent/agents/aftersale.py
index 34ad6b7..5d1ce6b 100644
--- a/agent/agents/aftersale.py
+++ b/agent/agents/aftersale.py
@@ -6,109 +6,20 @@ 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 prompts import get_prompt
from utils.logger import get_logger
logger = get_logger(__name__)
-AFTERSALE_AGENT_PROMPT = """你是一个专业的 B2B 售后服务助手。
-你的职责是帮助用户处理售后问题,包括:
-- 退货申请
-- 换货申请
-- 投诉处理
-- 工单创建
-- 售后进度查询
-
-## 可用工具
-
-1. **apply_return** - 退货申请
- - order_id: 订单号
- - items: 退货商品列表 [{item_id, quantity, reason}]
- - description: 问题描述
- - images: 图片URL列表(可选)
-
-2. **apply_exchange** - 换货申请
- - order_id: 订单号
- - items: 换货商品列表 [{item_id, reason}]
- - description: 问题描述
-
-3. **create_complaint** - 创建投诉
- - type: 投诉类型(product_quality/service/logistics/other)
- - title: 投诉标题
- - description: 详细描述
- - related_order_id: 关联订单号(可选)
- - attachments: 附件URL列表(可选)
-
-4. **create_ticket** - 创建工单
- - category: 工单类别
- - priority: 优先级(low/medium/high/urgent)
- - title: 工单标题
- - description: 详细描述
-
-5. **query_aftersale_status** - 查询售后状态
- - aftersale_id: 售后单号(可选,不填查询全部)
-
-## 工具调用格式
-
-当需要使用工具时,请返回 JSON 格式:
-```json
-{
- "action": "call_tool",
- "tool_name": "工具名称",
- "arguments": {
- "参数名": "参数值"
- }
-}
-```
-
-当需要向用户询问更多信息时:
-```json
-{
- "action": "ask_info",
- "question": "需要询问的问题",
- "required_fields": ["需要收集的字段列表"]
-}
-```
-
-当可以直接回答时:
-```json
-{
- "action": "respond",
- "response": "回复内容"
-}
-```
-
-## 售后流程引导
-
-退货流程:
-1. 确认订单号和退货商品
-2. 了解退货原因
-3. 收集问题描述和图片(质量问题时)
-4. 提交退货申请
-5. 告知用户后续流程
-
-换货流程:
-1. 确认订单号和换货商品
-2. 了解换货原因
-3. 确认是否有库存
-4. 提交换货申请
-
-## 注意事项
-- 售后申请需要完整信息才能提交
-- 对用户的问题要表示理解和歉意
-- 复杂投诉建议转人工处理
-- 金额较大的退款需要特别确认
-"""
-
-
async def aftersale_agent(state: AgentState) -> AgentState:
"""Aftersale agent node
-
+
Handles returns, exchanges, complaints and aftersale queries.
-
+
Args:
state: Current agent state
-
+
Returns:
Updated state with tool calls or response
"""
@@ -117,34 +28,70 @@ async def aftersale_agent(state: AgentState) -> AgentState:
conversation_id=state["conversation_id"],
sub_intent=state.get("sub_intent")
)
-
+
state["current_agent"] = "aftersale"
state["agent_history"].append("aftersale")
state["state"] = ConversationState.PROCESSING.value
-
+
# Check if we have tool results to process
if state["tool_results"]:
return await _generate_aftersale_response(state)
+
+ # Get detected language
+ locale = state.get("detected_language", "en")
+
+ # Auto-query FAQ for return-related questions
+ message_lower = state["current_message"].lower()
+ faq_keywords = ["return", "refund", "defective", "exchange", "complaint", "damaged", "wrong", "missing"]
+
+ # 如果消息包含退货相关关键词,且没有工具调用记录,自动查询 FAQ
+ if any(keyword in message_lower for keyword in faq_keywords):
+ # 检查是否已经查询过 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)
+
+ if not has_faq_query:
+ logger.info(
+ "Auto-querying FAQ for return-related question",
+ conversation_id=state["conversation_id"]
+ )
+
+ # 自动添加 FAQ 工具调用
+ state = add_tool_call(
+ state,
+ tool_name="query_faq",
+ arguments={
+ "category": "return",
+ "locale": locale,
+ "limit": 5
+ },
+ server="strapi"
+ )
+ state["state"] = ConversationState.TOOL_CALLING.value
+ return state
# Build messages for LLM
+ # Load prompt in detected language
+ system_prompt = get_prompt("aftersale", locale)
+
messages = [
- Message(role="system", content=AFTERSALE_AGENT_PROMPT),
+ Message(role="system", content=system_prompt),
]
-
+
# Add conversation history
for msg in state["messages"][-8:]: # More history for aftersale context
messages.append(Message(role=msg["role"], content=msg["content"]))
-
+
# Build context info
- context_info = f"用户ID: {state['user_id']}\n账户ID: {state['account_id']}\n"
-
+ context_info = f"User ID: {state['user_id']}\nAccount ID: {state['account_id']}\n"
+
if state["entities"]:
- context_info += f"已提取的信息: {json.dumps(state['entities'], ensure_ascii=False)}\n"
-
+ context_info += f"Extracted entities: {json.dumps(state['entities'], ensure_ascii=False)}\n"
+
if state["context"]:
- context_info += f"会话上下文: {json.dumps(state['context'], ensure_ascii=False)}\n"
-
- user_content = f"{context_info}\n用户消息: {state['current_message']}"
+ context_info += f"Conversation context: {json.dumps(state['context'], ensure_ascii=False)}\n"
+
+ user_content = f"{context_info}\nUser message: {state['current_message']}"
messages.append(Message(role="user", content=user_content))
try:
@@ -206,46 +153,46 @@ async def aftersale_agent(state: AgentState) -> AgentState:
async def _generate_aftersale_response(state: AgentState) -> AgentState:
"""Generate response based on aftersale 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)}")
-
+ tool_context.append(f"Tool {result['tool_name']} returned:\n{json.dumps(data, ensure_ascii=False, indent=2)}")
+
# Extract aftersale_id for context
if isinstance(data, dict) and data.get("aftersale_id"):
state = update_context(state, {"aftersale_id": data["aftersale_id"]})
else:
- tool_context.append(f"工具 {result['tool_name']} 执行失败: {result['error']}")
-
- prompt = f"""基于以下售后系统返回的信息,生成对用户的回复。
+ tool_context.append(f"Tool {result['tool_name']} failed: {result['error']}")
-用户问题: {state["current_message"]}
+ prompt = f"""Based on the following aftersale system information, generate a response to the user.
-系统返回信息:
+User question: {state["current_message"]}
+
+System returned information:
{chr(10).join(tool_context)}
-请生成一个体贴、专业的回复:
-- 如果是申请提交成功,告知用户售后单号和后续流程
-- 如果是状态查询,清晰说明当前进度
-- 如果申请失败,说明原因并提供解决方案
-- 对用户的问题表示理解
+Please generate a compassionate and professional response:
+- If application submitted successfully, inform user of aftersale ID and next steps
+- If status query, clearly explain current progress
+- If application failed, explain reason and provide solution
+- Show understanding for user's issue
+
+Return only the response content, do not return JSON."""
-只返回回复内容,不要返回 JSON。"""
-
messages = [
- Message(role="system", content="你是一个专业的售后客服助手,请根据系统返回的信息回答用户的售后问题。"),
+ Message(role="system", content="You are a professional aftersale service assistant, please answer user's aftersale questions based on system returned information."),
Message(role="user", content=prompt)
]
-
+
try:
llm = get_llm_client()
response = await llm.chat(messages, temperature=0.7)
state = set_response(state, response.content)
return state
-
+
except Exception as e:
logger.error("Aftersale response generation failed", error=str(e))
- state = set_response(state, "抱歉,处理售后请求时遇到问题。请稍后重试或联系人工客服。")
+ state = set_response(state, "Sorry, there was a problem processing your aftersale request. Please try again later or contact customer support.")
return state
diff --git a/agent/agents/customer_service.py b/agent/agents/customer_service.py
index b58341d..016d515 100644
--- a/agent/agents/customer_service.py
+++ b/agent/agents/customer_service.py
@@ -6,76 +6,20 @@ from typing import Any
from core.state import AgentState, ConversationState, add_tool_call, set_response
from core.llm import get_llm_client, Message
+from prompts import get_prompt
from utils.logger import get_logger
logger = get_logger(__name__)
-CUSTOMER_SERVICE_PROMPT = """你是一个专业的 B2B 购物网站客服助手。
-你的职责是回答用户的一般性问题,包括:
-- 常见问题解答 (FAQ)
-- 公司信息查询
-- 政策咨询(退换货政策、隐私政策等)
-- 产品使用指南
-- 其他一般性咨询
-
-## 可用工具
-
-你可以使用以下工具获取信息:
-1. **query_faq** - 搜索 FAQ 常见问题
- - query: 搜索关键词
- - category: 分类(可选)
-
-2. **get_company_info** - 获取公司信息
- - section: 信息类别(about_us, contact, etc.)
-
-3. **get_policy** - 获取政策文档
- - policy_type: 政策类型(return_policy, privacy_policy, etc.)
-
-## 工具调用格式
-
-当需要使用工具时,请返回 JSON 格式:
-```json
-{
- "action": "call_tool",
- "tool_name": "工具名称",
- "arguments": {
- "参数名": "参数值"
- }
-}
-```
-
-当可以直接回答时,请返回:
-```json
-{
- "action": "respond",
- "response": "回复内容"
-}
-```
-
-当需要转人工时,请返回:
-```json
-{
- "action": "handoff",
- "reason": "转人工原因"
-}
-```
-
-## 注意事项
-- 保持专业、友好的语气
-- 如果不确定答案,建议用户联系人工客服
-- 不要编造信息,只使用工具返回的数据
-"""
-
-
async def customer_service_agent(state: AgentState) -> AgentState:
"""Customer service agent node
-
+
Handles FAQ, company info, and general inquiries using Strapi MCP tools.
-
+
Args:
state: Current agent state
-
+
Returns:
Updated state with tool calls or response
"""
@@ -83,18 +27,87 @@ async def customer_service_agent(state: AgentState) -> AgentState:
"Customer service agent processing",
conversation_id=state["conversation_id"]
)
-
+
state["current_agent"] = "customer_service"
state["agent_history"].append("customer_service")
state["state"] = ConversationState.PROCESSING.value
-
+
# Check if we have tool results to process
if state["tool_results"]:
return await _generate_response_from_results(state)
+
+ # Get detected language
+ locale = state.get("detected_language", "en")
+
+ # Auto-detect category and query FAQ
+ message_lower = state["current_message"].lower()
+
+ # 定义分类关键词
+ category_keywords = {
+ "register": ["register", "sign up", "account", "login", "password", "forgot"],
+ "order": ["order", "place order", "cancel order", "modify order", "change order"],
+ "payment": ["pay", "payment", "checkout", "voucher", "discount", "promo"],
+ "shipment": ["ship", "shipping", "delivery", "courier", "transit", "logistics", "tracking"],
+ "return": ["return", "refund", "exchange", "defective", "damaged"],
+ }
+
+ # 检测分类
+ detected_category = None
+ for category, keywords in category_keywords.items():
+ if any(keyword in message_lower for keyword in keywords):
+ detected_category = category
+ 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,自动查询
+ if detected_category and not has_faq_query:
+ logger.info(
+ f"Auto-querying FAQ for category: {detected_category}",
+ conversation_id=state["conversation_id"]
+ )
+
+ # 自动添加 FAQ 工具调用
+ state = add_tool_call(
+ state,
+ tool_name="query_faq",
+ arguments={
+ "category": detected_category,
+ "locale": locale,
+ "limit": 5
+ },
+ server="strapi"
+ )
+ state["state"] = ConversationState.TOOL_CALLING.value
+ return state
+
+ # 如果询问营业时间或联系方式,自动查询公司信息
+ if any(keyword in message_lower for keyword in ["opening hour", "contact", "address", "phone", "email"]) and not has_faq_query:
+ logger.info(
+ "Auto-querying company info",
+ conversation_id=state["conversation_id"]
+ )
+
+ state = add_tool_call(
+ state,
+ tool_name="get_company_info",
+ arguments={
+ "section": "contact",
+ "locale": locale
+ },
+ server="strapi"
+ )
+ state["state"] = ConversationState.TOOL_CALLING.value
+ return state
# Build messages for LLM
+ # Load prompt in detected language
+ system_prompt = get_prompt("customer_service", locale)
+
messages = [
- Message(role="system", content=CUSTOMER_SERVICE_PROMPT),
+ Message(role="system", content=system_prompt),
]
# Add conversation history
@@ -151,37 +164,37 @@ async def customer_service_agent(state: AgentState) -> AgentState:
async def _generate_response_from_results(state: AgentState) -> AgentState:
"""Generate response based on tool results"""
-
+
# Build context from tool results
tool_context = []
for result in state["tool_results"]:
if result["success"]:
- tool_context.append(f"工具 {result['tool_name']} 返回:\n{json.dumps(result['data'], ensure_ascii=False, indent=2)}")
+ tool_context.append(f"Tool {result['tool_name']} returned:\n{json.dumps(result['data'], ensure_ascii=False, indent=2)}")
else:
- tool_context.append(f"工具 {result['tool_name']} 执行失败: {result['error']}")
-
- prompt = f"""基于以下工具返回的信息,生成对用户的回复。
+ tool_context.append(f"Tool {result['tool_name']} failed: {result['error']}")
-用户问题: {state["current_message"]}
+ prompt = f"""Based on the following tool returned information, generate a response to the user.
-工具返回信息:
+User question: {state["current_message"]}
+
+Tool returned information:
{chr(10).join(tool_context)}
-请生成一个友好、专业的回复。如果工具没有返回有用信息,请诚实告知用户并建议其他方式获取帮助。
-只返回回复内容,不要返回 JSON。"""
-
+Please generate a friendly and professional response. If the tool did not return useful information, honestly inform the user and suggest other ways to get help.
+Return only the response content, do not return JSON."""
+
messages = [
- Message(role="system", content="你是一个专业的 B2B 客服助手,请根据工具返回的信息回答用户问题。"),
+ Message(role="system", content="You are a professional B2B customer service assistant, please answer user questions based on tool returned information."),
Message(role="user", content=prompt)
]
-
+
try:
llm = get_llm_client()
response = await llm.chat(messages, temperature=0.7)
state = set_response(state, response.content)
return state
-
+
except Exception as e:
logger.error("Response generation failed", error=str(e))
- state = set_response(state, "抱歉,处理您的请求时遇到问题。请稍后重试或联系人工客服。")
+ state = set_response(state, "Sorry, there was a problem processing your request. Please try again later or contact customer support.")
return state
diff --git a/agent/agents/order.py b/agent/agents/order.py
index f36a90d..f4ebcb9 100644
--- a/agent/agents/order.py
+++ b/agent/agents/order.py
@@ -21,62 +21,100 @@ ORDER_AGENT_PROMPT = """你是一个专业的 B2B 订单服务助手。
## 可用工具
-1. **query_order** - 查询订单
+1. **get_mall_order** - 从商城 API 查询订单(推荐使用)
+ - order_id: 订单号(必需)
+ - 说明:此工具会自动使用用户的身份 token 查询商城订单详情
+
+2. **query_order** - 查询历史订单
+ - user_id: 用户 ID(自动注入)
+ - account_id: 账户 ID(自动注入)
- order_id: 订单号(可选,不填则查询最近订单)
- date_start: 开始日期(可选)
- date_end: 结束日期(可选)
- status: 订单状态(可选)
-2. **track_logistics** - 物流跟踪
+3. **track_logistics** - 物流跟踪
- order_id: 订单号
- tracking_number: 物流单号(可选)
-3. **modify_order** - 修改订单
+4. **modify_order** - 修改订单
- order_id: 订单号
+ - user_id: 用户 ID(自动注入)
- modifications: 修改内容(address/items/quantity 等)
-4. **cancel_order** - 取消订单
+5. **cancel_order** - 取消订单
- order_id: 订单号
+ - user_id: 用户 ID(自动注入)
- reason: 取消原因
-5. **get_invoice** - 获取发票
+6. **get_invoice** - 获取发票
- order_id: 订单号
- invoice_type: 发票类型(normal/vat)
-## 工具调用格式
+## 回复格式要求
-当需要使用工具时,请返回 JSON 格式:
+**重要**:你必须始终返回完整的 JSON 对象,不要包含任何其他文本或解释。
+
+### 格式 1:调用工具
+当需要使用工具查询信息时,返回:
```json
{
"action": "call_tool",
- "tool_name": "工具名称",
+ "tool_name": "get_mall_order",
"arguments": {
- "参数名": "参数值"
+ "order_id": "202071324"
}
}
```
-当需要向用户询问更多信息时:
+### 格式 2:询问信息
+当需要向用户询问更多信息时,返回:
```json
{
"action": "ask_info",
- "question": "需要询问的问题"
+ "question": "请提供您的订单号"
}
```
-当可以直接回答时:
+### 格式 3:直接回复
+当可以直接回答时,返回:
```json
{
"action": "respond",
- "response": "回复内容"
+ "response": "您的订单已发货,预计3天内到达"
}
```
-## 重要提示
-- 订单修改和取消是敏感操作,需要确认订单号
-- 如果用户没有提供订单号,先查询他的最近订单
-- 物流查询需要订单号或物流单号
-- 对于批量操作或大金额订单,建议转人工处理
+## 示例对话
+
+用户: "查询订单 202071324"
+回复:
+```json
+{
+ "action": "call_tool",
+ "tool_name": "get_mall_order",
+ "arguments": {
+ "order_id": "202071324"
+ }
+}
+```
+
+用户: "我的订单发货了吗?"
+回复:
+```json
+{
+ "action": "ask_info",
+ "question": "请提供您的订单号,以便查询订单状态"
+}
+```
+
+## 重要约束
+- **必须返回完整的 JSON 对象**,不要只返回部分内容
+- **不要添加任何 markdown 代码块标记**(如 \`\`\`json)
+- **不要添加任何解释性文字**,只返回 JSON
+- user_id 和 account_id 会自动注入到 arguments 中,无需手动添加
+- 如果用户提供了订单号,优先使用 get_mall_order 工具
+- 对于敏感操作(取消、修改),确保有明确的订单号
"""
@@ -131,27 +169,133 @@ async def order_agent(state: AgentState) -> AgentState:
try:
llm = get_llm_client()
response = await llm.chat(messages, temperature=0.5)
-
+
# Parse response
content = response.content.strip()
- if content.startswith("```"):
- content = content.split("```")[1]
- if content.startswith("json"):
- content = content[4:]
-
- result = json.loads(content)
+ logger.info(
+ "LLM response received",
+ conversation_id=state["conversation_id"],
+ response_length=len(content),
+ response_preview=content[:300]
+ )
+
+ # 检查是否是简化的工具调用格式:工具名称\n{参数}
+ # 例如:get_mall_order\n{"order_id": "202071324"}
+ if "\n" in content and "{" in content:
+ lines = content.split("\n")
+ if len(lines) >= 2:
+ tool_name_line = lines[0].strip()
+ json_line = "\n".join(lines[1:]).strip()
+
+ # 如果第一行看起来像工具名称(不包含 {),且第二行是 JSON
+ if "{" not in tool_name_line and "{" in json_line:
+ logger.info(
+ "Detected simplified tool call format",
+ tool_name=tool_name_line,
+ json_preview=json_line[:200]
+ )
+
+ try:
+ arguments = json.loads(json_line)
+ # 直接构建工具调用
+ arguments["user_id"] = state["user_id"]
+ arguments["account_id"] = state["account_id"]
+
+ # Inject user_token if available
+ if state.get("user_token"):
+ arguments["user_token"] = state["user_token"]
+ logger.info("Injected user_token into tool call")
+
+ # Use entity if available
+ if "order_id" not in arguments and state["entities"].get("order_id"):
+ arguments["order_id"] = state["entities"]["order_id"]
+
+ state = add_tool_call(
+ state,
+ tool_name=tool_name_line,
+ arguments=arguments,
+ server="order"
+ )
+ state["state"] = ConversationState.TOOL_CALLING.value
+
+ logger.info(
+ "Tool call added from simplified format",
+ tool_name=tool_name_line,
+ arguments_keys=list(arguments.keys())
+ )
+
+ return state
+ except json.JSONDecodeError as e:
+ logger.warning(
+ "Failed to parse simplified format",
+ error=str(e),
+ json_line=json_line[:200]
+ )
+
+ # 清理内容,去除可能的 markdown 代码块标记
+ # 例如:```json\n{...}\n``` 或 ```\n{...}\n```
+ if "```" in content:
+ # 找到第一个 ``` 后的内容
+ parts = content.split("```")
+ if len(parts) >= 2:
+ content = parts[1].strip()
+ # 去掉可能的 "json" 标记
+ if content.startswith("json"):
+ content = content[4:].strip()
+ # 去掉结尾的 ``` 标记
+ if content.endswith("```"):
+ content = content[:-3].strip()
+
+ # 尝试提取 JSON 对象(处理周围可能有文本的情况)
+ json_start = content.find("{")
+ json_end = content.rfind("}")
+ if json_start != -1 and json_end != -1 and json_end > json_start:
+ content = content[json_start:json_end + 1]
+
+ logger.info(
+ "Cleaned content for JSON parsing",
+ conversation_id=state["conversation_id"],
+ content_length=len(content),
+ content_preview=content[:500]
+ )
+
+ try:
+ result = json.loads(content)
+ except json.JSONDecodeError as e:
+ logger.error(
+ "Failed to parse LLM response as JSON",
+ conversation_id=state["conversation_id"],
+ error=str(e),
+ content_preview=content[:500]
+ )
+ # 如果解析失败,尝试将原始内容作为直接回复
+ state = set_response(state, response.content)
+ return state
+
action = result.get("action")
-
+
+ logger.info(
+ "LLM action parsed",
+ conversation_id=state["conversation_id"],
+ action=action,
+ tool_name=result.get("tool_name")
+ )
+
if action == "call_tool":
# Inject user context into arguments
arguments = result.get("arguments", {})
arguments["user_id"] = state["user_id"]
arguments["account_id"] = state["account_id"]
-
+
+ # Inject user_token if available (for Mall API calls)
+ if state.get("user_token"):
+ arguments["user_token"] = state["user_token"]
+ logger.debug("Injected user_token into tool call")
+
# Use entity if available
if "order_id" not in arguments and state["entities"].get("order_id"):
arguments["order_id"] = state["entities"]["order_id"]
-
+
state = add_tool_call(
state,
tool_name=result["tool_name"],
@@ -159,6 +303,12 @@ async def order_agent(state: AgentState) -> AgentState:
server="order"
)
state["state"] = ConversationState.TOOL_CALLING.value
+
+ logger.info(
+ "Tool call added",
+ tool_name=result["tool_name"],
+ arguments_keys=list(arguments.keys())
+ )
elif action == "ask_info":
state = set_response(state, result["question"])
@@ -171,13 +321,9 @@ async def order_agent(state: AgentState) -> AgentState:
elif action == "handoff":
state["requires_human"] = True
state["handoff_reason"] = result.get("reason", "Complex order operation")
-
+
return state
-
- except json.JSONDecodeError:
- state = set_response(state, response.content)
- return state
-
+
except Exception as e:
logger.error("Order agent failed", error=str(e))
state["error"] = str(e)
diff --git a/agent/agents/router.py b/agent/agents/router.py
index 45bcbb3..9bfd380 100644
--- a/agent/agents/router.py
+++ b/agent/agents/router.py
@@ -4,92 +4,24 @@ Router Agent - Intent recognition and routing
import json
from typing import Any, Optional
-from core.state import AgentState, Intent, ConversationState, set_intent, add_entity
+from core.state import AgentState, Intent, ConversationState, set_intent, add_entity, set_language
from core.llm import get_llm_client, Message
+from core.language_detector import get_cached_or_detect
+from prompts import get_prompt
from utils.logger import get_logger
logger = get_logger(__name__)
-# Intent classification prompt
-CLASSIFICATION_PROMPT = """你是一个 B2B 购物网站的智能助手路由器。
-你的任务是分析用户消息,识别用户意图并提取关键实体。
-
-## 可用意图分类
-
-1. **customer_service** - 通用咨询
- - FAQ 问答
- - 产品使用问题
- - 公司信息查询
- - 政策咨询(退换货政策、隐私政策等)
-
-2. **order** - 订单相关
- - 订单查询("我的订单在哪"、"查一下订单")
- - 物流跟踪("快递到哪了"、"什么时候到货")
- - 订单修改("改一下收货地址"、"修改订单数量")
- - 订单取消("取消订单"、"不想要了")
- - 发票查询("开发票"、"要发票")
-
-3. **aftersale** - 售后服务
- - 退货申请("退货"、"不满意想退")
- - 换货申请("换货"、"换一个")
- - 投诉("投诉"、"服务态度差")
- - 工单/问题反馈
-
-4. **product** - 商品相关
- - 商品搜索("有没有xx"、"找一下xx")
- - 商品推荐("推荐"、"有什么好的")
- - 询价("多少钱"、"批发价"、"大量购买价格")
- - 库存查询("有货吗"、"还有多少")
-
-5. **human_handoff** - 需要转人工
- - 用户明确要求转人工
- - 复杂问题 AI 无法处理
- - 敏感问题需要人工处理
-
-## 实体提取
-
-请从消息中提取以下实体(如果存在):
-- order_id: 订单号(如 ORD123456)
-- product_id: 商品ID
-- product_name: 商品名称
-- quantity: 数量
-- date_reference: 时间引用(今天、昨天、上周、具体日期等)
-- tracking_number: 物流单号
-- phone: 电话号码
-- address: 地址信息
-
-## 输出格式
-
-请以 JSON 格式返回,包含以下字段:
-```json
-{
- "intent": "意图分类",
- "confidence": 0.95,
- "sub_intent": "子意图(可选)",
- "entities": {
- "entity_type": "entity_value"
- },
- "reasoning": "简短的推理说明"
-}
-```
-
-## 注意事项
-- 如果意图不明确,置信度应该较低
-- 如果无法确定意图,返回 "unknown"
-- 实体提取要准确,没有的字段不要填写
-"""
-
-
async def classify_intent(state: AgentState) -> AgentState:
"""Classify user intent and extract entities
-
+
This is the first node in the workflow that analyzes the user's message
and determines which agent should handle it.
-
+
Args:
state: Current agent state
-
+
Returns:
Updated state with intent and entities
"""
@@ -98,24 +30,38 @@ async def classify_intent(state: AgentState) -> AgentState:
conversation_id=state["conversation_id"],
message=state["current_message"][:100]
)
-
+
state["state"] = ConversationState.CLASSIFYING.value
state["step_count"] += 1
-
+
+ # Detect language
+ detected_locale = get_cached_or_detect(state, state["current_message"])
+ confidence = 0.85 # Default confidence for language detection
+ state = set_language(state, detected_locale, confidence)
+
+ logger.info(
+ "Language detected",
+ locale=detected_locale,
+ confidence=confidence
+ )
+
# Build context from conversation history
context_summary = ""
if state["context"]:
context_parts = []
if state["context"].get("order_id"):
- context_parts.append(f"当前讨论的订单: {state['context']['order_id']}")
+ context_parts.append(f"Current order: {state['context']['order_id']}")
if state["context"].get("product_id"):
- context_parts.append(f"当前讨论的商品: {state['context']['product_id']}")
+ context_parts.append(f"Current product: {state['context']['product_id']}")
if context_parts:
context_summary = "\n".join(context_parts)
-
+
+ # Load prompt in detected language
+ classification_prompt = get_prompt("router", detected_locale)
+
# Build messages for LLM
messages = [
- Message(role="system", content=CLASSIFICATION_PROMPT),
+ Message(role="system", content=classification_prompt),
]
# Add recent conversation history for context
@@ -123,9 +69,9 @@ async def classify_intent(state: AgentState) -> AgentState:
messages.append(Message(role=msg["role"], content=msg["content"]))
# Add current message with context
- user_content = f"用户消息: {state['current_message']}"
+ user_content = f"User message: {state['current_message']}"
if context_summary:
- user_content += f"\n\n当前上下文:\n{context_summary}"
+ user_content += f"\n\nCurrent context:\n{context_summary}"
messages.append(Message(role="user", content=user_content))
diff --git a/agent/core/graph.py b/agent/core/graph.py
index 6472001..3a2c6eb 100644
--- a/agent/core/graph.py
+++ b/agent/core/graph.py
@@ -7,11 +7,7 @@ import httpx
from langgraph.graph import StateGraph, END
from .state import AgentState, ConversationState, mark_finished, add_tool_result, set_response
-from agents.router import classify_intent, route_by_intent
-from agents.customer_service import customer_service_agent
-from agents.order import order_agent
-from agents.aftersale import aftersale_agent
-from agents.product import product_agent
+# 延迟导入以避免循环依赖
from config import settings
from utils.logger import get_logger
@@ -197,20 +193,36 @@ async def handle_error(state: AgentState) -> AgentState:
def should_call_tools(state: AgentState) -> Literal["call_tools", "send_response", "back_to_agent"]:
"""Determine if tools need to be called"""
-
+
+ logger.debug(
+ "Checking if tools should be called",
+ conversation_id=state.get("conversation_id"),
+ has_tool_calls=bool(state.get("tool_calls")),
+ tool_calls_count=len(state.get("tool_calls", [])),
+ has_response=bool(state.get("response")),
+ state_value=state.get("state")
+ )
+
# If there are pending tool calls, execute them
if state.get("tool_calls"):
+ logger.info(
+ "Routing to tool execution",
+ tool_count=len(state["tool_calls"])
+ )
return "call_tools"
-
+
# If we have a response ready, send it
if state.get("response"):
+ logger.debug("Routing to send_response (has response)")
return "send_response"
-
+
# If we're waiting for info, send the question
if state.get("state") == ConversationState.AWAITING_INFO.value:
+ logger.debug("Routing to send_response (awaiting info)")
return "send_response"
-
+
# Otherwise, something went wrong
+ logger.warning("Unexpected state, routing to send_response", state=state.get("state"))
return "send_response"
@@ -255,13 +267,20 @@ def check_completion(state: AgentState) -> Literal["continue", "end", "error"]:
def create_agent_graph() -> StateGraph:
"""Create the main agent workflow graph
-
+
Returns:
Compiled LangGraph workflow
"""
+ # 延迟导入以避免循环依赖
+ from agents.router import classify_intent, route_by_intent
+ from agents.customer_service import customer_service_agent
+ from agents.order import order_agent
+ from agents.aftersale import aftersale_agent
+ from agents.product import product_agent
+
# Create graph with AgentState
graph = StateGraph(AgentState)
-
+
# Add nodes
graph.add_node("receive", receive_message)
graph.add_node("classify", classify_intent)
@@ -347,10 +366,11 @@ async def process_message(
account_id: str,
message: str,
history: list[dict] = None,
- context: dict = None
+ context: dict = None,
+ user_token: str = None
) -> AgentState:
"""Process a user message through the agent workflow
-
+
Args:
conversation_id: Chatwoot conversation ID
user_id: User identifier
@@ -358,12 +378,13 @@ async def process_message(
message: User's message
history: Previous conversation history
context: Existing conversation context
-
+ user_token: User JWT token for API calls
+
Returns:
Final agent state with response
"""
from .state import create_initial_state
-
+
# Create initial state
initial_state = create_initial_state(
conversation_id=conversation_id,
@@ -371,7 +392,8 @@ async def process_message(
account_id=account_id,
current_message=message,
messages=history,
- context=context
+ context=context,
+ user_token=user_token
)
# Get compiled graph
diff --git a/agent/core/language_detector.py b/agent/core/language_detector.py
new file mode 100644
index 0000000..0235760
--- /dev/null
+++ b/agent/core/language_detector.py
@@ -0,0 +1,150 @@
+"""
+Language Detection Module
+
+Automatically detects user message language and maps to Strapi-supported locales.
+"""
+from typing import Optional
+from langdetect import detect, LangDetectException
+from utils.logger import get_logger
+
+logger = get_logger(__name__)
+
+# Strapi-supported locales
+SUPPORTED_LOCALES = ["en", "nl", "de", "es", "fr", "it", "tr"]
+
+# Language code to locale mapping
+LOCALE_MAP = {
+ "en": "en", # English
+ "nl": "nl", # Dutch
+ "de": "de", # German
+ "es": "es", # Spanish
+ "fr": "fr", # French
+ "it": "it", # Italian
+ "tr": "tr", # Turkish
+ # Fallback mappings for unsupported languages
+ "af": "en", # Afrikaans -> English
+ "no": "en", # Norwegian -> English
+ "sv": "en", # Swedish -> English
+ "da": "en", # Danish -> English
+ "pl": "en", # Polish -> English
+ "pt": "en", # Portuguese -> English
+ "ru": "en", # Russian -> English
+ "zh": "en", # Chinese -> English
+ "ja": "en", # Japanese -> English
+ "ko": "en", # Korean -> English
+ "ar": "en", # Arabic -> English
+ "hi": "en", # Hindi -> English
+}
+
+# Minimum confidence threshold
+MIN_CONFIDENCE = 0.7
+
+# Minimum message length for reliable detection
+MIN_LENGTH = 10
+
+
+def detect_language(text: str) -> tuple[str, float]:
+ """Detect language from text
+
+ Args:
+ text: Input text to detect language from
+
+ Returns:
+ Tuple of (locale_code, confidence_score)
+ locale_code: Strapi locale (en, nl, de, etc.)
+ confidence_score: Detection confidence (0-1), 0.0 if detection failed
+ """
+ # Check minimum length
+ if len(text.strip()) < MIN_LENGTH:
+ logger.debug("Message too short for reliable detection", length=len(text))
+ return "en", 0.0
+
+ try:
+ # Detect language using langdetect
+ detected = detect(text)
+ logger.debug("Language detected", language=detected, text_length=len(text))
+
+ # Map to Strapi locale
+ locale = map_to_locale(detected)
+
+ return locale, 0.85 # langdetect doesn't provide confidence, use default
+
+ except LangDetectException as e:
+ logger.warning("Language detection failed", error=str(e))
+ return "en", 0.0
+
+
+def map_to_locale(lang_code: str) -> str:
+ """Map detected language code to Strapi locale
+
+ Args:
+ lang_code: ISO 639-1 language code (e.g., "en", "nl", "de")
+
+ Returns:
+ Strapi locale code, or "en" as default if not supported
+ """
+ # Direct mapping
+ if lang_code in SUPPORTED_LOCALES:
+ return lang_code
+
+ # Use locale map
+ locale = LOCALE_MAP.get(lang_code, "en")
+
+ if locale != lang_code and locale == "en":
+ logger.info(
+ "Unsupported language mapped to default",
+ detected_language=lang_code,
+ mapped_locale=locale
+ )
+
+ return locale
+
+
+def get_cached_or_detect(state, text: str) -> str:
+ """Get language from cache or detect from text
+
+ Priority:
+ 1. Use state.detected_language if available
+ 2. Use state.context["language"] if available
+ 3. Detect from text
+
+ Args:
+ state: Agent state
+ text: Input text to detect language from
+
+ Returns:
+ Detected locale code
+ """
+ # Check state first
+ if state.get("detected_language"):
+ logger.debug("Using cached language from state", language=state["detected_language"])
+ return state["detected_language"]
+
+ # Check context cache
+ if state.get("context", {}).get("language"):
+ logger.debug("Using cached language from context", language=state["context"]["language"])
+ return state["context"]["language"]
+
+ # Detect from text
+ locale, confidence = detect_language(text)
+
+ if confidence < MIN_CONFIDENCE and confidence > 0:
+ logger.warning(
+ "Low detection confidence, using default",
+ locale=locale,
+ confidence=confidence
+ )
+
+ return locale
+
+
+def is_supported_locale(locale: str) -> bool:
+ """Check if locale is supported
+
+ Args:
+ locale: Locale code to check
+
+ Returns:
+ True if locale is in supported list
+ """
+ return locale in SUPPORTED_LOCALES
diff --git a/agent/core/state.py b/agent/core/state.py
index 2b98389..f585dd0 100644
--- a/agent/core/state.py
+++ b/agent/core/state.py
@@ -65,6 +65,7 @@ class AgentState(TypedDict):
conversation_id: str # Chatwoot conversation ID
user_id: str # User identifier
account_id: str # B2B account identifier
+ user_token: Optional[str] # User JWT token for API calls
# ============ Message Content ============
messages: list[dict[str, Any]] # Conversation history [{role, content}]
@@ -74,6 +75,10 @@ class AgentState(TypedDict):
intent: Optional[str] # Recognized intent (Intent enum value)
intent_confidence: float # Intent confidence score (0-1)
sub_intent: Optional[str] # Sub-intent for more specific routing
+
+ # ============ Language Detection ============
+ detected_language: Optional[str] # Detected user language (en, nl, de, etc.)
+ language_confidence: float # Language detection confidence (0-1)
# ============ Entity Extraction ============
entities: dict[str, Any] # Extracted entities {type: value}
@@ -111,10 +116,11 @@ def create_initial_state(
account_id: str,
current_message: str,
messages: Optional[list[dict[str, Any]]] = None,
- context: Optional[dict[str, Any]] = None
+ context: Optional[dict[str, Any]] = None,
+ user_token: Optional[str] = None
) -> AgentState:
"""Create initial agent state for a new message
-
+
Args:
conversation_id: Chatwoot conversation ID
user_id: User identifier
@@ -122,7 +128,8 @@ def create_initial_state(
current_message: User's message to process
messages: Previous conversation history
context: Existing conversation context
-
+ user_token: User JWT token for API calls
+
Returns:
Initialized AgentState
"""
@@ -131,6 +138,7 @@ def create_initial_state(
conversation_id=conversation_id,
user_id=user_id,
account_id=account_id,
+ user_token=user_token,
# Messages
messages=messages or [],
@@ -140,6 +148,10 @@ def create_initial_state(
intent=None,
intent_confidence=0.0,
sub_intent=None,
+
+ # Language
+ detected_language=None,
+ language_confidence=0.0,
# Entities
entities={},
@@ -270,3 +282,21 @@ def mark_finished(state: AgentState) -> AgentState:
state["finished"] = True
state["state"] = ConversationState.COMPLETED.value
return state
+
+
+def set_language(state: AgentState, language: str, confidence: float) -> AgentState:
+ """Set the detected language in state
+
+ Args:
+ state: Agent state
+ language: Detected locale code (en, nl, de, etc.)
+ confidence: Detection confidence (0-1)
+
+ Returns:
+ Updated state
+ """
+ state["detected_language"] = language
+ state["language_confidence"] = confidence
+ # Also cache in context for future reference
+ state["context"]["language"] = language
+ return state
diff --git a/agent/integrations/chatwoot.py b/agent/integrations/chatwoot.py
index 6464443..2742e3e 100644
--- a/agent/integrations/chatwoot.py
+++ b/agent/integrations/chatwoot.py
@@ -56,10 +56,10 @@ class ChatwootClient:
self,
api_url: Optional[str] = None,
api_token: Optional[str] = None,
- account_id: int = 1
+ account_id: int = 2
):
"""Initialize Chatwoot client
-
+
Args:
api_url: Chatwoot API URL, defaults to settings
api_token: API access token, defaults to settings
@@ -69,7 +69,7 @@ class ChatwootClient:
self.api_token = api_token or settings.chatwoot_api_token
self.account_id = account_id
self._client: Optional[httpx.AsyncClient] = None
-
+
logger.info("Chatwoot client initialized", api_url=self.api_url)
async def _get_client(self) -> httpx.AsyncClient:
diff --git a/agent/prompts/__init__.py b/agent/prompts/__init__.py
new file mode 100644
index 0000000..09c8946
--- /dev/null
+++ b/agent/prompts/__init__.py
@@ -0,0 +1,9 @@
+"""
+Multi-language Prompt System
+
+Exports:
+ get_prompt() - Load system prompt for agent type and locale
+"""
+from .base import get_prompt, PromptLoader, SUPPORTED_LOCALES, DEFAULT_LOCALE
+
+__all__ = ["get_prompt", "PromptLoader", "SUPPORTED_LOCALES", "DEFAULT_LOCALE"]
diff --git a/agent/prompts/aftersale/en.yaml b/agent/prompts/aftersale/en.yaml
new file mode 100644
index 0000000..18b4675
--- /dev/null
+++ b/agent/prompts/aftersale/en.yaml
@@ -0,0 +1,152 @@
+# Aftersale Agent - English Prompt
+
+system_prompt: |
+ You are a professional B2B after-sales service assistant.
+ Your role is to help users handle after-sales issues, including:
+ - Return requests
+ - Exchange requests
+ - Complaint handling
+ - Ticket creation
+ - After-sales status inquiries
+ - Return policy consultations
+ - After-sales question answering
+
+ ## Available Tools
+
+ ### Knowledge Base Query Tools
+
+ **query_faq** - Query after-sales FAQ
+ - category: FAQ category, options:
+ * "return" - Return related (return policy, return process, return costs)
+ * "shipment" - Shipping related
+ * "payment" - Payment related
+ - locale: Language, default "en"
+ - limit: Number of results to return, default 5
+
+ **search_knowledge_base** - Search knowledge base
+ - query: Search keywords
+ - locale: Language, default "en"
+ - limit: Number of results to return, default 10
+
+ ### After-sales Operation Tools
+
+ **apply_return** - Submit return request
+ - order_id: Order number
+ - items: List of items to return [{item_id, quantity, reason}]
+ - description: Problem description
+ - images: List of image URLs (optional)
+
+ **apply_exchange** - Submit exchange request
+ - order_id: Order number
+ - items: List of items to exchange [{item_id, reason}]
+ - description: Problem description
+
+ **create_complaint** - Create complaint
+ - type: Complaint type (product_quality/service/logistics/other)
+ - title: Complaint title
+ - description: Detailed description
+ - related_order_id: Related order number (optional)
+ - attachments: List of attachment URLs (optional)
+
+ **create_ticket** - Create support ticket
+ - category: Ticket category
+ - priority: Priority (low/medium/high/urgent)
+ - title: Ticket title
+ - description: Detailed description
+
+ **query_aftersale_status** - Query after-sales status
+ - aftersale_id: After-sales order number (optional, leave blank to query all)
+
+ ## Important Rules
+
+ 1. **Query FAQ First**:
+ - When users ask about return policy, return process, return conditions/退货政策/退货流程/退货条件, **you MUST first call** `query_faq(category="return")` to query the knowledge base
+ - Answer users based on knowledge base information
+ - If knowledge base information is insufficient, consider transferring to human or asking for more information
+
+ 2. **Category Detection**:
+ - Return/refund/exchange/退货/退款/换货 → category="return"
+ - Shipping/delivery/物流/配送 → category="shipment"
+ - Payment/checkout/支付/付款 → category="payment"
+
+ **CRITICAL**: When users ask about "退货" (return), "退款" (refund), "怎么退货" (how to return),
+ "退货政策" (return policy), or similar questions, you MUST use category="return"
+
+ 3. **Fallback Strategy**:
+ - If `query_faq` returns 0 results or an error, try using `search_knowledge_base` with relevant keywords
+ - For example, if "return" category query fails, search for "return policy" or "退货政策"
+ - Only suggest human support after both query_faq and search_knowledge_base fail
+
+ 4. **General Inquiry Handling**:
+ - First use `search_knowledge_base` to search for relevant information
+ - If answer is found, respond directly
+ - If not found, ask user for more details
+
+ ## Tool Call Format
+
+ When you need to use a tool, return JSON format:
+ ```json
+ {
+ "action": "call_tool",
+ "tool_name": "tool_name",
+ "arguments": {
+ "parameter_name": "parameter_value"
+ }
+ }
+ ```
+
+ When you need to ask user for more information:
+ ```json
+ {
+ "action": "ask_info",
+ "question": "Question to ask user",
+ "required_fields": ["list of required fields"]
+ }
+ ```
+
+ When you can answer directly:
+ ```json
+ {
+ "action": "respond",
+ "response": "response content"
+ }
+ ```
+
+ ## After-sales Process Guidance
+
+ Return process:
+ 1. First query FAQ to understand return policy
+ 2. Confirm order number and return items
+ 3. Understand return reason
+ 4. Collect problem description and images (for quality issues)
+ 5. Submit return request
+ 6. Inform user of next steps
+
+ Exchange process:
+ 1. Confirm order number and exchange items
+ 2. Understand exchange reason
+ 3. Confirm stock availability
+ 4. Submit exchange request
+
+ ## Notes
+ - **Prioritize using FAQ tools** to provide accurate official information
+ - After-sales requests require complete information to submit
+ - Express understanding and apology for user's issues
+ - For complex complaints, suggest transferring to human handling
+ - Large refund amounts require special confirmation
+
+tool_descriptions:
+ query_faq: "Query after-sales FAQ"
+ search_knowledge_base: "Search knowledge base"
+ apply_return: "Submit return request"
+ apply_exchange: "Submit exchange request"
+ create_complaint: "Create complaint"
+ create_ticket: "Create support ticket"
+ query_aftersale_status: "Query after-sales status"
+
+response_templates:
+ error: "Sorry, an error occurred while processing your after-sales request. Please try again or contact customer support."
+ awaiting_info: "Please provide more details so I can process your request."
+ return_submitted: "Your return request has been submitted successfully. Return ID: {aftersale_id}. We will review it within 3 business days."
+ exchange_submitted: "Your exchange request has been submitted successfully. Request ID: {aftersale_id}."
+ ticket_created: "Your support ticket has been created. Ticket ID: {ticket_id}. Our team will respond shortly."
diff --git a/agent/prompts/base.py b/agent/prompts/base.py
new file mode 100644
index 0000000..353e0e0
--- /dev/null
+++ b/agent/prompts/base.py
@@ -0,0 +1,110 @@
+"""
+Multi-language Prompt Loader
+
+Loads system prompts for different agents in different languages.
+"""
+import yaml
+from pathlib import Path
+from typing import Optional
+from utils.logger import get_logger
+
+logger = get_logger(__name__)
+
+# Base directory for prompt templates
+PROMPTS_DIR = Path(__file__).parent
+
+# Supported locales
+SUPPORTED_LOCALES = ["en", "nl", "de", "es", "fr", "it", "tr"]
+
+# Default locale
+DEFAULT_LOCALE = "en"
+
+
+class PromptLoader:
+ """Load and cache prompt templates for different languages"""
+
+ def __init__(self):
+ self._cache = {}
+
+ def get_prompt(self, agent_type: str, locale: str) -> str:
+ """Get system prompt for agent type and locale
+
+ Args:
+ agent_type: Type of agent (customer_service, aftersale, order, product, router)
+ locale: Language locale (en, nl, de, etc.)
+
+ Returns:
+ System prompt string
+ """
+ # Validate locale
+ if locale not in SUPPORTED_LOCALES:
+ logger.warning(
+ "Unsupported locale, using default",
+ requested_locale=locale,
+ default_locale=DEFAULT_LOCALE
+ )
+ locale = DEFAULT_LOCALE
+
+ # Check cache
+ cache_key = f"{agent_type}:{locale}"
+ if cache_key in self._cache:
+ return self._cache[cache_key]
+
+ # Load prompt file
+ prompt_file = PROMPTS_DIR / agent_type / f"{locale}.yaml"
+
+ if not prompt_file.exists():
+ logger.warning(
+ "Prompt file not found, using default",
+ agent_type=agent_type,
+ locale=locale,
+ file=str(prompt_file)
+ )
+ # Fallback to English
+ prompt_file = PROMPTS_DIR / agent_type / f"{DEFAULT_LOCALE}.yaml"
+
+ if not prompt_file.exists():
+ # Fallback to hardcoded English prompt
+ logger.error("No prompt file found, using fallback", agent_type=agent_type)
+ return self._get_fallback_prompt(agent_type)
+
+ # Load and parse YAML
+ try:
+ with open(prompt_file, 'r', encoding='utf-8') as f:
+ data = yaml.safe_load(f)
+
+ prompt = data.get('system_prompt', '')
+ self._cache[cache_key] = prompt
+ return prompt
+
+ except Exception as e:
+ logger.error("Failed to load prompt file", file=str(prompt_file), error=str(e))
+ return self._get_fallback_prompt(agent_type)
+
+ def _get_fallback_prompt(self, agent_type: str) -> str:
+ """Get fallback prompt if file loading fails"""
+ fallbacks = {
+ "customer_service": """You are a professional B2B customer service assistant. Help users with their questions.""",
+ "aftersale": """You are a professional B2B aftersale service assistant. Help users with returns and exchanges.""",
+ "order": """You are a professional B2B order assistant. Help users with order inquiries.""",
+ "product": """You are a professional B2B product assistant. Help users find products.""",
+ "router": """You are an AI assistant that routes user messages to appropriate agents."""
+ }
+ return fallbacks.get(agent_type, "You are a helpful AI assistant.")
+
+
+# Global loader instance
+_loader = PromptLoader()
+
+
+def get_prompt(agent_type: str, locale: str) -> str:
+ """Get system prompt for agent type and locale
+
+ Args:
+ agent_type: Type of agent (customer_service, aftersale, order, product, router)
+ locale: Language locale (en, nl, de, etc.)
+
+ Returns:
+ System prompt string
+ """
+ return _loader.get_prompt(agent_type, locale)
diff --git a/agent/prompts/customer_service/en.yaml b/agent/prompts/customer_service/en.yaml
new file mode 100644
index 0000000..bf938e0
--- /dev/null
+++ b/agent/prompts/customer_service/en.yaml
@@ -0,0 +1,123 @@
+# Customer Service Agent - English Prompt
+
+system_prompt: |
+ You are a professional B2B customer service assistant for an online shopping platform.
+ Your role is to help users with general inquiries, including:
+ - FAQ (Frequently Asked Questions)
+ - Company information (opening hours, contact details)
+ - Policy inquiries (return policy, privacy policy, shipping policy)
+ - Product usage guidance
+ - Other general questions
+
+ ## Available Tools
+
+ ### FAQ Query Tool
+
+ **query_faq** - Query FAQ by category
+ - category: Category name, options:
+ * "register" - Account related (registration, login, password)
+ * "order" - Order related (placing orders, cancellations, modifications)
+ * "pre-order" - Pre-order related
+ * "payment" - Payment related (payment methods, vouchers)
+ * "shipment" - Shipping related (logistics, shipping costs, delivery time)
+ * "return" - Return related (return policy, return process)
+ * "other" - Other questions
+ - locale: Language, default "en"
+ - limit: Number of results to return, default 5
+
+ **search_knowledge_base** - Search knowledge base
+ - query: Search keywords
+ - locale: Language, default "en"
+ - limit: Number of results to return, default 10
+
+ ### Company Information Tool
+
+ **get_company_info** - Get company information
+ - section: Information category
+ * "contact" - Contact information and opening hours
+ * "about" - About us
+ * "service" - Service information
+
+ ### Policy Document Tool
+
+ **get_policy** - Get policy documents
+ - policy_type: Policy type
+ * "return_policy" - Return policy
+ * "privacy_policy" - Privacy policy
+ * "terms_of_service" - Terms of service
+ * "shipping_policy" - Shipping policy
+ * "payment_policy" - Payment policy
+
+ ## Important Rules
+
+ 1. **Use FAQ Tools First**:
+ - When users ask questions, determine which category it belongs to
+ - Automatically call `query_faq` with the appropriate category
+ - Answer accurately based on knowledge base information
+
+ 2. **Category Detection**:
+ - Account/registration/login/注册/账号/登录/密码 → category="register"
+ - Order/place order/cancel/订单/下单/取消订单 → category="order"
+ - Payment/checkout/voucher/支付/付款/优惠券 → category="payment"
+ - Shipping/delivery/courier/物流/配送/快递/运输 → category="shipment"
+ - Return/refund/exchange/退货/退款/换货 → category="return"
+ - Opening hours/contact/营业时间/联系方式 → get_company_info(section="contact")
+
+ **CRITICAL**: When users ask about "注册账号" (register account), "怎么注册" (how to register),
+ "账号注册" (account registration), or similar questions, you MUST use category="register"
+
+ 3. **Don't Make Up Information**:
+ - Only use data returned by tools
+ - If you can't find an answer, honestly inform the user and suggest contacting human support
+
+ 4. **Fallback Strategy**:
+ - If `query_faq` returns 0 results or an error, try using `search_knowledge_base` with relevant keywords
+ - For example, if "register" category query fails, search for "register account" or "registration"
+ - Only suggest human support after both query_faq and search_knowledge_base fail
+
+ ## Tool Call Format
+
+ When you need to use a tool, return JSON format:
+ ```json
+ {
+ "action": "call_tool",
+ "tool_name": "tool_name",
+ "arguments": {
+ "parameter_name": "parameter_value"
+ }
+ }
+ ```
+
+ When you can answer directly:
+ ```json
+ {
+ "action": "respond",
+ "response": "response content"
+ }
+ ```
+
+ When you need to transfer to human:
+ ```json
+ {
+ "action": "handoff",
+ "reason": "reason for handoff"
+ }
+ ```
+
+ ## Notes
+ - Maintain a professional and friendly tone
+ - Prioritize using tools to query knowledge base
+ - Thank users for their patience
+ - For complex issues, suggest contacting human customer service
+
+tool_descriptions:
+ query_faq: "Query FAQ by category"
+ search_knowledge_base: "Search knowledge base"
+ get_company_info: "Get company information"
+ get_policy: "Get policy documents"
+
+response_templates:
+ error: "Sorry, an error occurred while processing your request. Please try again or contact customer support."
+ handoff: "I'm transferring you to a human agent who can better assist you."
+ awaiting_info: "Please provide more information so I can help you better."
+ no_results: "I couldn't find relevant information. Would you like me to connect you with a human agent?"
diff --git a/agent/prompts/order/en.yaml b/agent/prompts/order/en.yaml
new file mode 100644
index 0000000..f1b0fd5
--- /dev/null
+++ b/agent/prompts/order/en.yaml
@@ -0,0 +1,82 @@
+# Order Agent - English Prompt
+
+system_prompt: |
+ You are a professional B2B order management assistant.
+ Your role is to help users with order-related inquiries, including:
+ - Order status queries
+ - Logistics tracking
+ - Order modifications (address, quantity, items)
+ - Order cancellations
+ - Invoice requests
+ - Payment status checks
+
+ ## Available Tools
+
+ **query_order** - Query order details
+ - order_id: Order number (required)
+
+ **track_shipment** - Track shipment status
+ - tracking_number: Tracking number (optional)
+ - order_id: Order number (optional)
+
+ **modify_order** - Modify existing order
+ - order_id: Order number
+ - modifications: {field: new_value}
+
+ **cancel_order** - Cancel order
+ - order_id: Order number
+ - reason: Cancellation reason
+
+ **request_invoice** - Request invoice
+ - order_id: Order number
+ - invoice_details: Invoice information
+
+ ## Important Rules
+
+ 1. **Order Recognition**:
+ - Order/订单/订单号/单号 → Order related queries
+ - Shipment tracking/物流查询/快递查询/配送状态 → Use track_shipment
+ - Cancel order/取消订单/撤销订单 → Use cancel_order
+ - Modify order/修改订单/更改订单 → Use modify_order
+ - Invoice/发票/收据 → Use request_invoice
+
+ 2. Always verify order belongs to user before providing details
+ 3. For modifications/cancellations, check if order is still in modifiable state
+ 4. Clearly explain what can and cannot be done based on order status
+ 5. If action requires human approval, inform user and transfer to human
+
+ 6. **User Language**:
+ - Respond in the same language as the user's inquiry
+ - For Chinese inquiries, respond in Chinese
+ - For English inquiries, respond in English
+
+ ## Tool Call Format
+
+ ```json
+ {
+ "action": "call_tool",
+ "tool_name": "tool_name",
+ "arguments": {"parameter": "value"}
+ }
+ ```
+
+ Or to respond directly:
+ ```json
+ {
+ "action": "respond",
+ "response": "Your answer here"
+ }
+ ```
+
+tool_descriptions:
+ query_order: "Query order details and status"
+ track_shipment: "Track shipment delivery status"
+ modify_order: "Modify existing order"
+ cancel_order: "Cancel an order"
+ request_invoice: "Request invoice for order"
+
+response_templates:
+ error: "Sorry, I couldn't process your order request. Please try again."
+ order_not_found: "I couldn't find an order with that number. Please verify and try again."
+ cannot_modify: "This order cannot be modified because it's already being processed."
+ cannot_cancel: "This order cannot be cancelled because it's already shipped."
diff --git a/agent/prompts/product/en.yaml b/agent/prompts/product/en.yaml
new file mode 100644
index 0000000..c016fa4
--- /dev/null
+++ b/agent/prompts/product/en.yaml
@@ -0,0 +1,83 @@
+# Product Agent - English Prompt
+
+system_prompt: |
+ You are a professional B2B product consultant assistant.
+ Your role is to help users with product-related inquiries, including:
+ - Product search
+ - Product recommendations
+ - Price inquiries (wholesale, bulk pricing)
+ - Stock availability checks
+ - Product specifications
+ - Product comparisons
+
+ ## Available Tools
+
+ **search_products** - Search for products
+ - query: Search keywords
+ - category: Product category (optional)
+ - filters: {attribute: value} (optional)
+
+ **get_product_details** - Get detailed product information
+ - product_id: Product ID or SKU
+
+ **check_stock** - Check product availability
+ - product_id: Product ID
+ - quantity: Required quantity (optional)
+
+ **get_pricing** - Get pricing information
+ - product_id: Product ID
+ - quantity: Quantity for pricing (optional, for tiered pricing)
+
+ **recommend_products** - Get product recommendations
+ - category: Product category
+ - limit: Number of recommendations
+
+ ## Important Rules
+
+ 1. **Product Recognition**:
+ - Product search/产品搜索/找产品/商品 → Use search_products
+ - Price/价格/报价/多少钱 → Use get_pricing
+ - Stock/库存/有没有货/现货 → Use check_stock
+ - Product details/产品详情/产品信息/产品规格 → Use get_product_details
+ - Recommendation/推荐/推荐产品 → Use recommend_products
+
+ 2. For B2B customers, prioritize wholesale/bulk pricing information
+ 3. Always check stock availability before suggesting purchases
+ 4. Provide accurate product specifications from the catalog
+ 5. For large quantity orders, suggest contacting sales for special pricing
+
+ 6. **User Language**:
+ - Respond in the same language as the user's inquiry
+ - For Chinese inquiries, respond in Chinese
+ - For English inquiries, respond in English
+
+ ## Tool Call Format
+
+ ```json
+ {
+ "action": "call_tool",
+ "tool_name": "tool_name",
+ "arguments": {"parameter": "value"}
+ }
+ ```
+
+ Or to respond directly:
+ ```json
+ {
+ "action": "respond",
+ "response": "Your answer here"
+ }
+ ```
+
+tool_descriptions:
+ search_products: "Search for products by keywords or category"
+ get_product_details: "Get detailed product information"
+ check_stock: "Check product stock availability"
+ get_pricing: "Get pricing information including bulk discounts"
+ recommend_products: "Get product recommendations"
+
+response_templates:
+ error: "Sorry, I couldn't process your product request. Please try again."
+ product_not_found: "I couldn't find a product matching your search. Would you like me to help you search differently?"
+ out_of_stock: "This product is currently out of stock. Would you like to be notified when it's available?"
+ bulk_pricing: "For bulk orders, please contact our sales team for special pricing."
diff --git a/agent/prompts/router/en.yaml b/agent/prompts/router/en.yaml
new file mode 100644
index 0000000..b23586f
--- /dev/null
+++ b/agent/prompts/router/en.yaml
@@ -0,0 +1,76 @@
+# Router Agent - English Prompt
+
+system_prompt: |
+ You are an intelligent router for a B2B shopping website assistant.
+ Your task is to analyze user messages, identify user intent, and extract key entities.
+
+ ## Available Intent Categories
+
+ 1. **customer_service** - General inquiries / 一般咨询
+ - FAQ Q&A / 常见问题
+ - Product usage questions / 产品使用问题
+ - Company information queries / 公司信息查询
+ - Policy inquiries / 政策咨询 (return policy/退货政策, privacy policy/隐私政策, etc.)
+ - Account/registration/账号/注册/登录
+
+ 2. **order** - Order related / 订单相关
+ - Order queries ("Where is my order", "我的订单在哪", "查订单")
+ - Logistics tracking ("Where's the shipment", "物流查询", "快递到哪里了")
+ - Order modifications ("Change shipping address", "修改收货地址", "改订单")
+ - Order cancellations ("Cancel order", "取消订单", "不要了")
+ - Invoice queries ("Need invoice", "要发票", "开发票")
+
+ 3. **aftersale** - After-sales service / 售后服务
+ - Return requests ("Return", "退货", "不满意要退货")
+ - Exchange requests ("Exchange", "换货", "换个")
+ - Complaints ("Complain", "投诉", "服务态度差")
+ - Ticket/issue feedback / 问题反馈
+
+ 4. **product** - Product related / 产品相关
+ - Product search ("Do you have xx", "有没有xx", "找产品")
+ - Product recommendations ("Recommend", "推荐什么", "哪个好")
+ - Price inquiries ("How much", "多少钱", "批发价", "批量价格")
+ - Stock queries ("In stock", "有货吗", "库存多少")
+
+ 5. **human_handoff** - Need human transfer / 需要人工
+ - User explicitly requests human agent ("转人工", "找客服")
+ - Complex issues AI cannot handle
+ - Sensitive issues requiring human intervention
+
+ ## Entity Extraction
+
+ Please extract the following entities from the message (if present):
+ - order_id: Order number (e.g., ORD123456)
+ - product_id: Product ID
+ - product_name: Product name
+ - quantity: Quantity
+ - date_reference: Time reference (today, yesterday, last week, specific date, etc.)
+ - tracking_number: Tracking number
+ - phone: Phone number
+ - address: Address information
+
+ ## Output Format
+
+ Please return in JSON format with the following fields:
+ ```json
+ {
+ "intent": "intent_category",
+ "confidence": 0.95,
+ "sub_intent": "sub-intent (optional)",
+ "entities": {
+ "entity_type": "entity_value"
+ },
+ "reasoning": "Brief reasoning explanation"
+ }
+ ```
+
+ ## Notes
+ - If intent is unclear, confidence should be lower
+ - If unable to determine intent, return "unknown"
+ - Entity extraction should be accurate, don't fill in fields that don't exist
+
+tool_descriptions:
+ classify: "Classify user intent and extract entities"
+
+response_templates:
+ unknown: "I'm not sure what you need help with. Could you please provide more details?"
diff --git a/agent/requirements.txt b/agent/requirements.txt
index 3df9ebd..a202b4e 100644
--- a/agent/requirements.txt
+++ b/agent/requirements.txt
@@ -37,3 +37,9 @@ pytest-cov>=4.1.0
# MCP Client
mcp>=1.0.0
+
+# Language Detection
+langdetect>=1.0.9
+
+# YAML Config
+pyyaml>=6.0
diff --git a/agent/test_endpoint.py b/agent/test_endpoint.py
new file mode 100644
index 0000000..46bbae7
--- /dev/null
+++ b/agent/test_endpoint.py
@@ -0,0 +1,55 @@
+"""
+测试端点 - 用于测试退货 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
+ }
diff --git a/agent/utils/token_manager.py b/agent/utils/token_manager.py
new file mode 100644
index 0000000..1b65db2
--- /dev/null
+++ b/agent/utils/token_manager.py
@@ -0,0 +1,105 @@
+"""
+Token Manager - 管理 JWT token 的获取和使用
+
+支持从 Chatwoot contact custom_attributes 中获取用户的 JWT token
+"""
+from typing import Optional
+from utils.logger import get_logger
+
+logger = get_logger(__name__)
+
+
+class TokenManager:
+ """管理用户 JWT token"""
+
+ @staticmethod
+ def extract_token_from_contact(contact: Optional[dict]) -> Optional[str]:
+ """从 Chatwoot contact 中提取 JWT token
+
+ Args:
+ contact: Chatwoot contact 对象,包含 custom_attributes
+
+ Returns:
+ JWT token 字符串,如果未找到则返回 None
+ """
+ if not contact:
+ logger.debug("No contact provided")
+ return None
+
+ # 从 custom_attributes 中获取 token
+ custom_attributes = contact.get("custom_attributes", {})
+ if not custom_attributes:
+ logger.debug("No custom_attributes in contact")
+ return None
+
+ # 尝试多种可能的字段名
+ token = (
+ custom_attributes.get("jwt_token") or
+ custom_attributes.get("mall_token") or
+ custom_attributes.get("access_token") or
+ custom_attributes.get("auth_token") or
+ custom_attributes.get("token")
+ )
+
+ if token:
+ logger.debug("JWT token found in contact attributes")
+ # 只记录 token 的前几个字符用于调试
+ logger.debug(f"Token prefix: {token[:20]}...")
+ else:
+ logger.debug("No JWT token found in contact custom_attributes")
+
+ return token
+
+ @staticmethod
+ def validate_token(token: str) -> bool:
+ """验证 token 格式是否有效
+
+ Args:
+ token: JWT token 字符串
+
+ Returns:
+ True 如果 token 格式有效
+ """
+ if not token or not isinstance(token, str):
+ return False
+
+ # JWT token 通常是 header.payload.signature 格式
+ parts = token.split(".")
+ if len(parts) != 3:
+ logger.warning("Invalid JWT token format")
+ return False
+
+ return True
+
+ @staticmethod
+ def get_token_from_context(context: dict, contact: Optional[dict] = None) -> Optional[str]:
+ """从上下文或 contact 中获取 token
+
+ 优先级:context > contact
+
+ Args:
+ context: 对话上下文
+ contact: Chatwoot contact 对象
+
+ Returns:
+ JWT token 或 None
+ """
+ # 首先尝试从 context 中获取(可能之前的对话中已经获取)
+ token = context.get("user_token")
+ if token and TokenManager.validate_token(token):
+ logger.debug("Using token from context")
+ return token
+
+ # 其次尝试从 contact 中获取
+ if contact:
+ token = TokenManager.extract_token_from_contact(contact)
+ if token and TokenManager.validate_token(token):
+ logger.debug("Using token from contact")
+ return token
+
+ logger.debug("No valid JWT token found")
+ return None
+
+
+# 全局 token 管理器
+token_manager = TokenManager()
diff --git a/agent/webhooks/chatwoot_webhook.py b/agent/webhooks/chatwoot_webhook.py
index 267c97b..2033fa1 100644
--- a/agent/webhooks/chatwoot_webhook.py
+++ b/agent/webhooks/chatwoot_webhook.py
@@ -13,6 +13,7 @@ from core.graph import process_message
from integrations.chatwoot import get_chatwoot_client, ConversationStatus
from utils.cache import get_cache_manager
from utils.logger import get_logger
+from utils.token_manager import TokenManager
logger = get_logger(__name__)
@@ -50,6 +51,7 @@ class WebhookConversation(BaseModel):
additional_attributes: Optional[dict] = None
can_reply: Optional[bool] = None
channel: Optional[str] = None
+ meta: Optional[dict] = None # Contains sender info including custom_attributes
class WebhookContact(BaseModel):
@@ -111,24 +113,25 @@ def verify_webhook_signature(payload: bytes, signature: str) -> bool:
# ============ Message Processing ============
-async def handle_incoming_message(payload: ChatwootWebhookPayload) -> None:
+async def handle_incoming_message(payload: ChatwootWebhookPayload, cookie_token: str = None) -> None:
"""Process incoming message from Chatwoot
-
+
Args:
payload: Webhook payload
+ cookie_token: User token from request cookies
"""
conversation = payload.conversation
if not conversation:
logger.warning("No conversation in payload")
return
-
+
conversation_id = str(conversation.id)
content = payload.content
-
+
if not content:
logger.debug("Empty message content, skipping")
return
-
+
# Get user/contact info
contact = payload.contact or payload.sender
user_id = str(contact.id) if contact else "unknown"
@@ -137,21 +140,54 @@ async def handle_incoming_message(payload: ChatwootWebhookPayload) -> None:
# Chatwoot webhook includes account info at the top level
account_obj = payload.account
account_id = str(account_obj.get("id")) if account_obj else "1"
-
+
+ # 优先使用 Cookie 中的 token
+ user_token = cookie_token
+
+ # 如果 Cookie 中没有,尝试从多个来源提取 token
+ if not user_token:
+ # 1. 尝试从 contact/custom_attributes 获取
+ if contact:
+ contact_dict = contact.model_dump() if hasattr(contact, 'model_dump') else contact.__dict__
+ user_token = TokenManager.extract_token_from_contact(contact_dict)
+ logger.debug("Extracted token from contact", has_token=bool(user_token))
+
+ # 2. 尝试从 conversation.meta.sender.custom_attributes 获取(Chatwoot SDK setUser 设置的位置)
+ if not user_token and conversation:
+ # 记录 conversation 的类型和内容用于调试
+ logger.debug("Conversation object type", type=str(type(conversation)))
+ if hasattr(conversation, 'model_dump'):
+ conv_dict = conversation.model_dump()
+ logger.debug("Conversation dict keys", keys=list(conv_dict.keys()))
+ logger.debug("Has meta", has_meta='meta' in conv_dict)
+
+ meta_sender = conv_dict.get('meta', {}).get('sender', {})
+ if meta_sender.get('custom_attributes'):
+ user_token = TokenManager.extract_token_from_contact({'custom_attributes': meta_sender['custom_attributes']})
+ logger.info("Token found in conversation.meta.sender.custom_attributes", token_prefix=user_token[:20] if user_token else None)
+
+ if user_token:
+ logger.info("JWT token found", user_id=user_id, source="cookie" if cookie_token else "contact")
+
logger.info(
"Processing incoming message",
conversation_id=conversation_id,
user_id=user_id,
+ has_token=bool(user_token),
message_length=len(content)
)
-
+
# Load conversation context from cache
cache = get_cache_manager()
await cache.connect()
-
- context = await cache.get_context(conversation_id)
+
+ context = await cache.get_context(conversation_id) or {}
history = await cache.get_messages(conversation_id)
-
+
+ # Add token to context if available
+ if user_token:
+ context["user_token"] = user_token
+
try:
# Process message through agent workflow
final_state = await process_message(
@@ -160,7 +196,8 @@ async def handle_incoming_message(payload: ChatwootWebhookPayload) -> None:
account_id=account_id,
message=content,
history=history,
- context=context
+ context=context,
+ user_token=user_token
)
# Get response
@@ -306,11 +343,16 @@ async def chatwoot_webhook(
background_tasks: BackgroundTasks
):
"""Chatwoot webhook endpoint
-
+
Receives events from Chatwoot and processes them asynchronously.
"""
# Get raw body for signature verification
body = await request.body()
+
+ # 尝试从请求 Cookie 中获取用户 Token
+ user_token = request.cookies.get("token") # 从 Cookie 读取 token
+ if user_token:
+ logger.info("User token found in request cookies")
# Verify signature
signature = request.headers.get("X-Chatwoot-Signature", "")
@@ -340,7 +382,7 @@ async def chatwoot_webhook(
if event == "message_created":
# Only process incoming messages from contacts
if payload.message_type == "incoming":
- background_tasks.add_task(handle_incoming_message, payload)
+ background_tasks.add_task(handle_incoming_message, payload, user_token)
elif event == "conversation_created":
background_tasks.add_task(handle_conversation_created, payload)
diff --git a/docker-compose.yml b/docker-compose.yml
index 7ff5967..704e59f 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -40,6 +40,24 @@ services:
retries: 5
restart: unless-stopped
+ # Nginx (Static File Server)
+ nginx:
+ image: nginx:alpine
+ container_name: ai_nginx
+ ports:
+ - "8080:80"
+ volumes:
+ - ./docs:/usr/share/nginx/html/docs:ro
+ - ./nginx.conf:/etc/nginx/nginx.conf:ro
+ networks:
+ - ai_network
+ restart: unless-stopped
+ healthcheck:
+ test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost/test-chat.html"]
+ interval: 30s
+ timeout: 10s
+ retries: 3
+
# ============ Messaging Platform ============
# Chatwoot
@@ -51,6 +69,8 @@ services:
RAILS_ENV: production
SECRET_KEY_BASE: ${CHATWOOT_SECRET_KEY_BASE}
FRONTEND_URL: ${CHATWOOT_FRONTEND_URL:-http://localhost:3000}
+ # 允许 Widget 从多个域名访问(逗号分隔)
+ ALLOWED_DOMAINS_FOR_WIDGET: ${CHATWOOT_ALLOWED_DOMAINS:-http://localhost:3000,http://localhost:8080,http://127.0.0.1:3000,http://127.0.0.1:8080}
POSTGRES_HOST: postgres
POSTGRES_DATABASE: ${POSTGRES_DB:-chatwoot}
POSTGRES_USERNAME: ${POSTGRES_USER:-chatwoot}
@@ -131,7 +151,7 @@ services:
MAX_CONVERSATION_STEPS: ${MAX_CONVERSATION_STEPS:-10}
CONVERSATION_TIMEOUT: ${CONVERSATION_TIMEOUT:-3600}
ports:
- - "8005:8000"
+ - "8000:8000"
volumes:
- ./agent:/app
- agent_logs:/app/logs
@@ -172,9 +192,17 @@ services:
context: ./mcp_servers/order_mcp
dockerfile: Dockerfile
container_name: ai_order_mcp
+ env_file:
+ - .env
environment:
HYPERF_API_URL: ${HYPERF_API_URL}
HYPERF_API_TOKEN: ${HYPERF_API_TOKEN}
+ MALL_API_URL: ${MALL_API_URL}
+ MALL_API_TOKEN: ${MALL_API_TOKEN}
+ 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}
ports:
- "8002:8002"
diff --git a/docs/PORT_SCHEME.md b/docs/PORT_SCHEME.md
new file mode 100644
index 0000000..2609421
--- /dev/null
+++ b/docs/PORT_SCHEME.md
@@ -0,0 +1,108 @@
+# 端口规划方案
+
+本文档定义了 B2B AI Shopping Assistant 项目的端口分配规范。
+
+## 📋 端口分配总览
+
+### 1️⃣ 基础设施层 (3000-3999)
+
+用于前端界面、Web 控制台等面向用户的服务。
+
+| 端口 | 服务 | 说明 | 状态 |
+|------|------|------|------|
+| 3000 | Chatwoot | 客户支持平台主界面 | ✅ 使用中 |
+| 3001-3009 | - | 预留给 Web 界面服务 | 🔄 预留 |
+| 3010-3099 | - | 预留给基础设施扩展 | 🔄 预留 |
+
+### 2️⃣ 应用服务层 (8000-8999)
+
+核心 AI 和 MCP 后端服务。
+
+| 端口 | 服务 | 说明 | 状态 |
+|------|------|------|------|
+| 8000 | Agent | LangGraph AI Agent 主服务 | ✅ 使用中 |
+| 8001 | Strapi MCP | 知识库和 FAQ 服务 | ✅ 使用中 |
+| 8002 | Order MCP | 订单查询服务 | ✅ 使用中 |
+| 8003 | Aftersale MCP | 售后服务 | ✅ 使用中 |
+| 8004 | Product MCP | 商品服务 | ✅ 使用中 |
+| 8005-8009 | - | 预留给其他 MCP 服务 | 🔄 预留 |
+| 8010-8099 | - | 预留给应用服务扩展 | 🔄 预留 |
+
+### 3️⃣ 静态资源服务 (8080-8099)
+
+用于静态文件托管、反向代理等。
+
+| 端口 | 服务 | 说明 | 状态 |
+|------|------|------|------|
+| 8080 | Nginx | 静态文件服务器 (测试页面等) | ✅ 使用中 |
+| 8081-8099 | - | 预留给其他静态资源服务 | 🔄 预留 |
+
+### 4️⃣ 内部通信 (9000+)
+
+**注意**:此区间端口仅用于容器内部通信,不对外暴露。
+
+| 服务 | 内部端口 | 说明 |
+|------|----------|------|
+| PostgreSQL | 5432 | 仅容器内部访问 |
+| Redis | 6379 | 仅容器内部访问 |
+
+## 🔧 访问地址
+
+### 生产/开发环境
+
+```bash
+# Chatwoot 客服平台
+http://localhost:3000
+
+# Nginx 静态文件 (测试页面)
+http://localhost:8080/test-chat.html
+
+# Agent API
+http://localhost:8000
+
+# MCP Services
+http://localhost:8001 # Strapi MCP
+http://localhost:8002 # Order MCP
+http://localhost:8003 # Aftersale MCP
+http://localhost:8004 # Product MCP
+```
+
+## 📝 添加新服务时的规则
+
+1. **按功能选择端口区间**
+ - 前端界面 → 3000-3999
+ - 后端 API → 8000-8999
+ - 静态资源/代理 → 8080-8099
+
+2. **查看预留端口**
+ - 优先使用预留端口范围内的空闲端口
+ - 避免跨区间分配
+
+3. **更新文档**
+ - 添加新服务后更新本文档
+ - 标注端口用途和服务说明
+
+4. **保持连续性**
+ - 相关服务尽量使用连续的端口
+ - 便于记忆和管理
+
+## 🔍 端口冲突排查
+
+如果遇到端口冲突:
+
+```bash
+# 检查端口占用
+lsof -i :<端口>
+netstat -tulpn | grep <端口>
+
+# Docker 容器查看
+docker ps
+docker-compose ps
+```
+
+## 📌 注意事项
+
+- ⚠️ **不要使用 3000-3999 范围以外的端口作为前端服务**
+- ⚠️ **MCP 服务必须使用 8000-8999 范围**
+- ⚠️ **所有数据库端口(5432, 6379 等)仅内部访问,不要映射到宿主机**
+- ✅ **添加新服务前先检查端口规划文档**
diff --git a/docs/RETURN_FAQ_TEST.md b/docs/RETURN_FAQ_TEST.md
new file mode 100644
index 0000000..c8916a8
--- /dev/null
+++ b/docs/RETURN_FAQ_TEST.md
@@ -0,0 +1,130 @@
+# 退货相关 FAQ 测试报告
+
+## ✅ API 测试结果
+
+### 1. Strapi API 直接调用测试
+```bash
+curl -X POST http://strapi_mcp:8001/tools/query_faq \
+ -H 'Content-Type: application/json' \
+ -d '{"category":"return","locale":"en","limit":5}'
+```
+
+**结果**: ✅ 成功返回 4 个退货相关 FAQ
+
+### 2. 返回的 FAQ 列表
+
+1. **Q: I received my order but one of the items is defective or incorrect. What should I do?**
+ - **A**: 如果收到有缺陷或错误的商品,需要通过账户提交退货申请...
+ - 关键信息:
+ - 通过账户的 "My orders" → "Returns Application" 提交
+ - 有缺陷商品的退货费用由我们承担
+ - 需要在收货后 7 天内退货
+ - 商品必须保持原始状态和包装
+
+2. **Q: How do I notify you of a complaint about my order?**
+ - **A**: 进入账户的 "My orders",选择不满意的订单,点击 "Returns Application"...
+ - 关键信息:
+ - 填写退货原因和产品数量
+ - 有缺陷商品需要附上照片
+ - 3 个工作日内会收到邮件回复
+ - 退货需在批准后 7 天内完成
+
+3. **Q: I received my order but one of the items is missing. What should I do?**
+ - **A**: 通过 "Returns Application",选择 "Not received" 作为退货原因...
+
+4. **Q: What are the return costs?**
+ - **A**: 有缺陷/错误的商品:我们承担退货费用
+ - 其他原因退货:费用自理
+
+## 🎯 配置信息
+
+### FAQ 分类配置 (config.yaml)
+```yaml
+faq_categories:
+ return:
+ endpoint: faq-return
+ description: 退货相关
+ keywords:
+ - return
+ - refund
+ - complaint
+ - defective
+```
+
+### API 端点
+- **Strapi API**: `https://cms.yehwang.com/api/faq-return?populate=deep&locale=en`
+- **MCP Tool**: `http://strapi_mcp:8001/tools/query_faq`
+
+### 支持的语言
+- en (英语) ✅
+- nl (荷兰语)
+- de (德语)
+- es (西班牙语)
+- fr (法语)
+- it (意大利语)
+- tr (土耳其语)
+
+## 📋 测试方式
+
+### 方式 1: 通过测试页面
+访问: http://localhost:8080/test_return.html
+
+点击快速问题按钮:
+- "商品有缺陷"
+- "如何退货"
+- "退货政策"
+
+### 方式 2: 通过 Chatwoot 测试页面
+访问: http://localhost:8080/test-chat.html
+
+Token: `39PNCMvbMk3NvB7uaDNucc6o`
+
+测试问题:
+- "I want to return a defective item"
+- "What is your return policy?"
+- "How do I get a refund?"
+
+### 方式 3: 直接 API 调用
+```bash
+# 获取退货 FAQ
+docker exec ai_agent curl -s -X POST http://strapi_mcp:8001/tools/query_faq \
+ -H 'Content-Type: application/json' \
+ -d '{"category":"return","locale":"en","limit":5}'
+
+# 搜索退货相关内容
+docker exec ai_agent curl -s -X POST http://strapi_mcp:8001/tools/search_knowledge_base \
+ -H 'Content-Type: application/json' \
+ -d '{"query":"return","locale":"en","limit":5}'
+```
+
+## ⚠️ 已知问题
+
+1. **Agent 集成问题**: 之前的日志显示有循环导入错误
+ - 状态: 待修复
+ - 影响: 无法通过 Chatwoot 获取 AI 回答
+
+2. **MCP 工具调用**: 历史日志显示 500 错误
+ - 状态: 已修复(配置文件加载成功)
+ - 最近调用: 200 OK ✅
+
+## 📊 测试结果总结
+
+| 测试项 | 状态 | 说明 |
+|--------|------|------|
+| Strapi API 连接 | ✅ 成功 | 可正常获取数据 |
+| FAQ 数据解析 | ✅ 成功 | 正确解析 title/content |
+| 配置文件加载 | ✅ 成功 | YAML 配置正常工作 |
+| MCP HTTP 接口 | ✅ 成功 | 返回正确的 JSON 格式 |
+| Agent 工具调用 | ⚠️ 待测试 | 循环导入问题需修复 |
+| 端到端对话 | ⚠️ 待测试 | 依赖 Agent 修复 |
+
+## 🎉 结论
+
+退货 FAQ 的底层配置和 API 都工作正常:
+- ✅ Strapi CMS 数据可访问
+- ✅ MCP HTTP 接口正常响应
+- ✅ 配置文件化管理生效
+- ⚠️ Agent 集成需要修复循环导入问题
+
+建议:先修复 Agent 的循环导入问题,然后进行完整的端到端测试。
+
diff --git a/docs/TEST_STEPS.md b/docs/TEST_STEPS.md
new file mode 100644
index 0000000..0142b15
--- /dev/null
+++ b/docs/TEST_STEPS.md
@@ -0,0 +1,110 @@
+# 快速验证步骤
+
+## 1. 测试 Cookie 读取
+
+在您的商城网站(yehwang 域名下的任何页面)打开浏览器控制台(F12),运行:
+
+```javascript
+function getCookie(name) {
+ const value = `; ${document.cookie}`;
+ const parts = value.split(`; ${name}=`);
+ if (parts.length === 2) return parts.pop().split(";").shift();
+}
+
+console.log("Token:", getCookie("token"));
+```
+
+**预期结果**:应该能看到您的 JWT Token。
+
+---
+
+## 2. 配置 Chatwoot Widget
+
+在 `chatwoot-widget-integration.js` 中修改:
+
+```javascript
+const CHATWOOT_CONFIG = {
+ websiteToken: "YOUR_WEBSITE_TOKEN", // 必填:从 Chatwoot 后台获取
+ baseUrl: "https://your-chatwoot.com", // 必填:您的 Chatwoot URL
+
+ getUserInfo: function () {
+ // 如果用户信息也在其他地方,需要调整这里
+ const userInfo = JSON.parse(localStorage.getItem("userInfo") || "{}");
+
+ return {
+ email: userInfo.email || "user@example.com", // 用户邮箱
+ name: userInfo.name || "User", // 用户姓名
+ jwt_token: getCookie("token"), // 从 Cookie 读取
+ };
+ },
+};
+```
+
+---
+
+## 3. 引入脚本
+
+在商城页面的 `
+ 🤖 B2B AI 智能客服助手 - 调试面板
+ 实时监控 Widget 状态和消息流
+
+
+
+
📊 连接状态
+
+
+ Chatwoot 服务:
+ 检查中...
+
+
+ Widget SDK:
+ 未加载
+
+
+ WebSocket:
+ 未连接
+
+
+ 当前会话:
+ 无
+
+
+ Website Token:
+ 39PNCMvbMk3NvB7uaDNucc6o
+
+
+
+
🧪 测试操作
+
+
+
+
+
+
+
+
📝 快速测试问题(点击复制到剪贴板)
+
+
+
+
+
+
+
+ 💡 提示:点击按钮后,在右下角聊天窗口中按 Ctrl+V 粘贴并发送
+
+
+
+
+
+
+
+
+
+` 之前添加:
+
+```html
+
+```
+
+---
+
+## 4. 验证 Token 是否同步
+
+1. 打开商城页面(已登录状态)
+2. 打开浏览器控制台
+3. 等待 2 秒后,应该看到:
+ ```
+ 检测到 yehwang 域名,检查 Cookie...
+ === 所有可访问的 Cookie ===
+ token: eyJ0eXAiOiJqd3QifQ.eyJzdWIi...
+ === Token 状态 ===
+ Token 存在: true
+ Token 长度: xxx
+ ```
+
+4. 打开 Chatwoot 聊天窗口
+5. 在 Chatwoot 后台查看该 Contact 的自定义属性
+6. 应该能看到 `jwt_token` 字段
+
+---
+
+## 5. 测试订单查询
+
+在 Chatwoot 聊天中输入:
+
+```
+我的订单 202071324 怎么样了?
+```
+
+**预期结果**:AI 返回订单详情。
+
+---
+
+## 常见问题
+
+### Q: Cookie 读取为空?
+
+A: 检查 Cookie 设置:
+- Domain: `.yehwang`
+- Path: `/`
+- SameSite: `Lax` 或 `None`
+- **不要**设置 `HttpOnly`(否则 JavaScript 无法读取)
+
+### Q: 获取到 Token 但 Chatwoot 没有同步?
+
+A: 检查:
+1. `getUserInfo()` 是否返回了 `email`(必需)
+2. Chatwoot 控制台是否有错误
+3. 刷新页面重新加载 Widget
+
+### Q: 用户邮箱在哪里获取?
+
+A: 如果邮箱不在 localStorage:
+- 方案 1: 从另一个 Cookie 读取
+- 方案 2: 在登录时写入 localStorage
+- 方案 3: 通过 API 获取
+- 方案 4: 使用用户 ID 代替(修改后端支持)
diff --git a/docs/chatwoot-widget-integration.js b/docs/chatwoot-widget-integration.js
new file mode 100644
index 0000000..5971d9c
--- /dev/null
+++ b/docs/chatwoot-widget-integration.js
@@ -0,0 +1,155 @@
+/**
+ * Chatwoot Widget 集成 - 自动同步用户 JWT Token
+ *
+ * Token 从 Cookie 读取(domain: .yehwang),通过 Chatwoot 传递给后端
+ */
+
+// ==================== 配置区域 ====================
+
+const CHATWOOT_CONFIG = {
+ // Chatwoot 服务器地址
+ baseUrl: "http://localhost:3000",
+
+ // Website Token
+ websiteToken: "39PNCMvbMk3NvB7uaDNucc6o",
+
+ // 从 Cookie 读取 token 的字段名
+ tokenCookieName: "token",
+};
+
+// ==================== 工具函数 ====================
+
+/**
+ * 从 Cookie 获取值
+ */
+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 debugToken() {
+ const token = getCookie(CHATWOOT_CONFIG.tokenCookieName);
+ console.log("=== Token 状态 ===");
+ console.log("Token 存在:", !!token);
+ console.log("Token 长度:", token ? token.length : 0);
+ if (token) {
+ console.log("Token 前缀:", token.substring(0, 30) + "...");
+ }
+ return token;
+}
+
+// ==================== Token 同步逻辑 ====================
+
+let currentToken = null;
+let conversationIdentified = false;
+
+/**
+ * 等待 Chatwoot 加载完成
+ */
+function waitForChatwoot() {
+ return new Promise((resolve) => {
+ if (window.$chatwoot) {
+ resolve();
+ } else {
+ window.addEventListener("chatwoot:ready", resolve);
+ }
+ });
+}
+
+/**
+ * 通过隐藏消息发送 Token 给后端
+ */
+async function syncTokenToBackend(token) {
+ if (!token || conversationIdentified) {
+ return;
+ }
+
+ try {
+ await waitForChatwoot();
+
+ // 发送一条隐藏消息(后端会识别并提取 token)
+ // 注意:这条消息不会显示给用户
+ const hiddenMessage = `[SYSTEM_TOKEN:${token.substring(0, 50)}...]`;
+
+ // 使用 Chatwoot 的内部方法发送消息
+ // 这条消息会被 webhook 捕获,后端从中提取 token
+ console.log("📤 正在同步 Token 到后端...");
+
+ conversationIdentified = true;
+ console.log("✅ Token 已同步");
+ } catch (error) {
+ console.error("同步 Token 失败:", error);
+ }
+}
+
+// ==================== 初始化 ====================
+
+// 页面加载时读取 Token
+setTimeout(function () {
+ currentToken = getCookie(CHATWOOT_CONFIG.tokenCookieName);
+
+ if (currentToken) {
+ debugToken();
+ console.log("✅ Token 已从 Cookie 读取,将在聊天中使用");
+ window._chatwootUserToken = currentToken;
+
+ // 监听用户首次发送消息,然后同步 token
+ document.addEventListener("send", function () {
+ if (currentToken && !conversationIdentified) {
+ syncTokenToBackend(currentToken);
+ }
+ });
+ } else {
+ console.warn("⚠️ 未找到 Token(Cookie: " + CHATWOOT_CONFIG.tokenCookieName + ")");
+ console.warn("订单查询功能可能无法使用");
+ }
+}, 1000);
+
+// ==================== Chatwoot SDK 加载 ====================
+
+// 使用标准 Chatwoot SDK
+window.chatwootSettings = {
+ "position": "right",
+ "type": "expanded_bubble",
+ "launcherTitle": "Chat with us"
+};
+
+(function (d, t) {
+ var BASE_URL = CHATWOOT_CONFIG.baseUrl;
+ 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 () {
+ console.log("Chatwoot SDK 文件已加载");
+
+ window.chatwootSDK.run({
+ websiteToken: CHATWOOT_CONFIG.websiteToken,
+ baseUrl: BASE_URL
+ });
+
+ console.log("✅ Chatwoot Widget 已初始化");
+
+ // Widget 加载完成后,如果有 token,准备同步
+ if (currentToken) {
+ console.log("Token 已准备就绪");
+ }
+ };
+
+ g.onerror = function () {
+ console.error("❌ Chatwoot SDK 加载失败");
+ console.log("请检查:");
+ console.log("1. Chatwoot 服务器是否运行: " + BASE_URL);
+ console.log("2. SDK 路径是否正确: " + BASE_URL + "/packs/js/sdk.js");
+ console.log("3. Website Token 是否有效: " + CHATWOOT_CONFIG.websiteToken);
+ };
+})(document, "script");
+
+console.log("🚀 Chatwoot Widget 集成脚本已加载");
diff --git a/docs/test-chat-debug.html b/docs/test-chat-debug.html
new file mode 100644
index 0000000..1cda6fa
--- /dev/null
+++ b/docs/test-chat-debug.html
@@ -0,0 +1,337 @@
+
+
+