diff --git a/src/dialogue/dialogue_manager.py b/src/dialogue/dialogue_manager.py index 40a0bce..2a57fd4 100644 --- a/src/dialogue/dialogue_manager.py +++ b/src/dialogue/dialogue_manager.py @@ -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 ) # 更新内存中的对话历史 diff --git a/src/web/app.py b/src/web/app.py index 9f20210..b40d384 100644 --- a/src/web/app.py +++ b/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/') -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/') -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/', 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 蓝图 diff --git a/src/web/blueprints/chat.py b/src/web/blueprints/chat.py new file mode 100644 index 0000000..3794809 --- /dev/null +++ b/src/web/blueprints/chat.py @@ -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/') +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/') +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/', 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 diff --git a/src/web/decorators.py b/src/web/decorators.py index 9e84ac0..b09eb81 100644 --- a/src/web/decorators.py +++ b/src/web/decorators.py @@ -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 diff --git a/src/web/service_manager.py b/src/web/service_manager.py index 50fb162..fd7f211 100644 --- a/src/web/service_manager.py +++ b/src/web/service_manager.py @@ -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): """清除指定服务实例""" diff --git a/src/web/static/js/dashboard.js b/src/web/static/js/dashboard.js index 8ec2791..b458243 100644 --- a/src/web/static/js/dashboard.js +++ b/src/web/static/js/dashboard.js @@ -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); } - } } // 初始化应用