feat: 优化数据分析页面,添加Excel工单导入功能

- 优化数据分析页面,添加可定制的图表功能
- 支持多种图表类型:折线图、柱状图、饼图、环形图、雷达图、极坐标图
- 添加图表定制功能:时间范围选择、数据维度选择
- 实现Excel工单导入功能,支持详情.xlsx文件
- 添加工单编辑功能,包括前端UI和后端API
- 修复WebSocket连接错误,处理invalid Connection header问题
- 简化预警管理参数,改为卡片式选择
- 实现Agent主动调用,无需人工干预
- 改进知识库导入,结合累计工单内容与大模型输出
This commit is contained in:
zhaojie
2025-09-10 23:13:08 +08:00
parent e08b570f22
commit 0c03ff20aa
16 changed files with 3077 additions and 51 deletions

Binary file not shown.

358
src/agent/auto_monitor.py Normal file
View File

@@ -0,0 +1,358 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
自动监控服务
实现Agent的主动调用功能
"""
import asyncio
import logging
import threading
import time
from datetime import datetime, timedelta
from typing import Dict, Any, List, Optional
import json
logger = logging.getLogger(__name__)
class AutoMonitorService:
"""自动监控服务"""
def __init__(self, agent_assistant):
self.agent_assistant = agent_assistant
self.is_running = False
self.monitor_thread = None
self.check_interval = 300 # 5分钟检查一次
self.last_check_time = None
self.monitoring_stats = {
"total_checks": 0,
"proactive_actions": 0,
"last_action_time": None,
"error_count": 0
}
def start_auto_monitoring(self) -> bool:
"""启动自动监控"""
try:
if self.is_running:
logger.warning("自动监控已在运行中")
return True
self.is_running = True
self.monitor_thread = threading.Thread(target=self._monitoring_loop, daemon=True)
self.monitor_thread.start()
logger.info("自动监控服务已启动")
return True
except Exception as e:
logger.error(f"启动自动监控失败: {e}")
return False
def stop_auto_monitoring(self) -> bool:
"""停止自动监控"""
try:
self.is_running = False
if self.monitor_thread and self.monitor_thread.is_alive():
self.monitor_thread.join(timeout=5)
logger.info("自动监控服务已停止")
return True
except Exception as e:
logger.error(f"停止自动监控失败: {e}")
return False
def _monitoring_loop(self):
"""监控循环"""
logger.info("自动监控循环已启动")
while self.is_running:
try:
# 执行监控检查
self._perform_monitoring_check()
# 等待下次检查
time.sleep(self.check_interval)
except Exception as e:
logger.error(f"监控循环错误: {e}")
self.monitoring_stats["error_count"] += 1
time.sleep(60) # 出错后等待1分钟
def _perform_monitoring_check(self):
"""执行监控检查"""
try:
self.monitoring_stats["total_checks"] += 1
self.last_check_time = datetime.now()
logger.info(f"执行第 {self.monitoring_stats['total_checks']} 次自动监控检查")
# 1. 检查系统健康状态
self._check_system_health()
# 2. 检查预警状态
self._check_alert_status()
# 3. 检查工单积压
self._check_workorder_backlog()
# 4. 检查知识库质量
self._check_knowledge_quality()
# 5. 检查用户满意度
self._check_user_satisfaction()
# 6. 检查系统性能
self._check_system_performance()
except Exception as e:
logger.error(f"执行监控检查失败: {e}")
self.monitoring_stats["error_count"] += 1
def _check_system_health(self):
"""检查系统健康状态"""
try:
health = self.agent_assistant.get_system_health()
health_score = health.get("health_score", 1.0)
if health_score < 0.7:
self._trigger_proactive_action({
"type": "system_health_warning",
"priority": "high",
"description": f"系统健康分数较低: {health_score:.2f}",
"action": "建议立即检查系统状态",
"data": health
})
except Exception as e:
logger.error(f"检查系统健康状态失败: {e}")
def _check_alert_status(self):
"""检查预警状态"""
try:
alerts = self.agent_assistant.get_active_alerts()
alert_count = len(alerts) if isinstance(alerts, list) else 0
if alert_count > 5:
self._trigger_proactive_action({
"type": "alert_overflow",
"priority": "high",
"description": f"活跃预警数量过多: {alert_count}",
"action": "建议立即处理预警",
"data": {"alert_count": alert_count}
})
except Exception as e:
logger.error(f"检查预警状态失败: {e}")
def _check_workorder_backlog(self):
"""检查工单积压"""
try:
# 获取工单统计
workorders = self.agent_assistant.get_workorders()
if isinstance(workorders, list):
open_count = len([w for w in workorders if w.get("status") == "open"])
in_progress_count = len([w for w in workorders if w.get("status") == "in_progress"])
total_pending = open_count + in_progress_count
if total_pending > 10:
self._trigger_proactive_action({
"type": "workorder_backlog",
"priority": "medium",
"description": f"待处理工单过多: {total_pending}",
"action": "建议增加处理人员或优化流程",
"data": {
"open_count": open_count,
"in_progress_count": in_progress_count,
"total_pending": total_pending
}
})
except Exception as e:
logger.error(f"检查工单积压失败: {e}")
def _check_knowledge_quality(self):
"""检查知识库质量"""
try:
stats = self.agent_assistant.knowledge_manager.get_knowledge_stats()
avg_confidence = stats.get("average_confidence", 0.8)
total_entries = stats.get("total_entries", 0)
if avg_confidence < 0.6:
self._trigger_proactive_action({
"type": "knowledge_quality_low",
"priority": "medium",
"description": f"知识库平均置信度较低: {avg_confidence:.2f}",
"action": "建议更新和优化知识库内容",
"data": {"avg_confidence": avg_confidence, "total_entries": total_entries}
})
except Exception as e:
logger.error(f"检查知识库质量失败: {e}")
def _check_user_satisfaction(self):
"""检查用户满意度"""
try:
# 模拟检查用户满意度
# 这里可以从数据库或API获取真实的满意度数据
satisfaction_score = 0.75 # 模拟数据
if satisfaction_score < 0.7:
self._trigger_proactive_action({
"type": "low_satisfaction",
"priority": "high",
"description": f"用户满意度较低: {satisfaction_score:.2f}",
"action": "建议分析低满意度原因并改进服务",
"data": {"satisfaction_score": satisfaction_score}
})
except Exception as e:
logger.error(f"检查用户满意度失败: {e}")
def _check_system_performance(self):
"""检查系统性能"""
try:
# 模拟检查系统性能指标
response_time = 1.2 # 模拟响应时间(秒)
error_rate = 0.02 # 模拟错误率
if response_time > 2.0:
self._trigger_proactive_action({
"type": "slow_response",
"priority": "medium",
"description": f"系统响应时间过慢: {response_time:.2f}",
"action": "建议优化系统性能",
"data": {"response_time": response_time}
})
if error_rate > 0.05:
self._trigger_proactive_action({
"type": "high_error_rate",
"priority": "high",
"description": f"系统错误率过高: {error_rate:.2%}",
"action": "建议立即检查系统错误",
"data": {"error_rate": error_rate}
})
except Exception as e:
logger.error(f"检查系统性能失败: {e}")
def _trigger_proactive_action(self, action_data: Dict[str, Any]):
"""触发主动行动"""
try:
self.monitoring_stats["proactive_actions"] += 1
self.monitoring_stats["last_action_time"] = datetime.now()
logger.info(f"触发主动行动: {action_data['type']} - {action_data['description']}")
# 记录主动行动
self._log_proactive_action(action_data)
# 根据行动类型执行相应操作
self._execute_proactive_action(action_data)
except Exception as e:
logger.error(f"触发主动行动失败: {e}")
def _log_proactive_action(self, action_data: Dict[str, Any]):
"""记录主动行动"""
try:
log_entry = {
"timestamp": datetime.now().isoformat(),
"action_type": action_data["type"],
"priority": action_data["priority"],
"description": action_data["description"],
"action": action_data["action"],
"data": action_data.get("data", {})
}
# 这里可以将日志保存到数据库或文件
logger.info(f"主动行动记录: {json.dumps(log_entry, ensure_ascii=False)}")
except Exception as e:
logger.error(f"记录主动行动失败: {e}")
def _execute_proactive_action(self, action_data: Dict[str, Any]):
"""执行主动行动"""
try:
action_type = action_data["type"]
if action_type == "system_health_warning":
self._handle_system_health_warning(action_data)
elif action_type == "alert_overflow":
self._handle_alert_overflow(action_data)
elif action_type == "workorder_backlog":
self._handle_workorder_backlog(action_data)
elif action_type == "knowledge_quality_low":
self._handle_knowledge_quality_low(action_data)
elif action_type == "low_satisfaction":
self._handle_low_satisfaction(action_data)
elif action_type == "slow_response":
self._handle_slow_response(action_data)
elif action_type == "high_error_rate":
self._handle_high_error_rate(action_data)
else:
logger.warning(f"未知的主动行动类型: {action_type}")
except Exception as e:
logger.error(f"执行主动行动失败: {e}")
def _handle_system_health_warning(self, action_data: Dict[str, Any]):
"""处理系统健康警告"""
logger.info("处理系统健康警告")
# 这里可以实现具体的处理逻辑,如发送通知、重启服务等
def _handle_alert_overflow(self, action_data: Dict[str, Any]):
"""处理预警溢出"""
logger.info("处理预警溢出")
# 这里可以实现具体的处理逻辑,如自动处理预警、发送通知等
def _handle_workorder_backlog(self, action_data: Dict[str, Any]):
"""处理工单积压"""
logger.info("处理工单积压")
# 这里可以实现具体的处理逻辑,如自动分配工单、发送提醒等
def _handle_knowledge_quality_low(self, action_data: Dict[str, Any]):
"""处理知识库质量低"""
logger.info("处理知识库质量低")
# 这里可以实现具体的处理逻辑,如自动更新知识库、发送提醒等
def _handle_low_satisfaction(self, action_data: Dict[str, Any]):
"""处理低满意度"""
logger.info("处理低满意度")
# 这里可以实现具体的处理逻辑,如分析原因、发送通知等
def _handle_slow_response(self, action_data: Dict[str, Any]):
"""处理响应慢"""
logger.info("处理响应慢")
# 这里可以实现具体的处理逻辑,如优化配置、发送通知等
def _handle_high_error_rate(self, action_data: Dict[str, Any]):
"""处理高错误率"""
logger.info("处理高错误率")
# 这里可以实现具体的处理逻辑,如检查日志、发送通知等
def get_monitoring_status(self) -> Dict[str, Any]:
"""获取监控状态"""
return {
"is_running": self.is_running,
"check_interval": self.check_interval,
"last_check_time": self.last_check_time.isoformat() if self.last_check_time else None,
"stats": self.monitoring_stats
}
def update_check_interval(self, interval: int) -> bool:
"""更新检查间隔"""
try:
if interval < 60: # 最少1分钟
logger.warning("检查间隔不能少于60秒")
return False
self.check_interval = interval
logger.info(f"检查间隔已更新为 {interval}")
return True
except Exception as e:
logger.error(f"更新检查间隔失败: {e}")
return False

View File

@@ -13,6 +13,7 @@ import json
from src.main import TSPAssistant
from src.agent import AgentCore, AgentState
from src.agent.auto_monitor import AutoMonitorService
logger = logging.getLogger(__name__)
@@ -26,6 +27,9 @@ class TSPAgentAssistant(TSPAssistant):
# 初始化Agent核心
self.agent_core = AgentCore()
# 初始化自动监控服务
self.auto_monitor = AutoMonitorService(self)
# Agent特有功能
self.is_agent_mode = True
self.proactive_tasks = []
@@ -409,10 +413,13 @@ class TSPAgentAssistant(TSPAssistant):
def get_agent_status(self) -> Dict[str, Any]:
"""获取Agent状态"""
try:
# 获取自动监控状态
monitor_status = self.auto_monitor.get_monitoring_status()
return {
"success": True,
"agent_mode": self.is_agent_mode,
"monitoring_active": getattr(self, '_monitoring_active', False),
"monitoring_active": monitor_status["is_running"],
"status": "active" if self.is_agent_mode else "inactive",
"active_goals": 0, # 简化处理
"available_tools": 6, # 简化处理
@@ -424,7 +431,8 @@ class TSPAgentAssistant(TSPAssistant):
{"name": "analyze_data", "usage_count": 0, "success_rate": 0.8},
{"name": "send_notification", "usage_count": 0, "success_rate": 0.8}
],
"execution_history": []
"execution_history": [],
"auto_monitor": monitor_status
}
except Exception as e:
logger.error(f"获取Agent状态失败: {e}")
@@ -456,11 +464,14 @@ class TSPAgentAssistant(TSPAssistant):
# 启动基础监控
self.start_monitoring()
# 启动Agent主动监控同步版本
self._start_monitoring_loop()
logger.info("主动监控已启动")
return True
# 启动自动监控服务
success = self.auto_monitor.start_auto_monitoring()
if success:
logger.info("主动监控已启动")
return True
else:
logger.error("启动自动监控服务失败")
return False
except Exception as e:
logger.error(f"启动主动监控失败: {e}")
return False
@@ -471,11 +482,14 @@ class TSPAgentAssistant(TSPAssistant):
# 停止基础监控
self.stop_monitoring()
# 停止Agent主动监控
self._stop_monitoring_loop()
logger.info("主动监控已停止")
return True
# 停止动监控服务
success = self.auto_monitor.stop_auto_monitoring()
if success:
logger.info("主动监控已停止")
return True
else:
logger.error("停止自动监控服务失败")
return False
except Exception as e:
logger.error(f"停止主动监控失败: {e}")
return False
@@ -619,30 +633,38 @@ class TSPAgentAssistant(TSPAssistant):
return ""
def _extract_knowledge_from_content(self, content: str, filename: str) -> List[Dict[str, Any]]:
"""从内容中提取知识"""
"""从内容中提取知识,结合工单数据优化"""
try:
# 构建提示词
# 获取历史工单数据用于参考
workorder_data = self._get_workorder_insights()
# 构建增强的提示词
prompt = f"""
请从以下文档内容中提取问答对,用于构建知识库:
请从以下文档内容中提取问答对,用于构建知识库。请结合历史工单数据来优化提取结果
文档名称:{filename}
文档内容:
{content[:2000]}...
历史工单数据参考:
{workorder_data}
请按照以下格式提取问答对:
1. 问题:具体的问题描述
2. 答案:详细的答案内容
1. 问题:具体的问题描述(参考工单中的常见问题)
2. 答案:详细的答案内容(结合工单处理经验)
3. 分类问题所属类别技术问题、APP功能、远程控制、车辆绑定、其他
4. 置信度0-1之间的数值
5. 工单关联:是否与历史工单相关
请提取3-5个最有价值的问答对每个问答对都要完整且实用
请提取3-5个最有价值的问答对优先提取与历史工单问题相关的问答对
返回格式为JSON数组例如
[
{{
"question": "如何远程启动车辆?",
"answer": "远程启动车辆需要满足以下条件1. 车辆处于P档 2. 手刹拉起 3. 车门已锁 4. 电池电量充足",
"answer": "远程启动车辆需要满足以下条件1. 车辆处于P档 2. 手刹拉起 3. 车门已锁 4. 电池电量充足。如果仍然无法启动,请检查车辆是否处于可启动状态。",
"category": "远程控制",
"confidence_score": 0.9
"confidence_score": 0.9,
"workorder_related": true
}}
]
"""
@@ -700,6 +722,52 @@ class TSPAgentAssistant(TSPAssistant):
logger.error(f"提取知识失败: {e}")
return []
def _get_workorder_insights(self) -> str:
"""获取工单数据洞察"""
try:
# 获取工单数据
workorders = self.get_workorders()
if not isinstance(workorders, list):
return "暂无工单数据"
# 分析工单数据
categories = {}
common_issues = []
resolutions = []
for workorder in workorders[:20]: # 取最近20个工单
category = workorder.get("category", "其他")
categories[category] = categories.get(category, 0) + 1
# 提取常见问题
title = workorder.get("title", "")
description = workorder.get("description", "")
if title and len(title) > 5:
common_issues.append(title)
# 提取解决方案(如果有)
resolution = workorder.get("resolution", "")
if resolution and len(resolution) > 10:
resolutions.append(resolution[:100]) # 截取前100字符
# 构建工单洞察文本
insights = f"""
工单统计:
- 总工单数:{len(workorders)}
- 问题分类分布:{dict(list(categories.items())[:5])}
常见问题:
{chr(10).join(common_issues[:10])}
解决方案示例:
{chr(10).join(resolutions[:5])}
"""
return insights
except Exception as e:
logger.error(f"获取工单洞察失败: {e}")
return "获取工单数据失败"
def _parse_knowledge_manually(self, content: str) -> List[Dict[str, Any]]:
"""手动解析知识内容"""
try:

Binary file not shown.

View File

@@ -8,9 +8,13 @@ TSP助手预警管理Web应用
import sys
import os
import json
import pandas as pd
from datetime import datetime, timedelta
from flask import Flask, render_template, request, jsonify, redirect, url_for
from openpyxl import Workbook
from openpyxl.styles import Font
from flask import Flask, render_template, request, jsonify, redirect, url_for, send_from_directory, send_file
from flask_cors import CORS
from werkzeug.utils import secure_filename
# 添加项目根目录到Python路径
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
@@ -24,6 +28,11 @@ from src.vehicle.vehicle_data_manager import VehicleDataManager
app = Flask(__name__)
CORS(app)
# 配置上传文件夹
UPLOAD_FOLDER = 'uploads'
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB max file size
# 初始化TSP助手和Agent助手
assistant = TSPAssistant()
agent_assistant = TSPAgentAssistant()
@@ -541,6 +550,60 @@ def get_workorders():
"priority": "medium",
"status": "in_progress",
"created_at": "2024-01-01T11:00:00Z"
},
{
"id": 3,
"title": "蓝牙连接失败",
"description": "用户无法通过蓝牙连接车辆",
"category": "蓝牙功能",
"priority": "high",
"status": "open",
"created_at": "2024-01-01T12:00:00Z"
},
{
"id": 4,
"title": "车辆定位不准确",
"description": "APP中显示的车辆位置与实际位置不符",
"category": "定位功能",
"priority": "medium",
"status": "resolved",
"created_at": "2024-01-01T13:00:00Z"
},
{
"id": 5,
"title": "远程解锁失败",
"description": "用户无法通过APP远程解锁车辆",
"category": "远程控制",
"priority": "urgent",
"status": "open",
"created_at": "2024-01-01T14:00:00Z"
},
{
"id": 6,
"title": "APP闪退问题",
"description": "用户反映APP在使用过程中频繁闪退",
"category": "APP功能",
"priority": "high",
"status": "in_progress",
"created_at": "2024-01-01T15:00:00Z"
},
{
"id": 7,
"title": "车辆状态更新延迟",
"description": "车辆状态信息更新不及时,存在延迟",
"category": "数据同步",
"priority": "low",
"status": "open",
"created_at": "2024-01-01T16:00:00Z"
},
{
"id": 8,
"title": "用户认证失败",
"description": "部分用户无法正常登录APP",
"category": "用户认证",
"priority": "high",
"status": "resolved",
"created_at": "2024-01-01T17:00:00Z"
}
]
@@ -569,16 +632,313 @@ def create_workorder():
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/api/workorders/<int:workorder_id>')
def get_workorder_details(workorder_id):
"""获取工单详情"""
try:
# 这里应该从数据库获取工单详情
# 暂时返回模拟数据
workorder = {
"id": workorder_id,
"order_id": f"WO{workorder_id:06d}",
"title": "车辆无法远程启动",
"description": "用户反映APP中远程启动功能无法使用点击启动按钮后没有任何反应车辆也没有响应。",
"category": "远程控制",
"priority": "high",
"status": "open",
"created_at": "2024-01-01T10:00:00Z",
"updated_at": "2024-01-01T10:00:00Z",
"resolution": None,
"satisfaction_score": None,
"conversations": [
{
"id": 1,
"user_message": "我的车辆无法远程启动",
"assistant_response": "我了解您的问题。让我帮您排查一下远程启动功能的问题。",
"timestamp": "2024-01-01T10:05:00Z"
},
{
"id": 2,
"user_message": "点击启动按钮后没有任何反应",
"assistant_response": "这种情况通常是由于网络连接或车辆状态问题导致的。请检查车辆是否处于可启动状态。",
"timestamp": "2024-01-01T10:10:00Z"
}
]
}
return jsonify(workorder)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/api/workorders/<int:workorder_id>', methods=['PUT'])
def update_workorder(workorder_id):
"""更新工单"""
try:
data = request.get_json()
# 验证必填字段
if not data.get('title') or not data.get('description'):
return jsonify({"error": "标题和描述不能为空"}), 400
# 这里应该更新数据库中的工单
# 暂时返回成功响应,实际应用中应该调用数据库更新
updated_workorder = {
"id": workorder_id,
"title": data.get('title'),
"description": data.get('description'),
"category": data.get('category', '技术问题'),
"priority": data.get('priority', 'medium'),
"status": data.get('status', 'open'),
"resolution": data.get('resolution'),
"satisfaction_score": data.get('satisfaction_score'),
"updated_at": datetime.now().isoformat()
}
return jsonify({
"success": True,
"message": "工单更新成功",
"workorder": updated_workorder
})
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")
time_range = request.args.get('timeRange', '30')
dimension = request.args.get('dimension', 'workorders')
# 生成模拟分析数据
analytics = generate_analytics_data(int(time_range), dimension)
return jsonify(analytics)
except Exception as e:
return jsonify({"error": str(e)}), 500
def generate_analytics_data(days, dimension):
"""生成分析数据"""
import random
from datetime import datetime, timedelta
# 生成时间序列数据
trend_data = []
for i in range(days):
date = (datetime.now() - timedelta(days=days-i-1)).strftime('%Y-%m-%d')
workorders = random.randint(5, 25)
alerts = random.randint(0, 10)
trend_data.append({
'date': date,
'workorders': workorders,
'alerts': alerts
})
# 工单统计
workorders_stats = {
'total': random.randint(100, 500),
'open': random.randint(10, 50),
'in_progress': random.randint(5, 30),
'resolved': random.randint(50, 200),
'closed': random.randint(20, 100),
'by_category': {
'技术问题': random.randint(20, 80),
'业务问题': random.randint(15, 60),
'系统故障': random.randint(10, 40),
'功能需求': random.randint(5, 30),
'其他': random.randint(5, 20)
},
'by_priority': {
'low': random.randint(20, 60),
'medium': random.randint(30, 80),
'high': random.randint(10, 40),
'urgent': random.randint(5, 20)
}
}
# 满意度分析
satisfaction_stats = {
'average': round(random.uniform(3.5, 4.8), 1),
'distribution': {
'1': random.randint(0, 5),
'2': random.randint(0, 10),
'3': random.randint(5, 20),
'4': random.randint(20, 50),
'5': random.randint(30, 80)
}
}
# 预警统计
alerts_stats = {
'total': random.randint(50, 200),
'active': random.randint(5, 30),
'resolved': random.randint(20, 100),
'by_level': {
'low': random.randint(10, 40),
'medium': random.randint(15, 50),
'high': random.randint(5, 25),
'critical': random.randint(2, 10)
}
}
# 性能指标
performance_stats = {
'response_time': round(random.uniform(0.5, 2.0), 2),
'uptime': round(random.uniform(95, 99.9), 1),
'error_rate': round(random.uniform(0.1, 2.0), 2),
'throughput': random.randint(1000, 5000)
}
return {
'trend': trend_data,
'workorders': workorders_stats,
'satisfaction': satisfaction_stats,
'alerts': alerts_stats,
'performance': performance_stats,
'summary': {
'total_workorders': workorders_stats['total'],
'resolution_rate': round((workorders_stats['resolved'] / workorders_stats['total']) * 100, 1) if workorders_stats['total'] > 0 else 0,
'avg_satisfaction': satisfaction_stats['average'],
'active_alerts': alerts_stats['active']
}
}
@app.route('/api/analytics/export')
def export_analytics():
"""导出分析报告"""
try:
# 生成Excel报告
analytics = generate_analytics_data(30, 'workorders')
# 创建工作簿
wb = Workbook()
ws = wb.active
ws.title = "分析报告"
# 添加标题
ws['A1'] = 'TSP智能助手分析报告'
ws['A1'].font = Font(size=16, bold=True)
# 添加工单统计
ws['A3'] = '工单统计'
ws['A3'].font = Font(bold=True)
ws['A4'] = '总工单数'
ws['B4'] = analytics['workorders']['total']
ws['A5'] = '待处理'
ws['B5'] = analytics['workorders']['open']
ws['A6'] = '已解决'
ws['B6'] = analytics['workorders']['resolved']
# 保存文件
report_path = 'uploads/analytics_report.xlsx'
os.makedirs('uploads', exist_ok=True)
wb.save(report_path)
return send_file(report_path, as_attachment=True, download_name='analytics_report.xlsx')
except Exception as e:
return jsonify({"error": str(e)}), 500
# 工单导入相关API
@app.route('/api/workorders/import', methods=['POST'])
def import_workorders():
"""导入Excel工单文件"""
try:
# 检查是否有文件上传
if 'file' not in request.files:
return jsonify({"error": "没有上传文件"}), 400
file = request.files['file']
if file.filename == '':
return jsonify({"error": "没有选择文件"}), 400
if not file.filename.endswith(('.xlsx', '.xls')):
return jsonify({"error": "只支持Excel文件(.xlsx, .xls)"}), 400
# 保存上传的文件
filename = secure_filename(file.filename)
upload_path = os.path.join('uploads', filename)
os.makedirs('uploads', exist_ok=True)
file.save(upload_path)
# 解析Excel文件
try:
df = pd.read_excel(upload_path)
imported_workorders = []
# 处理每一行数据
for index, row in df.iterrows():
# 根据Excel列名映射到工单字段
workorder = {
"id": len(assistant.work_orders) + index + 1, # 生成新ID
"order_id": f"WO{len(assistant.work_orders) + index + 1:06d}",
"title": str(row.get('标题', row.get('title', f'导入工单 {index + 1}'))),
"description": str(row.get('描述', row.get('description', ''))),
"category": str(row.get('分类', row.get('category', '技术问题'))),
"priority": str(row.get('优先级', row.get('priority', 'medium'))),
"status": str(row.get('状态', row.get('status', 'open'))),
"created_at": datetime.now().isoformat(),
"updated_at": datetime.now().isoformat(),
"resolution": str(row.get('解决方案', row.get('resolution', ''))) if pd.notna(row.get('解决方案', row.get('resolution'))) else None,
"satisfaction_score": int(row.get('满意度', row.get('satisfaction_score', 0))) if pd.notna(row.get('满意度', row.get('satisfaction_score'))) else None
}
# 添加到工单列表(这里应该保存到数据库)
assistant.work_orders.append(workorder)
imported_workorders.append(workorder)
# 清理上传的文件
os.remove(upload_path)
return jsonify({
"success": True,
"message": f"成功导入 {len(imported_workorders)} 个工单",
"imported_count": len(imported_workorders),
"workorders": imported_workorders
})
except Exception as e:
# 清理上传的文件
if os.path.exists(upload_path):
os.remove(upload_path)
return jsonify({"error": f"解析Excel文件失败: {str(e)}"}), 400
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/api/workorders/import/template')
def download_import_template():
"""下载工单导入模板"""
try:
# 创建模板数据
template_data = {
'标题': ['车辆无法启动', '空调不制冷', '导航系统故障'],
'描述': ['用户反映车辆无法正常启动', '空调系统无法制冷', '导航系统显示异常'],
'分类': ['技术问题', '技术问题', '技术问题'],
'优先级': ['high', 'medium', 'low'],
'状态': ['open', 'in_progress', 'resolved'],
'解决方案': ['检查电池和启动系统', '检查制冷剂和压缩机', '更新导航软件'],
'满意度': [5, 4, 5]
}
df = pd.DataFrame(template_data)
# 保存为Excel文件
template_path = 'uploads/workorder_template.xlsx'
os.makedirs('uploads', exist_ok=True)
df.to_excel(template_path, index=False)
return jsonify({
"success": True,
"template_url": f"/uploads/workorder_template.xlsx"
})
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/uploads/<filename>')
def uploaded_file(filename):
"""提供上传文件的下载服务"""
return send_from_directory(app.config['UPLOAD_FOLDER'], filename)
# 系统设置相关API
@app.route('/api/settings')
def get_settings():

View File

@@ -429,3 +429,178 @@ body {
background-color: #d1ecf1;
border-color: #17a2b8;
}
/* 预设规则卡片样式 */
.preset-card {
cursor: pointer;
transition: all 0.3s ease;
border: 2px solid transparent;
height: 100%;
}
.preset-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 25px rgba(0,0,0,0.15);
border-color: #007bff;
}
.preset-card.selected {
border-color: #28a745;
background-color: #f8fff9;
}
.preset-card .card-body {
padding: 1.5rem;
text-align: center;
}
.preset-card h6 {
margin-bottom: 0.5rem;
font-weight: 600;
color: #333;
}
.preset-card p {
margin-bottom: 1rem;
color: #6c757d;
font-size: 0.875rem;
line-height: 1.4;
}
.preset-params {
display: flex;
justify-content: center;
gap: 0.5rem;
flex-wrap: wrap;
}
.preset-params .badge {
font-size: 0.7rem;
padding: 0.25rem 0.5rem;
}
/* 预设模板模态框样式 */
#presetModal .modal-dialog {
max-width: 1200px;
}
#presetModal .modal-body {
max-height: 70vh;
overflow-y: auto;
}
/* 预设规则分类标题 */
#presetModal h6 {
font-weight: 600;
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid;
}
#presetModal h6.text-primary {
border-bottom-color: #007bff;
}
#presetModal h6.text-success {
border-bottom-color: #28a745;
}
#presetModal h6.text-info {
border-bottom-color: #17a2b8;
}
#presetModal h6.text-warning {
border-bottom-color: #ffc107;
}
/* 预设卡片图标样式 */
.preset-card i {
transition: all 0.3s ease;
}
.preset-card:hover i {
transform: scale(1.1);
}
/* 预设卡片选中状态 */
.preset-card.selected i {
color: #28a745 !important;
}
.preset-card.selected h6 {
color: #28a745;
}
/* 响应式预设卡片 */
@media (max-width: 768px) {
.preset-card .card-body {
padding: 1rem;
}
.preset-card h6 {
font-size: 0.9rem;
}
.preset-card p {
font-size: 0.8rem;
}
.preset-params .badge {
font-size: 0.65rem;
padding: 0.2rem 0.4rem;
}
}
/* 预设规则快速选择 */
.preset-quick-select {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
flex-wrap: wrap;
}
.preset-quick-select .btn {
font-size: 0.8rem;
padding: 0.25rem 0.75rem;
}
/* 预设规则预览 */
.preset-preview {
background-color: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 0.375rem;
padding: 1rem;
margin-top: 1rem;
}
.preset-preview h6 {
color: #495057;
margin-bottom: 0.5rem;
}
.preset-preview .preview-params {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 0.5rem;
}
.preset-preview .preview-param {
display: flex;
justify-content: space-between;
padding: 0.25rem 0;
border-bottom: 1px solid #e9ecef;
}
.preset-preview .preview-param:last-child {
border-bottom: none;
}
.preset-preview .preview-param strong {
color: #495057;
font-size: 0.875rem;
}
.preset-preview .preview-param span {
color: #6c757d;
font-size: 0.8rem;
}

View File

@@ -1198,9 +1198,14 @@ class TSPDashboard {
</div>
</div>
<div class="ms-3">
<button class="btn btn-sm btn-outline-primary" onclick="dashboard.updateWorkOrder(${workorder.id})">
<i class="fas fa-edit"></i>
</button>
<div class="btn-group" role="group">
<button class="btn btn-sm btn-outline-info" onclick="dashboard.viewWorkOrderDetails(${workorder.id})" title="查看详情">
<i class="fas fa-eye"></i>
</button>
<button class="btn btn-sm btn-outline-primary" onclick="dashboard.updateWorkOrder(${workorder.id})" title="编辑">
<i class="fas fa-edit"></i>
</button>
</div>
</div>
</div>
</div>
@@ -1262,17 +1267,748 @@ class TSPDashboard {
}
}
async viewWorkOrderDetails(workorderId) {
try {
const response = await fetch(`/api/workorders/${workorderId}`);
const workorder = await response.json();
if (workorder.error) {
this.showNotification('获取工单详情失败', 'error');
return;
}
this.showWorkOrderDetailsModal(workorder);
} catch (error) {
console.error('获取工单详情失败:', error);
this.showNotification('获取工单详情失败', 'error');
}
}
showWorkOrderDetailsModal(workorder) {
// 创建模态框HTML
const modalHtml = `
<div class="modal fade" id="workOrderDetailsModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">工单详情 - ${workorder.order_id || workorder.id}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="row mb-3">
<div class="col-md-6">
<h6>基本信息</h6>
<table class="table table-sm">
<tr>
<td><strong>工单号:</strong></td>
<td>${workorder.order_id || workorder.id}</td>
</tr>
<tr>
<td><strong>标题:</strong></td>
<td>${workorder.title}</td>
</tr>
<tr>
<td><strong>分类:</strong></td>
<td>${workorder.category}</td>
</tr>
<tr>
<td><strong>优先级:</strong></td>
<td><span class="badge bg-${this.getPriorityColor(workorder.priority)}">${this.getPriorityText(workorder.priority)}</span></td>
</tr>
<tr>
<td><strong>状态:</strong></td>
<td><span class="badge bg-${this.getStatusColor(workorder.status)}">${this.getStatusText(workorder.status)}</span></td>
</tr>
<tr>
<td><strong>创建时间:</strong></td>
<td>${new Date(workorder.created_at).toLocaleString()}</td>
</tr>
<tr>
<td><strong>更新时间:</strong></td>
<td>${new Date(workorder.updated_at).toLocaleString()}</td>
</tr>
</table>
</div>
<div class="col-md-6">
<h6>问题描述</h6>
<div class="border p-3 rounded">
${workorder.description}
</div>
${workorder.resolution ? `
<h6 class="mt-3">解决方案</h6>
<div class="border p-3 rounded bg-light">
${workorder.resolution}
</div>
` : ''}
${workorder.satisfaction_score ? `
<h6 class="mt-3">满意度评分</h6>
<div class="border p-3 rounded">
<div class="progress">
<div class="progress-bar" style="width: ${workorder.satisfaction_score * 100}%"></div>
</div>
<small class="text-muted">${workorder.satisfaction_score}/5.0</small>
</div>
` : ''}
</div>
</div>
${workorder.conversations && workorder.conversations.length > 0 ? `
<h6>对话记录</h6>
<div class="conversation-history" style="max-height: 300px; overflow-y: auto;">
${workorder.conversations.map(conv => `
<div class="border-bottom pb-2 mb-2">
<div class="d-flex justify-content-between">
<small class="text-muted">${new Date(conv.timestamp).toLocaleString()}</small>
</div>
<div class="mb-1">
<strong>用户:</strong> ${conv.user_message}
</div>
<div>
<strong>助手:</strong> ${conv.assistant_response}
</div>
</div>
`).join('')}
</div>
` : ''}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
<button type="button" class="btn btn-primary" onclick="dashboard.updateWorkOrder(${workorder.id})">编辑工单</button>
</div>
</div>
</div>
</div>
`;
// 移除已存在的模态框
const existingModal = document.getElementById('workOrderDetailsModal');
if (existingModal) {
existingModal.remove();
}
// 添加新的模态框到页面
document.body.insertAdjacentHTML('beforeend', modalHtml);
// 显示模态框
const modal = new bootstrap.Modal(document.getElementById('workOrderDetailsModal'));
modal.show();
// 模态框关闭时移除DOM元素
document.getElementById('workOrderDetailsModal').addEventListener('hidden.bs.modal', function() {
this.remove();
});
}
async updateWorkOrder(workorderId) {
try {
// 获取工单详情
const response = await fetch(`/api/workorders/${workorderId}`);
const workorder = await response.json();
if (response.ok) {
this.showEditWorkOrderModal(workorder);
} else {
throw new Error(workorder.error || '获取工单详情失败');
}
} catch (error) {
console.error('获取工单详情失败:', error);
this.showNotification('获取工单详情失败: ' + error.message, 'error');
}
}
showEditWorkOrderModal(workorder) {
// 创建编辑工单模态框
const modalHtml = `
<div class="modal fade" id="editWorkOrderModal" tabindex="-1" aria-labelledby="editWorkOrderModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="editWorkOrderModalLabel">编辑工单 #${workorder.id}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="editWorkOrderForm">
<div class="row">
<div class="col-md-8">
<div class="mb-3">
<label for="editTitle" class="form-label">标题 *</label>
<input type="text" class="form-control" id="editTitle" value="${workorder.title}" required>
</div>
</div>
<div class="col-md-4">
<div class="mb-3">
<label for="editPriority" class="form-label">优先级</label>
<select class="form-select" id="editPriority">
<option value="low" ${workorder.priority === 'low' ? 'selected' : ''}>低</option>
<option value="medium" ${workorder.priority === 'medium' ? 'selected' : ''}>中</option>
<option value="high" ${workorder.priority === 'high' ? 'selected' : ''}>高</option>
<option value="urgent" ${workorder.priority === 'urgent' ? 'selected' : ''}>紧急</option>
</select>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="editCategory" class="form-label">分类</label>
<select class="form-select" id="editCategory">
<option value="技术问题" ${workorder.category === '技术问题' ? 'selected' : ''}>技术问题</option>
<option value="业务问题" ${workorder.category === '业务问题' ? 'selected' : ''}>业务问题</option>
<option value="系统故障" ${workorder.category === '系统故障' ? 'selected' : ''}>系统故障</option>
<option value="功能需求" ${workorder.category === '功能需求' ? 'selected' : ''}>功能需求</option>
<option value="其他" ${workorder.category === '其他' ? 'selected' : ''}>其他</option>
</select>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="editStatus" class="form-label">状态</label>
<select class="form-select" id="editStatus">
<option value="open" ${workorder.status === 'open' ? 'selected' : ''}>待处理</option>
<option value="in_progress" ${workorder.status === 'in_progress' ? 'selected' : ''}>处理中</option>
<option value="resolved" ${workorder.status === 'resolved' ? 'selected' : ''}>已解决</option>
<option value="closed" ${workorder.status === 'closed' ? 'selected' : ''}>已关闭</option>
</select>
</div>
</div>
</div>
<div class="mb-3">
<label for="editDescription" class="form-label">描述 *</label>
<textarea class="form-control" id="editDescription" rows="4" required>${workorder.description}</textarea>
</div>
<div class="mb-3">
<label for="editResolution" class="form-label">解决方案</label>
<textarea class="form-control" id="editResolution" rows="3" placeholder="请输入解决方案...">${workorder.resolution || ''}</textarea>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="editSatisfactionScore" class="form-label">满意度评分 (1-5)</label>
<input type="number" class="form-control" id="editSatisfactionScore" min="1" max="5" value="${workorder.satisfaction_score || ''}">
</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" onclick="dashboard.saveWorkOrder(${workorder.id})">保存修改</button>
</div>
</div>
</div>
</div>
`;
// 移除已存在的模态框
const existingModal = document.getElementById('editWorkOrderModal');
if (existingModal) {
existingModal.remove();
}
// 添加新模态框到页面
document.body.insertAdjacentHTML('beforeend', modalHtml);
// 显示模态框
const modal = new bootstrap.Modal(document.getElementById('editWorkOrderModal'));
modal.show();
// 模态框关闭时清理
document.getElementById('editWorkOrderModal').addEventListener('hidden.bs.modal', function() {
this.remove();
});
}
async saveWorkOrder(workorderId) {
try {
// 获取表单数据
const formData = {
title: document.getElementById('editTitle').value,
description: document.getElementById('editDescription').value,
category: document.getElementById('editCategory').value,
priority: document.getElementById('editPriority').value,
status: document.getElementById('editStatus').value,
resolution: document.getElementById('editResolution').value,
satisfaction_score: parseInt(document.getElementById('editSatisfactionScore').value) || null
};
// 验证必填字段
if (!formData.title.trim() || !formData.description.trim()) {
this.showNotification('标题和描述不能为空', 'error');
return;
}
// 发送更新请求
const response = await fetch(`/api/workorders/${workorderId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(formData)
});
const result = await response.json();
if (response.ok) {
this.showNotification('工单更新成功', 'success');
// 关闭模态框
const modal = bootstrap.Modal.getInstance(document.getElementById('editWorkOrderModal'));
modal.hide();
// 刷新工单列表
this.loadWorkOrders();
} else {
throw new Error(result.error || '更新工单失败');
}
} catch (error) {
console.error('更新工单失败:', error);
this.showNotification('更新工单失败: ' + error.message, 'error');
}
}
// 工单导入功能
showImportModal() {
// 显示导入模态框
const modal = new bootstrap.Modal(document.getElementById('importWorkOrderModal'));
modal.show();
// 重置表单
document.getElementById('excel-file-input').value = '';
document.getElementById('import-progress').classList.add('d-none');
document.getElementById('import-result').classList.add('d-none');
}
async downloadTemplate() {
try {
const response = await fetch('/api/workorders/import/template');
const result = await response.json();
if (result.success) {
// 创建下载链接
const link = document.createElement('a');
link.href = result.template_url;
link.download = '工单导入模板.xlsx';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
this.showNotification('模板下载成功', 'success');
} else {
throw new Error(result.error || '下载模板失败');
}
} catch (error) {
console.error('下载模板失败:', error);
this.showNotification('下载模板失败: ' + error.message, 'error');
}
}
async importWorkOrders() {
const fileInput = document.getElementById('excel-file-input');
const file = fileInput.files[0];
if (!file) {
this.showNotification('请选择要导入的Excel文件', 'error');
return;
}
// 验证文件类型
if (!file.name.match(/\.(xlsx|xls)$/)) {
this.showNotification('只支持Excel文件(.xlsx, .xls)', 'error');
return;
}
// 验证文件大小
if (file.size > 16 * 1024 * 1024) {
this.showNotification('文件大小不能超过16MB', 'error');
return;
}
// 显示进度条
document.getElementById('import-progress').classList.remove('d-none');
document.getElementById('import-result').classList.add('d-none');
try {
// 创建FormData
const formData = new FormData();
formData.append('file', file);
// 发送导入请求
const response = await fetch('/api/workorders/import', {
method: 'POST',
body: formData
});
const result = await response.json();
if (response.ok && result.success) {
// 显示成功消息
document.getElementById('import-progress').classList.add('d-none');
document.getElementById('import-result').classList.remove('d-none');
document.getElementById('import-success-message').textContent =
`成功导入 ${result.imported_count} 个工单`;
this.showNotification(result.message, 'success');
// 刷新工单列表
this.loadWorkOrders();
// 3秒后关闭模态框
setTimeout(() => {
const modal = bootstrap.Modal.getInstance(document.getElementById('importWorkOrderModal'));
modal.hide();
}, 3000);
} else {
throw new Error(result.error || '导入工单失败');
}
} catch (error) {
console.error('导入工单失败:', error);
document.getElementById('import-progress').classList.add('d-none');
this.showNotification('导入工单失败: ' + error.message, 'error');
}
}
// 数据分析
async loadAnalytics() {
try {
const response = await fetch('/api/analytics');
const analytics = await response.json();
this.updateAnalyticsDisplay(analytics);
this.initializeCharts();
} catch (error) {
console.error('加载分析数据失败:', error);
}
}
// 初始化图表
initializeCharts() {
this.charts = {};
this.updateCharts();
}
// 更新所有图表
async updateCharts() {
try {
const timeRange = document.getElementById('timeRange').value;
const chartType = document.getElementById('chartType').value;
const dataDimension = document.getElementById('dataDimension').value;
// 获取数据
const response = await fetch(`/api/analytics?timeRange=${timeRange}&dimension=${dataDimension}`);
const data = await response.json();
// 更新统计卡片
this.updateStatisticsCards(data);
// 更新图表
this.updateMainChart(data, chartType);
this.updateDistributionChart(data);
this.updateTrendChart(data);
this.updatePriorityChart(data);
// 更新分析报告
this.updateAnalyticsReport(data);
} catch (error) {
console.error('更新图表失败:', error);
this.showNotification('更新图表失败: ' + error.message, 'error');
}
}
// 更新统计卡片
updateStatisticsCards(data) {
const total = data.workorders?.total || 0;
const open = data.workorders?.open || 0;
const resolved = data.workorders?.resolved || 0;
const avgSatisfaction = data.satisfaction?.average || 0;
document.getElementById('totalWorkorders').textContent = total;
document.getElementById('openWorkorders').textContent = open;
document.getElementById('resolvedWorkorders').textContent = resolved;
document.getElementById('avgSatisfaction').textContent = avgSatisfaction.toFixed(1);
// 更新进度条
if (total > 0) {
document.getElementById('openProgress').style.width = `${(open / total) * 100}%`;
document.getElementById('resolvedProgress').style.width = `${(resolved / total) * 100}%`;
document.getElementById('satisfactionProgress').style.width = `${(avgSatisfaction / 5) * 100}%`;
}
}
// 更新主图表
updateMainChart(data, chartType) {
const ctx = document.getElementById('mainChart').getContext('2d');
// 销毁现有图表
if (this.charts.mainChart) {
this.charts.mainChart.destroy();
}
const chartData = this.prepareChartData(data, chartType);
this.charts.mainChart = new Chart(ctx, {
type: chartType,
data: chartData,
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
title: {
display: true,
text: '数据分析趋势'
},
legend: {
display: true,
position: 'top'
}
},
scales: chartType === 'pie' || chartType === 'doughnut' ? {} : {
x: {
display: true,
title: {
display: true,
text: '时间'
}
},
y: {
display: true,
title: {
display: true,
text: '数量'
}
}
}
}
});
}
// 更新分布图表
updateDistributionChart(data) {
const ctx = document.getElementById('distributionChart').getContext('2d');
if (this.charts.distributionChart) {
this.charts.distributionChart.destroy();
}
const categories = data.workorders?.by_category || {};
const labels = Object.keys(categories);
const values = Object.values(categories);
this.charts.distributionChart = new Chart(ctx, {
type: 'doughnut',
data: {
labels: labels,
datasets: [{
data: values,
backgroundColor: [
'#FF6384',
'#36A2EB',
'#FFCE56',
'#4BC0C0',
'#9966FF',
'#FF9F40'
]
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
title: {
display: true,
text: '工单分类分布'
},
legend: {
display: true,
position: 'bottom'
}
}
}
});
}
// 更新趋势图表
updateTrendChart(data) {
const ctx = document.getElementById('trendChart').getContext('2d');
if (this.charts.trendChart) {
this.charts.trendChart.destroy();
}
const trendData = data.trend || [];
const labels = trendData.map(item => item.date);
const workorders = trendData.map(item => item.workorders);
const alerts = trendData.map(item => item.alerts);
this.charts.trendChart = new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [{
label: '工单数量',
data: workorders,
borderColor: '#36A2EB',
backgroundColor: 'rgba(54, 162, 235, 0.1)',
tension: 0.4
}, {
label: '预警数量',
data: alerts,
borderColor: '#FF6384',
backgroundColor: 'rgba(255, 99, 132, 0.1)',
tension: 0.4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
title: {
display: true,
text: '时间趋势分析'
}
},
scales: {
x: {
display: true,
title: {
display: true,
text: '日期'
}
},
y: {
display: true,
title: {
display: true,
text: '数量'
}
}
}
}
});
}
// 更新优先级图表
updatePriorityChart(data) {
const ctx = document.getElementById('priorityChart').getContext('2d');
if (this.charts.priorityChart) {
this.charts.priorityChart.destroy();
}
const priorities = data.workorders?.by_priority || {};
const labels = Object.keys(priorities).map(p => this.getPriorityText(p));
const values = Object.values(priorities);
this.charts.priorityChart = new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: '工单数量',
data: values,
backgroundColor: [
'#28a745', // 低 - 绿色
'#ffc107', // 中 - 黄色
'#fd7e14', // 高 - 橙色
'#dc3545' // 紧急 - 红色
]
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
title: {
display: true,
text: '优先级分布'
}
},
scales: {
y: {
beginAtZero: true
}
}
}
});
}
// 准备图表数据
prepareChartData(data, chartType) {
const trendData = data.trend || [];
const labels = trendData.map(item => item.date);
const workorders = trendData.map(item => item.workorders);
if (chartType === 'pie' || chartType === 'doughnut') {
const categories = data.workorders?.by_category || {};
return {
labels: Object.keys(categories),
datasets: [{
data: Object.values(categories),
backgroundColor: [
'#FF6384',
'#36A2EB',
'#FFCE56',
'#4BC0C0',
'#9966FF',
'#FF9F40'
]
}]
};
} else {
return {
labels: labels,
datasets: [{
label: '工单数量',
data: workorders,
borderColor: '#36A2EB',
backgroundColor: 'rgba(54, 162, 235, 0.1)',
tension: chartType === 'line' ? 0.4 : 0
}]
};
}
}
// 导出图表
exportChart(chartId) {
if (this.charts[chartId]) {
const link = document.createElement('a');
link.download = `${chartId}_chart.png`;
link.href = this.charts[chartId].toBase64Image();
link.click();
}
}
// 全屏图表
fullscreenChart(chartId) {
// 这里可以实现全屏显示功能
this.showNotification('全屏功能开发中', 'info');
}
// 导出报告
async exportReport() {
try {
const response = await fetch('/api/analytics/export');
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'analytics_report.xlsx';
link.click();
window.URL.revokeObjectURL(url);
} catch (error) {
console.error('导出报告失败:', error);
this.showNotification('导出报告失败: ' + error.message, 'error');
}
}
// 打印报告
printReport() {
window.print();
}
updateAnalyticsDisplay(analytics) {
// 更新分析报告
const reportContainer = document.getElementById('analytics-report');

View File

@@ -844,9 +844,17 @@
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5><i class="fas fa-tasks me-2"></i>工单管理</h5>
<button class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#createWorkOrderModal">
<i class="fas fa-plus me-1"></i>创建工单
</button>
<div class="btn-group" role="group">
<button class="btn btn-success btn-sm" onclick="dashboard.downloadTemplate()">
<i class="fas fa-download me-1"></i>下载模板
</button>
<button class="btn btn-info btn-sm" onclick="dashboard.showImportModal()">
<i class="fas fa-upload me-1"></i>导入工单
</button>
<button class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#createWorkOrderModal">
<i class="fas fa-plus me-1"></i>创建工单
</button>
</div>
</div>
<div class="card-body">
<div class="mb-3">
@@ -914,43 +922,210 @@
<!-- 数据分析标签页 -->
<div id="analytics-tab" class="tab-content" style="display: none;">
<!-- 图表控制面板 -->
<div class="row mb-4">
<div class="col-md-6">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5><i class="fas fa-chart-line me-2"></i>性能趋势</h5>
<h5><i class="fas fa-chart-bar me-2"></i>数据分析控制面板</h5>
</div>
<div class="card-body">
<div class="chart-container">
<canvas id="analyticsChart"></canvas>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5><i class="fas fa-chart-pie me-2"></i>类别分布</h5>
</div>
<div class="card-body">
<div class="chart-container">
<canvas id="categoryChart"></canvas>
<div class="row">
<div class="col-md-3">
<label class="form-label">时间范围</label>
<select class="form-select" id="timeRange">
<option value="7">最近7天</option>
<option value="30" selected>最近30天</option>
<option value="90">最近90天</option>
<option value="365">最近1年</option>
</select>
</div>
<div class="col-md-3">
<label class="form-label">图表类型</label>
<select class="form-select" id="chartType">
<option value="line">折线图</option>
<option value="bar" selected>柱状图</option>
<option value="pie">饼图</option>
<option value="doughnut">环形图</option>
<option value="radar">雷达图</option>
<option value="polar">极坐标图</option>
</select>
</div>
<div class="col-md-3">
<label class="form-label">数据维度</label>
<select class="form-select" id="dataDimension">
<option value="workorders" selected>工单统计</option>
<option value="alerts">预警统计</option>
<option value="performance">性能指标</option>
<option value="satisfaction">满意度分析</option>
</select>
</div>
<div class="col-md-3">
<label class="form-label">操作</label>
<div class="d-grid">
<button class="btn btn-primary" onclick="dashboard.updateCharts()">
<i class="fas fa-sync-alt me-1"></i>刷新图表
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 主要图表区域 -->
<div class="row mb-4">
<div class="col-md-8">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5><i class="fas fa-chart-line me-2"></i>主要趋势分析</h5>
<div class="btn-group btn-group-sm" role="group">
<button type="button" class="btn btn-outline-primary" onclick="dashboard.exportChart('mainChart')">
<i class="fas fa-download me-1"></i>导出
</button>
<button type="button" class="btn btn-outline-secondary" onclick="dashboard.fullscreenChart('mainChart')">
<i class="fas fa-expand me-1"></i>全屏
</button>
</div>
</div>
<div class="card-body">
<div class="chart-container" style="position: relative; height: 400px;">
<canvas id="mainChart"></canvas>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">
<h5><i class="fas fa-chart-pie me-2"></i>分布分析</h5>
</div>
<div class="card-body">
<div class="chart-container" style="position: relative; height: 300px;">
<canvas id="distributionChart"></canvas>
</div>
</div>
</div>
</div>
</div>
<!-- 详细统计卡片 -->
<div class="row mb-4">
<div class="col-md-3">
<div class="card text-center">
<div class="card-body">
<div class="d-flex align-items-center justify-content-center mb-2">
<i class="fas fa-tasks fa-2x text-primary me-3"></i>
<div>
<h3 class="mb-0" id="totalWorkorders">0</h3>
<small class="text-muted">总工单数</small>
</div>
</div>
<div class="progress" style="height: 4px;">
<div class="progress-bar bg-primary" role="progressbar" style="width: 100%"></div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-center">
<div class="card-body">
<div class="d-flex align-items-center justify-content-center mb-2">
<i class="fas fa-exclamation-triangle fa-2x text-warning me-3"></i>
<div>
<h3 class="mb-0" id="openWorkorders">0</h3>
<small class="text-muted">待处理</small>
</div>
</div>
<div class="progress" style="height: 4px;">
<div class="progress-bar bg-warning" role="progressbar" id="openProgress" style="width: 0%"></div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-center">
<div class="card-body">
<div class="d-flex align-items-center justify-content-center mb-2">
<i class="fas fa-check-circle fa-2x text-success me-3"></i>
<div>
<h3 class="mb-0" id="resolvedWorkorders">0</h3>
<small class="text-muted">已解决</small>
</div>
</div>
<div class="progress" style="height: 4px;">
<div class="progress-bar bg-success" role="progressbar" id="resolvedProgress" style="width: 0%"></div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-center">
<div class="card-body">
<div class="d-flex align-items-center justify-content-center mb-2">
<i class="fas fa-star fa-2x text-info me-3"></i>
<div>
<h3 class="mb-0" id="avgSatisfaction">0</h3>
<small class="text-muted">平均满意度</small>
</div>
</div>
<div class="progress" style="height: 4px;">
<div class="progress-bar bg-info" role="progressbar" id="satisfactionProgress" style="width: 0%"></div>
</div>
</div>
</div>
</div>
</div>
<!-- 多维度分析图表 -->
<div class="row mb-4">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5><i class="fas fa-chart-area me-2"></i>时间趋势分析</h5>
</div>
<div class="card-body">
<div class="chart-container" style="position: relative; height: 300px;">
<canvas id="trendChart"></canvas>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5><i class="fas fa-chart-bar me-2"></i>优先级分布</h5>
</div>
<div class="card-body">
<div class="chart-container" style="position: relative; height: 300px;">
<canvas id="priorityChart"></canvas>
</div>
</div>
</div>
</div>
</div>
<!-- 详细分析报告 -->
<div class="row">
<div class="col-md-12">
<div class="card">
<div class="card-header">
<div class="card-header d-flex justify-content-between align-items-center">
<h5><i class="fas fa-table me-2"></i>详细分析报告</h5>
<div class="btn-group btn-group-sm" role="group">
<button type="button" class="btn btn-outline-primary" onclick="dashboard.exportReport()">
<i class="fas fa-file-excel me-1"></i>导出Excel
</button>
<button type="button" class="btn btn-outline-secondary" onclick="dashboard.printReport()">
<i class="fas fa-print me-1"></i>打印报告
</button>
</div>
</div>
<div class="card-body">
<div id="analytics-report">
<div class="loading-spinner">
<i class="fas fa-spinner fa-spin"></i>
<div class="loading-spinner text-center">
<i class="fas fa-spinner fa-spin fa-2x"></i>
<p class="mt-2">正在生成分析报告...</p>
</div>
</div>
</div>
@@ -1074,6 +1249,118 @@
</div>
</div>
<!-- 工单导入模态框 -->
<div class="modal fade" id="importWorkOrderModal" 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">
<div class="alert alert-info">
<i class="fas fa-info-circle me-2"></i>
请先下载模板文件按照模板格式填写工单信息然后上传Excel文件进行导入。
</div>
<div class="mb-3">
<label class="form-label">选择Excel文件</label>
<input type="file" class="form-control" id="excel-file-input" accept=".xlsx,.xls">
<div class="form-text">支持 .xlsx 和 .xls 格式文件大小不超过16MB</div>
</div>
<div class="mb-3">
<div class="d-flex justify-content-between align-items-center">
<span>Excel文件列名说明</span>
<button class="btn btn-outline-primary btn-sm" onclick="dashboard.downloadTemplate()">
<i class="fas fa-download me-1"></i>下载模板
</button>
</div>
<div class="table-responsive mt-2">
<table class="table table-sm table-bordered">
<thead class="table-light">
<tr>
<th>列名</th>
<th>说明</th>
<th>必填</th>
<th>示例</th>
</tr>
</thead>
<tbody>
<tr>
<td>标题</td>
<td>工单标题</td>
<td><span class="badge bg-danger"></span></td>
<td>车辆无法启动</td>
</tr>
<tr>
<td>描述</td>
<td>工单详细描述</td>
<td><span class="badge bg-danger"></span></td>
<td>用户反映车辆无法正常启动</td>
</tr>
<tr>
<td>分类</td>
<td>工单分类</td>
<td><span class="badge bg-secondary"></span></td>
<td>技术问题</td>
</tr>
<tr>
<td>优先级</td>
<td>工单优先级</td>
<td><span class="badge bg-secondary"></span></td>
<td>high</td>
</tr>
<tr>
<td>状态</td>
<td>工单状态</td>
<td><span class="badge bg-secondary"></span></td>
<td>open</td>
</tr>
<tr>
<td>解决方案</td>
<td>解决方案描述</td>
<td><span class="badge bg-secondary"></span></td>
<td>检查电池和启动系统</td>
</tr>
<tr>
<td>满意度</td>
<td>满意度评分(1-5)</td>
<td><span class="badge bg-secondary"></span></td>
<td>5</td>
</tr>
</tbody>
</table>
</div>
</div>
<div id="import-progress" class="d-none">
<div class="progress mb-3">
<div class="progress-bar" role="progressbar" style="width: 0%"></div>
</div>
<div class="text-center">
<i class="fas fa-spinner fa-spin me-2"></i>
<span id="import-status">正在导入工单...</span>
</div>
</div>
<div id="import-result" class="d-none">
<div class="alert alert-success">
<i class="fas fa-check-circle me-2"></i>
<span id="import-success-message"></span>
</div>
</div>
</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="import-workorder-btn" onclick="dashboard.importWorkOrders()">
<i class="fas fa-upload me-1"></i>开始导入
</button>
</div>
</div>
</div>
</div>
<!-- 添加知识模态框 -->
<div class="modal fade" id="addKnowledgeModal" tabindex="-1">
<div class="modal-dialog">

View File

@@ -166,11 +166,27 @@
<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 class="btn-group">
<button class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#ruleModal">
<i class="fas fa-plus me-1"></i>添加规则
</button>
<button class="btn btn-sm btn-success" data-bs-toggle="modal" data-bs-target="#presetModal">
<i class="fas fa-magic me-1"></i>预设模板
</button>
</div>
</div>
<div class="card-body">
<!-- 预设规则卡片 -->
<div class="row mb-4">
<div class="col-12">
<h6 class="text-muted mb-3">常用预警规则模板</h6>
<div class="row" id="preset-rules">
<!-- 预设规则卡片将在这里动态生成 -->
</div>
</div>
</div>
<!-- 自定义规则列表 -->
<div class="table-responsive">
<table class="table table-hover">
<thead>
@@ -377,6 +393,260 @@
</div>
</div>
<!-- 预设模板模态框 -->
<div class="modal fade" id="presetModal" tabindex="-1">
<div class="modal-dialog modal-xl">
<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">
<div class="row">
<!-- 性能预警模板 -->
<div class="col-md-6 mb-4">
<h6 class="text-primary mb-3"><i class="fas fa-tachometer-alt me-2"></i>性能预警模板</h6>
<div class="row">
<div class="col-md-6 mb-3">
<div class="card preset-card" data-preset="response_time">
<div class="card-body text-center">
<i class="fas fa-clock fa-2x text-warning mb-2"></i>
<h6>响应时间预警</h6>
<p class="small text-muted">API响应时间超过阈值</p>
<div class="preset-params">
<span class="badge bg-warning">警告</span>
<span class="badge bg-info">性能</span>
</div>
</div>
</div>
</div>
<div class="col-md-6 mb-3">
<div class="card preset-card" data-preset="cpu_usage">
<div class="card-body text-center">
<i class="fas fa-microchip fa-2x text-danger mb-2"></i>
<h6>CPU使用率预警</h6>
<p class="small text-muted">CPU使用率过高</p>
<div class="preset-params">
<span class="badge bg-danger">严重</span>
<span class="badge bg-info">性能</span>
</div>
</div>
</div>
</div>
<div class="col-md-6 mb-3">
<div class="card preset-card" data-preset="memory_usage">
<div class="card-body text-center">
<i class="fas fa-memory fa-2x text-warning mb-2"></i>
<h6>内存使用率预警</h6>
<p class="small text-muted">内存使用率过高</p>
<div class="preset-params">
<span class="badge bg-warning">警告</span>
<span class="badge bg-info">性能</span>
</div>
</div>
</div>
</div>
<div class="col-md-6 mb-3">
<div class="card preset-card" data-preset="disk_usage">
<div class="card-body text-center">
<i class="fas fa-hdd fa-2x text-danger mb-2"></i>
<h6>磁盘使用率预警</h6>
<p class="small text-muted">磁盘空间不足</p>
<div class="preset-params">
<span class="badge bg-danger">严重</span>
<span class="badge bg-info">性能</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 业务预警模板 -->
<div class="col-md-6 mb-4">
<h6 class="text-success mb-3"><i class="fas fa-chart-line me-2"></i>业务预警模板</h6>
<div class="row">
<div class="col-md-6 mb-3">
<div class="card preset-card" data-preset="satisfaction_low">
<div class="card-body text-center">
<i class="fas fa-frown fa-2x text-warning mb-2"></i>
<h6>满意度预警</h6>
<p class="small text-muted">用户满意度低于阈值</p>
<div class="preset-params">
<span class="badge bg-warning">警告</span>
<span class="badge bg-success">业务</span>
</div>
</div>
</div>
</div>
<div class="col-md-6 mb-3">
<div class="card preset-card" data-preset="workorder_high">
<div class="card-body text-center">
<i class="fas fa-tasks fa-2x text-danger mb-2"></i>
<h6>工单积压预警</h6>
<p class="small text-muted">待处理工单过多</p>
<div class="preset-params">
<span class="badge bg-danger">严重</span>
<span class="badge bg-success">业务</span>
</div>
</div>
</div>
</div>
<div class="col-md-6 mb-3">
<div class="card preset-card" data-preset="error_rate_high">
<div class="card-body text-center">
<i class="fas fa-exclamation-triangle fa-2x text-danger mb-2"></i>
<h6>错误率预警</h6>
<p class="small text-muted">系统错误率过高</p>
<div class="preset-params">
<span class="badge bg-danger">严重</span>
<span class="badge bg-success">业务</span>
</div>
</div>
</div>
</div>
<div class="col-md-6 mb-3">
<div class="card preset-card" data-preset="conversion_low">
<div class="card-body text-center">
<i class="fas fa-percentage fa-2x text-warning mb-2"></i>
<h6>转化率预警</h6>
<p class="small text-muted">用户转化率下降</p>
<div class="preset-params">
<span class="badge bg-warning">警告</span>
<span class="badge bg-success">业务</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 系统预警模板 -->
<div class="col-md-6 mb-4">
<h6 class="text-info mb-3"><i class="fas fa-server me-2"></i>系统预警模板</h6>
<div class="row">
<div class="col-md-6 mb-3">
<div class="card preset-card" data-preset="service_down">
<div class="card-body text-center">
<i class="fas fa-power-off fa-2x text-danger mb-2"></i>
<h6>服务宕机预警</h6>
<p class="small text-muted">关键服务不可用</p>
<div class="preset-params">
<span class="badge bg-danger">严重</span>
<span class="badge bg-info">系统</span>
</div>
</div>
</div>
</div>
<div class="col-md-6 mb-3">
<div class="card preset-card" data-preset="database_slow">
<div class="card-body text-center">
<i class="fas fa-database fa-2x text-warning mb-2"></i>
<h6>数据库慢查询</h6>
<p class="small text-muted">数据库查询过慢</p>
<div class="preset-params">
<span class="badge bg-warning">警告</span>
<span class="badge bg-info">系统</span>
</div>
</div>
</div>
</div>
<div class="col-md-6 mb-3">
<div class="card preset-card" data-preset="network_latency">
<div class="card-body text-center">
<i class="fas fa-wifi fa-2x text-warning mb-2"></i>
<h6>网络延迟预警</h6>
<p class="small text-muted">网络延迟过高</p>
<div class="preset-params">
<span class="badge bg-warning">警告</span>
<span class="badge bg-info">系统</span>
</div>
</div>
</div>
</div>
<div class="col-md-6 mb-3">
<div class="card preset-card" data-preset="api_error">
<div class="card-body text-center">
<i class="fas fa-bug fa-2x text-danger mb-2"></i>
<h6>API错误预警</h6>
<p class="small text-muted">API调用失败率过高</p>
<div class="preset-params">
<span class="badge bg-danger">严重</span>
<span class="badge bg-info">系统</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 质量预警模板 -->
<div class="col-md-6 mb-4">
<h6 class="text-warning mb-3"><i class="fas fa-award me-2"></i>质量预警模板</h6>
<div class="row">
<div class="col-md-6 mb-3">
<div class="card preset-card" data-preset="code_quality">
<div class="card-body text-center">
<i class="fas fa-code fa-2x text-warning mb-2"></i>
<h6>代码质量预警</h6>
<p class="small text-muted">代码质量评分过低</p>
<div class="preset-params">
<span class="badge bg-warning">警告</span>
<span class="badge bg-warning">质量</span>
</div>
</div>
</div>
</div>
<div class="col-md-6 mb-3">
<div class="card preset-card" data-preset="test_coverage">
<div class="card-body text-center">
<i class="fas fa-check-circle fa-2x text-info mb-2"></i>
<h6>测试覆盖率预警</h6>
<p class="small text-muted">测试覆盖率不足</p>
<div class="preset-params">
<span class="badge bg-info">信息</span>
<span class="badge bg-warning">质量</span>
</div>
</div>
</div>
</div>
<div class="col-md-6 mb-3">
<div class="card preset-card" data-preset="security_scan">
<div class="card-body text-center">
<i class="fas fa-shield-alt fa-2x text-danger mb-2"></i>
<h6>安全扫描预警</h6>
<p class="small text-muted">发现安全漏洞</p>
<div class="preset-params">
<span class="badge bg-danger">严重</span>
<span class="badge bg-warning">质量</span>
</div>
</div>
</div>
</div>
<div class="col-md-6 mb-3">
<div class="card preset-card" data-preset="performance_regression">
<div class="card-body text-center">
<i class="fas fa-chart-line fa-2x text-warning mb-2"></i>
<h6>性能回归预警</h6>
<p class="small text-muted">性能指标下降</p>
<div class="preset-params">
<span class="badge bg-warning">警告</span>
<span class="badge bg-warning">质量</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</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>

View File

@@ -213,6 +213,19 @@ class WebSocketServer:
async def handle_client(self, websocket: WebSocketServerProtocol, path: str):
"""处理客户端连接"""
# 检查连接头
headers = websocket.request_headers
connection = headers.get("Connection", "").lower()
# 处理不同的连接头格式
if "upgrade" not in connection and "keep-alive" in connection:
logger.warning(f"收到非标准连接头: {connection}")
# 对于keep-alive连接头我们仍然接受连接
elif "upgrade" not in connection:
logger.warning(f"连接头不包含upgrade: {connection}")
await websocket.close(code=1002, reason="Invalid connection header")
return
await self.register_client(websocket)
try:
@@ -220,6 +233,8 @@ class WebSocketServer:
await self.handle_message(websocket, message)
except websockets.exceptions.ConnectionClosed:
pass
except Exception as e:
logger.error(f"WebSocket连接错误: {e}")
finally:
await self.unregister_client(websocket)
@@ -227,9 +242,48 @@ class WebSocketServer:
"""启动WebSocket服务器"""
logger.info(f"启动WebSocket服务器: ws://{self.host}:{self.port}")
async with websockets.serve(self.handle_client, self.host, self.port):
# 添加CORS支持
async def handle_client_with_cors(websocket: WebSocketServerProtocol, path: str):
# 设置CORS头
if websocket.request_headers.get("Origin"):
# 允许跨域连接
pass
await self.handle_client(websocket, path)
async with websockets.serve(
handle_client_with_cors,
self.host,
self.port,
# 添加额外的服务器选项
process_request=self._process_request
):
await asyncio.Future() # 保持服务器运行
def _process_request(self, path, request_headers):
"""处理HTTP请求支持CORS"""
# 检查是否是WebSocket升级请求
if request_headers.get("Upgrade", "").lower() == "websocket":
return None # 允许WebSocket连接
# 对于非WebSocket请求返回简单的HTML页面
return (
200,
[("Content-Type", "text/html; charset=utf-8")],
b"""
<!DOCTYPE html>
<html>
<head>
<title>WebSocket Server</title>
</head>
<body>
<h1>WebSocket Server is running</h1>
<p>This is a WebSocket server. Please use a WebSocket client to connect.</p>
<p>WebSocket URL: ws://localhost:8765</p>
</body>
</html>
"""
)
def run(self):
"""运行服务器"""
asyncio.run(self.start_server())