feat: 娣诲姞澶氫釜鏂板姛鑳藉拰淇 - 鍖呮嫭鐢ㄦ埛绠$悊銆佹暟鎹簱杩佺Щ銆丟it鎺ㄩ€佸伐鍏风瓑
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -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({
|
||||
|
||||
BIN
src/web/blueprints/__pycache__/auth.cpython-311.pyc
Normal file
BIN
src/web/blueprints/__pycache__/auth.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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() # 保持服务器运行
|
||||
|
||||
|
||||
Reference in New Issue
Block a user