first commit
This commit is contained in:
BIN
src/web/__pycache__/app.cpython-311.pyc
Normal file
BIN
src/web/__pycache__/app.cpython-311.pyc
Normal file
Binary file not shown.
671
src/web/app.py
Normal file
671
src/web/app.py
Normal file
@@ -0,0 +1,671 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
TSP助手预警管理Web应用
|
||||
提供预警系统的Web界面和API接口
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
from flask import Flask, render_template, request, jsonify, redirect, url_for
|
||||
from flask_cors import CORS
|
||||
|
||||
# 添加项目根目录到Python路径
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from src.main import TSPAssistant
|
||||
from src.agent_assistant import TSPAgentAssistant
|
||||
from src.analytics.alert_system import AlertRule, AlertLevel, AlertType
|
||||
from src.dialogue.realtime_chat import RealtimeChatManager
|
||||
from src.vehicle.vehicle_data_manager import VehicleDataManager
|
||||
|
||||
app = Flask(__name__)
|
||||
CORS(app)
|
||||
|
||||
# 初始化TSP助手和Agent助手
|
||||
assistant = TSPAssistant()
|
||||
agent_assistant = TSPAgentAssistant()
|
||||
chat_manager = RealtimeChatManager()
|
||||
vehicle_manager = VehicleDataManager()
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
"""主页 - 综合管理平台"""
|
||||
return render_template('dashboard.html')
|
||||
|
||||
@app.route('/alerts')
|
||||
def alerts():
|
||||
"""预警管理页面"""
|
||||
return render_template('index.html')
|
||||
|
||||
@app.route('/api/health')
|
||||
def get_health():
|
||||
"""获取系统健康状态"""
|
||||
try:
|
||||
health = assistant.get_system_health()
|
||||
return jsonify(health)
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route('/api/alerts')
|
||||
def get_alerts():
|
||||
"""获取预警列表"""
|
||||
try:
|
||||
alerts = assistant.get_active_alerts()
|
||||
return jsonify(alerts)
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route('/api/alerts/statistics')
|
||||
def get_alert_statistics():
|
||||
"""获取预警统计"""
|
||||
try:
|
||||
stats = assistant.get_alert_statistics()
|
||||
return jsonify(stats)
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route('/api/alerts/<int:alert_id>/resolve', methods=['POST'])
|
||||
def resolve_alert(alert_id):
|
||||
"""解决预警"""
|
||||
try:
|
||||
success = assistant.resolve_alert(alert_id)
|
||||
if success:
|
||||
return jsonify({"success": True, "message": "预警已解决"})
|
||||
else:
|
||||
return jsonify({"success": False, "message": "解决预警失败"}), 400
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route('/api/rules')
|
||||
def get_rules():
|
||||
"""获取预警规则列表"""
|
||||
try:
|
||||
rules = assistant.alert_system.rules
|
||||
rules_data = []
|
||||
for name, rule in rules.items():
|
||||
rules_data.append({
|
||||
"name": rule.name,
|
||||
"description": rule.description,
|
||||
"alert_type": rule.alert_type.value,
|
||||
"level": rule.level.value,
|
||||
"threshold": rule.threshold,
|
||||
"condition": rule.condition,
|
||||
"enabled": rule.enabled,
|
||||
"check_interval": rule.check_interval,
|
||||
"cooldown": rule.cooldown
|
||||
})
|
||||
return jsonify(rules_data)
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route('/api/rules', methods=['POST'])
|
||||
def create_rule():
|
||||
"""创建预警规则"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
rule = AlertRule(
|
||||
name=data['name'],
|
||||
description=data['description'],
|
||||
alert_type=AlertType(data['alert_type']),
|
||||
level=AlertLevel(data['level']),
|
||||
threshold=float(data['threshold']),
|
||||
condition=data['condition'],
|
||||
enabled=data.get('enabled', True),
|
||||
check_interval=int(data.get('check_interval', 300)),
|
||||
cooldown=int(data.get('cooldown', 3600))
|
||||
)
|
||||
|
||||
success = assistant.alert_system.add_custom_rule(rule)
|
||||
if success:
|
||||
return jsonify({"success": True, "message": "规则创建成功"})
|
||||
else:
|
||||
return jsonify({"success": False, "message": "规则创建失败"}), 400
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route('/api/rules/<rule_name>', methods=['PUT'])
|
||||
def update_rule(rule_name):
|
||||
"""更新预警规则"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
success = assistant.alert_system.update_rule(rule_name, **data)
|
||||
if success:
|
||||
return jsonify({"success": True, "message": "规则更新成功"})
|
||||
else:
|
||||
return jsonify({"success": False, "message": "规则更新失败"}), 400
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route('/api/rules/<rule_name>', methods=['DELETE'])
|
||||
def delete_rule(rule_name):
|
||||
"""删除预警规则"""
|
||||
try:
|
||||
success = assistant.alert_system.delete_rule(rule_name)
|
||||
if success:
|
||||
return jsonify({"success": True, "message": "规则删除成功"})
|
||||
else:
|
||||
return jsonify({"success": False, "message": "规则删除失败"}), 400
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route('/api/monitor/start', methods=['POST'])
|
||||
def start_monitoring():
|
||||
"""启动监控服务"""
|
||||
try:
|
||||
success = assistant.start_monitoring()
|
||||
if success:
|
||||
return jsonify({"success": True, "message": "监控服务已启动"})
|
||||
else:
|
||||
return jsonify({"success": False, "message": "启动监控服务失败"}), 400
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route('/api/monitor/stop', methods=['POST'])
|
||||
def stop_monitoring():
|
||||
"""停止监控服务"""
|
||||
try:
|
||||
success = assistant.stop_monitoring()
|
||||
if success:
|
||||
return jsonify({"success": True, "message": "监控服务已停止"})
|
||||
else:
|
||||
return jsonify({"success": False, "message": "停止监控服务失败"}), 400
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route('/api/monitor/status')
|
||||
def get_monitor_status():
|
||||
"""获取监控服务状态"""
|
||||
try:
|
||||
health = assistant.get_system_health()
|
||||
return jsonify({
|
||||
"monitor_status": health.get("monitor_status", "unknown"),
|
||||
"health_score": health.get("health_score", 0),
|
||||
"active_alerts": health.get("active_alerts", 0)
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route('/api/check-alerts', methods=['POST'])
|
||||
def check_alerts():
|
||||
"""手动检查预警"""
|
||||
try:
|
||||
alerts = assistant.check_alerts()
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"alerts": alerts,
|
||||
"count": len(alerts)
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
# 实时对话相关路由
|
||||
@app.route('/chat')
|
||||
def chat():
|
||||
"""实时对话页面 (WebSocket版本)"""
|
||||
return render_template('chat.html')
|
||||
|
||||
@app.route('/chat-http')
|
||||
def chat_http():
|
||||
"""实时对话页面 (HTTP版本)"""
|
||||
return render_template('chat_http.html')
|
||||
|
||||
@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')
|
||||
|
||||
session_id = chat_manager.create_session(user_id, work_order_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 = chat_manager.process_message(session_id, message)
|
||||
return jsonify(result)
|
||||
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 = 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 = 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 = 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 = 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:
|
||||
sessions = chat_manager.get_active_sessions()
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"sessions": sessions
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
# Agent相关API
|
||||
@app.route('/api/agent/status')
|
||||
def get_agent_status():
|
||||
"""获取Agent状态"""
|
||||
try:
|
||||
status = agent_assistant.get_agent_status()
|
||||
return jsonify(status)
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route('/api/agent/toggle', methods=['POST'])
|
||||
def toggle_agent_mode():
|
||||
"""切换Agent模式"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
enabled = data.get('enabled', True)
|
||||
success = agent_assistant.toggle_agent_mode(enabled)
|
||||
return jsonify({
|
||||
"success": success,
|
||||
"message": f"Agent模式已{'启用' if enabled else '禁用'}"
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route('/api/agent/monitoring/start', methods=['POST'])
|
||||
def start_agent_monitoring():
|
||||
"""启动Agent监控"""
|
||||
try:
|
||||
success = agent_assistant.start_proactive_monitoring()
|
||||
return jsonify({
|
||||
"success": success,
|
||||
"message": "Agent监控已启动" if success else "启动失败"
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route('/api/agent/monitoring/stop', methods=['POST'])
|
||||
def stop_agent_monitoring():
|
||||
"""停止Agent监控"""
|
||||
try:
|
||||
success = agent_assistant.stop_proactive_monitoring()
|
||||
return jsonify({
|
||||
"success": success,
|
||||
"message": "Agent监控已停止" if success else "停止失败"
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route('/api/agent/proactive-monitoring', methods=['POST'])
|
||||
def proactive_monitoring():
|
||||
"""主动监控检查"""
|
||||
try:
|
||||
result = agent_assistant.run_proactive_monitoring()
|
||||
return jsonify(result)
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route('/api/agent/intelligent-analysis', methods=['POST'])
|
||||
def intelligent_analysis():
|
||||
"""智能分析"""
|
||||
try:
|
||||
analysis = agent_assistant.run_intelligent_analysis()
|
||||
return jsonify({"success": True, "analysis": analysis})
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
# 知识库相关API
|
||||
@app.route('/api/knowledge')
|
||||
def get_knowledge():
|
||||
"""获取知识库列表"""
|
||||
try:
|
||||
# 获取分页参数
|
||||
page = request.args.get('page', 1, type=int)
|
||||
per_page = request.args.get('per_page', 10, type=int)
|
||||
|
||||
# 从数据库获取知识库数据
|
||||
knowledge_entries = assistant.knowledge_manager.get_knowledge_entries(
|
||||
page=page, per_page=per_page
|
||||
)
|
||||
|
||||
return jsonify(knowledge_entries)
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route('/api/knowledge/search')
|
||||
def search_knowledge():
|
||||
"""搜索知识库"""
|
||||
try:
|
||||
query = request.args.get('q', '')
|
||||
# 这里应该调用知识库管理器的搜索方法
|
||||
results = assistant.search_knowledge(query, top_k=5)
|
||||
return jsonify(results.get('results', []))
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route('/api/knowledge', methods=['POST'])
|
||||
def add_knowledge():
|
||||
"""添加知识库条目"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
success = assistant.knowledge_manager.add_knowledge_entry(
|
||||
question=data['question'],
|
||||
answer=data['answer'],
|
||||
category=data['category'],
|
||||
confidence_score=data['confidence_score']
|
||||
)
|
||||
return jsonify({"success": success, "message": "知识添加成功" if success else "添加失败"})
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route('/api/knowledge/stats')
|
||||
def get_knowledge_stats():
|
||||
"""获取知识库统计"""
|
||||
try:
|
||||
stats = assistant.knowledge_manager.get_knowledge_stats()
|
||||
return jsonify(stats)
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route('/api/knowledge/upload', methods=['POST'])
|
||||
def upload_knowledge_file():
|
||||
"""上传文件并生成知识库"""
|
||||
try:
|
||||
if 'file' not in request.files:
|
||||
return jsonify({"error": "没有上传文件"}), 400
|
||||
|
||||
file = request.files['file']
|
||||
if file.filename == '':
|
||||
return jsonify({"error": "没有选择文件"}), 400
|
||||
|
||||
# 保存文件到临时目录
|
||||
import tempfile
|
||||
import os
|
||||
import uuid
|
||||
|
||||
# 创建唯一的临时文件名
|
||||
temp_filename = f"upload_{uuid.uuid4()}{os.path.splitext(file.filename)[1]}"
|
||||
temp_path = os.path.join(tempfile.gettempdir(), temp_filename)
|
||||
|
||||
try:
|
||||
# 保存文件
|
||||
file.save(temp_path)
|
||||
|
||||
# 使用Agent助手处理文件
|
||||
result = agent_assistant.process_file_to_knowledge(temp_path, file.filename)
|
||||
|
||||
return jsonify(result)
|
||||
|
||||
finally:
|
||||
# 确保删除临时文件
|
||||
try:
|
||||
if os.path.exists(temp_path):
|
||||
os.unlink(temp_path)
|
||||
except Exception as cleanup_error:
|
||||
logger.warning(f"清理临时文件失败: {cleanup_error}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"文件上传处理失败: {e}")
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route('/api/knowledge/delete/<int:knowledge_id>', methods=['DELETE'])
|
||||
def delete_knowledge(knowledge_id):
|
||||
"""删除知识库条目"""
|
||||
try:
|
||||
success = assistant.knowledge_manager.delete_knowledge_entry(knowledge_id)
|
||||
return jsonify({"success": success, "message": "删除成功" if success else "删除失败"})
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route('/api/knowledge/verify/<int:knowledge_id>', methods=['POST'])
|
||||
def verify_knowledge(knowledge_id):
|
||||
"""验证知识库条目"""
|
||||
try:
|
||||
data = request.get_json() or {}
|
||||
verified_by = data.get('verified_by', 'admin')
|
||||
success = assistant.knowledge_manager.verify_knowledge_entry(knowledge_id, verified_by)
|
||||
return jsonify({"success": success, "message": "验证成功" if success else "验证失败"})
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route('/api/knowledge/unverify/<int:knowledge_id>', methods=['POST'])
|
||||
def unverify_knowledge(knowledge_id):
|
||||
"""取消验证知识库条目"""
|
||||
try:
|
||||
success = assistant.knowledge_manager.unverify_knowledge_entry(knowledge_id)
|
||||
return jsonify({"success": success, "message": "取消验证成功" if success else "取消验证失败"})
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
# 工单相关API
|
||||
@app.route('/api/workorders')
|
||||
def get_workorders():
|
||||
"""获取工单列表"""
|
||||
try:
|
||||
status_filter = request.args.get('status')
|
||||
priority_filter = request.args.get('priority')
|
||||
|
||||
# 这里应该调用工单管理器的获取方法
|
||||
workorders = [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "车辆无法远程启动",
|
||||
"description": "用户反映APP中远程启动功能无法使用",
|
||||
"category": "远程控制",
|
||||
"priority": "high",
|
||||
"status": "open",
|
||||
"created_at": "2024-01-01T10:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"title": "APP显示异常",
|
||||
"description": "APP中车辆信息显示不正确",
|
||||
"category": "APP功能",
|
||||
"priority": "medium",
|
||||
"status": "in_progress",
|
||||
"created_at": "2024-01-01T11:00:00Z"
|
||||
}
|
||||
]
|
||||
|
||||
# 应用过滤
|
||||
if status_filter and status_filter != 'all':
|
||||
workorders = [w for w in workorders if w['status'] == status_filter]
|
||||
if priority_filter and priority_filter != 'all':
|
||||
workorders = [w for w in workorders if w['priority'] == priority_filter]
|
||||
|
||||
return jsonify(workorders)
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route('/api/workorders', methods=['POST'])
|
||||
def create_workorder():
|
||||
"""创建工单"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
result = assistant.create_work_order(
|
||||
title=data['title'],
|
||||
description=data['description'],
|
||||
category=data['category'],
|
||||
priority=data['priority']
|
||||
)
|
||||
return jsonify({"success": True, "workorder": result})
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
# 分析相关API
|
||||
@app.route('/api/analytics')
|
||||
def get_analytics():
|
||||
"""获取分析数据"""
|
||||
try:
|
||||
analytics = assistant.generate_analytics("last_7_days")
|
||||
return jsonify(analytics)
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
# 系统设置相关API
|
||||
@app.route('/api/settings')
|
||||
def get_settings():
|
||||
"""获取系统设置"""
|
||||
try:
|
||||
settings = {
|
||||
"api_timeout": 30,
|
||||
"max_history": 10,
|
||||
"refresh_interval": 10,
|
||||
"auto_monitoring": True,
|
||||
"agent_mode": True
|
||||
}
|
||||
return jsonify(settings)
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route('/api/settings', methods=['POST'])
|
||||
def save_settings():
|
||||
"""保存系统设置"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
# 这里应该保存设置到配置文件
|
||||
return jsonify({"success": True, "message": "设置保存成功"})
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route('/api/system/info')
|
||||
def get_system_info():
|
||||
"""获取系统信息"""
|
||||
try:
|
||||
import sys
|
||||
import platform
|
||||
info = {
|
||||
"version": "1.0.0",
|
||||
"python_version": sys.version,
|
||||
"database": "SQLite",
|
||||
"uptime": "2天3小时",
|
||||
"memory_usage": 128
|
||||
}
|
||||
return jsonify(info)
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
# 车辆数据相关API
|
||||
@app.route('/api/vehicle/data')
|
||||
def get_vehicle_data():
|
||||
"""获取车辆数据"""
|
||||
try:
|
||||
vehicle_id = request.args.get('vehicle_id')
|
||||
data_type = request.args.get('data_type')
|
||||
limit = request.args.get('limit', 10, type=int)
|
||||
|
||||
if vehicle_id:
|
||||
data = vehicle_manager.get_vehicle_data(vehicle_id, data_type, limit)
|
||||
else:
|
||||
data = vehicle_manager.search_vehicle_data(limit=limit)
|
||||
|
||||
return jsonify(data)
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route('/api/vehicle/data/<vehicle_id>/latest')
|
||||
def get_latest_vehicle_data(vehicle_id):
|
||||
"""获取车辆最新数据"""
|
||||
try:
|
||||
data = vehicle_manager.get_latest_vehicle_data(vehicle_id)
|
||||
return jsonify(data)
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route('/api/vehicle/data/<vehicle_id>/summary')
|
||||
def get_vehicle_summary(vehicle_id):
|
||||
"""获取车辆数据摘要"""
|
||||
try:
|
||||
summary = vehicle_manager.get_vehicle_summary(vehicle_id)
|
||||
return jsonify(summary)
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route('/api/vehicle/data', methods=['POST'])
|
||||
def add_vehicle_data():
|
||||
"""添加车辆数据"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
success = vehicle_manager.add_vehicle_data(
|
||||
vehicle_id=data['vehicle_id'],
|
||||
data_type=data['data_type'],
|
||||
data_value=data['data_value'],
|
||||
vehicle_vin=data.get('vehicle_vin')
|
||||
)
|
||||
return jsonify({"success": success, "message": "数据添加成功" if success else "添加失败"})
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route('/api/vehicle/init-sample-data', methods=['POST'])
|
||||
def init_sample_vehicle_data():
|
||||
"""初始化示例车辆数据"""
|
||||
try:
|
||||
success = vehicle_manager.add_sample_vehicle_data()
|
||||
return jsonify({"success": success, "message": "示例数据初始化成功" if success else "初始化失败"})
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(debug=True, host='0.0.0.0', port=5000)
|
||||
431
src/web/static/css/style.css
Normal file
431
src/web/static/css/style.css
Normal file
@@ -0,0 +1,431 @@
|
||||
/* TSP助手预警管理系统样式 */
|
||||
|
||||
body {
|
||||
background-color: #f8f9fa;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-weight: bold;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
/* 健康状态圆圈 */
|
||||
.health-score {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.score-circle {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto 10px;
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.score-circle.excellent {
|
||||
background: linear-gradient(135deg, #28a745, #20c997);
|
||||
}
|
||||
|
||||
.score-circle.good {
|
||||
background: linear-gradient(135deg, #17a2b8, #6f42c1);
|
||||
}
|
||||
|
||||
.score-circle.fair {
|
||||
background: linear-gradient(135deg, #ffc107, #fd7e14);
|
||||
}
|
||||
|
||||
.score-circle.poor {
|
||||
background: linear-gradient(135deg, #dc3545, #e83e8c);
|
||||
}
|
||||
|
||||
.score-circle.critical {
|
||||
background: linear-gradient(135deg, #6c757d, #343a40);
|
||||
}
|
||||
|
||||
.health-status {
|
||||
font-size: 0.9rem;
|
||||
color: #6c757d;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
/* 预警卡片 */
|
||||
.alert-card {
|
||||
border-left: 4px solid;
|
||||
margin-bottom: 1rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.alert-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.alert-card.critical {
|
||||
border-left-color: #dc3545;
|
||||
}
|
||||
|
||||
.alert-card.error {
|
||||
border-left-color: #fd7e14;
|
||||
}
|
||||
|
||||
.alert-card.warning {
|
||||
border-left-color: #ffc107;
|
||||
}
|
||||
|
||||
.alert-card.info {
|
||||
border-left-color: #17a2b8;
|
||||
}
|
||||
|
||||
.alert-level {
|
||||
font-size: 0.8rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.alert-level.critical {
|
||||
background-color: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.alert-level.error {
|
||||
background-color: #fd7e14;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.alert-level.warning {
|
||||
background-color: #ffc107;
|
||||
color: #212529;
|
||||
}
|
||||
|
||||
.alert-level.info {
|
||||
background-color: #17a2b8;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 规则表格 */
|
||||
.table th {
|
||||
background-color: #f8f9fa;
|
||||
border-top: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.rule-status {
|
||||
font-size: 0.8rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.rule-status.enabled {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.rule-status.disabled {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
/* 统计卡片动画 */
|
||||
.card {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
/* 按钮样式 */
|
||||
.btn {
|
||||
border-radius: 0.375rem;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* 加载动画 */
|
||||
.loading {
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 3px solid #f3f3f3;
|
||||
border-top: 3px solid #007bff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.container-fluid {
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
.score-circle {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* 自定义滚动条 */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #c1c1c1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #a8a8a8;
|
||||
}
|
||||
|
||||
/* 状态指示器 */
|
||||
.status-indicator {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.status-indicator.running {
|
||||
background-color: #28a745;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
.status-indicator.stopped {
|
||||
background-color: #dc3545;
|
||||
}
|
||||
|
||||
.status-indicator.unknown {
|
||||
background-color: #6c757d;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 rgba(40, 167, 69, 0.7);
|
||||
}
|
||||
70% {
|
||||
box-shadow: 0 0 0 10px rgba(40, 167, 69, 0);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 rgba(40, 167, 69, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 模态框样式 */
|
||||
.modal-content {
|
||||
border-radius: 0.5rem;
|
||||
border: none;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
border-top: 1px solid #e9ecef;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
/* 表格样式 */
|
||||
.table-hover tbody tr:hover {
|
||||
background-color: rgba(0,123,255,0.1);
|
||||
}
|
||||
|
||||
/* 空状态样式 */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 3rem 1rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.empty-state i {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* 工具提示样式 */
|
||||
.tooltip {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.tooltip-inner {
|
||||
background-color: #212529;
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
/* 进度条样式 */
|
||||
.progress {
|
||||
height: 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* 徽章样式 */
|
||||
.badge {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.375rem 0.5rem;
|
||||
}
|
||||
|
||||
/* 卡片标题样式 */
|
||||
.card-header h5 {
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.card-header h5 i {
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
/* 统计数字样式 */
|
||||
.stat-number {
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.875rem;
|
||||
opacity: 0.8;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
/* 预警数据展示优化 */
|
||||
.alert-data {
|
||||
background-color: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
max-height: 100px;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.alert-message {
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.alert-meta {
|
||||
font-size: 12px;
|
||||
color: #6c757d;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
/* 过滤和排序控件 */
|
||||
.alert-controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.alert-controls .form-select {
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
/* 预警卡片内容优化 */
|
||||
.alert-card .card-body {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.alert-card .d-flex {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.alert-card .flex-grow-1 {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* 规则表格操作按钮 */
|
||||
.table .btn-sm {
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
/* 响应式设计优化 */
|
||||
@media (max-width: 768px) {
|
||||
.alert-controls {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.alert-controls .form-select {
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
.alert-card .d-flex {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.alert-card .ms-3 {
|
||||
margin-left: 0 !important;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.alert-data {
|
||||
font-size: 10px;
|
||||
max-height: 80px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 预警级别颜色优化 */
|
||||
.alert-card.critical {
|
||||
background-color: #f8d7da;
|
||||
border-color: #dc3545;
|
||||
}
|
||||
|
||||
.alert-card.error {
|
||||
background-color: #fff3cd;
|
||||
border-color: #fd7e14;
|
||||
}
|
||||
|
||||
.alert-card.warning {
|
||||
background-color: #fff3cd;
|
||||
border-color: #ffc107;
|
||||
}
|
||||
|
||||
.alert-card.info {
|
||||
background-color: #d1ecf1;
|
||||
border-color: #17a2b8;
|
||||
}
|
||||
556
src/web/static/js/app.js
Normal file
556
src/web/static/js/app.js
Normal file
@@ -0,0 +1,556 @@
|
||||
// TSP助手预警管理系统前端脚本
|
||||
|
||||
class AlertManager {
|
||||
constructor() {
|
||||
this.alerts = [];
|
||||
this.rules = [];
|
||||
this.health = {};
|
||||
this.monitorStatus = 'unknown';
|
||||
this.refreshInterval = null;
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.bindEvents();
|
||||
this.loadInitialData();
|
||||
this.startAutoRefresh();
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
// 监控控制按钮
|
||||
document.getElementById('start-monitor').addEventListener('click', () => this.startMonitoring());
|
||||
document.getElementById('stop-monitor').addEventListener('click', () => this.stopMonitoring());
|
||||
document.getElementById('check-alerts').addEventListener('click', () => this.checkAlerts());
|
||||
document.getElementById('refresh-alerts').addEventListener('click', () => this.loadAlerts());
|
||||
|
||||
// 规则管理
|
||||
document.getElementById('save-rule').addEventListener('click', () => this.saveRule());
|
||||
document.getElementById('update-rule').addEventListener('click', () => this.updateRule());
|
||||
|
||||
// 预警过滤和排序
|
||||
document.getElementById('alert-filter').addEventListener('change', () => this.updateAlertsDisplay());
|
||||
document.getElementById('alert-sort').addEventListener('change', () => this.updateAlertsDisplay());
|
||||
|
||||
// 自动刷新
|
||||
setInterval(() => {
|
||||
this.loadHealth();
|
||||
this.loadMonitorStatus();
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
async loadInitialData() {
|
||||
await Promise.all([
|
||||
this.loadHealth(),
|
||||
this.loadAlerts(),
|
||||
this.loadRules(),
|
||||
this.loadMonitorStatus()
|
||||
]);
|
||||
}
|
||||
|
||||
startAutoRefresh() {
|
||||
this.refreshInterval = setInterval(() => {
|
||||
this.loadAlerts();
|
||||
}, 10000); // 每10秒刷新一次预警
|
||||
}
|
||||
|
||||
async loadHealth() {
|
||||
try {
|
||||
const response = await fetch('/api/health');
|
||||
const data = await response.json();
|
||||
this.health = data;
|
||||
this.updateHealthDisplay();
|
||||
} catch (error) {
|
||||
console.error('加载健康状态失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async loadAlerts() {
|
||||
try {
|
||||
const response = await fetch('/api/alerts');
|
||||
const data = await response.json();
|
||||
this.alerts = data;
|
||||
this.updateAlertsDisplay();
|
||||
this.updateAlertStatistics();
|
||||
} catch (error) {
|
||||
console.error('加载预警失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async loadRules() {
|
||||
try {
|
||||
const response = await fetch('/api/rules');
|
||||
const data = await response.json();
|
||||
this.rules = data;
|
||||
this.updateRulesDisplay();
|
||||
} catch (error) {
|
||||
console.error('加载规则失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async loadMonitorStatus() {
|
||||
try {
|
||||
const response = await fetch('/api/monitor/status');
|
||||
const data = await response.json();
|
||||
this.monitorStatus = data.monitor_status;
|
||||
this.updateMonitorStatusDisplay();
|
||||
} catch (error) {
|
||||
console.error('加载监控状态失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
updateHealthDisplay() {
|
||||
const healthScore = this.health.health_score || 0;
|
||||
const healthStatus = this.health.health_status || 'unknown';
|
||||
|
||||
const scoreElement = document.getElementById('health-score-text');
|
||||
const circleElement = document.getElementById('health-score-circle');
|
||||
const statusElement = document.getElementById('health-status');
|
||||
|
||||
if (scoreElement) scoreElement.textContent = Math.round(healthScore);
|
||||
if (statusElement) statusElement.textContent = this.getHealthStatusText(healthStatus);
|
||||
|
||||
if (circleElement) {
|
||||
circleElement.className = `score-circle ${healthStatus}`;
|
||||
}
|
||||
}
|
||||
|
||||
updateAlertsDisplay() {
|
||||
const container = document.getElementById('alerts-container');
|
||||
|
||||
if (this.alerts.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
<h5>暂无活跃预警</h5>
|
||||
<p>系统运行正常,没有需要处理的预警</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
// 应用过滤和排序
|
||||
let filteredAlerts = this.filterAndSortAlerts(this.alerts);
|
||||
|
||||
const alertsHtml = filteredAlerts.map(alert => {
|
||||
const dataStr = alert.data ? JSON.stringify(alert.data, null, 2) : '无数据';
|
||||
return `
|
||||
<div class="alert-card ${alert.level}">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div class="flex-grow-1">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<span class="alert-level ${alert.level}">${this.getLevelText(alert.level)}</span>
|
||||
<span class="ms-2 text-muted fw-bold">${alert.rule_name || '未知规则'}</span>
|
||||
<span class="ms-auto text-muted small">${this.formatTime(alert.created_at)}</span>
|
||||
</div>
|
||||
<div class="alert-message">${alert.message}</div>
|
||||
<div class="alert-meta">
|
||||
类型: ${this.getTypeText(alert.alert_type)} |
|
||||
级别: ${this.getLevelText(alert.level)}
|
||||
</div>
|
||||
<div class="alert-data">${dataStr}</div>
|
||||
</div>
|
||||
<div class="ms-3">
|
||||
<button class="btn btn-sm btn-outline-success" onclick="alertManager.resolveAlert(${alert.id})">
|
||||
<i class="fas fa-check me-1"></i>解决
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
container.innerHTML = alertsHtml;
|
||||
}
|
||||
|
||||
updateRulesDisplay() {
|
||||
const tbody = document.getElementById('rules-table');
|
||||
|
||||
if (this.rules.length === 0) {
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="6" class="text-center text-muted">暂无规则</td>
|
||||
</tr>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
const rulesHtml = this.rules.map(rule => `
|
||||
<tr>
|
||||
<td>${rule.name}</td>
|
||||
<td>${this.getTypeText(rule.alert_type)}</td>
|
||||
<td><span class="alert-level ${rule.level}">${this.getLevelText(rule.level)}</span></td>
|
||||
<td>${rule.threshold}</td>
|
||||
<td><span class="rule-status ${rule.enabled ? 'enabled' : 'disabled'}">${rule.enabled ? '启用' : '禁用'}</span></td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-outline-primary me-1" onclick="alertManager.editRule('${rule.name}')">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-danger" onclick="alertManager.deleteRule('${rule.name}')">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
|
||||
tbody.innerHTML = rulesHtml;
|
||||
}
|
||||
|
||||
updateAlertStatistics() {
|
||||
const stats = this.alerts.reduce((acc, alert) => {
|
||||
acc[alert.level] = (acc[alert.level] || 0) + 1;
|
||||
acc.total = (acc.total || 0) + 1;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
document.getElementById('critical-alerts').textContent = stats.critical || 0;
|
||||
document.getElementById('warning-alerts').textContent = stats.warning || 0;
|
||||
document.getElementById('info-alerts').textContent = stats.info || 0;
|
||||
document.getElementById('total-alerts').textContent = stats.total || 0;
|
||||
}
|
||||
|
||||
updateMonitorStatusDisplay() {
|
||||
const statusElement = document.getElementById('monitor-status');
|
||||
const icon = statusElement.querySelector('i');
|
||||
const text = statusElement.querySelector('span') || statusElement;
|
||||
|
||||
let statusText = '';
|
||||
let statusClass = '';
|
||||
|
||||
switch (this.monitorStatus) {
|
||||
case 'running':
|
||||
statusText = '监控运行中';
|
||||
statusClass = 'text-success';
|
||||
icon.className = 'fas fa-circle text-success';
|
||||
break;
|
||||
case 'stopped':
|
||||
statusText = '监控已停止';
|
||||
statusClass = 'text-danger';
|
||||
icon.className = 'fas fa-circle text-danger';
|
||||
break;
|
||||
default:
|
||||
statusText = '监控状态未知';
|
||||
statusClass = 'text-warning';
|
||||
icon.className = 'fas fa-circle text-warning';
|
||||
}
|
||||
|
||||
if (text.textContent) {
|
||||
text.textContent = statusText;
|
||||
} else {
|
||||
statusElement.innerHTML = `<i class="fas fa-circle ${statusClass}"></i> ${statusText}`;
|
||||
}
|
||||
}
|
||||
|
||||
async startMonitoring() {
|
||||
try {
|
||||
const response = await fetch('/api/monitor/start', { method: 'POST' });
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
this.showNotification('监控服务已启动', 'success');
|
||||
this.loadMonitorStatus();
|
||||
} else {
|
||||
this.showNotification(data.message || '启动监控失败', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('启动监控失败:', error);
|
||||
this.showNotification('启动监控失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async stopMonitoring() {
|
||||
try {
|
||||
const response = await fetch('/api/monitor/stop', { method: 'POST' });
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
this.showNotification('监控服务已停止', 'success');
|
||||
this.loadMonitorStatus();
|
||||
} else {
|
||||
this.showNotification(data.message || '停止监控失败', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('停止监控失败:', error);
|
||||
this.showNotification('停止监控失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async checkAlerts() {
|
||||
try {
|
||||
const response = await fetch('/api/check-alerts', { method: 'POST' });
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
this.showNotification(`检查完成,发现 ${data.count} 个预警`, 'info');
|
||||
this.loadAlerts();
|
||||
} else {
|
||||
this.showNotification('检查预警失败', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('检查预警失败:', error);
|
||||
this.showNotification('检查预警失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async resolveAlert(alertId) {
|
||||
try {
|
||||
const response = await fetch(`/api/alerts/${alertId}/resolve`, { method: 'POST' });
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
this.showNotification('预警已解决', 'success');
|
||||
this.loadAlerts();
|
||||
} else {
|
||||
this.showNotification(data.message || '解决预警失败', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('解决预警失败:', error);
|
||||
this.showNotification('解决预警失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async saveRule() {
|
||||
const formData = {
|
||||
name: document.getElementById('rule-name').value,
|
||||
description: document.getElementById('rule-description').value,
|
||||
alert_type: document.getElementById('rule-type').value,
|
||||
level: document.getElementById('rule-level').value,
|
||||
threshold: parseFloat(document.getElementById('rule-threshold').value),
|
||||
condition: document.getElementById('rule-condition').value,
|
||||
enabled: document.getElementById('rule-enabled').checked,
|
||||
check_interval: parseInt(document.getElementById('rule-interval').value),
|
||||
cooldown: parseInt(document.getElementById('rule-cooldown').value)
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/rules', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(formData)
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
this.showNotification('规则创建成功', 'success');
|
||||
this.hideModal('ruleModal');
|
||||
this.loadRules();
|
||||
this.resetRuleForm();
|
||||
} else {
|
||||
this.showNotification(data.message || '创建规则失败', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('创建规则失败:', error);
|
||||
this.showNotification('创建规则失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async deleteRule(ruleName) {
|
||||
if (!confirm(`确定要删除规则 "${ruleName}" 吗?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/rules/${ruleName}`, { method: 'DELETE' });
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
this.showNotification('规则删除成功', 'success');
|
||||
this.loadRules();
|
||||
} else {
|
||||
this.showNotification(data.message || '删除规则失败', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除规则失败:', error);
|
||||
this.showNotification('删除规则失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
filterAndSortAlerts(alerts) {
|
||||
// 应用过滤
|
||||
const filter = document.getElementById('alert-filter').value;
|
||||
let filtered = alerts;
|
||||
|
||||
if (filter !== 'all') {
|
||||
filtered = alerts.filter(alert => alert.level === filter);
|
||||
}
|
||||
|
||||
// 应用排序
|
||||
const sort = document.getElementById('alert-sort').value;
|
||||
filtered.sort((a, b) => {
|
||||
switch (sort) {
|
||||
case 'time-desc':
|
||||
return new Date(b.created_at) - new Date(a.created_at);
|
||||
case 'time-asc':
|
||||
return new Date(a.created_at) - new Date(b.created_at);
|
||||
case 'level-desc':
|
||||
const levelOrder = { 'critical': 4, 'error': 3, 'warning': 2, 'info': 1 };
|
||||
return (levelOrder[b.level] || 0) - (levelOrder[a.level] || 0);
|
||||
case 'level-asc':
|
||||
const levelOrderAsc = { 'critical': 4, 'error': 3, 'warning': 2, 'info': 1 };
|
||||
return (levelOrderAsc[a.level] || 0) - (levelOrderAsc[b.level] || 0);
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
editRule(ruleName) {
|
||||
// 查找规则数据
|
||||
const rule = this.rules.find(r => r.name === ruleName);
|
||||
if (!rule) {
|
||||
this.showNotification('规则不存在', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// 填充编辑表单
|
||||
document.getElementById('edit-rule-name-original').value = rule.name;
|
||||
document.getElementById('edit-rule-name').value = rule.name;
|
||||
document.getElementById('edit-rule-type').value = rule.alert_type;
|
||||
document.getElementById('edit-rule-level').value = rule.level;
|
||||
document.getElementById('edit-rule-threshold').value = rule.threshold;
|
||||
document.getElementById('edit-rule-description').value = rule.description || '';
|
||||
document.getElementById('edit-rule-condition').value = rule.condition;
|
||||
document.getElementById('edit-rule-interval').value = rule.check_interval;
|
||||
document.getElementById('edit-rule-cooldown').value = rule.cooldown;
|
||||
document.getElementById('edit-rule-enabled').checked = rule.enabled;
|
||||
|
||||
// 显示编辑模态框
|
||||
const modal = new bootstrap.Modal(document.getElementById('editRuleModal'));
|
||||
modal.show();
|
||||
}
|
||||
|
||||
async updateRule() {
|
||||
const originalName = document.getElementById('edit-rule-name-original').value;
|
||||
const formData = {
|
||||
name: document.getElementById('edit-rule-name').value,
|
||||
description: document.getElementById('edit-rule-description').value,
|
||||
alert_type: document.getElementById('edit-rule-type').value,
|
||||
level: document.getElementById('edit-rule-level').value,
|
||||
threshold: parseFloat(document.getElementById('edit-rule-threshold').value),
|
||||
condition: document.getElementById('edit-rule-condition').value,
|
||||
enabled: document.getElementById('edit-rule-enabled').checked,
|
||||
check_interval: parseInt(document.getElementById('edit-rule-interval').value),
|
||||
cooldown: parseInt(document.getElementById('edit-rule-cooldown').value)
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/rules/${originalName}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(formData)
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
this.showNotification('规则更新成功', 'success');
|
||||
this.hideModal('editRuleModal');
|
||||
this.loadRules();
|
||||
} else {
|
||||
this.showNotification(data.message || '更新规则失败', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('更新规则失败:', error);
|
||||
this.showNotification('更新规则失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
resetRuleForm() {
|
||||
document.getElementById('rule-form').reset();
|
||||
document.getElementById('rule-interval').value = '300';
|
||||
document.getElementById('rule-cooldown').value = '3600';
|
||||
document.getElementById('rule-enabled').checked = true;
|
||||
}
|
||||
|
||||
hideModal(modalId) {
|
||||
const modal = document.getElementById(modalId);
|
||||
const bsModal = bootstrap.Modal.getInstance(modal);
|
||||
if (bsModal) {
|
||||
bsModal.hide();
|
||||
}
|
||||
}
|
||||
|
||||
showNotification(message, type = 'info') {
|
||||
// 创建通知元素
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `alert alert-${type === 'error' ? 'danger' : type} alert-dismissible fade show position-fixed`;
|
||||
notification.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 300px;';
|
||||
notification.innerHTML = `
|
||||
${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
`;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
// 3秒后自动移除
|
||||
setTimeout(() => {
|
||||
if (notification.parentNode) {
|
||||
notification.parentNode.removeChild(notification);
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
getLevelText(level) {
|
||||
const levelMap = {
|
||||
'critical': '严重',
|
||||
'error': '错误',
|
||||
'warning': '警告',
|
||||
'info': '信息'
|
||||
};
|
||||
return levelMap[level] || level;
|
||||
}
|
||||
|
||||
getTypeText(type) {
|
||||
const typeMap = {
|
||||
'performance': '性能',
|
||||
'quality': '质量',
|
||||
'volume': '量级',
|
||||
'system': '系统',
|
||||
'business': '业务'
|
||||
};
|
||||
return typeMap[type] || type;
|
||||
}
|
||||
|
||||
getHealthStatusText(status) {
|
||||
const statusMap = {
|
||||
'excellent': '优秀',
|
||||
'good': '良好',
|
||||
'fair': '一般',
|
||||
'poor': '较差',
|
||||
'critical': '严重',
|
||||
'unknown': '未知'
|
||||
};
|
||||
return statusMap[status] || status;
|
||||
}
|
||||
|
||||
formatTime(timestamp) {
|
||||
const date = new Date(timestamp);
|
||||
const now = new Date();
|
||||
const diff = now - date;
|
||||
|
||||
if (diff < 60000) { // 1分钟内
|
||||
return '刚刚';
|
||||
} else if (diff < 3600000) { // 1小时内
|
||||
return `${Math.floor(diff / 60000)}分钟前`;
|
||||
} else if (diff < 86400000) { // 1天内
|
||||
return `${Math.floor(diff / 3600000)}小时前`;
|
||||
} else {
|
||||
return date.toLocaleDateString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化应用
|
||||
let alertManager;
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
alertManager = new AlertManager();
|
||||
});
|
||||
410
src/web/static/js/chat.js
Normal file
410
src/web/static/js/chat.js
Normal file
@@ -0,0 +1,410 @@
|
||||
// 实时对话前端脚本
|
||||
|
||||
class ChatClient {
|
||||
constructor() {
|
||||
this.websocket = null;
|
||||
this.sessionId = null;
|
||||
this.isConnected = false;
|
||||
this.messageCount = 0;
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.bindEvents();
|
||||
this.updateConnectionStatus(false);
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
// 开始对话
|
||||
document.getElementById('start-chat').addEventListener('click', () => this.startChat());
|
||||
|
||||
// 结束对话
|
||||
document.getElementById('end-chat').addEventListener('click', () => this.endChat());
|
||||
|
||||
// 发送消息
|
||||
document.getElementById('send-button').addEventListener('click', () => this.sendMessage());
|
||||
|
||||
// 回车发送
|
||||
document.getElementById('message-input').addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
this.sendMessage();
|
||||
}
|
||||
});
|
||||
|
||||
// 创建工单
|
||||
document.getElementById('create-work-order').addEventListener('click', () => this.showWorkOrderModal());
|
||||
document.getElementById('create-work-order-btn').addEventListener('click', () => this.createWorkOrder());
|
||||
|
||||
// 快速操作按钮
|
||||
document.querySelectorAll('.quick-action-btn').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
const message = e.target.getAttribute('data-message');
|
||||
document.getElementById('message-input').value = message;
|
||||
this.sendMessage();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async startChat() {
|
||||
try {
|
||||
// 连接WebSocket
|
||||
await this.connectWebSocket();
|
||||
|
||||
// 创建会话
|
||||
const userId = document.getElementById('user-id').value || 'anonymous';
|
||||
const workOrderId = document.getElementById('work-order-id').value || null;
|
||||
|
||||
const response = await this.sendWebSocketMessage({
|
||||
type: 'create_session',
|
||||
user_id: userId,
|
||||
work_order_id: workOrderId ? parseInt(workOrderId) : null
|
||||
});
|
||||
|
||||
if (response.type === 'session_created') {
|
||||
this.sessionId = response.session_id;
|
||||
this.updateSessionInfo();
|
||||
this.enableChat();
|
||||
this.addSystemMessage('对话已开始,请描述您的问题。');
|
||||
} else {
|
||||
this.showError('创建会话失败');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('启动对话失败:', error);
|
||||
this.showError('启动对话失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async endChat() {
|
||||
try {
|
||||
if (this.sessionId) {
|
||||
await this.sendWebSocketMessage({
|
||||
type: 'end_session',
|
||||
session_id: this.sessionId
|
||||
});
|
||||
}
|
||||
|
||||
this.sessionId = null;
|
||||
this.disableChat();
|
||||
this.addSystemMessage('对话已结束。');
|
||||
|
||||
} catch (error) {
|
||||
console.error('结束对话失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async sendMessage() {
|
||||
const input = document.getElementById('message-input');
|
||||
const message = input.value.trim();
|
||||
|
||||
if (!message || !this.sessionId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 清空输入框
|
||||
input.value = '';
|
||||
|
||||
// 添加用户消息
|
||||
this.addMessage('user', message);
|
||||
|
||||
// 显示打字指示器
|
||||
this.showTypingIndicator();
|
||||
|
||||
try {
|
||||
const response = await this.sendWebSocketMessage({
|
||||
type: 'send_message',
|
||||
session_id: this.sessionId,
|
||||
message: message
|
||||
});
|
||||
|
||||
this.hideTypingIndicator();
|
||||
|
||||
if (response.type === 'message_response' && response.result.success) {
|
||||
const result = response.result;
|
||||
|
||||
// 添加助手回复
|
||||
this.addMessage('assistant', result.content, {
|
||||
knowledge_used: result.knowledge_used,
|
||||
confidence_score: result.confidence_score,
|
||||
work_order_id: result.work_order_id
|
||||
});
|
||||
|
||||
// 更新工单ID
|
||||
if (result.work_order_id) {
|
||||
document.getElementById('work-order-id').value = result.work_order_id;
|
||||
}
|
||||
|
||||
} else {
|
||||
this.addMessage('assistant', '抱歉,我暂时无法处理您的问题。请稍后再试。');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
this.hideTypingIndicator();
|
||||
console.error('发送消息失败:', error);
|
||||
this.addMessage('assistant', '发送消息失败,请检查网络连接。');
|
||||
}
|
||||
}
|
||||
|
||||
async createWorkOrder() {
|
||||
const title = document.getElementById('wo-title').value;
|
||||
const description = document.getElementById('wo-description').value;
|
||||
const category = document.getElementById('wo-category').value;
|
||||
const priority = document.getElementById('wo-priority').value;
|
||||
|
||||
if (!title || !description) {
|
||||
this.showError('请填写工单标题和描述');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this.sendWebSocketMessage({
|
||||
type: 'create_work_order',
|
||||
session_id: this.sessionId,
|
||||
title: title,
|
||||
description: description,
|
||||
category: category,
|
||||
priority: priority
|
||||
});
|
||||
|
||||
if (response.type === 'work_order_created' && response.result.success) {
|
||||
const workOrderId = response.result.work_order_id;
|
||||
document.getElementById('work-order-id').value = workOrderId;
|
||||
this.addSystemMessage(`工单创建成功!工单号: ${response.result.order_id}`);
|
||||
|
||||
// 关闭模态框
|
||||
const modal = bootstrap.Modal.getInstance(document.getElementById('workOrderModal'));
|
||||
modal.hide();
|
||||
|
||||
// 清空表单
|
||||
document.getElementById('work-order-form').reset();
|
||||
|
||||
} else {
|
||||
this.showError('创建工单失败: ' + (response.result.error || '未知错误'));
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('创建工单失败:', error);
|
||||
this.showError('创建工单失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
connectWebSocket() {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
this.websocket = new WebSocket('ws://localhost:8765');
|
||||
|
||||
// 设置连接超时
|
||||
const timeout = setTimeout(() => {
|
||||
if (this.websocket.readyState !== WebSocket.OPEN) {
|
||||
this.websocket.close();
|
||||
reject(new Error('WebSocket连接超时,请检查服务器是否启动'));
|
||||
}
|
||||
}, 5000); // 5秒超时
|
||||
|
||||
this.websocket.onopen = () => {
|
||||
clearTimeout(timeout);
|
||||
this.isConnected = true;
|
||||
this.updateConnectionStatus(true);
|
||||
resolve();
|
||||
};
|
||||
|
||||
this.websocket.onclose = () => {
|
||||
clearTimeout(timeout);
|
||||
this.isConnected = false;
|
||||
this.updateConnectionStatus(false);
|
||||
};
|
||||
|
||||
this.websocket.onerror = (error) => {
|
||||
clearTimeout(timeout);
|
||||
console.error('WebSocket错误:', error);
|
||||
reject(new Error('WebSocket连接失败,请检查服务器是否启动'));
|
||||
};
|
||||
|
||||
this.websocket.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
this.handleWebSocketMessage(data);
|
||||
} catch (error) {
|
||||
console.error('解析WebSocket消息失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
sendWebSocketMessage(message) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.websocket || this.websocket.readyState !== WebSocket.OPEN) {
|
||||
reject(new Error('WebSocket未连接'));
|
||||
return;
|
||||
}
|
||||
|
||||
const messageId = 'msg_' + Date.now();
|
||||
message.messageId = messageId;
|
||||
|
||||
// 设置超时
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error('请求超时'));
|
||||
}, 10000);
|
||||
|
||||
// 监听响应
|
||||
const handleResponse = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
if (data.messageId === messageId) {
|
||||
clearTimeout(timeout);
|
||||
this.websocket.removeEventListener('message', handleResponse);
|
||||
resolve(data);
|
||||
}
|
||||
} catch (error) {
|
||||
// 忽略解析错误
|
||||
}
|
||||
};
|
||||
|
||||
this.websocket.addEventListener('message', handleResponse);
|
||||
this.websocket.send(JSON.stringify(message));
|
||||
});
|
||||
}
|
||||
|
||||
handleWebSocketMessage(data) {
|
||||
// 处理WebSocket消息
|
||||
console.log('收到WebSocket消息:', data);
|
||||
}
|
||||
|
||||
addMessage(role, content, metadata = {}) {
|
||||
const messagesContainer = document.getElementById('chat-messages');
|
||||
|
||||
// 如果是第一条消息,清空欢迎信息
|
||||
if (this.messageCount === 0) {
|
||||
messagesContainer.innerHTML = '';
|
||||
}
|
||||
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = `message ${role}`;
|
||||
|
||||
const avatar = document.createElement('div');
|
||||
avatar.className = 'message-avatar';
|
||||
avatar.textContent = role === 'user' ? 'U' : 'A';
|
||||
|
||||
const contentDiv = document.createElement('div');
|
||||
contentDiv.className = 'message-content';
|
||||
contentDiv.innerHTML = content;
|
||||
|
||||
// 添加时间戳
|
||||
const timeDiv = document.createElement('div');
|
||||
timeDiv.className = 'message-time';
|
||||
timeDiv.textContent = new Date().toLocaleTimeString();
|
||||
contentDiv.appendChild(timeDiv);
|
||||
|
||||
// 添加元数据
|
||||
if (metadata.knowledge_used && metadata.knowledge_used.length > 0) {
|
||||
const knowledgeDiv = document.createElement('div');
|
||||
knowledgeDiv.className = 'knowledge-info';
|
||||
knowledgeDiv.innerHTML = `<i class="fas fa-lightbulb me-1"></i>基于 ${metadata.knowledge_used.length} 条知识库信息生成`;
|
||||
contentDiv.appendChild(knowledgeDiv);
|
||||
}
|
||||
|
||||
if (metadata.confidence_score) {
|
||||
const confidenceDiv = document.createElement('div');
|
||||
confidenceDiv.className = 'confidence-score';
|
||||
confidenceDiv.textContent = `置信度: ${(metadata.confidence_score * 100).toFixed(1)}%`;
|
||||
contentDiv.appendChild(confidenceDiv);
|
||||
}
|
||||
|
||||
if (metadata.work_order_id) {
|
||||
const workOrderDiv = document.createElement('div');
|
||||
workOrderDiv.className = 'work-order-info';
|
||||
workOrderDiv.innerHTML = `<i class="fas fa-ticket-alt me-1"></i>关联工单: ${metadata.work_order_id}`;
|
||||
contentDiv.appendChild(workOrderDiv);
|
||||
}
|
||||
|
||||
if (role === 'user') {
|
||||
messageDiv.appendChild(contentDiv);
|
||||
messageDiv.appendChild(avatar);
|
||||
} else {
|
||||
messageDiv.appendChild(avatar);
|
||||
messageDiv.appendChild(contentDiv);
|
||||
}
|
||||
|
||||
messagesContainer.appendChild(messageDiv);
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
|
||||
this.messageCount++;
|
||||
}
|
||||
|
||||
addSystemMessage(content) {
|
||||
const messagesContainer = document.getElementById('chat-messages');
|
||||
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = 'text-center text-muted py-2';
|
||||
messageDiv.innerHTML = `<small><i class="fas fa-info-circle me-1"></i>${content}</small>`;
|
||||
|
||||
messagesContainer.appendChild(messageDiv);
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
}
|
||||
|
||||
showTypingIndicator() {
|
||||
document.getElementById('typing-indicator').classList.add('show');
|
||||
}
|
||||
|
||||
hideTypingIndicator() {
|
||||
document.getElementById('typing-indicator').classList.remove('show');
|
||||
}
|
||||
|
||||
updateConnectionStatus(connected) {
|
||||
const statusElement = document.getElementById('connection-status');
|
||||
if (connected) {
|
||||
statusElement.className = 'connection-status connected';
|
||||
statusElement.innerHTML = '<i class="fas fa-circle me-1"></i>已连接';
|
||||
} else {
|
||||
statusElement.className = 'connection-status disconnected';
|
||||
statusElement.innerHTML = '<i class="fas fa-circle me-1"></i>未连接';
|
||||
}
|
||||
}
|
||||
|
||||
updateSessionInfo() {
|
||||
const sessionInfo = document.getElementById('session-info');
|
||||
sessionInfo.innerHTML = `
|
||||
<div><strong>会话ID:</strong> ${this.sessionId}</div>
|
||||
<div><strong>消息数:</strong> ${this.messageCount}</div>
|
||||
<div><strong>状态:</strong> 活跃</div>
|
||||
`;
|
||||
}
|
||||
|
||||
enableChat() {
|
||||
document.getElementById('start-chat').disabled = true;
|
||||
document.getElementById('end-chat').disabled = false;
|
||||
document.getElementById('message-input').disabled = false;
|
||||
document.getElementById('send-button').disabled = false;
|
||||
}
|
||||
|
||||
disableChat() {
|
||||
document.getElementById('start-chat').disabled = false;
|
||||
document.getElementById('end-chat').disabled = true;
|
||||
document.getElementById('message-input').disabled = true;
|
||||
document.getElementById('send-button').disabled = true;
|
||||
}
|
||||
|
||||
showWorkOrderModal() {
|
||||
if (!this.sessionId) {
|
||||
this.showError('请先开始对话');
|
||||
return;
|
||||
}
|
||||
|
||||
const modal = new bootstrap.Modal(document.getElementById('workOrderModal'));
|
||||
modal.show();
|
||||
}
|
||||
|
||||
showError(message) {
|
||||
this.addSystemMessage(`<span class="text-danger">错误: ${message}</span>`);
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化聊天客户端
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
window.chatClient = new ChatClient();
|
||||
});
|
||||
334
src/web/static/js/chat_http.js
Normal file
334
src/web/static/js/chat_http.js
Normal file
@@ -0,0 +1,334 @@
|
||||
// HTTP版本实时对话前端脚本
|
||||
|
||||
class ChatHttpClient {
|
||||
constructor() {
|
||||
this.sessionId = null;
|
||||
this.messageCount = 0;
|
||||
this.apiBase = '/api/chat';
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.bindEvents();
|
||||
this.updateConnectionStatus(true);
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
// 开始对话
|
||||
document.getElementById('start-chat').addEventListener('click', () => this.startChat());
|
||||
|
||||
// 结束对话
|
||||
document.getElementById('end-chat').addEventListener('click', () => this.endChat());
|
||||
|
||||
// 发送消息
|
||||
document.getElementById('send-button').addEventListener('click', () => this.sendMessage());
|
||||
|
||||
// 回车发送
|
||||
document.getElementById('message-input').addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
this.sendMessage();
|
||||
}
|
||||
});
|
||||
|
||||
// 创建工单
|
||||
document.getElementById('create-work-order').addEventListener('click', () => this.showWorkOrderModal());
|
||||
document.getElementById('create-work-order-btn').addEventListener('click', () => this.createWorkOrder());
|
||||
|
||||
// 快速操作按钮
|
||||
document.querySelectorAll('.quick-action-btn').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
const message = e.target.getAttribute('data-message');
|
||||
document.getElementById('message-input').value = message;
|
||||
this.sendMessage();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async startChat() {
|
||||
try {
|
||||
// 创建会话
|
||||
const userId = document.getElementById('user-id').value || 'anonymous';
|
||||
const workOrderId = document.getElementById('work-order-id').value || null;
|
||||
|
||||
const response = await this.sendRequest('POST', '/session', {
|
||||
user_id: userId,
|
||||
work_order_id: workOrderId ? parseInt(workOrderId) : null
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
this.sessionId = response.session_id;
|
||||
this.updateSessionInfo();
|
||||
this.enableChat();
|
||||
this.addSystemMessage('对话已开始,请描述您的问题。');
|
||||
} else {
|
||||
this.showError('创建会话失败');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('启动对话失败:', error);
|
||||
this.showError('启动对话失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async endChat() {
|
||||
try {
|
||||
if (this.sessionId) {
|
||||
await this.sendRequest('DELETE', `/session/${this.sessionId}`);
|
||||
}
|
||||
|
||||
this.sessionId = null;
|
||||
this.disableChat();
|
||||
this.addSystemMessage('对话已结束。');
|
||||
|
||||
} catch (error) {
|
||||
console.error('结束对话失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async sendMessage() {
|
||||
const input = document.getElementById('message-input');
|
||||
const message = input.value.trim();
|
||||
|
||||
if (!message || !this.sessionId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 清空输入框
|
||||
input.value = '';
|
||||
|
||||
// 添加用户消息
|
||||
this.addMessage('user', message);
|
||||
|
||||
// 显示打字指示器
|
||||
this.showTypingIndicator();
|
||||
|
||||
try {
|
||||
const response = await this.sendRequest('POST', '/message', {
|
||||
session_id: this.sessionId,
|
||||
message: message
|
||||
});
|
||||
|
||||
this.hideTypingIndicator();
|
||||
|
||||
if (response.success) {
|
||||
// 添加助手回复
|
||||
this.addMessage('assistant', response.content, {
|
||||
knowledge_used: response.knowledge_used,
|
||||
confidence_score: response.confidence_score,
|
||||
work_order_id: response.work_order_id
|
||||
});
|
||||
|
||||
// 更新工单ID
|
||||
if (response.work_order_id) {
|
||||
document.getElementById('work-order-id').value = response.work_order_id;
|
||||
}
|
||||
|
||||
} else {
|
||||
this.addMessage('assistant', '抱歉,我暂时无法处理您的问题。请稍后再试。');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
this.hideTypingIndicator();
|
||||
console.error('发送消息失败:', error);
|
||||
this.addMessage('assistant', '发送消息失败,请检查网络连接。');
|
||||
}
|
||||
}
|
||||
|
||||
async createWorkOrder() {
|
||||
const title = document.getElementById('wo-title').value;
|
||||
const description = document.getElementById('wo-description').value;
|
||||
const category = document.getElementById('wo-category').value;
|
||||
const priority = document.getElementById('wo-priority').value;
|
||||
|
||||
if (!title || !description) {
|
||||
this.showError('请填写工单标题和描述');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this.sendRequest('POST', '/work-order', {
|
||||
session_id: this.sessionId,
|
||||
title: title,
|
||||
description: description,
|
||||
category: category,
|
||||
priority: priority
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
const workOrderId = response.work_order_id;
|
||||
document.getElementById('work-order-id').value = workOrderId;
|
||||
this.addSystemMessage(`工单创建成功!工单号: ${response.order_id}`);
|
||||
|
||||
// 关闭模态框
|
||||
const modal = bootstrap.Modal.getInstance(document.getElementById('workOrderModal'));
|
||||
modal.hide();
|
||||
|
||||
// 清空表单
|
||||
document.getElementById('work-order-form').reset();
|
||||
|
||||
} else {
|
||||
this.showError('创建工单失败: ' + (response.error || '未知错误'));
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('创建工单失败:', error);
|
||||
this.showError('创建工单失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async sendRequest(method, endpoint, data = null) {
|
||||
const url = this.apiBase + endpoint;
|
||||
const options = {
|
||||
method: method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
};
|
||||
|
||||
if (data) {
|
||||
options.body = JSON.stringify(data);
|
||||
}
|
||||
|
||||
const response = await fetch(url, options);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP错误: ${response.status}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
addMessage(role, content, metadata = {}) {
|
||||
const messagesContainer = document.getElementById('chat-messages');
|
||||
|
||||
// 如果是第一条消息,清空欢迎信息
|
||||
if (this.messageCount === 0) {
|
||||
messagesContainer.innerHTML = '';
|
||||
}
|
||||
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = `message ${role}`;
|
||||
|
||||
const avatar = document.createElement('div');
|
||||
avatar.className = 'message-avatar';
|
||||
avatar.textContent = role === 'user' ? 'U' : 'A';
|
||||
|
||||
const contentDiv = document.createElement('div');
|
||||
contentDiv.className = 'message-content';
|
||||
contentDiv.innerHTML = content;
|
||||
|
||||
// 添加时间戳
|
||||
const timeDiv = document.createElement('div');
|
||||
timeDiv.className = 'message-time';
|
||||
timeDiv.textContent = new Date().toLocaleTimeString();
|
||||
contentDiv.appendChild(timeDiv);
|
||||
|
||||
// 添加元数据
|
||||
if (metadata.knowledge_used && metadata.knowledge_used.length > 0) {
|
||||
const knowledgeDiv = document.createElement('div');
|
||||
knowledgeDiv.className = 'knowledge-info';
|
||||
knowledgeDiv.innerHTML = `<i class="fas fa-lightbulb me-1"></i>基于 ${metadata.knowledge_used.length} 条知识库信息生成`;
|
||||
contentDiv.appendChild(knowledgeDiv);
|
||||
}
|
||||
|
||||
if (metadata.confidence_score) {
|
||||
const confidenceDiv = document.createElement('div');
|
||||
confidenceDiv.className = 'confidence-score';
|
||||
confidenceDiv.textContent = `置信度: ${(metadata.confidence_score * 100).toFixed(1)}%`;
|
||||
contentDiv.appendChild(confidenceDiv);
|
||||
}
|
||||
|
||||
if (metadata.work_order_id) {
|
||||
const workOrderDiv = document.createElement('div');
|
||||
workOrderDiv.className = 'work-order-info';
|
||||
workOrderDiv.innerHTML = `<i class="fas fa-ticket-alt me-1"></i>关联工单: ${metadata.work_order_id}`;
|
||||
contentDiv.appendChild(workOrderDiv);
|
||||
}
|
||||
|
||||
if (role === 'user') {
|
||||
messageDiv.appendChild(contentDiv);
|
||||
messageDiv.appendChild(avatar);
|
||||
} else {
|
||||
messageDiv.appendChild(avatar);
|
||||
messageDiv.appendChild(contentDiv);
|
||||
}
|
||||
|
||||
messagesContainer.appendChild(messageDiv);
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
|
||||
this.messageCount++;
|
||||
}
|
||||
|
||||
addSystemMessage(content) {
|
||||
const messagesContainer = document.getElementById('chat-messages');
|
||||
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = 'text-center text-muted py-2';
|
||||
messageDiv.innerHTML = `<small><i class="fas fa-info-circle me-1"></i>${content}</small>`;
|
||||
|
||||
messagesContainer.appendChild(messageDiv);
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
}
|
||||
|
||||
showTypingIndicator() {
|
||||
document.getElementById('typing-indicator').classList.add('show');
|
||||
}
|
||||
|
||||
hideTypingIndicator() {
|
||||
document.getElementById('typing-indicator').classList.remove('show');
|
||||
}
|
||||
|
||||
updateConnectionStatus(connected) {
|
||||
const statusElement = document.getElementById('connection-status');
|
||||
if (connected) {
|
||||
statusElement.className = 'connection-status connected';
|
||||
statusElement.innerHTML = '<i class="fas fa-circle me-1"></i>HTTP连接';
|
||||
} else {
|
||||
statusElement.className = 'connection-status disconnected';
|
||||
statusElement.innerHTML = '<i class="fas fa-circle me-1"></i>连接断开';
|
||||
}
|
||||
}
|
||||
|
||||
updateSessionInfo() {
|
||||
const sessionInfo = document.getElementById('session-info');
|
||||
sessionInfo.innerHTML = `
|
||||
<div><strong>会话ID:</strong> ${this.sessionId}</div>
|
||||
<div><strong>消息数:</strong> ${this.messageCount}</div>
|
||||
<div><strong>状态:</strong> 活跃</div>
|
||||
`;
|
||||
}
|
||||
|
||||
enableChat() {
|
||||
document.getElementById('start-chat').disabled = true;
|
||||
document.getElementById('end-chat').disabled = false;
|
||||
document.getElementById('message-input').disabled = false;
|
||||
document.getElementById('send-button').disabled = false;
|
||||
}
|
||||
|
||||
disableChat() {
|
||||
document.getElementById('start-chat').disabled = false;
|
||||
document.getElementById('end-chat').disabled = true;
|
||||
document.getElementById('message-input').disabled = true;
|
||||
document.getElementById('send-button').disabled = true;
|
||||
}
|
||||
|
||||
showWorkOrderModal() {
|
||||
if (!this.sessionId) {
|
||||
this.showError('请先开始对话');
|
||||
return;
|
||||
}
|
||||
|
||||
const modal = new bootstrap.Modal(document.getElementById('workOrderModal'));
|
||||
modal.show();
|
||||
}
|
||||
|
||||
showError(message) {
|
||||
this.addSystemMessage(`<span class="text-danger">错误: ${message}</span>`);
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化聊天客户端
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
window.chatClient = new ChatHttpClient();
|
||||
});
|
||||
1507
src/web/static/js/dashboard.js
Normal file
1507
src/web/static/js/dashboard.js
Normal file
File diff suppressed because it is too large
Load Diff
332
src/web/templates/chat.html
Normal file
332
src/web/templates/chat.html
Normal file
@@ -0,0 +1,332 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>TSP助手实时对话</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||||
<style>
|
||||
.chat-container {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
background: linear-gradient(135deg, #007bff, #0056b3);
|
||||
color: white;
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem 0.5rem 0 0;
|
||||
}
|
||||
|
||||
.chat-messages {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 1rem;
|
||||
background-color: #f8f9fa;
|
||||
border-left: 1px solid #dee2e6;
|
||||
border-right: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-bottom: 1rem;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.message.user {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.message.assistant {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
max-width: 70%;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 1rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.message.user .message-content {
|
||||
background: linear-gradient(135deg, #007bff, #0056b3);
|
||||
color: white;
|
||||
border-bottom-right-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.message.assistant .message-content {
|
||||
background: white;
|
||||
color: #333;
|
||||
border: 1px solid #dee2e6;
|
||||
border-bottom-left-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.message-time {
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.7;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.message-avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.message.user .message-avatar {
|
||||
background: linear-gradient(135deg, #007bff, #0056b3);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.message.assistant .message-avatar {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.chat-input {
|
||||
padding: 1rem;
|
||||
background: white;
|
||||
border-top: 1px solid #dee2e6;
|
||||
border-radius: 0 0 0.5rem 0.5rem;
|
||||
}
|
||||
|
||||
.typing-indicator {
|
||||
display: none;
|
||||
padding: 0.75rem 1rem;
|
||||
color: #6c757d;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.typing-indicator.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.knowledge-info {
|
||||
background: #e3f2fd;
|
||||
border-left: 4px solid #2196f3;
|
||||
padding: 0.5rem 1rem;
|
||||
margin: 0.5rem 0;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.confidence-score {
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.7;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.work-order-info {
|
||||
background: #fff3cd;
|
||||
border: 1px solid #ffeaa7;
|
||||
padding: 0.5rem 1rem;
|
||||
margin: 0.5rem 0;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.connection-status {
|
||||
position: fixed;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 1rem;
|
||||
font-size: 0.875rem;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.connection-status.connected {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
.connection-status.disconnected {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
.quick-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.quick-action-btn {
|
||||
padding: 0.25rem 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
border-radius: 1rem;
|
||||
border: 1px solid #007bff;
|
||||
background: white;
|
||||
color: #007bff;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.quick-action-btn:hover {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<!-- 侧边栏 -->
|
||||
<div class="col-md-3">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<h5><i class="fas fa-cog me-2"></i>对话控制</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">用户ID</label>
|
||||
<input type="text" class="form-control" id="user-id" value="user_001">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">工单ID (可选)</label>
|
||||
<input type="number" class="form-control" id="work-order-id" placeholder="留空则自动创建">
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
<button class="btn btn-primary" id="start-chat">
|
||||
<i class="fas fa-play me-2"></i>开始对话
|
||||
</button>
|
||||
<button class="btn btn-secondary" id="end-chat" disabled>
|
||||
<i class="fas fa-stop me-2"></i>结束对话
|
||||
</button>
|
||||
<button class="btn btn-info" id="create-work-order">
|
||||
<i class="fas fa-plus me-2"></i>创建工单
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="mb-3">
|
||||
<h6>快速操作</h6>
|
||||
<div class="quick-actions">
|
||||
<button class="quick-action-btn" data-message="我的车辆无法远程启动">远程启动问题</button>
|
||||
<button class="quick-action-btn" data-message="APP显示车辆信息错误">APP显示问题</button>
|
||||
<button class="quick-action-btn" data-message="蓝牙授权失败">蓝牙授权问题</button>
|
||||
<button class="quick-action-btn" data-message="如何解绑车辆">解绑车辆</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<h6>会话信息</h6>
|
||||
<div id="session-info" class="text-muted">
|
||||
未开始对话
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 聊天区域 -->
|
||||
<div class="col-md-9">
|
||||
<div class="card chat-container">
|
||||
<div class="chat-header">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h4><i class="fas fa-robot me-2"></i>TSP智能助手</h4>
|
||||
<small>基于知识库的智能客服系统</small>
|
||||
</div>
|
||||
<div id="connection-status" class="connection-status disconnected">
|
||||
<i class="fas fa-circle me-1"></i>未连接
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chat-messages" id="chat-messages">
|
||||
<div class="text-center text-muted py-5">
|
||||
<i class="fas fa-comments fa-3x mb-3"></i>
|
||||
<h5>欢迎使用TSP智能助手</h5>
|
||||
<p>请点击"开始对话"按钮开始聊天</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="typing-indicator" id="typing-indicator">
|
||||
<i class="fas fa-spinner fa-spin me-2"></i>助手正在思考中...
|
||||
</div>
|
||||
|
||||
<div class="chat-input">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="message-input"
|
||||
placeholder="请输入您的问题..." disabled>
|
||||
<button class="btn btn-primary" id="send-button" disabled>
|
||||
<i class="fas fa-paper-plane"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 创建工单模态框 -->
|
||||
<div class="modal fade" id="workOrderModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">创建工单</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="work-order-form">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">工单标题</label>
|
||||
<input type="text" class="form-control" id="wo-title" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">问题描述</label>
|
||||
<textarea class="form-control" id="wo-description" rows="3" required></textarea>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">问题分类</label>
|
||||
<select class="form-select" id="wo-category">
|
||||
<option value="技术问题">技术问题</option>
|
||||
<option value="APP功能">APP功能</option>
|
||||
<option value="远程控制">远程控制</option>
|
||||
<option value="车辆绑定">车辆绑定</option>
|
||||
<option value="其他">其他</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">优先级</label>
|
||||
<select class="form-select" id="wo-priority">
|
||||
<option value="low">低</option>
|
||||
<option value="medium" selected>中</option>
|
||||
<option value="high">高</option>
|
||||
<option value="urgent">紧急</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
||||
<button type="button" class="btn btn-primary" id="create-work-order-btn">创建工单</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="{{ url_for('static', filename='js/chat.js') }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
332
src/web/templates/chat_http.html
Normal file
332
src/web/templates/chat_http.html
Normal file
@@ -0,0 +1,332 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>TSP助手实时对话 (HTTP版本)</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||||
<style>
|
||||
.chat-container {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
background: linear-gradient(135deg, #007bff, #0056b3);
|
||||
color: white;
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem 0.5rem 0 0;
|
||||
}
|
||||
|
||||
.chat-messages {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 1rem;
|
||||
background-color: #f8f9fa;
|
||||
border-left: 1px solid #dee2e6;
|
||||
border-right: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-bottom: 1rem;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.message.user {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.message.assistant {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
max-width: 70%;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 1rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.message.user .message-content {
|
||||
background: linear-gradient(135deg, #007bff, #0056b3);
|
||||
color: white;
|
||||
border-bottom-right-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.message.assistant .message-content {
|
||||
background: white;
|
||||
color: #333;
|
||||
border: 1px solid #dee2e6;
|
||||
border-bottom-left-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.message-time {
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.7;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.message-avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.message.user .message-avatar {
|
||||
background: linear-gradient(135deg, #007bff, #0056b3);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.message.assistant .message-avatar {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.chat-input {
|
||||
padding: 1rem;
|
||||
background: white;
|
||||
border-top: 1px solid #dee2e6;
|
||||
border-radius: 0 0 0.5rem 0.5rem;
|
||||
}
|
||||
|
||||
.typing-indicator {
|
||||
display: none;
|
||||
padding: 0.75rem 1rem;
|
||||
color: #6c757d;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.typing-indicator.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.knowledge-info {
|
||||
background: #e3f2fd;
|
||||
border-left: 4px solid #2196f3;
|
||||
padding: 0.5rem 1rem;
|
||||
margin: 0.5rem 0;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.confidence-score {
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.7;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.work-order-info {
|
||||
background: #fff3cd;
|
||||
border: 1px solid #ffeaa7;
|
||||
padding: 0.5rem 1rem;
|
||||
margin: 0.5rem 0;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.connection-status {
|
||||
position: fixed;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 1rem;
|
||||
font-size: 0.875rem;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.connection-status.connected {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
.connection-status.disconnected {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
.quick-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.quick-action-btn {
|
||||
padding: 0.25rem 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
border-radius: 1rem;
|
||||
border: 1px solid #007bff;
|
||||
background: white;
|
||||
color: #007bff;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.quick-action-btn:hover {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<!-- 侧边栏 -->
|
||||
<div class="col-md-3">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<h5><i class="fas fa-cog me-2"></i>对话控制</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">用户ID</label>
|
||||
<input type="text" class="form-control" id="user-id" value="user_001">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">工单ID (可选)</label>
|
||||
<input type="number" class="form-control" id="work-order-id" placeholder="留空则自动创建">
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
<button class="btn btn-primary" id="start-chat">
|
||||
<i class="fas fa-play me-2"></i>开始对话
|
||||
</button>
|
||||
<button class="btn btn-secondary" id="end-chat" disabled>
|
||||
<i class="fas fa-stop me-2"></i>结束对话
|
||||
</button>
|
||||
<button class="btn btn-info" id="create-work-order">
|
||||
<i class="fas fa-plus me-2"></i>创建工单
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="mb-3">
|
||||
<h6>快速操作</h6>
|
||||
<div class="quick-actions">
|
||||
<button class="quick-action-btn" data-message="我的车辆无法远程启动">远程启动问题</button>
|
||||
<button class="quick-action-btn" data-message="APP显示车辆信息错误">APP显示问题</button>
|
||||
<button class="quick-action-btn" data-message="蓝牙授权失败">蓝牙授权问题</button>
|
||||
<button class="quick-action-btn" data-message="如何解绑车辆">解绑车辆</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<h6>会话信息</h6>
|
||||
<div id="session-info" class="text-muted">
|
||||
未开始对话
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 聊天区域 -->
|
||||
<div class="col-md-9">
|
||||
<div class="card chat-container">
|
||||
<div class="chat-header">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h4><i class="fas fa-robot me-2"></i>TSP智能助手 (HTTP版本)</h4>
|
||||
<small>基于知识库的智能客服系统</small>
|
||||
</div>
|
||||
<div id="connection-status" class="connection-status connected">
|
||||
<i class="fas fa-circle me-1"></i>HTTP连接
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chat-messages" id="chat-messages">
|
||||
<div class="text-center text-muted py-5">
|
||||
<i class="fas fa-comments fa-3x mb-3"></i>
|
||||
<h5>欢迎使用TSP智能助手</h5>
|
||||
<p>请点击"开始对话"按钮开始聊天</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="typing-indicator" id="typing-indicator">
|
||||
<i class="fas fa-spinner fa-spin me-2"></i>助手正在思考中...
|
||||
</div>
|
||||
|
||||
<div class="chat-input">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="message-input"
|
||||
placeholder="请输入您的问题..." disabled>
|
||||
<button class="btn btn-primary" id="send-button" disabled>
|
||||
<i class="fas fa-paper-plane"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 创建工单模态框 -->
|
||||
<div class="modal fade" id="workOrderModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">创建工单</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="work-order-form">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">工单标题</label>
|
||||
<input type="text" class="form-control" id="wo-title" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">问题描述</label>
|
||||
<textarea class="form-control" id="wo-description" rows="3" required></textarea>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">问题分类</label>
|
||||
<select class="form-select" id="wo-category">
|
||||
<option value="技术问题">技术问题</option>
|
||||
<option value="APP功能">APP功能</option>
|
||||
<option value="远程控制">远程控制</option>
|
||||
<option value="车辆绑定">车辆绑定</option>
|
||||
<option value="其他">其他</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">优先级</label>
|
||||
<select class="form-select" id="wo-priority">
|
||||
<option value="low">低</option>
|
||||
<option value="medium" selected>中</option>
|
||||
<option value="high">高</option>
|
||||
<option value="urgent">紧急</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
||||
<button type="button" class="btn btn-primary" id="create-work-order-btn">创建工单</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="{{ url_for('static', filename='js/chat_http.js') }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
1193
src/web/templates/dashboard.html
Normal file
1193
src/web/templates/dashboard.html
Normal file
File diff suppressed because it is too large
Load Diff
385
src/web/templates/index.html
Normal file
385
src/web/templates/index.html
Normal file
@@ -0,0 +1,385 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>TSP助手预警管理系统</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||||
<link href="{{ url_for('static', filename='css/style.css') }}" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<!-- 导航栏 -->
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="#">
|
||||
<i class="fas fa-shield-alt me-2"></i>
|
||||
TSP助手预警管理
|
||||
</a>
|
||||
<div class="navbar-nav ms-auto">
|
||||
<span class="navbar-text" id="monitor-status">
|
||||
<i class="fas fa-circle text-warning"></i> 监控状态检查中...
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container-fluid mt-4">
|
||||
<div class="row">
|
||||
<!-- 侧边栏 -->
|
||||
<div class="col-md-3">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5><i class="fas fa-tachometer-alt me-2"></i>控制面板</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-grid gap-2">
|
||||
<button class="btn btn-success" id="start-monitor">
|
||||
<i class="fas fa-play me-2"></i>启动监控
|
||||
</button>
|
||||
<button class="btn btn-danger" id="stop-monitor">
|
||||
<i class="fas fa-stop me-2"></i>停止监控
|
||||
</button>
|
||||
<button class="btn btn-info" id="check-alerts">
|
||||
<i class="fas fa-search me-2"></i>检查预警
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 系统健康状态 -->
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<h5><i class="fas fa-heartbeat me-2"></i>系统健康</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="health-score">
|
||||
<div class="score-circle" id="health-score-circle">
|
||||
<span id="health-score-text">0</span>
|
||||
</div>
|
||||
<div class="health-status" id="health-status">检查中...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<div class="col-md-9">
|
||||
<!-- 预警统计卡片 -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-3">
|
||||
<div class="card bg-danger text-white">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>
|
||||
<h4 id="critical-alerts">0</h4>
|
||||
<p class="mb-0">严重预警</p>
|
||||
</div>
|
||||
<div class="align-self-center">
|
||||
<i class="fas fa-exclamation-triangle fa-2x"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card bg-warning text-white">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>
|
||||
<h4 id="warning-alerts">0</h4>
|
||||
<p class="mb-0">警告预警</p>
|
||||
</div>
|
||||
<div class="align-self-center">
|
||||
<i class="fas fa-exclamation-circle fa-2x"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card bg-info text-white">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>
|
||||
<h4 id="info-alerts">0</h4>
|
||||
<p class="mb-0">信息预警</p>
|
||||
</div>
|
||||
<div class="align-self-center">
|
||||
<i class="fas fa-info-circle fa-2x"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card bg-success text-white">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>
|
||||
<h4 id="total-alerts">0</h4>
|
||||
<p class="mb-0">总预警数</p>
|
||||
</div>
|
||||
<div class="align-self-center">
|
||||
<i class="fas fa-chart-line fa-2x"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 预警列表 -->
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5><i class="fas fa-bell me-2"></i>活跃预警</h5>
|
||||
<div class="d-flex gap-2">
|
||||
<select class="form-select form-select-sm" id="alert-filter" style="width: auto;">
|
||||
<option value="all">全部预警</option>
|
||||
<option value="critical">严重</option>
|
||||
<option value="error">错误</option>
|
||||
<option value="warning">警告</option>
|
||||
<option value="info">信息</option>
|
||||
</select>
|
||||
<select class="form-select form-select-sm" id="alert-sort" style="width: auto;">
|
||||
<option value="time-desc">时间降序</option>
|
||||
<option value="time-asc">时间升序</option>
|
||||
<option value="level-desc">级别降序</option>
|
||||
<option value="level-asc">级别升序</option>
|
||||
</select>
|
||||
<button class="btn btn-sm btn-outline-primary" id="refresh-alerts">
|
||||
<i class="fas fa-sync-alt me-1"></i>刷新
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="alerts-container">
|
||||
<div class="text-center py-4">
|
||||
<i class="fas fa-spinner fa-spin fa-2x text-muted"></i>
|
||||
<p class="text-muted mt-2">加载中...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 预警规则管理 -->
|
||||
<div class="card mt-4">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5><i class="fas fa-cogs me-2"></i>预警规则管理</h5>
|
||||
<button class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#ruleModal">
|
||||
<i class="fas fa-plus me-1"></i>添加规则
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>规则名称</th>
|
||||
<th>类型</th>
|
||||
<th>级别</th>
|
||||
<th>阈值</th>
|
||||
<th>状态</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="rules-table">
|
||||
<tr>
|
||||
<td colspan="6" class="text-center">
|
||||
<i class="fas fa-spinner fa-spin me-2"></i>加载中...
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 添加/编辑规则模态框 -->
|
||||
<div class="modal fade" id="ruleModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="rule-modal-title">添加预警规则</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="rule-form">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">规则名称</label>
|
||||
<input type="text" class="form-control" id="rule-name" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">预警类型</label>
|
||||
<select class="form-select" id="rule-type" required>
|
||||
<option value="performance">性能预警</option>
|
||||
<option value="quality">质量预警</option>
|
||||
<option value="volume">量级预警</option>
|
||||
<option value="system">系统预警</option>
|
||||
<option value="business">业务预警</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">预警级别</label>
|
||||
<select class="form-select" id="rule-level" required>
|
||||
<option value="info">信息</option>
|
||||
<option value="warning">警告</option>
|
||||
<option value="error">错误</option>
|
||||
<option value="critical">严重</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">阈值</label>
|
||||
<input type="number" class="form-control" id="rule-threshold" step="0.01" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">规则描述</label>
|
||||
<textarea class="form-control" id="rule-description" rows="2"></textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">条件表达式</label>
|
||||
<input type="text" class="form-control" id="rule-condition" placeholder="例如: satisfaction_avg < threshold" required>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">检查间隔(秒)</label>
|
||||
<input type="number" class="form-control" id="rule-interval" value="300">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">冷却时间(秒)</label>
|
||||
<input type="number" class="form-control" id="rule-cooldown" value="3600">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<div class="form-check mt-4">
|
||||
<input class="form-check-input" type="checkbox" id="rule-enabled" checked>
|
||||
<label class="form-check-label">启用规则</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
||||
<button type="button" class="btn btn-primary" id="save-rule">保存规则</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 编辑规则模态框 -->
|
||||
<div class="modal fade" id="editRuleModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">编辑预警规则</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="edit-rule-form">
|
||||
<input type="hidden" id="edit-rule-name-original">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">规则名称</label>
|
||||
<input type="text" class="form-control" id="edit-rule-name" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">预警类型</label>
|
||||
<select class="form-select" id="edit-rule-type" required>
|
||||
<option value="performance">性能预警</option>
|
||||
<option value="quality">质量预警</option>
|
||||
<option value="volume">量级预警</option>
|
||||
<option value="system">系统预警</option>
|
||||
<option value="business">业务预警</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">预警级别</label>
|
||||
<select class="form-select" id="edit-rule-level" required>
|
||||
<option value="info">信息</option>
|
||||
<option value="warning">警告</option>
|
||||
<option value="error">错误</option>
|
||||
<option value="critical">严重</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">阈值</label>
|
||||
<input type="number" class="form-control" id="edit-rule-threshold" step="0.01" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">规则描述</label>
|
||||
<textarea class="form-control" id="edit-rule-description" rows="2"></textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">条件表达式</label>
|
||||
<input type="text" class="form-control" id="edit-rule-condition" placeholder="例如: satisfaction_avg < threshold" required>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">检查间隔(秒)</label>
|
||||
<input type="number" class="form-control" id="edit-rule-interval" value="300">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">冷却时间(秒)</label>
|
||||
<input type="number" class="form-control" id="edit-rule-cooldown" value="3600">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<div class="form-check mt-4">
|
||||
<input class="form-check-input" type="checkbox" id="edit-rule-enabled" checked>
|
||||
<label class="form-check-label">启用规则</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
||||
<button type="button" class="btn btn-primary" id="update-rule">更新规则</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 脚本 -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script src="{{ url_for('static', filename='js/app.js') }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
243
src/web/websocket_server.py
Normal file
243
src/web/websocket_server.py
Normal file
@@ -0,0 +1,243 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
WebSocket实时通信服务器
|
||||
提供实时对话功能
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Dict, Set
|
||||
import websockets
|
||||
from websockets.server import WebSocketServerProtocol
|
||||
|
||||
from ..dialogue.realtime_chat import RealtimeChatManager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class WebSocketServer:
|
||||
"""WebSocket服务器"""
|
||||
|
||||
def __init__(self, host: str = "localhost", port: int = 8765):
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.chat_manager = RealtimeChatManager()
|
||||
self.connected_clients: Set[WebSocketServerProtocol] = set()
|
||||
|
||||
async def register_client(self, websocket: WebSocketServerProtocol):
|
||||
"""注册客户端"""
|
||||
self.connected_clients.add(websocket)
|
||||
logger.info(f"客户端连接: {websocket.remote_address}")
|
||||
|
||||
async def unregister_client(self, websocket: WebSocketServerProtocol):
|
||||
"""注销客户端"""
|
||||
self.connected_clients.discard(websocket)
|
||||
logger.info(f"客户端断开: {websocket.remote_address}")
|
||||
|
||||
async def handle_message(self, websocket: WebSocketServerProtocol, message: str):
|
||||
"""处理客户端消息"""
|
||||
try:
|
||||
data = json.loads(message)
|
||||
message_type = data.get("type")
|
||||
message_id = data.get("messageId") # 获取消息ID
|
||||
|
||||
if message_type == "create_session":
|
||||
await self._handle_create_session(websocket, data, message_id)
|
||||
elif message_type == "send_message":
|
||||
await self._handle_send_message(websocket, data, message_id)
|
||||
elif message_type == "get_history":
|
||||
await self._handle_get_history(websocket, data, message_id)
|
||||
elif message_type == "create_work_order":
|
||||
await self._handle_create_work_order(websocket, data, message_id)
|
||||
elif message_type == "get_work_order_status":
|
||||
await self._handle_get_work_order_status(websocket, data, message_id)
|
||||
elif message_type == "end_session":
|
||||
await self._handle_end_session(websocket, data, message_id)
|
||||
else:
|
||||
await self._send_error(websocket, "未知消息类型", message_id)
|
||||
|
||||
except json.JSONDecodeError:
|
||||
await self._send_error(websocket, "JSON格式错误")
|
||||
except Exception as e:
|
||||
logger.error(f"处理消息失败: {e}")
|
||||
await self._send_error(websocket, f"处理消息失败: {str(e)}")
|
||||
|
||||
async def _handle_create_session(self, websocket: WebSocketServerProtocol, data: Dict, message_id: str = None):
|
||||
"""处理创建会话请求"""
|
||||
user_id = data.get("user_id", "anonymous")
|
||||
work_order_id = data.get("work_order_id")
|
||||
|
||||
session_id = self.chat_manager.create_session(user_id, work_order_id)
|
||||
|
||||
response = {
|
||||
"type": "session_created",
|
||||
"session_id": session_id,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
if message_id:
|
||||
response["messageId"] = message_id
|
||||
|
||||
await websocket.send(json.dumps(response, ensure_ascii=False))
|
||||
|
||||
async def _handle_send_message(self, websocket: WebSocketServerProtocol, data: Dict, message_id: str = None):
|
||||
"""处理发送消息请求"""
|
||||
session_id = data.get("session_id")
|
||||
message = data.get("message")
|
||||
|
||||
if not session_id or not message:
|
||||
await self._send_error(websocket, "缺少必要参数", message_id)
|
||||
return
|
||||
|
||||
# 处理消息
|
||||
result = self.chat_manager.process_message(session_id, message)
|
||||
|
||||
response = {
|
||||
"type": "message_response",
|
||||
"session_id": session_id,
|
||||
"result": result,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
if message_id:
|
||||
response["messageId"] = message_id
|
||||
|
||||
await websocket.send(json.dumps(response, ensure_ascii=False))
|
||||
|
||||
async def _handle_get_history(self, websocket: WebSocketServerProtocol, data: Dict, message_id: str = None):
|
||||
"""处理获取历史记录请求"""
|
||||
session_id = data.get("session_id")
|
||||
|
||||
if not session_id:
|
||||
await self._send_error(websocket, "缺少会话ID", message_id)
|
||||
return
|
||||
|
||||
history = self.chat_manager.get_session_history(session_id)
|
||||
|
||||
response = {
|
||||
"type": "history_response",
|
||||
"session_id": session_id,
|
||||
"history": history,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
if message_id:
|
||||
response["messageId"] = message_id
|
||||
|
||||
await websocket.send(json.dumps(response, ensure_ascii=False))
|
||||
|
||||
async def _handle_create_work_order(self, websocket: WebSocketServerProtocol, data: Dict, message_id: str = None):
|
||||
"""处理创建工单请求"""
|
||||
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:
|
||||
await self._send_error(websocket, "缺少必要参数", message_id)
|
||||
return
|
||||
|
||||
result = self.chat_manager.create_work_order(session_id, title, description, category, priority)
|
||||
|
||||
response = {
|
||||
"type": "work_order_created",
|
||||
"session_id": session_id,
|
||||
"result": result,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
if message_id:
|
||||
response["messageId"] = message_id
|
||||
|
||||
await websocket.send(json.dumps(response, ensure_ascii=False))
|
||||
|
||||
async def _handle_get_work_order_status(self, websocket: WebSocketServerProtocol, data: Dict, message_id: str = None):
|
||||
"""处理获取工单状态请求"""
|
||||
work_order_id = data.get("work_order_id")
|
||||
|
||||
if not work_order_id:
|
||||
await self._send_error(websocket, "缺少工单ID", message_id)
|
||||
return
|
||||
|
||||
result = self.chat_manager.get_work_order_status(work_order_id)
|
||||
|
||||
response = {
|
||||
"type": "work_order_status",
|
||||
"work_order_id": work_order_id,
|
||||
"result": result,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
if message_id:
|
||||
response["messageId"] = message_id
|
||||
|
||||
await websocket.send(json.dumps(response, ensure_ascii=False))
|
||||
|
||||
async def _handle_end_session(self, websocket: WebSocketServerProtocol, data: Dict, message_id: str = None):
|
||||
"""处理结束会话请求"""
|
||||
session_id = data.get("session_id")
|
||||
|
||||
if not session_id:
|
||||
await self._send_error(websocket, "缺少会话ID", message_id)
|
||||
return
|
||||
|
||||
success = self.chat_manager.end_session(session_id)
|
||||
|
||||
response = {
|
||||
"type": "session_ended",
|
||||
"session_id": session_id,
|
||||
"success": success,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
if message_id:
|
||||
response["messageId"] = message_id
|
||||
|
||||
await websocket.send(json.dumps(response, ensure_ascii=False))
|
||||
|
||||
async def _send_error(self, websocket: WebSocketServerProtocol, error_message: str, message_id: str = None):
|
||||
"""发送错误消息"""
|
||||
response = {
|
||||
"type": "error",
|
||||
"message": error_message,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
if message_id:
|
||||
response["messageId"] = message_id
|
||||
|
||||
await websocket.send(json.dumps(response, ensure_ascii=False))
|
||||
|
||||
async def handle_client(self, websocket: WebSocketServerProtocol, path: str):
|
||||
"""处理客户端连接"""
|
||||
await self.register_client(websocket)
|
||||
|
||||
try:
|
||||
async for message in websocket:
|
||||
await self.handle_message(websocket, message)
|
||||
except websockets.exceptions.ConnectionClosed:
|
||||
pass
|
||||
finally:
|
||||
await self.unregister_client(websocket)
|
||||
|
||||
async def start_server(self):
|
||||
"""启动WebSocket服务器"""
|
||||
logger.info(f"启动WebSocket服务器: ws://{self.host}:{self.port}")
|
||||
|
||||
async with websockets.serve(self.handle_client, self.host, self.port):
|
||||
await asyncio.Future() # 保持服务器运行
|
||||
|
||||
def run(self):
|
||||
"""运行服务器"""
|
||||
asyncio.run(self.start_server())
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 设置日志
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
# 启动服务器
|
||||
server = WebSocketServer()
|
||||
server.run()
|
||||
Reference in New Issue
Block a user