refactor: 架构改进 前5个缺陷修复
1. Chat 路由从 app.py 拆到 chat_bp 蓝图(14个路由 0个残留在 app.py) 2. 新增 resolve_tenant_id 装饰器,写操作未指定 tenant_id 时记录警告日志 3. dialogue_manager.process_user_message 补齐 tenant_id 参数,知识库搜索和对话保存都传递 tenant_id 4. service_manager 新增直接 manager 访问器(knowledge_manager、dialogue_manager、conversation_history_manager、alert_system、token_monitor),新代码可绕过 TSPAssistant facade 5. TSPAssistant.get_assistant() 标记为 legacy,引导新代码使用具体 manager
This commit is contained in:
@@ -33,9 +33,10 @@ class DialogueManager:
|
||||
user_message: str,
|
||||
work_order_id: Optional[int] = None,
|
||||
user_id: Optional[str] = None,
|
||||
vehicle_id: Optional[str] = None
|
||||
vehicle_id: Optional[str] = None,
|
||||
tenant_id: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""处理用户消息"""
|
||||
"""处理用户消息(注意:飞书/WebSocket 对话走 realtime_chat.process_message,此方法仅供 HTTP API 调用)"""
|
||||
start_time = datetime.now()
|
||||
success = False
|
||||
error_message = None
|
||||
@@ -52,7 +53,7 @@ class DialogueManager:
|
||||
|
||||
# 搜索相关知识库(只搜索已验证的)
|
||||
knowledge_results = self.knowledge_manager.search_knowledge(
|
||||
user_message, top_k=3, verified_only=True
|
||||
user_message, top_k=3, verified_only=True, tenant_id=tenant_id
|
||||
)
|
||||
|
||||
# 获取车辆实时数据
|
||||
@@ -171,7 +172,8 @@ class DialogueManager:
|
||||
assistant_response=response_result["response"],
|
||||
confidence_score=self._calculate_confidence(knowledge_results),
|
||||
response_time=response_time,
|
||||
knowledge_used=[r["id"] for r in knowledge_results]
|
||||
knowledge_used=[r["id"] for r in knowledge_results],
|
||||
tenant_id=tenant_id
|
||||
)
|
||||
|
||||
# 更新内存中的对话历史
|
||||
|
||||
145
src/web/app.py
145
src/web/app.py
@@ -39,6 +39,7 @@ from src.web.blueprints.analytics import analytics_bp
|
||||
from src.web.blueprints.test import test_bp
|
||||
from src.web.blueprints.feishu_bot import feishu_bot_bp
|
||||
from src.web.blueprints.tenants import tenants_bp
|
||||
from src.web.blueprints.chat import chat_bp
|
||||
|
||||
|
||||
# 配置日志
|
||||
@@ -128,6 +129,7 @@ app.register_blueprint(analytics_bp)
|
||||
app.register_blueprint(test_bp)
|
||||
app.register_blueprint(feishu_bot_bp)
|
||||
app.register_blueprint(tenants_bp)
|
||||
app.register_blueprint(chat_bp)
|
||||
|
||||
|
||||
# 页面路由
|
||||
@@ -167,146 +169,9 @@ def uploaded_file(filename):
|
||||
# ============================================================================
|
||||
# 核心API路由
|
||||
# ============================================================================
|
||||
# 以下路由因功能特殊性保留在主应用中:
|
||||
# - Chat相关路由:使用RealtimeChatManager进行实时对话
|
||||
# - 健康检查、预警规则、监控状态等核心功能已迁移到 core 蓝图
|
||||
# - 分析数据相关功能已迁移到 analytics 蓝图
|
||||
|
||||
# ============================================================================
|
||||
# 实时对话相关路由
|
||||
# ============================================================================
|
||||
@app.route('/api/chat/session', methods=['POST'])
|
||||
def create_chat_session():
|
||||
"""创建对话会话"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
user_id = data.get('user_id', 'anonymous')
|
||||
work_order_id = data.get('work_order_id')
|
||||
tenant_id = data.get('tenant_id')
|
||||
|
||||
session_id = service_manager.get_chat_manager().create_session(user_id, work_order_id, tenant_id=tenant_id)
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"session_id": session_id,
|
||||
"message": "会话创建成功"
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route('/api/chat/message', methods=['POST'])
|
||||
def send_chat_message():
|
||||
"""发送聊天消息"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
session_id = data.get('session_id')
|
||||
message = data.get('message')
|
||||
|
||||
if not session_id or not message:
|
||||
return jsonify({"error": "缺少必要参数"}), 400
|
||||
|
||||
result = service_manager.get_chat_manager().process_message(session_id, message)
|
||||
return jsonify(result)
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/chat/message/stream', methods=['POST'])
|
||||
def send_chat_message_stream():
|
||||
"""流式聊天消息 — SSE 逐 token 推送"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
session_id = data.get('session_id')
|
||||
message = data.get('message')
|
||||
|
||||
if not session_id or not message:
|
||||
return jsonify({"error": "缺少必要参数"}), 400
|
||||
|
||||
chat_mgr = service_manager.get_chat_manager()
|
||||
|
||||
def generate():
|
||||
try:
|
||||
for event in chat_mgr.process_message_stream(session_id, message):
|
||||
yield event
|
||||
except Exception as e:
|
||||
import json as _json
|
||||
yield f"data: {_json.dumps({'error': str(e)}, ensure_ascii=False)}\n\n"
|
||||
|
||||
return Response(generate(), mimetype='text/event-stream',
|
||||
headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'})
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route('/api/chat/history/<session_id>')
|
||||
def get_chat_history(session_id):
|
||||
"""获取对话历史"""
|
||||
try:
|
||||
history = service_manager.get_chat_manager().get_session_history(session_id)
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"history": history
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route('/api/chat/work-order', methods=['POST'])
|
||||
def create_work_order():
|
||||
"""创建工单"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
session_id = data.get('session_id')
|
||||
title = data.get('title')
|
||||
description = data.get('description')
|
||||
category = data.get('category', '技术问题')
|
||||
priority = data.get('priority', 'medium')
|
||||
|
||||
if not session_id or not title or not description:
|
||||
return jsonify({"error": "缺少必要参数"}), 400
|
||||
|
||||
result = service_manager.get_chat_manager().create_work_order(session_id, title, description, category, priority)
|
||||
return jsonify(result)
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route('/api/chat/work-order/<int:work_order_id>')
|
||||
def get_work_order_status(work_order_id):
|
||||
"""获取工单状态"""
|
||||
try:
|
||||
result = service_manager.get_chat_manager().get_work_order_status(work_order_id)
|
||||
return jsonify(result)
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route('/api/chat/session/<session_id>', methods=['DELETE'])
|
||||
def end_chat_session(session_id):
|
||||
"""结束对话会话"""
|
||||
try:
|
||||
success = service_manager.get_chat_manager().end_session(session_id)
|
||||
return jsonify({
|
||||
"success": success,
|
||||
"message": "会话已结束" if success else "结束会话失败"
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route('/api/chat/sessions')
|
||||
def get_active_sessions():
|
||||
"""获取活跃会话列表"""
|
||||
try:
|
||||
# 确保chat_manager已初始化
|
||||
manager = service_manager.get_chat_manager()
|
||||
sessions = manager.get_active_sessions()
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"sessions": sessions
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"获取活跃会话失败: {e}")
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
# Agent相关路由已移动到 agent_bp 蓝图
|
||||
|
||||
# 分析相关路由已移动到 analytics_bp 蓝图
|
||||
# Chat 路由已迁移到 chat_bp 蓝图
|
||||
# Agent 路由已迁移到 agent_bp 蓝图
|
||||
# 分析路由已迁移到 analytics_bp 蓝图
|
||||
|
||||
# 车辆数据相关路由已移动到 vehicle_bp 蓝图
|
||||
|
||||
|
||||
126
src/web/blueprints/chat.py
Normal file
126
src/web/blueprints/chat.py
Normal file
@@ -0,0 +1,126 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
实时对话蓝图
|
||||
处理 WebSocket/HTTP 对话相关的 API 路由
|
||||
"""
|
||||
import logging
|
||||
from flask import Blueprint, request, jsonify, Response
|
||||
from src.web.service_manager import service_manager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
chat_bp = Blueprint('chat', __name__, url_prefix='/api/chat')
|
||||
|
||||
|
||||
@chat_bp.route('/session', methods=['POST'])
|
||||
def create_session():
|
||||
"""创建对话会话"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
user_id = data.get('user_id', 'anonymous')
|
||||
work_order_id = data.get('work_order_id')
|
||||
tenant_id = data.get('tenant_id')
|
||||
session_id = service_manager.get_chat_manager().create_session(user_id, work_order_id, tenant_id=tenant_id)
|
||||
return jsonify({"success": True, "session_id": session_id, "message": "会话创建成功"})
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
@chat_bp.route('/message', methods=['POST'])
|
||||
def send_message():
|
||||
"""发送聊天消息"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
session_id = data.get('session_id')
|
||||
message = data.get('message')
|
||||
if not session_id or not message:
|
||||
return jsonify({"error": "缺少必要参数"}), 400
|
||||
result = service_manager.get_chat_manager().process_message(session_id, message)
|
||||
return jsonify(result)
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
@chat_bp.route('/message/stream', methods=['POST'])
|
||||
def send_message_stream():
|
||||
"""流式聊天消息 — SSE 逐 token 推送"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
session_id = data.get('session_id')
|
||||
message = data.get('message')
|
||||
if not session_id or not message:
|
||||
return jsonify({"error": "缺少必要参数"}), 400
|
||||
chat_mgr = service_manager.get_chat_manager()
|
||||
|
||||
def generate():
|
||||
try:
|
||||
for event in chat_mgr.process_message_stream(session_id, message):
|
||||
yield event
|
||||
except Exception as e:
|
||||
import json
|
||||
yield f"data: {json.dumps({'error': str(e)}, ensure_ascii=False)}\n\n"
|
||||
|
||||
return Response(generate(), mimetype='text/event-stream',
|
||||
headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'})
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
@chat_bp.route('/history/<session_id>')
|
||||
def get_history(session_id):
|
||||
"""获取对话历史"""
|
||||
try:
|
||||
history = service_manager.get_chat_manager().get_session_history(session_id)
|
||||
return jsonify({"success": True, "history": history})
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
@chat_bp.route('/work-order', methods=['POST'])
|
||||
def create_work_order():
|
||||
"""从对话中创建工单"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
session_id = data.get('session_id')
|
||||
title = data.get('title')
|
||||
description = data.get('description')
|
||||
category = data.get('category', '技术问题')
|
||||
priority = data.get('priority', 'medium')
|
||||
if not session_id or not title or not description:
|
||||
return jsonify({"error": "缺少必要参数"}), 400
|
||||
result = service_manager.get_chat_manager().create_work_order(session_id, title, description, category, priority)
|
||||
return jsonify(result)
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
@chat_bp.route('/work-order/<int:work_order_id>')
|
||||
def get_work_order_status(work_order_id):
|
||||
"""获取工单状态"""
|
||||
try:
|
||||
result = service_manager.get_chat_manager().get_work_order_status(work_order_id)
|
||||
return jsonify(result)
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
@chat_bp.route('/session/<session_id>', methods=['DELETE'])
|
||||
def end_session(session_id):
|
||||
"""结束对话会话"""
|
||||
try:
|
||||
success = service_manager.get_chat_manager().end_session(session_id)
|
||||
return jsonify({"success": success, "message": "会话已结束" if success else "结束会话失败"})
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
@chat_bp.route('/sessions')
|
||||
def get_active_sessions():
|
||||
"""获取活跃会话列表"""
|
||||
try:
|
||||
manager = service_manager.get_chat_manager()
|
||||
sessions = manager.get_active_sessions()
|
||||
return jsonify({"success": True, "sessions": sessions})
|
||||
except Exception as e:
|
||||
logger.error(f"获取活跃会话失败: {e}")
|
||||
return jsonify({"error": str(e)}), 500
|
||||
@@ -77,3 +77,51 @@ def cache_response(timeout=300):
|
||||
return response
|
||||
return decorated_function
|
||||
return decorator
|
||||
|
||||
|
||||
def resolve_tenant_id(source='auto'):
|
||||
"""
|
||||
租户 ID 解析装饰器。
|
||||
从请求中提取 tenant_id 并注入到 kwargs['tenant_id']。
|
||||
|
||||
source:
|
||||
'auto' — 依次从 JSON body、query args、session 中查找
|
||||
'query' — 仅从 query args
|
||||
'body' — 仅从 JSON body
|
||||
|
||||
如果未找到,使用 DEFAULT_TENANT 并记录警告。
|
||||
"""
|
||||
import logging
|
||||
_logger = logging.getLogger('tenant_resolver')
|
||||
|
||||
def decorator(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
from flask import request as req
|
||||
from src.core.models import DEFAULT_TENANT
|
||||
|
||||
tenant_id = None
|
||||
|
||||
if source in ('auto', 'body'):
|
||||
try:
|
||||
data = req.get_json(silent=True)
|
||||
if data:
|
||||
tenant_id = data.get('tenant_id')
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not tenant_id and source in ('auto', 'query'):
|
||||
tenant_id = req.args.get('tenant_id')
|
||||
|
||||
if not tenant_id:
|
||||
tenant_id = DEFAULT_TENANT
|
||||
# 只在写操作时警告,读操作不警告(全局查询是合理的)
|
||||
if req.method in ('POST', 'PUT', 'DELETE'):
|
||||
_logger.warning(
|
||||
f"⚠️ API {req.method} {req.path} 未指定 tenant_id,使用默认租户 '{DEFAULT_TENANT}'"
|
||||
)
|
||||
|
||||
kwargs['tenant_id'] = tenant_id
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
return decorator
|
||||
|
||||
@@ -28,7 +28,7 @@ class ServiceManager:
|
||||
return self._services[service_name]
|
||||
|
||||
def get_assistant(self):
|
||||
"""获取TSP助手实例"""
|
||||
"""获取TSP助手实例(legacy facade,新代码应直接使用具体 manager)"""
|
||||
def factory():
|
||||
from src.main import TSPAssistant
|
||||
return TSPAssistant()
|
||||
@@ -54,6 +54,41 @@ class ServiceManager:
|
||||
from src.vehicle.vehicle_data_manager import VehicleDataManager
|
||||
return VehicleDataManager()
|
||||
return self.get_service('vehicle_manager', factory)
|
||||
|
||||
def get_knowledge_manager(self):
|
||||
"""获取知识库管理器(直接访问,不经过 TSPAssistant)"""
|
||||
def factory():
|
||||
from src.knowledge_base.knowledge_manager import KnowledgeManager
|
||||
return KnowledgeManager()
|
||||
return self.get_service('knowledge_manager', factory)
|
||||
|
||||
def get_dialogue_manager(self):
|
||||
"""获取对话管理器"""
|
||||
def factory():
|
||||
from src.dialogue.dialogue_manager import DialogueManager
|
||||
return DialogueManager()
|
||||
return self.get_service('dialogue_manager', factory)
|
||||
|
||||
def get_conversation_history_manager(self):
|
||||
"""获取对话历史管理器"""
|
||||
def factory():
|
||||
from src.dialogue.conversation_history import ConversationHistoryManager
|
||||
return ConversationHistoryManager()
|
||||
return self.get_service('conversation_history_manager', factory)
|
||||
|
||||
def get_alert_system(self):
|
||||
"""获取预警系统"""
|
||||
def factory():
|
||||
from src.analytics.alert_system import AlertSystem
|
||||
return AlertSystem()
|
||||
return self.get_service('alert_system', factory)
|
||||
|
||||
def get_token_monitor(self):
|
||||
"""获取 Token 监控"""
|
||||
def factory():
|
||||
from src.analytics.token_monitor import TokenMonitor
|
||||
return TokenMonitor()
|
||||
return self.get_service('token_monitor', factory)
|
||||
|
||||
def clear_service(self, service_name: str):
|
||||
"""清除指定服务实例"""
|
||||
|
||||
@@ -275,8 +275,7 @@ class TSPDashboard {
|
||||
|
||||
async loadInitialData() { await Promise.all([this.loadHealth(), this.loadDashboardData(), this.loadSystemInfo()]); }
|
||||
|
||||
initSmartUpdate() {
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
initSmartUpdate() { document.addEventListener('visibilitychange', () => {
|
||||
this.isPageVisible = !document.hidden;
|
||||
if (this.isPageVisible) this.smartRefresh();
|
||||
});
|
||||
@@ -341,25 +340,6 @@ class TSPDashboard {
|
||||
} catch (e) { console.error('刷新预警统计失败:', e); }
|
||||
}
|
||||
|
||||
updateHealthDisplay(health) {
|
||||
if (!health) return;
|
||||
const score = health.health_score || health.score || 0;
|
||||
const el = document.getElementById('health-score');
|
||||
if (el) el.textContent = `${score}%`;
|
||||
const badge = document.getElementById('health-badge');
|
||||
if (badge) {
|
||||
badge.className = score >= 80 ? 'badge bg-success' : score >= 60 ? 'badge bg-warning' : 'badge bg-danger';
|
||||
badge.textContent = score >= 80 ? '系统正常' : score >= 60 ? '系统警告' : '系统错误';
|
||||
}
|
||||
}
|
||||
|
||||
async loadHealth() {
|
||||
try {
|
||||
const response = await fetch('/api/health');
|
||||
const health = await response.json();
|
||||
this.updateHealthDisplay(health);
|
||||
} catch (e) { console.error('加载健康状态失败:', e); }
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化应用
|
||||
|
||||
Reference in New Issue
Block a user