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:
2026-04-02 22:09:59 +08:00
parent 53f3629f9e
commit 61ef86d779
6 changed files with 222 additions and 166 deletions

View File

@@ -33,9 +33,10 @@ class DialogueManager:
user_message: str, user_message: str,
work_order_id: Optional[int] = None, work_order_id: Optional[int] = None,
user_id: Optional[str] = None, user_id: Optional[str] = None,
vehicle_id: Optional[str] = None vehicle_id: Optional[str] = None,
tenant_id: Optional[str] = None
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""处理用户消息""" """处理用户消息(注意:飞书/WebSocket 对话走 realtime_chat.process_message此方法仅供 HTTP API 调用)"""
start_time = datetime.now() start_time = datetime.now()
success = False success = False
error_message = None error_message = None
@@ -52,7 +53,7 @@ class DialogueManager:
# 搜索相关知识库(只搜索已验证的) # 搜索相关知识库(只搜索已验证的)
knowledge_results = self.knowledge_manager.search_knowledge( 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"], assistant_response=response_result["response"],
confidence_score=self._calculate_confidence(knowledge_results), confidence_score=self._calculate_confidence(knowledge_results),
response_time=response_time, 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
) )
# 更新内存中的对话历史 # 更新内存中的对话历史

View File

@@ -39,6 +39,7 @@ from src.web.blueprints.analytics import analytics_bp
from src.web.blueprints.test import test_bp from src.web.blueprints.test import test_bp
from src.web.blueprints.feishu_bot import feishu_bot_bp from src.web.blueprints.feishu_bot import feishu_bot_bp
from src.web.blueprints.tenants import tenants_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(test_bp)
app.register_blueprint(feishu_bot_bp) app.register_blueprint(feishu_bot_bp)
app.register_blueprint(tenants_bp) app.register_blueprint(tenants_bp)
app.register_blueprint(chat_bp)
# 页面路由 # 页面路由
@@ -167,146 +169,9 @@ def uploaded_file(filename):
# ============================================================================ # ============================================================================
# 核心API路由 # 核心API路由
# ============================================================================ # ============================================================================
# 以下路由因功能特殊性保留在主应用中: # Chat 路由已迁移到 chat_bp 蓝图
# - Chat相关路由使用RealtimeChatManager进行实时对话 # Agent 路由已迁移到 agent_bp 蓝图
# - 健康检查、预警规则、监控状态等核心功能已迁移到 core 蓝图 # 分析路由已迁移到 analytics_bp 蓝图
# - 分析数据相关功能已迁移到 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 蓝图
# 车辆数据相关路由已移动到 vehicle_bp 蓝图 # 车辆数据相关路由已移动到 vehicle_bp 蓝图

126
src/web/blueprints/chat.py Normal file
View 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

View File

@@ -77,3 +77,51 @@ def cache_response(timeout=300):
return response return response
return decorated_function return decorated_function
return decorator 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

View File

@@ -28,7 +28,7 @@ class ServiceManager:
return self._services[service_name] return self._services[service_name]
def get_assistant(self): def get_assistant(self):
"""获取TSP助手实例""" """获取TSP助手实例legacy facade新代码应直接使用具体 manager"""
def factory(): def factory():
from src.main import TSPAssistant from src.main import TSPAssistant
return TSPAssistant() return TSPAssistant()
@@ -54,6 +54,41 @@ class ServiceManager:
from src.vehicle.vehicle_data_manager import VehicleDataManager from src.vehicle.vehicle_data_manager import VehicleDataManager
return VehicleDataManager() return VehicleDataManager()
return self.get_service('vehicle_manager', factory) 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): def clear_service(self, service_name: str):
"""清除指定服务实例""" """清除指定服务实例"""

View File

@@ -275,8 +275,7 @@ class TSPDashboard {
async loadInitialData() { await Promise.all([this.loadHealth(), this.loadDashboardData(), this.loadSystemInfo()]); } async loadInitialData() { await Promise.all([this.loadHealth(), this.loadDashboardData(), this.loadSystemInfo()]); }
initSmartUpdate() { initSmartUpdate() { document.addEventListener('visibilitychange', () => {
document.addEventListener('visibilitychange', () => {
this.isPageVisible = !document.hidden; this.isPageVisible = !document.hidden;
if (this.isPageVisible) this.smartRefresh(); if (this.isPageVisible) this.smartRefresh();
}); });
@@ -341,25 +340,6 @@ class TSPDashboard {
} catch (e) { console.error('刷新预警统计失败:', e); } } 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); }
}
} }
// 初始化应用 // 初始化应用