feat: 娣诲姞澶氫釜鏂板姛鑳藉拰淇 - 鍖呮嫭鐢ㄦ埛绠$悊銆佹暟鎹簱杩佺Щ銆丟it鎺ㄩ€佸伐鍏风瓑

This commit is contained in:
赵杰 Jie Zhao (雄狮汽车科技)
2025-11-05 10:16:34 +08:00
parent a4261ef06f
commit c9d5c80f42
43 changed files with 4435 additions and 7439 deletions

View File

@@ -115,9 +115,9 @@ def create_chat_session():
data = request.get_json()
user_id = data.get('user_id', 'anonymous')
work_order_id = data.get('work_order_id')
session_id = service_manager.get_chat_manager().create_session(user_id, work_order_id)
return jsonify({
"success": True,
"session_id": session_id,
@@ -133,10 +133,10 @@ def send_chat_message():
data = request.get_json()
session_id = data.get('session_id')
message = data.get('message')
if not session_id or not message:
return jsonify({"error": "缺少必要参数"}), 400
result = service_manager.get_chat_manager().process_message(session_id, message)
return jsonify(result)
except Exception as e:
@@ -164,10 +164,10 @@ def create_work_order():
description = data.get('description')
category = data.get('category', '技术问题')
priority = data.get('priority', 'medium')
if not session_id or not title or not description:
return jsonify({"error": "缺少必要参数"}), 400
result = service_manager.get_chat_manager().create_work_order(session_id, title, description, category, priority)
return jsonify(result)
except Exception as e:
@@ -281,7 +281,7 @@ def toggle_agent_mode():
enabled = data.get('enabled', True)
success = service_manager.get_agent_assistant().toggle_agent_mode(enabled)
return jsonify({
"success": success,
"success": success,
"message": f"Agent模式已{'启用' if enabled else '禁用'}"
})
except Exception as e:
@@ -293,7 +293,7 @@ def start_agent_monitoring():
try:
success = service_manager.get_agent_assistant().start_proactive_monitoring()
return jsonify({
"success": success,
"success": success,
"message": "Agent监控已启动" if success else "启动失败"
})
except Exception as e:
@@ -305,7 +305,7 @@ def stop_agent_monitoring():
try:
success = service_manager.get_agent_assistant().stop_proactive_monitoring()
return jsonify({
"success": success,
"success": success,
"message": "Agent监控已停止" if success else "停止失败"
})
except Exception as e:
@@ -336,13 +336,13 @@ def agent_chat():
data = request.get_json()
message = data.get('message', '')
context = data.get('context', {})
if not message:
return jsonify({"error": "消息不能为空"}), 400
# 使用Agent助手处理消息
agent_assistant = service_manager.get_agent_assistant()
# 模拟Agent处理实际应该调用真正的Agent处理逻辑
import asyncio
result = asyncio.run(agent_assistant.process_message_agent(
@@ -351,7 +351,7 @@ def agent_chat():
work_order_id=None,
enable_proactive=True
))
return jsonify({
"success": True,
"response": result.get('response', 'Agent已处理您的请求'),
@@ -440,18 +440,18 @@ def export_analytics():
try:
# 生成Excel报告使用数据库真实数据
analytics = query_optimizer.get_analytics_optimized(30)
# 创建工作簿
from openpyxl import Workbook
from openpyxl.styles import Font
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)
@@ -461,15 +461,15 @@ def export_analytics():
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)
from flask import send_file
return send_file(report_path, as_attachment=True, download_name='analytics_report.xlsx')
except Exception as e:
return jsonify({"error": str(e)}), 500
@@ -484,7 +484,7 @@ def get_vehicle_data():
vehicle_vin = request.args.get('vehicle_vin')
data_type = request.args.get('data_type')
limit = request.args.get('limit', 10, type=int)
vehicle_mgr = service_manager.get_vehicle_manager()
if vehicle_vin:
data = vehicle_mgr.get_vehicle_data_by_vin(vehicle_vin, data_type, limit)
@@ -492,7 +492,7 @@ def get_vehicle_data():
data = vehicle_mgr.get_vehicle_data(vehicle_id, data_type, limit)
else:
data = vehicle_mgr.search_vehicle_data(limit=limit)
return jsonify(data)
except Exception as e:
return jsonify({"error": str(e)}), 500
@@ -560,10 +560,10 @@ def test_api_connection():
api_base_url = data.get('api_base_url', '')
api_key = data.get('api_key', '')
model_name = data.get('model_name', 'qwen-turbo')
# 这里可以调用LLM客户端进行连接测试
# 暂时返回模拟结果
return jsonify({
"success": True,
"message": f"API连接测试成功 - {api_provider}",
@@ -579,7 +579,7 @@ def test_model_response():
try:
data = request.get_json()
test_message = data.get('test_message', '你好,请简单介绍一下你自己')
# 这里可以调用LLM客户端进行回答测试
# 暂时返回模拟结果
return jsonify({

Binary file not shown.

View File

@@ -10,6 +10,7 @@ import logging
import uuid
import time
from datetime import datetime
from typing import Optional
from flask import Blueprint, request, jsonify, send_file
from werkzeug.utils import secure_filename
from sqlalchemy import text
@@ -25,13 +26,13 @@ class SimpleAIAccuracyConfig:
self.manual_review_threshold = 0.80
self.ai_suggestion_confidence = 0.95
self.human_resolution_confidence = 0.90
def should_auto_approve(self, similarity: float) -> bool:
return similarity >= self.auto_approve_threshold
def should_use_human_resolution(self, similarity: float) -> bool:
return similarity < self.use_human_resolution_threshold
def get_confidence_score(self, similarity: float, use_human: bool = False) -> float:
if use_human:
return self.human_resolution_confidence
@@ -40,12 +41,83 @@ class SimpleAIAccuracyConfig:
from src.main import TSPAssistant
from src.core.database import db_manager
from src.core.models import WorkOrder, Conversation, WorkOrderSuggestion, KnowledgeEntry
from src.core.models import WorkOrder, Conversation, WorkOrderSuggestion, KnowledgeEntry, WorkOrderProcessHistory
from src.core.query_optimizer import query_optimizer
from src.web.service_manager import service_manager
from src.core.workorder_permissions import (
WorkOrderPermissionManager, WorkOrderDispatchManager,
UserRole, WorkOrderModule
)
workorders_bp = Blueprint('workorders', __name__, url_prefix='/api/workorders')
def get_current_user_role() -> UserRole:
"""获取当前用户角色(临时实现,实际需要集成认证系统)"""
# TODO: 从session或token中获取用户信息
# 在没有认证系统之前默认返回ADMIN以便可以查看所有工单
# 实际实现时需要从认证系统获取真实角色
role_str = request.headers.get('X-User-Role', 'admin') # 临时改为admin避免VIEWER无法查看数据
try:
return UserRole(role_str)
except ValueError:
return UserRole.ADMIN # 临时返回ADMIN避免VIEWER无法查看数据
def get_current_user_name() -> str:
"""获取当前用户名(临时实现,实际需要集成认证系统)"""
# TODO: 从session或token中获取用户信息
return request.headers.get('X-User-Name', 'anonymous')
def add_process_history(
workorder_id: int,
processor_name: str,
process_content: str,
action_type: str,
processor_role: Optional[str] = None,
processor_region: Optional[str] = None,
previous_status: Optional[str] = None,
new_status: Optional[str] = None,
assigned_module: Optional[str] = None
) -> WorkOrderProcessHistory:
"""
添加工单处理过程记录
Args:
workorder_id: 工单ID
processor_name: 处理人员姓名
process_content: 处理内容
action_type: 操作类型dispatch、process、close、reassign等
processor_role: 处理人员角色
processor_region: 处理人员区域
previous_status: 处理前的状态
new_status: 处理后的状态
assigned_module: 分配的模块
Returns:
创建的处理记录对象
"""
try:
with db_manager.get_session() as session:
history = WorkOrderProcessHistory(
work_order_id=workorder_id,
processor_name=processor_name,
processor_role=processor_role,
processor_region=processor_region,
process_content=process_content,
action_type=action_type,
previous_status=previous_status,
new_status=new_status,
assigned_module=assigned_module,
process_time=datetime.now()
)
session.add(history)
session.commit()
session.refresh(history)
logger.info(f"工单 {workorder_id} 添加处理记录: {action_type} by {processor_name}")
return history
except Exception as e:
logger.error(f"添加处理记录失败: {e}")
raise
# 移除get_assistant函数使用service_manager
def _ensure_workorder_template_file() -> str:
@@ -53,14 +125,14 @@ def _ensure_workorder_template_file() -> str:
# 获取项目根目录
current_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.abspath(os.path.join(current_dir, '..', '..', '..'))
# 模板文件路径项目根目录下的uploads
template_path = os.path.join(project_root, 'uploads', 'workorder_template.xlsx')
# 确保目录存在
uploads_dir = os.path.join(project_root, 'uploads')
os.makedirs(uploads_dir, exist_ok=True)
if not os.path.exists(template_path):
# 尝试从其他可能的位置复制模板
possible_locations = [
@@ -68,7 +140,7 @@ def _ensure_workorder_template_file() -> str:
os.path.join(current_dir, 'uploads', 'workorder_template.xlsx'),
os.path.join(os.getcwd(), 'uploads', 'workorder_template.xlsx')
]
source_found = False
for source_path in possible_locations:
if os.path.exists(source_path):
@@ -79,7 +151,7 @@ def _ensure_workorder_template_file() -> str:
break
except Exception as e:
logger.warning(f"复制模板文件失败: {e}")
if not source_found:
# 自动生成一个最小可用模板
try:
@@ -91,42 +163,66 @@ def _ensure_workorder_template_file() -> str:
logger.info(f"自动生成模板文件: {template_path}")
except Exception as gen_err:
raise FileNotFoundError('模板文件缺失且自动生成失败请检查依赖openpyxl/pandas') from gen_err
return template_path
@workorders_bp.route('')
def get_workorders():
"""获取工单列表(分页)"""
"""获取工单列表(分页,带权限过滤"""
try:
# 获取当前用户角色和权限
current_role = get_current_user_role()
# 获取分页参数
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 10, type=int)
status_filter = request.args.get('status', '')
priority_filter = request.args.get('priority', '')
module_filter = request.args.get('module', '') # 模块过滤
# 从数据库获取分页数据
from src.core.database import db_manager
from src.core.models import WorkOrder
with db_manager.get_session() as session:
# 构建查询
query = session.query(WorkOrder)
# 权限过滤:业务方只能看到自己模块的工单
if not WorkOrderPermissionManager.can_view_all_workorders(current_role):
# 获取用户可访问的模块
accessible_modules = WorkOrderPermissionManager.get_accessible_modules(current_role)
if accessible_modules:
# 构建模块列表过滤条件
module_names = [m.value for m in accessible_modules]
query = query.filter(WorkOrder.assigned_module.in_(module_names))
else:
# 如果没有可访问的模块,返回空列表
return jsonify({
"workorders": [],
"total": 0,
"page": page,
"per_page": per_page,
"total_pages": 0 # 统一使用total_pages字段
})
# 应用过滤器
if status_filter:
query = query.filter(WorkOrder.status == status_filter)
if priority_filter:
query = query.filter(WorkOrder.priority == priority_filter)
if module_filter:
query = query.filter(WorkOrder.assigned_module == module_filter)
# 按创建时间倒序排列
query = query.order_by(WorkOrder.created_at.desc())
# 计算总数
total = query.count()
# 分页查询
workorders = query.offset((page - 1) * per_page).limit(per_page).all()
# 转换为字典
workorders_data = []
for workorder in workorders:
@@ -135,6 +231,11 @@ def get_workorders():
'order_id': workorder.order_id,
'title': workorder.title,
'description': workorder.description,
'assigned_module': workorder.assigned_module,
'module_owner': workorder.module_owner,
'dispatcher': workorder.dispatcher,
'dispatch_time': workorder.dispatch_time.isoformat() if workorder.dispatch_time else None,
'region': workorder.region,
'category': workorder.category,
'priority': workorder.priority,
'status': workorder.status,
@@ -146,10 +247,10 @@ def get_workorders():
'updated_at': workorder.updated_at.isoformat() if workorder.updated_at else None,
'date_of_close': workorder.date_of_close.isoformat() if workorder.date_of_close else None
})
# 计算分页信息
total_pages = (total + per_page - 1) // per_page
return jsonify({
'workorders': workorders_data,
'page': page,
@@ -157,13 +258,13 @@ def get_workorders():
'total': total,
'total_pages': total_pages
})
except Exception as e:
return jsonify({"error": str(e)}), 500
@workorders_bp.route('', methods=['POST'])
def create_workorder():
"""创建工单"""
"""创建工单(初始状态为待分发)"""
try:
data = request.get_json()
result = service_manager.get_assistant().create_work_order(
@@ -172,23 +273,72 @@ def create_workorder():
category=data['category'],
priority=data['priority']
)
# 获取当前用户信息(用于记录创建人)
current_user = get_current_user_name()
current_role = get_current_user_role()
# 创建工单后,设置为待分发状态(未分配模块)
if result and 'id' in result:
workorder_id = result.get('id')
with db_manager.get_session() as session:
workorder = session.query(WorkOrder).filter(WorkOrder.id == workorder_id).first()
if workorder:
# 初始状态为待分发
workorder.assigned_module = WorkOrderModule.UNASSIGNED.value
workorder.status = "pending" # 待处理/待分发
workorder.created_by = current_user # 记录创建人
session.commit()
# 记录创建工单的处理历史
processor_region = "overseas" if current_role == UserRole.OVERSEAS_OPS else "domestic"
add_process_history(
workorder_id=workorder_id,
processor_name=current_user,
process_content=f"工单已创建:{data.get('title', '')[:50]}",
action_type="create",
processor_role=current_role.value,
processor_region=processor_region,
previous_status=None,
new_status="pending"
)
# 清除工单相关缓存
from src.core.cache_manager import cache_manager
cache_manager.clear() # 清除所有缓存
return jsonify({"success": True, "workorder": result})
except Exception as e:
return jsonify({"error": str(e)}), 500
@workorders_bp.route('/<int:workorder_id>')
def get_workorder_details(workorder_id):
"""获取工单详情(含数据库对话记录)"""
"""获取工单详情(含数据库对话记录,带权限检查"""
try:
# 获取当前用户角色和权限
current_role = get_current_user_role()
with db_manager.get_session() as session:
w = session.query(WorkOrder).filter(WorkOrder.id == workorder_id).first()
if not w:
return jsonify({"error": "工单不存在"}), 404
# 权限检查:业务方只能访问自己模块的工单
if not WorkOrderPermissionManager.can_view_all_workorders(current_role):
# 检查是否有权限访问该工单
assigned_module_str = w.assigned_module
if not assigned_module_str or assigned_module_str == WorkOrderModule.UNASSIGNED.value:
# 未分配的工单,业务方不能访问
return jsonify({"error": "无权访问该工单"}), 403
try:
assigned_module = WorkOrderModule(assigned_module_str)
accessible_modules = WorkOrderPermissionManager.get_accessible_modules(current_role)
if assigned_module not in accessible_modules:
return jsonify({"error": "无权访问该工单"}), 403
except ValueError:
# 如果模块值无效,业务方不能访问
return jsonify({"error": "无权访问该工单"}), 403
convs = session.query(Conversation).filter(Conversation.work_order_id == w.id).order_by(Conversation.timestamp.asc()).all()
conv_list = []
for c in convs:
@@ -198,6 +348,27 @@ def get_workorder_details(workorder_id):
"assistant_response": c.assistant_response,
"timestamp": c.timestamp.isoformat() if c.timestamp else None
})
# 获取处理过程记录
process_history_list = session.query(WorkOrderProcessHistory).filter(
WorkOrderProcessHistory.work_order_id == w.id
).order_by(WorkOrderProcessHistory.process_time.asc()).all()
process_history_data = []
for ph in process_history_list:
process_history_data.append({
"id": ph.id,
"processor_name": ph.processor_name,
"processor_role": ph.processor_role,
"processor_region": ph.processor_region,
"process_content": ph.process_content,
"action_type": ph.action_type,
"previous_status": ph.previous_status,
"new_status": ph.new_status,
"assigned_module": ph.assigned_module,
"process_time": ph.process_time.isoformat() if ph.process_time else None
})
# 在会话内构建工单数据
workorder = {
"id": w.id,
@@ -211,7 +382,13 @@ def get_workorder_details(workorder_id):
"updated_at": w.updated_at.isoformat() if w.updated_at else None,
"resolution": w.resolution,
"satisfaction_score": w.satisfaction_score,
"conversations": conv_list
"assigned_module": w.assigned_module,
"module_owner": w.module_owner,
"dispatcher": w.dispatcher,
"dispatch_time": w.dispatch_time.isoformat() if w.dispatch_time else None,
"region": w.region,
"conversations": conv_list,
"process_history": process_history_data # 处理过程记录
}
return jsonify(workorder)
except Exception as e:
@@ -219,29 +396,72 @@ def get_workorder_details(workorder_id):
@workorders_bp.route('/<int:workorder_id>', methods=['PUT'])
def update_workorder(workorder_id):
"""更新工单(写入数据库)"""
"""更新工单(写入数据库,自动记录处理历史"""
try:
# 获取当前用户信息
current_user = get_current_user_name()
current_role = get_current_user_role()
data = request.get_json()
if not data.get('title') or not data.get('description'):
return jsonify({"error": "标题和描述不能为空"}), 400
with db_manager.get_session() as session:
w = session.query(WorkOrder).filter(WorkOrder.id == workorder_id).first()
if not w:
return jsonify({"error": "工单不存在"}), 404
# 记录更新前的状态
previous_status = w.status
previous_priority = w.priority
# 更新工单信息
w.title = data.get('title', w.title)
w.description = data.get('description', w.description)
w.category = data.get('category', w.category)
w.priority = data.get('priority', w.priority)
w.status = data.get('status', w.status)
new_status = data.get('status', w.status)
w.status = new_status
w.resolution = data.get('resolution', w.resolution)
w.satisfaction_score = data.get('satisfaction_score', w.satisfaction_score)
w.updated_at = datetime.now()
session.commit()
# 如果状态或优先级发生变化,记录处理历史
has_status_change = previous_status != new_status
has_priority_change = previous_priority != data.get('priority', w.priority)
if has_status_change or has_priority_change:
# 构建处理内容
change_items = []
if has_status_change:
change_items.append(f"状态变更:{previous_status}{new_status}")
if has_priority_change:
change_items.append(f"优先级变更:{previous_priority}{data.get('priority', w.priority)}")
process_content = "".join(change_items)
if data.get('resolution'):
process_content += f";解决方案:{data.get('resolution', '')[:100]}"
# 判断区域
processor_region = "overseas" if current_role == UserRole.OVERSEAS_OPS else "domestic"
add_process_history(
workorder_id=workorder_id,
processor_name=current_user,
process_content=process_content or "更新工单信息",
action_type="update",
processor_role=current_role.value,
processor_region=processor_region,
previous_status=previous_status,
new_status=new_status
)
# 清除工单相关缓存
from src.core.cache_manager import cache_manager
cache_manager.clear() # 清除所有缓存
updated = {
"id": w.id,
"title": w.title,
@@ -265,25 +485,25 @@ def delete_workorder(workorder_id):
workorder = session.query(WorkOrder).filter(WorkOrder.id == workorder_id).first()
if not workorder:
return jsonify({"error": "工单不存在"}), 404
# 先删除所有相关的子记录(按外键依赖顺序)
# 1. 删除工单建议记录
try:
session.execute(text("DELETE FROM work_order_suggestions WHERE work_order_id = :id"), {"id": workorder_id})
except Exception as e:
print(f"删除工单建议记录失败: {e}")
# 2. 删除对话记录
session.query(Conversation).filter(Conversation.work_order_id == workorder_id).delete()
# 3. 删除工单
session.delete(workorder)
session.commit()
# 清除工单相关缓存
from src.core.cache_manager import cache_manager
cache_manager.clear() # 清除所有缓存
return jsonify({
"success": True,
"message": "工单删除成功"
@@ -362,22 +582,22 @@ def save_workorder_human_resolution(workorder_id):
except Exception:
sim = 0.0
rec.ai_similarity = sim
# 使用简化的配置
config = SimpleAIAccuracyConfig()
# 自动审批条件
approved = config.should_auto_approve(sim)
rec.approved = approved
# 记录使用人工描述入库的标记当AI准确率低于阈值时
use_human_resolution = config.should_use_human_resolution(sim)
rec.use_human_resolution = use_human_resolution
session.commit()
return jsonify({
"success": True,
"similarity": sim,
"success": True,
"similarity": sim,
"approved": approved,
"use_human_resolution": use_human_resolution
})
@@ -392,14 +612,14 @@ def approve_workorder_to_knowledge(workorder_id):
w = session.query(WorkOrder).filter(WorkOrder.id == workorder_id).first()
if not w:
return jsonify({"error": "工单不存在"}), 404
rec = session.query(WorkOrderSuggestion).filter(WorkOrderSuggestion.work_order_id == w.id).first()
if not rec:
return jsonify({"error": "未找到工单建议记录"}), 400
# 使用简化的配置
config = SimpleAIAccuracyConfig()
# 确定使用哪个内容入库
if rec.use_human_resolution and rec.human_resolution:
# AI准确率低于阈值使用人工描述入库
@@ -415,7 +635,7 @@ def approve_workorder_to_knowledge(workorder_id):
logger.info(f"工单 {workorder_id} 使用AI建议入库相似度: {rec.ai_similarity:.4f}")
else:
return jsonify({"error": "未找到可入库的内容"}), 400
# 入库为知识条目
entry = KnowledgeEntry(
question=w.title or (w.description[:20] if w.description else '工单问题'),
@@ -429,9 +649,9 @@ def approve_workorder_to_knowledge(workorder_id):
)
session.add(entry)
session.commit()
return jsonify({
"success": True,
"success": True,
"knowledge_id": entry.id,
"used_content": "human_resolution" if rec.use_human_resolution else "ai_suggestion",
"confidence_score": confidence_score
@@ -447,25 +667,25 @@ def import_workorders():
# 检查是否有文件上传
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列名映射到工单字段
@@ -474,16 +694,16 @@ def import_workorders():
category = str(row.get('分类', row.get('category', '技术问题')))
priority = str(row.get('优先级', row.get('priority', 'medium')))
status = str(row.get('状态', row.get('status', 'open')))
# 验证必填字段
if not title or title.strip() == '':
continue
# 生成唯一的工单ID
timestamp = int(time.time())
unique_id = str(uuid.uuid4())[:8]
order_id = f"IMP_{timestamp}_{unique_id}"
# 创建工单到数据库
try:
with db_manager.get_session() as session:
@@ -497,26 +717,26 @@ def import_workorders():
created_at=datetime.now(),
updated_at=datetime.now()
)
# 处理可选字段
if pd.notna(row.get('解决方案', row.get('resolution'))):
workorder.resolution = str(row.get('解决方案', row.get('resolution')))
if pd.notna(row.get('满意度', row.get('satisfaction_score'))):
try:
workorder.satisfaction_score = int(row.get('满意度', row.get('satisfaction_score')))
except (ValueError, TypeError):
workorder.satisfaction_score = None
session.add(workorder)
session.commit()
logger.info(f"成功导入工单: {order_id} - {title}")
except Exception as db_error:
logger.error(f"导入工单到数据库失败: {db_error}")
continue
# 添加到返回列表
imported_workorders.append({
"id": workorder.id,
@@ -531,23 +751,23 @@ def import_workorders():
"resolution": workorder.resolution,
"satisfaction_score": workorder.satisfaction_score
})
# 清理上传的文件
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
@@ -560,7 +780,7 @@ def download_import_template():
"success": True,
"template_url": f"/uploads/workorder_template.xlsx"
})
except Exception as e:
return jsonify({"error": str(e)}), 500
@@ -569,27 +789,256 @@ def download_import_template_file():
"""直接返回工单导入模板文件(下载)"""
try:
template_path = _ensure_workorder_template_file()
# 检查文件是否存在
if not os.path.exists(template_path):
logger.error(f"模板文件不存在: {template_path}")
return jsonify({"error": "模板文件不存在"}), 404
# 检查文件大小
file_size = os.path.getsize(template_path)
if file_size == 0:
logger.error(f"模板文件为空: {template_path}")
return jsonify({"error": "模板文件为空"}), 500
logger.info(f"准备下载模板文件: {template_path}, 大小: {file_size} bytes")
try:
# Flask>=2 使用 download_name
return send_file(template_path, as_attachment=True, download_name='工单导入模板.xlsx', mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
except TypeError:
# 兼容 Flask<2 的 attachment_filename
return send_file(template_path, as_attachment=True, attachment_filename='工单导入模板.xlsx', mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
except Exception as e:
logger.error(f"下载模板文件失败: {e}")
return jsonify({"error": f"下载失败: {str(e)}"}), 500
@workorders_bp.route('/<int:workorder_id>/dispatch', methods=['POST'])
def dispatch_workorder(workorder_id):
"""工单分发:运维将工单分配给业务模块"""
try:
# 获取当前用户角色和权限
current_role = get_current_user_role()
current_user = get_current_user_name()
# 检查分发权限
if not WorkOrderPermissionManager.can_dispatch_workorder(current_role):
return jsonify({
"success": False,
"error": "无权进行工单分发,只有属地运维和管理员可以分发工单"
}), 403
# 获取请求数据
data = request.get_json() or {}
target_module_str = data.get('target_module', '')
if not target_module_str:
return jsonify({"success": False, "error": "请指定目标模块"}), 400
# 验证模块
try:
target_module = WorkOrderModule(target_module_str)
except ValueError:
return jsonify({"success": False, "error": f"无效的模块: {target_module_str}"}), 400
# 获取工单
with db_manager.get_session() as session:
workorder = session.query(WorkOrder).filter(WorkOrder.id == workorder_id).first()
if not workorder:
return jsonify({"success": False, "error": "工单不存在"}), 404
# 执行分发
module_owner = WorkOrderDispatchManager.get_module_owner(target_module)
# 记录分发前的状态
previous_status = workorder.status
# 更新工单信息
workorder.assigned_module = target_module.value
workorder.module_owner = module_owner
workorder.dispatcher = current_user
workorder.dispatch_time = datetime.now()
workorder.status = "assigned" # 更新状态为已分配
# 根据区域自动设置可以从工单source或其他字段判断
# 这里简化处理,可以根据实际需求调整
if not workorder.region:
# 如果source包含特定关键词可以判断区域
source = workorder.source or ""
if any(keyword in source.lower() for keyword in ["overseas", "abroad", "海外"]):
workorder.region = "overseas"
else:
workorder.region = "domestic"
session.commit()
# 记录处理历史:工单分发
processor_region = "overseas" if current_role == UserRole.OVERSEAS_OPS else "domestic"
add_process_history(
workorder_id=workorder_id,
processor_name=current_user,
process_content=f"工单已分发到{target_module.value}模块,业务接口人:{module_owner}",
action_type="dispatch",
processor_role=current_role.value,
processor_region=processor_region,
previous_status=previous_status,
new_status="assigned",
assigned_module=target_module.value
)
logger.info(f"工单 {workorder_id} 已分发到 {target_module.value} 模块,分发人: {current_user}")
return jsonify({
"success": True,
"message": f"工单已成功分发到{target_module.value}模块",
"workorder": {
"id": workorder.id,
"assigned_module": workorder.assigned_module,
"module_owner": workorder.module_owner,
"dispatcher": workorder.dispatcher,
"dispatch_time": workorder.dispatch_time.isoformat() if workorder.dispatch_time else None,
"status": workorder.status
}
})
except Exception as e:
logger.error(f"工单分发失败: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@workorders_bp.route('/<int:workorder_id>/suggest-module', methods=['POST'])
def suggest_workorder_module(workorder_id):
"""AI建议工单应该分配的模块"""
try:
with db_manager.get_session() as session:
workorder = session.query(WorkOrder).filter(WorkOrder.id == workorder_id).first()
if not workorder:
return jsonify({"success": False, "error": "工单不存在"}), 404
# 使用AI分析建议模块
suggested_module = WorkOrderDispatchManager.suggest_module(
description=workorder.description or "",
title=workorder.title or ""
)
return jsonify({
"success": True,
"suggested_module": suggested_module.value if suggested_module else None,
"module_owner": WorkOrderDispatchManager.get_module_owner(suggested_module) if suggested_module else None
})
except Exception as e:
logger.error(f"模块建议失败: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@workorders_bp.route('/modules', methods=['GET'])
def get_available_modules():
"""获取所有可用的模块列表"""
try:
modules = [
{"value": m.value, "name": m.name, "owner": WorkOrderDispatchManager.get_module_owner(m)}
for m in WorkOrderModule
if m != WorkOrderModule.UNASSIGNED
]
return jsonify({"success": True, "modules": modules})
except Exception as e:
return jsonify({"success": False, "error": str(e)}), 500
@workorders_bp.route('/<int:workorder_id>/process-history', methods=['GET'])
def get_workorder_process_history(workorder_id):
"""获取工单处理过程记录"""
try:
with db_manager.get_session() as session:
workorder = session.query(WorkOrder).filter(WorkOrder.id == workorder_id).first()
if not workorder:
return jsonify({"error": "工单不存在"}), 404
# 获取处理历史
history_list = session.query(WorkOrderProcessHistory).filter(
WorkOrderProcessHistory.work_order_id == workorder_id
).order_by(WorkOrderProcessHistory.process_time.asc()).all()
history_data = []
for ph in history_list:
history_data.append({
"id": ph.id,
"processor_name": ph.processor_name,
"processor_role": ph.processor_role,
"processor_region": ph.processor_region,
"process_content": ph.process_content,
"action_type": ph.action_type,
"previous_status": ph.previous_status,
"new_status": ph.new_status,
"assigned_module": ph.assigned_module,
"process_time": ph.process_time.isoformat() if ph.process_time else None
})
return jsonify({
"success": True,
"workorder_id": workorder_id,
"process_history": history_data,
"total": len(history_data)
})
except Exception as e:
logger.error(f"获取处理历史失败: {e}")
return jsonify({"success": False, "error": str(e)}), 500
@workorders_bp.route('/<int:workorder_id>/process-history', methods=['POST'])
def add_workorder_process_history(workorder_id):
"""手动添加工单处理过程记录"""
try:
# 获取当前用户信息
current_user = get_current_user_name()
current_role = get_current_user_role()
data = request.get_json() or {}
process_content = data.get('process_content', '').strip()
if not process_content:
return jsonify({"success": False, "error": "处理内容不能为空"}), 400
with db_manager.get_session() as session:
workorder = session.query(WorkOrder).filter(WorkOrder.id == workorder_id).first()
if not workorder:
return jsonify({"success": False, "error": "工单不存在"}), 404
# 获取可选参数
action_type = data.get('action_type', 'process') # 默认操作类型为process
processor_role = data.get('processor_role', current_role.value)
processor_region = data.get('processor_region')
if not processor_region:
# 根据角色自动判断区域
processor_region = "overseas" if current_role == UserRole.OVERSEAS_OPS else "domestic"
previous_status = data.get('previous_status', workorder.status)
new_status = data.get('new_status', workorder.status)
assigned_module = data.get('assigned_module', workorder.assigned_module)
# 添加处理记录
history = add_process_history(
workorder_id=workorder_id,
processor_name=data.get('processor_name', current_user),
process_content=process_content,
action_type=action_type,
processor_role=processor_role,
processor_region=processor_region,
previous_status=previous_status,
new_status=new_status,
assigned_module=assigned_module
)
return jsonify({
"success": True,
"message": "处理记录已添加",
"history": {
"id": history.id,
"processor_name": history.processor_name,
"processor_role": history.processor_role,
"process_content": history.process_content,
"action_type": history.action_type,
"process_time": history.process_time.isoformat() if history.process_time else None
}
})
except Exception as e:
logger.error(f"添加处理记录失败: {e}")
return jsonify({"success": False, "error": str(e)}), 500

View File

@@ -6,7 +6,7 @@ class ChatClient {
this.sessionId = null;
this.isConnected = false;
this.messageCount = 0;
this.init();
}
@@ -18,24 +18,24 @@ class ChatClient {
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) => {
@@ -50,17 +50,17 @@ class ChatClient {
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();
@@ -69,7 +69,7 @@ class ChatClient {
} else {
this.showError('创建会话失败');
}
} catch (error) {
console.error('启动对话失败:', error);
this.showError('启动对话失败: ' + error.message);
@@ -84,11 +84,11 @@ class ChatClient {
session_id: this.sessionId
});
}
this.sessionId = null;
this.disableChat();
this.addSystemMessage('对话已结束。');
} catch (error) {
console.error('结束对话失败:', error);
}
@@ -97,48 +97,48 @@ class ChatClient {
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);
@@ -151,12 +151,12 @@ class ChatClient {
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',
@@ -166,23 +166,23 @@ class ChatClient {
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);
@@ -193,7 +193,7 @@ class ChatClient {
return new Promise((resolve, reject) => {
try {
this.websocket = new WebSocket('ws://localhost:8765');
// 设置连接超时
const timeout = setTimeout(() => {
if (this.websocket.readyState !== WebSocket.OPEN) {
@@ -201,26 +201,26 @@ class ChatClient {
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);
@@ -229,7 +229,7 @@ class ChatClient {
console.error('解析WebSocket消息失败:', error);
}
};
} catch (error) {
reject(error);
}
@@ -242,15 +242,15 @@ class ChatClient {
reject(new Error('WebSocket未连接'));
return;
}
const messageId = 'msg_' + Date.now();
message.messageId = messageId;
// 设置超时
const timeout = setTimeout(() => {
reject(new Error('请求超时'));
}, 10000);
// 监听响应
const handleResponse = (event) => {
try {
@@ -264,7 +264,7 @@ class ChatClient {
// 忽略解析错误
}
};
this.websocket.addEventListener('message', handleResponse);
this.websocket.send(JSON.stringify(message));
});
@@ -277,29 +277,29 @@ class ChatClient {
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');
@@ -307,21 +307,21 @@ class ChatClient {
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);
@@ -329,20 +329,20 @@ class ChatClient {
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;
}
@@ -394,7 +394,7 @@ class ChatClient {
this.showError('请先开始对话');
return;
}
const modal = new bootstrap.Modal(document.getElementById('workOrderModal'));
modal.show();
}

File diff suppressed because it is too large Load Diff

View File

@@ -572,12 +572,12 @@
<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>开始对话
@@ -589,9 +589,9 @@
<i class="fas fa-plus me-2"></i>创建工单
</button>
</div>
<hr>
<div class="mb-3">
<h6>快速操作</h6>
<div class="quick-actions">
@@ -601,7 +601,7 @@
<button class="quick-action-btn" data-message="如何解绑车辆">解绑车辆</button>
</div>
</div>
<div class="mb-3">
<h6>会话信息</h6>
<div id="session-info" class="text-muted">
@@ -611,7 +611,7 @@
</div>
</div>
</div>
<div class="col-md-9">
<div class="card chat-container">
<div class="chat-header">
@@ -625,7 +625,7 @@
</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>
@@ -633,10 +633,10 @@
<p>请点击"开始对话"按钮开始聊天</p>
</div>
</div>
<div class="chat-input">
<div class="input-group">
<input type="text" class="form-control" id="message-input"
<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>
@@ -721,7 +721,7 @@
</button>
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<button class="btn btn-agent w-100" id="proactive-monitoring">
@@ -1155,7 +1155,7 @@
<i class="fas fa-refresh me-1"></i>刷新状态
</button>
</div>
<!-- 字段映射管理区域 -->
<div class="row mb-4" id="fieldMappingSection" style="display: none;">
<div class="col-12">
@@ -1196,13 +1196,13 @@
<option value="100">前100条</option>
</select>
</div>
<!-- 同步进度 -->
<div class="progress mb-3" id="syncProgress" style="display: none;">
<div class="progress-bar progress-bar-striped progress-bar-animated"
<div class="progress-bar progress-bar-striped progress-bar-animated"
role="progressbar" style="width: 0%"></div>
</div>
<!-- 同步日志 -->
<div class="mt-3">
<h6>同步日志</h6>
@@ -1327,7 +1327,7 @@
</div>
</div>
</div>
<div class="card mt-3">
<div class="card-header">
<h5><i class="fas fa-memory me-2"></i>对话记忆</h5>
@@ -2230,13 +2230,13 @@
<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>
@@ -2301,7 +2301,7 @@
</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>
@@ -2311,7 +2311,7 @@
<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>

View File

@@ -213,18 +213,22 @@ 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
# 检查连接头(如果可用)
try:
if hasattr(websocket, 'request_headers'):
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}")
# 在websockets 15.x中连接已经在serve时验证所以这里只记录警告
except AttributeError:
# websockets 15.x版本可能没有request_headers属性跳过检查
pass
await self.register_client(websocket)
@@ -243,19 +247,15 @@ class WebSocketServer:
logger.info(f"启动WebSocket服务器: ws://{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 def handle_client_with_cors(websocket: WebSocketServerProtocol, path: str = None):
# CORS处理websockets库默认允许所有来源连接
# 如果需要限制可以在serve时使用additional_headers参数
await self.handle_client(websocket, path or "")
async with websockets.serve(
handle_client_with_cors,
self.host,
self.port,
# 添加额外的服务器选项
process_request=self._process_request
self.port
):
await asyncio.Future() # 保持服务器运行