- 配置 Docker Compose 多服务编排 - 实现 Chatwoot + Agent 集成 - 配置 Strapi MCP 知识库 - 支持 7 种语言的 FAQ 系统 - 实现 LangGraph AI 工作流 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
405 lines
13 KiB
Python
405 lines
13 KiB
Python
"""
|
|
HTTP Routes for Strapi MCP Server
|
|
Provides direct HTTP access to knowledge base functions
|
|
"""
|
|
from typing import Optional, List
|
|
import httpx
|
|
from fastapi import Request
|
|
from starlette.responses import JSONResponse
|
|
|
|
from pydantic_settings import BaseSettings
|
|
from pydantic import ConfigDict
|
|
|
|
|
|
class Settings(BaseSettings):
|
|
"""Server configuration"""
|
|
strapi_api_url: str
|
|
strapi_api_token: str = ""
|
|
log_level: str = "INFO"
|
|
|
|
model_config = ConfigDict(env_file=".env")
|
|
|
|
|
|
settings = Settings()
|
|
|
|
|
|
# ============ FAQ Categories ============
|
|
|
|
FAQ_CATEGORIES = {
|
|
"register": "faq-register",
|
|
"order": "faq-order",
|
|
"pre-order": "faq-pre-order",
|
|
"payment": "faq-payment",
|
|
"shipment": "faq-shipment",
|
|
"return": "faq-return",
|
|
"other": "faq-other-question",
|
|
}
|
|
|
|
|
|
# ============ Company Info ============
|
|
|
|
async def get_company_info_http(section: str = "contact", locale: str = "en"):
|
|
"""Get company information - HTTP wrapper
|
|
|
|
Args:
|
|
section: Section identifier (e.g., "contact")
|
|
locale: Language locale (default: en)
|
|
Supported: en, nl, de, es, fr, it, tr
|
|
"""
|
|
try:
|
|
# Map section names to API endpoints
|
|
section_map = {
|
|
"contact": "info-contact",
|
|
"about": "info-about",
|
|
"service": "info-service",
|
|
}
|
|
|
|
endpoint = section_map.get(section, f"info-{section}")
|
|
|
|
# Build query parameters
|
|
headers = {"Content-Type": "application/json"}
|
|
if settings.strapi_api_token and settings.strapi_api_token.strip():
|
|
headers["Authorization"] = f"Bearer {settings.strapi_api_token}"
|
|
|
|
# Request with populate=deep to get all related data
|
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
response = await client.get(
|
|
f"{settings.strapi_api_url}/api/{endpoint}",
|
|
params={"populate": "deep", "locale": locale},
|
|
headers=headers
|
|
)
|
|
response.raise_for_status()
|
|
data = response.json()
|
|
|
|
if not data.get("data"):
|
|
return {
|
|
"success": False,
|
|
"error": f"Section '{section}' not found",
|
|
"data": None
|
|
}
|
|
|
|
item = data["data"]
|
|
|
|
# Extract relevant information
|
|
result_data = {
|
|
"id": item.get("id"),
|
|
"title": item.get("title"),
|
|
"description": item.get("description"),
|
|
"section": section,
|
|
"locale": locale
|
|
}
|
|
|
|
# Add profile information if available
|
|
if item.get("yehwang_profile"):
|
|
profile = item["yehwang_profile"]
|
|
result_data["profile"] = {
|
|
"title": profile.get("title"),
|
|
"content": profile.get("content")
|
|
}
|
|
|
|
return {
|
|
"success": True,
|
|
"data": result_data
|
|
}
|
|
|
|
except Exception as e:
|
|
return {
|
|
"success": False,
|
|
"error": str(e),
|
|
"data": None
|
|
}
|
|
|
|
|
|
# ============ FAQ Query ============
|
|
|
|
async def query_faq_http(
|
|
category: str = "other",
|
|
locale: str = "en",
|
|
limit: int = 10
|
|
):
|
|
"""Get FAQ by category - HTTP wrapper
|
|
|
|
Args:
|
|
category: FAQ category (register, order, pre-order, payment, shipment, return, other)
|
|
locale: Language locale (default: en)
|
|
limit: Maximum results to return
|
|
"""
|
|
try:
|
|
# Map category to endpoint
|
|
endpoint = FAQ_CATEGORIES.get(category, f"faq-{category}")
|
|
|
|
headers = {"Content-Type": "application/json"}
|
|
if settings.strapi_api_token and settings.strapi_api_token.strip():
|
|
headers["Authorization"] = f"Bearer {settings.strapi_api_token}"
|
|
|
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
response = await client.get(
|
|
f"{settings.strapi_api_url}/api/{endpoint}",
|
|
params={"populate": "deep", "locale": locale},
|
|
headers=headers
|
|
)
|
|
response.raise_for_status()
|
|
data = response.json()
|
|
|
|
# Check if data exists
|
|
if not data.get("data"):
|
|
return {
|
|
"success": True,
|
|
"count": 0,
|
|
"category": category,
|
|
"locale": locale,
|
|
"results": []
|
|
}
|
|
|
|
# Handle different response formats
|
|
item_data = data["data"]
|
|
|
|
# If it's a single object with nested items
|
|
faq_list = []
|
|
if isinstance(item_data, dict):
|
|
# Check for content array (Yehwang format)
|
|
if item_data.get("content"):
|
|
faq_list = item_data["content"]
|
|
# Check for nested FAQ array
|
|
elif item_data.get("faqs"):
|
|
faq_list = item_data["faqs"]
|
|
# Check for questions array
|
|
elif item_data.get("questions"):
|
|
faq_list = item_data["questions"]
|
|
# The item itself might be the FAQ
|
|
elif item_data.get("question") and item_data.get("answer"):
|
|
faq_list = [item_data]
|
|
else:
|
|
# Return the whole data as one FAQ
|
|
faq_list = [item_data]
|
|
elif isinstance(item_data, list):
|
|
faq_list = item_data
|
|
|
|
# Format results
|
|
results = []
|
|
for item in faq_list[:limit]:
|
|
faq_item = {
|
|
"id": item.get("id"),
|
|
"category": category,
|
|
"locale": locale
|
|
}
|
|
|
|
# Yehwang format: title and content
|
|
if item.get("title"):
|
|
faq_item["question"] = item.get("title")
|
|
if item.get("content"):
|
|
faq_item["answer"] = item.get("content")
|
|
|
|
# Also support other field names
|
|
if item.get("question"):
|
|
faq_item["question"] = item.get("question")
|
|
if item.get("answer"):
|
|
faq_item["answer"] = item.get("answer")
|
|
if item.get("description"):
|
|
faq_item["description"] = item.get("description")
|
|
|
|
# Add any other fields
|
|
for key, value in item.items():
|
|
if key not in faq_item and key not in ["id", "createdAt", "updatedAt", "publishedAt", "locale"]:
|
|
faq_item[key] = value
|
|
|
|
if "question" in faq_item or "answer" in faq_item:
|
|
results.append(faq_item)
|
|
|
|
return {
|
|
"success": True,
|
|
"count": len(results),
|
|
"category": category,
|
|
"locale": locale,
|
|
"results": results
|
|
}
|
|
|
|
except Exception as e:
|
|
return {
|
|
"success": False,
|
|
"error": str(e),
|
|
"category": category,
|
|
"results": []
|
|
}
|
|
|
|
|
|
async def search_faq_http(
|
|
query: str,
|
|
locale: str = "en",
|
|
limit: int = 5
|
|
):
|
|
"""Search FAQ across all categories - HTTP wrapper
|
|
|
|
Args:
|
|
query: Search keywords
|
|
locale: Language locale (default: en)
|
|
limit: Maximum results per category
|
|
"""
|
|
try:
|
|
all_results = []
|
|
|
|
# Search all categories in parallel
|
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
tasks = []
|
|
for category_name, endpoint in FAQ_CATEGORIES.items():
|
|
headers = {"Content-Type": "application/json"}
|
|
if settings.strapi_api_token and settings.strapi_api_token.strip():
|
|
headers["Authorization"] = f"Bearer {settings.strapi_api_token}"
|
|
|
|
task = client.get(
|
|
f"{settings.strapi_api_url}/api/{endpoint}",
|
|
params={"populate": "deep", "locale": locale},
|
|
headers=headers
|
|
)
|
|
tasks.append((category_name, task))
|
|
|
|
# Execute all requests
|
|
for category_name, task in tasks:
|
|
try:
|
|
response = await task
|
|
if response.status_code == 200:
|
|
data = response.json()
|
|
|
|
if data.get("data"):
|
|
item_data = data["data"]
|
|
|
|
# Extract FAQ list
|
|
faq_list = []
|
|
if isinstance(item_data, dict):
|
|
# Yehwang format: content array
|
|
if item_data.get("content"):
|
|
faq_list = item_data["content"]
|
|
elif item_data.get("faqs"):
|
|
faq_list = item_data["faqs"]
|
|
elif item_data.get("questions"):
|
|
faq_list = item_data["questions"]
|
|
elif item_data.get("question") and item_data.get("answer"):
|
|
faq_list = [item_data]
|
|
elif isinstance(item_data, list):
|
|
faq_list = item_data
|
|
|
|
# Filter by query and add to results
|
|
for item in faq_list:
|
|
# Search in title/content (Yehwang format) or question/answer
|
|
item_text = (
|
|
str(item.get("title", "")) +
|
|
str(item.get("content", "")) +
|
|
str(item.get("question", "")) +
|
|
str(item.get("answer", "")) +
|
|
str(item.get("description", ""))
|
|
)
|
|
|
|
if query.lower() in item_text.lower():
|
|
result_item = {
|
|
"id": item.get("id"),
|
|
"category": category_name,
|
|
"locale": locale
|
|
}
|
|
|
|
# Use title as question (Yehwang format)
|
|
if item.get("title"):
|
|
result_item["question"] = item.get("title")
|
|
if item.get("content"):
|
|
result_item["answer"] = item.get("content")
|
|
|
|
# Also support other field names
|
|
if item.get("question"):
|
|
result_item["question"] = item.get("question")
|
|
if item.get("answer"):
|
|
result_item["answer"] = item.get("answer")
|
|
if item.get("description"):
|
|
result_item["description"] = item.get("description")
|
|
|
|
all_results.append(result_item)
|
|
|
|
except Exception as e:
|
|
# Continue with next category on error
|
|
pass
|
|
|
|
return {
|
|
"success": True,
|
|
"count": len(all_results),
|
|
"query": query,
|
|
"locale": locale,
|
|
"results": all_results[:limit]
|
|
}
|
|
|
|
except Exception as e:
|
|
return {
|
|
"success": False,
|
|
"error": str(e),
|
|
"results": []
|
|
}
|
|
|
|
|
|
async def search_knowledge_base_http(query: str, locale: str = "en", limit: int = 10):
|
|
"""Search knowledge base - HTTP wrapper
|
|
|
|
Args:
|
|
query: Search keywords
|
|
locale: Language locale
|
|
limit: Maximum results
|
|
"""
|
|
# Search FAQ across all categories
|
|
return await search_faq_http(query, locale, limit)
|
|
|
|
|
|
async def get_policy_http(policy_type: str, locale: str = "en"):
|
|
"""Get policy document - HTTP wrapper
|
|
|
|
Args:
|
|
policy_type: Type of policy (return_policy, privacy_policy, etc.)
|
|
locale: Language locale
|
|
"""
|
|
try:
|
|
# Map policy types to endpoints
|
|
policy_map = {
|
|
"return_policy": "policy-return",
|
|
"privacy_policy": "policy-privacy",
|
|
"terms_of_service": "policy-terms",
|
|
"shipping_policy": "policy-shipping",
|
|
"payment_policy": "policy-payment",
|
|
}
|
|
|
|
endpoint = policy_map.get(policy_type, f"policy-{policy_type}")
|
|
|
|
headers = {"Content-Type": "application/json"}
|
|
if settings.strapi_api_token and settings.strapi_api_token.strip():
|
|
headers["Authorization"] = f"Bearer {settings.strapi_api_token}"
|
|
|
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
response = await client.get(
|
|
f"{settings.strapi_api_url}/api/{endpoint}",
|
|
params={"populate": "deep", "locale": locale},
|
|
headers=headers
|
|
)
|
|
response.raise_for_status()
|
|
data = response.json()
|
|
|
|
if not data.get("data"):
|
|
return {
|
|
"success": False,
|
|
"error": f"Policy '{policy_type}' not found",
|
|
"data": None
|
|
}
|
|
|
|
item = data["data"]
|
|
return {
|
|
"success": True,
|
|
"data": {
|
|
"id": item.get("id"),
|
|
"type": policy_type,
|
|
"title": item.get("title"),
|
|
"content": item.get("content"),
|
|
"summary": item.get("summary"),
|
|
"locale": locale
|
|
}
|
|
}
|
|
|
|
except Exception as e:
|
|
return {
|
|
"success": False,
|
|
"error": str(e),
|
|
"data": None
|
|
}
|