feat: 增强 Agent 系统和完善项目结构

主要改进:
- Agent 增强: 订单查询、售后支持、客服路由等功能优化
- 新增语言检测和 Token 管理模块
- 改进 Chatwoot webhook 处理和用户标识
- MCP 服务器增强: 订单 MCP 和 Strapi MCP 功能扩展
- 新增商城客户端、知识库、缓存和同步模块
- 添加多语言提示词系统 (YAML)
- 完善项目结构: 整理文档、脚本和测试文件
- 新增调试和测试工具脚本

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
wangliang
2026-01-16 16:28:47 +08:00
parent 0e59f3067e
commit e093995368
48 changed files with 5263 additions and 395 deletions

View File

@@ -0,0 +1,418 @@
"""
Local Knowledge Base using SQLite
Stores FAQ, company info, and policies locally for fast access.
Syncs with Strapi CMS periodically.
"""
import sqlite3
import json
import asyncio
from datetime import datetime
from typing import List, Dict, Any, Optional
from pathlib import Path
import httpx
from pydantic_settings import BaseSettings
from pydantic import ConfigDict
class KnowledgeBaseSettings(BaseSettings):
"""Knowledge base configuration"""
strapi_api_url: str
strapi_api_token: str = ""
db_path: str = "/data/faq.db"
sync_interval: int = 3600 # Sync every hour
sync_on_startup: bool = True # Run initial sync on startup
sync_interval_minutes: int = 60 # Sync interval in minutes
model_config = ConfigDict(env_file=".env")
settings = KnowledgeBaseSettings()
class LocalKnowledgeBase:
"""Local SQLite knowledge base"""
def __init__(self, db_path: Optional[str] = None):
self.db_path = db_path or settings.db_path
self._conn: Optional[sqlite3.Connection] = None
def _get_conn(self) -> sqlite3.Connection:
"""Get database connection"""
if self._conn is None:
# Ensure directory exists
Path(self.db_path).parent.mkdir(parents=True, exist_ok=True)
self._conn = sqlite3.connect(self.db_path, check_same_thread=False)
self._conn.row_factory = sqlite3.Row
self._init_db()
return self._conn
def _init_db(self):
"""Initialize database schema"""
conn = self._get_conn()
# Create tables
conn.executescript("""
-- FAQ table
CREATE TABLE IF NOT EXISTS faq (
id INTEGER PRIMARY KEY AUTOINCREMENT,
strapi_id TEXT,
category TEXT NOT NULL,
locale TEXT NOT NULL,
question TEXT,
answer TEXT,
description TEXT,
extra_data TEXT,
synced_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(category, locale, strapi_id)
);
-- Create indexes for FAQ
CREATE INDEX IF NOT EXISTS idx_faq_category ON faq(category);
CREATE INDEX IF NOT EXISTS idx_faq_locale ON faq(locale);
CREATE INDEX IF NOT EXISTS idx_faq_search ON faq(question, answer);
-- Full-text search
CREATE VIRTUAL TABLE IF NOT EXISTS fts_faq USING fts5(
question, answer, category, locale, content='faq'
);
-- Trigger to update FTS
CREATE TRIGGER IF NOT EXISTS fts_faq_insert AFTER INSERT ON faq BEGIN
INSERT INTO fts_faq(rowid, question, answer, category, locale)
VALUES (new.rowid, new.question, new.answer, new.category, new.locale);
END;
CREATE TRIGGER IF NOT EXISTS fts_faq_delete AFTER DELETE ON faq BEGIN
INSERT INTO fts_faq(fts_faq, rowid, question, answer, category, locale)
VALUES ('delete', old.rowid, old.question, old.answer, old.category, old.locale);
END;
CREATE TRIGGER IF NOT EXISTS fts_faq_update AFTER UPDATE ON faq BEGIN
INSERT INTO fts_faq(fts_faq, rowid, question, answer, category, locale)
VALUES ('delete', old.rowid, old.question, old.answer, old.category, old.locale);
INSERT INTO fts_faq(rowid, question, answer, category, locale)
VALUES (new.rowid, new.question, new.answer, new.category, new.locale);
END;
-- Company info table
CREATE TABLE IF NOT EXISTS company_info (
id INTEGER PRIMARY KEY AUTOINCREMENT,
section TEXT NOT NULL UNIQUE,
locale TEXT NOT NULL,
title TEXT,
description TEXT,
content TEXT,
extra_data TEXT,
synced_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(section, locale)
);
CREATE INDEX IF NOT EXISTS idx_company_section ON company_info(section);
CREATE INDEX IF NOT EXISTS idx_company_locale ON company_info(locale);
-- Policy table
CREATE TABLE IF NOT EXISTS policy (
id INTEGER PRIMARY KEY AUTOINCREMENT,
type TEXT NOT NULL,
locale TEXT NOT NULL,
title TEXT,
summary TEXT,
content TEXT,
version TEXT,
effective_date TEXT,
synced_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(type, locale)
);
CREATE INDEX IF NOT EXISTS idx_policy_type ON policy(type);
CREATE INDEX IF NOT EXISTS idx_policy_locale ON policy(locale);
-- Sync status table
CREATE TABLE IF NOT EXISTS sync_status (
id INTEGER PRIMARY KEY AUTOINCREMENT,
data_type TEXT NOT NULL,
last_sync_at TIMESTAMP,
status TEXT,
error_message TEXT,
items_count INTEGER
);
""")
# ============ FAQ Operations ============
def query_faq(
self,
category: str,
locale: str,
limit: int = 10
) -> Dict[str, Any]:
"""Query FAQ from local database"""
conn = self._get_conn()
# Query FAQ
cursor = conn.execute(
"""SELECT id, strapi_id, category, locale, question, answer, description, extra_data
FROM faq
WHERE category = ? AND locale = ?
LIMIT ?""",
(category, locale, limit)
)
results = []
for row in cursor.fetchall():
item = {
"id": row["strapi_id"],
"category": row["category"],
"locale": row["locale"],
"question": row["question"],
"answer": row["answer"]
}
if row["description"]:
item["description"] = row["description"]
if row["extra_data"]:
item.update(json.loads(row["extra_data"]))
results.append(item)
return {
"success": True,
"count": len(results),
"category": category,
"locale": locale,
"results": results,
"_source": "local_cache"
}
def search_faq(
self,
query: str,
locale: str = "en",
limit: int = 10
) -> Dict[str, Any]:
"""Full-text search FAQ"""
conn = self._get_conn()
# Use FTS for search
cursor = conn.execute(
"""SELECT fts_faq.question, fts_faq.answer, faq.category, faq.locale
FROM fts_faq
JOIN faq ON fts_faq.rowid = faq.id
WHERE fts_faq MATCH ? AND faq.locale = ?
LIMIT ?""",
(query, locale, limit)
)
results = []
for row in cursor.fetchall():
results.append({
"question": row["question"],
"answer": row["answer"],
"category": row["category"],
"locale": row["locale"]
})
return {
"success": True,
"count": len(results),
"query": query,
"locale": locale,
"results": results,
"_source": "local_cache"
}
def save_faq_batch(self, faq_list: List[Dict[str, Any]], category: str, locale: str):
"""Save batch of FAQ to database"""
conn = self._get_conn()
count = 0
for item in faq_list:
try:
# Extract fields
question = item.get("question") or item.get("title") or item.get("content", "")
answer = item.get("answer") or item.get("content") or ""
description = item.get("description") or ""
strapi_id = item.get("id", "")
# Extra data as JSON
extra_data = json.dumps({
k: v for k, v in item.items()
if k not in ["id", "question", "answer", "title", "content", "description"]
}, ensure_ascii=False)
# Insert or replace
conn.execute(
"""INSERT OR REPLACE INTO faq
(strapi_id, category, locale, question, answer, description, extra_data, synced_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
(strapi_id, category, locale, question, answer, description, extra_data, datetime.now().isoformat())
)
count += 1
except Exception as e:
print(f"Error saving FAQ: {e}")
conn.commit()
return count
# ============ Company Info Operations ============
def get_company_info(self, section: str, locale: str = "en") -> Dict[str, Any]:
"""Get company info from local database"""
conn = self._get_conn()
cursor = conn.execute(
"""SELECT section, locale, title, description, content, extra_data
FROM company_info
WHERE section = ? AND locale = ?""",
(section, locale)
)
row = cursor.fetchone()
if not row:
return {
"success": False,
"error": f"Section '{section}' not found",
"data": None,
"_source": "local_cache"
}
result_data = {
"section": row["section"],
"locale": row["locale"],
"title": row["title"],
"description": row["description"],
"content": row["content"]
}
if row["extra_data"]:
result_data.update(json.loads(row["extra_data"]))
return {
"success": True,
"data": result_data,
"_source": "local_cache"
}
def save_company_info(self, section: str, locale: str, data: Dict[str, Any]):
"""Save company info to database"""
conn = self._get_conn()
title = data.get("title") or data.get("section_title") or ""
description = data.get("description") or ""
content = data.get("content") or ""
extra_data = json.dumps({
k: v for k, v in data.items()
if k not in ["section", "title", "description", "content"]
}, ensure_ascii=False)
conn.execute(
"""INSERT OR REPLACE INTO company_info
(section, locale, title, description, content, extra_data, synced_at)
VALUES (?, ?, ?, ?, ?, ?, ?)""",
(section, locale, title, description, content, extra_data, datetime.now().isoformat())
)
conn.commit()
# ============ Policy Operations ============
def get_policy(self, policy_type: str, locale: str = "en") -> Dict[str, Any]:
"""Get policy from local database"""
conn = self._get_conn()
cursor = conn.execute(
"""SELECT type, locale, title, summary, content, version, effective_date
FROM policy
WHERE type = ? AND locale = ?""",
(policy_type, locale)
)
row = cursor.fetchone()
if not row:
return {
"success": False,
"error": f"Policy '{policy_type}' not found",
"data": None,
"_source": "local_cache"
}
return {
"success": True,
"data": {
"type": row["type"],
"locale": row["locale"],
"title": row["title"],
"summary": row["summary"],
"content": row["content"],
"version": row["version"],
"effective_date": row["effective_date"]
},
"_source": "local_cache"
}
def save_policy(self, policy_type: str, locale: str, data: Dict[str, Any]):
"""Save policy to database"""
conn = self._get_conn()
title = data.get("title") or ""
summary = data.get("summary") or ""
content = data.get("content") or ""
version = data.get("version") or ""
effective_date = data.get("effective_date") or ""
conn.execute(
"""INSERT OR REPLACE INTO policy
(type, locale, title, summary, content, version, effective_date, synced_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
(policy_type, locale, title, summary, content, version, effective_date, datetime.now().isoformat())
)
conn.commit()
# ============ Sync Status ============
def update_sync_status(self, data_type: str, status: str, items_count: int = 0, error: Optional[str] = None):
"""Update sync status"""
conn = self._get_conn()
conn.execute(
"""INSERT INTO sync_status (data_type, last_sync_at, status, items_count, error_message)
VALUES (?, ?, ?, ?, ?)""",
(data_type, datetime.now().isoformat(), status, items_count, error)
)
conn.commit()
def get_sync_status(self, data_type: Optional[str] = None) -> List[Dict[str, Any]]:
"""Get sync status"""
conn = self._get_conn()
if data_type:
cursor = conn.execute(
"""SELECT * FROM sync_status WHERE data_type = ? ORDER BY last_sync_at DESC LIMIT 1""",
(data_type,)
)
else:
cursor = conn.execute(
"""SELECT * FROM sync_status ORDER BY last_sync_at DESC LIMIT 10"""
)
return [dict(row) for row in cursor.fetchall()]
def close(self):
"""Close database connection"""
if self._conn:
self._conn.close()
self._conn = None
# Global knowledge base instance
kb = LocalKnowledgeBase()
def get_kb() -> LocalKnowledgeBase:
"""Get global knowledge base instance"""
return kb