feat: 娣诲姞澶氫釜鏂板姛鑳藉拰淇 - 鍖呮嫭鐢ㄦ埛绠$悊銆佹暟鎹簱杩佺Щ銆丟it鎺ㄩ€佸伐鍏风瓑
This commit is contained in:
Binary file not shown.
@@ -212,7 +212,7 @@ class TSPAgentAssistant:
|
||||
try:
|
||||
self.is_agent_mode = enabled
|
||||
logger.info(f"Agent模式: {'启用' if enabled else '禁用'}")
|
||||
return True
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"切换Agent模式失败: {e}")
|
||||
return False
|
||||
@@ -233,8 +233,8 @@ class TSPAgentAssistant:
|
||||
"""停止主动监控"""
|
||||
try:
|
||||
self.ai_monitoring_active = False
|
||||
logger.info("主动监控已停止")
|
||||
return True
|
||||
logger.info("主动监控已停止")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"停止主动监控失败: {e}")
|
||||
return False
|
||||
@@ -261,14 +261,14 @@ class TSPAgentAssistant:
|
||||
recent_executions = self.get_action_history(20)
|
||||
|
||||
# 生成分析报告
|
||||
analysis = {
|
||||
analysis = {
|
||||
"tool_performance": tool_performance,
|
||||
"recent_activity": len(recent_executions),
|
||||
"success_rate": tool_performance.get("success_rate", 0),
|
||||
"recommendations": self._generate_recommendations(tool_performance)
|
||||
}
|
||||
|
||||
return analysis
|
||||
}
|
||||
|
||||
return analysis
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"运行智能分析失败: {e}")
|
||||
@@ -357,8 +357,8 @@ class TSPAgentAssistant:
|
||||
try:
|
||||
logger.info(f"保存知识条目 {i+1}: {entry.get('question', '')[:50]}...")
|
||||
# 这里应该调用知识库管理器保存
|
||||
saved_count += 1
|
||||
logger.info(f"知识条目 {i+1} 保存成功")
|
||||
saved_count += 1
|
||||
logger.info(f"知识条目 {i+1} 保存成功")
|
||||
except Exception as save_error:
|
||||
logger.error(f"保存知识条目 {i+1} 时出错: {save_error}")
|
||||
|
||||
@@ -380,9 +380,9 @@ class TSPAgentAssistant:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
return f.read()
|
||||
elif file_ext == '.pdf':
|
||||
return "PDF文件需要安装PyPDF2库"
|
||||
return "PDF文件需要安装PyPDF2库"
|
||||
elif file_ext in ['.doc', '.docx']:
|
||||
return "Word文件需要安装python-docx库"
|
||||
return "Word文件需要安装python-docx库"
|
||||
else:
|
||||
return "不支持的文件格式"
|
||||
except Exception as e:
|
||||
|
||||
Binary file not shown.
BIN
src/core/__pycache__/workorder_permissions.cpython-311.pyc
Normal file
BIN
src/core/__pycache__/workorder_permissions.cpython-311.pyc
Normal file
Binary file not shown.
@@ -8,7 +8,7 @@ Base = declarative_base()
|
||||
class WorkOrder(Base):
|
||||
"""工单模型"""
|
||||
__tablename__ = "work_orders"
|
||||
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
order_id = Column(String(50), unique=True, nullable=False)
|
||||
title = Column(String(200), nullable=False)
|
||||
@@ -20,13 +20,13 @@ class WorkOrder(Base):
|
||||
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||
resolution = Column(Text)
|
||||
satisfaction_score = Column(Float)
|
||||
|
||||
|
||||
# 飞书集成字段
|
||||
feishu_record_id = Column(String(100), unique=True, nullable=True) # 飞书记录ID
|
||||
assignee = Column(String(100), nullable=True) # 负责人
|
||||
solution = Column(Text, nullable=True) # 解决方案
|
||||
ai_suggestion = Column(Text, nullable=True) # AI建议
|
||||
|
||||
|
||||
# 扩展飞书字段
|
||||
source = Column(String(50), nullable=True) # 来源(Mail, Telegram bot等)
|
||||
module = Column(String(100), nullable=True) # 模块(local O&M, OTA等)
|
||||
@@ -40,14 +40,23 @@ class WorkOrder(Base):
|
||||
parent_record = Column(String(100), nullable=True) # 父记录
|
||||
has_updated_same_day = Column(String(50), nullable=True) # 是否同日更新
|
||||
operating_time = Column(String(100), nullable=True) # 操作时间
|
||||
|
||||
|
||||
# 工单分发和权限管理字段
|
||||
assigned_module = Column(String(50), nullable=True) # 分配的模块(TBOX、OTA等)
|
||||
module_owner = Column(String(100), nullable=True) # 业务接口人/模块负责人
|
||||
dispatcher = Column(String(100), nullable=True) # 分发人(运维人员)
|
||||
dispatch_time = Column(DateTime, nullable=True) # 分发时间
|
||||
region = Column(String(50), nullable=True) # 区域(overseas/domestic)- 用于区分海外/国内
|
||||
|
||||
# 关联对话记录
|
||||
conversations = relationship("Conversation", back_populates="work_order")
|
||||
# 关联处理过程记录
|
||||
process_history = relationship("WorkOrderProcessHistory", back_populates="work_order", order_by="WorkOrderProcessHistory.process_time")
|
||||
|
||||
class Conversation(Base):
|
||||
"""对话记录模型"""
|
||||
__tablename__ = "conversations"
|
||||
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
work_order_id = Column(Integer, ForeignKey("work_orders.id"))
|
||||
user_message = Column(Text, nullable=False)
|
||||
@@ -56,13 +65,13 @@ class Conversation(Base):
|
||||
confidence_score = Column(Float)
|
||||
knowledge_used = Column(Text) # 使用的知识库条目
|
||||
response_time = Column(Float) # 响应时间(秒)
|
||||
|
||||
|
||||
work_order = relationship("WorkOrder", back_populates="conversations")
|
||||
|
||||
class KnowledgeEntry(Base):
|
||||
"""知识库条目模型"""
|
||||
__tablename__ = "knowledge_entries"
|
||||
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
question = Column(Text, nullable=False)
|
||||
answer = Column(Text, nullable=False)
|
||||
@@ -80,7 +89,7 @@ class KnowledgeEntry(Base):
|
||||
class VehicleData(Base):
|
||||
"""车辆实时数据模型"""
|
||||
__tablename__ = "vehicle_data"
|
||||
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
vehicle_id = Column(String(50), nullable=False) # 车辆ID
|
||||
vehicle_vin = Column(String(17)) # 车架号
|
||||
@@ -88,7 +97,7 @@ class VehicleData(Base):
|
||||
data_value = Column(Text, nullable=False) # 数据值(JSON格式)
|
||||
timestamp = Column(DateTime, default=datetime.now) # 数据时间戳
|
||||
is_active = Column(Boolean, default=True) # 是否有效
|
||||
|
||||
|
||||
# 索引
|
||||
__table_args__ = (
|
||||
{'extend_existing': True}
|
||||
@@ -97,7 +106,7 @@ class VehicleData(Base):
|
||||
class Analytics(Base):
|
||||
"""分析统计模型"""
|
||||
__tablename__ = "analytics"
|
||||
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
date = Column(DateTime, nullable=False)
|
||||
total_orders = Column(Integer, default=0)
|
||||
@@ -111,7 +120,7 @@ class Analytics(Base):
|
||||
class Alert(Base):
|
||||
"""预警模型"""
|
||||
__tablename__ = "alerts"
|
||||
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
rule_name = Column(String(100), nullable=False)
|
||||
alert_type = Column(String(50), nullable=False)
|
||||
@@ -126,7 +135,7 @@ class Alert(Base):
|
||||
class WorkOrderSuggestion(Base):
|
||||
"""工单AI建议与人工描述表"""
|
||||
__tablename__ = "work_order_suggestions"
|
||||
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
work_order_id = Column(Integer, ForeignKey("work_orders.id"), nullable=False)
|
||||
ai_suggestion = Column(Text)
|
||||
@@ -136,3 +145,31 @@ class WorkOrderSuggestion(Base):
|
||||
use_human_resolution = Column(Boolean, default=False) # 是否使用人工描述入库
|
||||
created_at = Column(DateTime, default=datetime.now)
|
||||
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||
|
||||
class WorkOrderProcessHistory(Base):
|
||||
"""工单处理过程记录表"""
|
||||
__tablename__ = "work_order_process_history"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
work_order_id = Column(Integer, ForeignKey("work_orders.id"), nullable=False)
|
||||
|
||||
# 处理人员信息
|
||||
processor_name = Column(String(100), nullable=False) # 处理人员姓名
|
||||
processor_role = Column(String(50), nullable=True) # 处理人员角色(运维、业务方等)
|
||||
processor_region = Column(String(50), nullable=True) # 处理人员区域(overseas/domestic)
|
||||
|
||||
# 处理内容
|
||||
process_content = Column(Text, nullable=False) # 处理内容/操作描述
|
||||
action_type = Column(String(50), nullable=False) # 操作类型(dispatch、process、close、reassign等)
|
||||
|
||||
# 处理结果
|
||||
previous_status = Column(String(50), nullable=True) # 处理前的状态
|
||||
new_status = Column(String(50), nullable=True) # 处理后的状态
|
||||
assigned_module = Column(String(50), nullable=True) # 分配的模块(如果是分发操作)
|
||||
|
||||
# 时间戳
|
||||
process_time = Column(DateTime, default=datetime.now, nullable=False) # 处理时间
|
||||
created_at = Column(DateTime, default=datetime.now)
|
||||
|
||||
# 关联工单
|
||||
work_order = relationship("WorkOrder", back_populates="process_history")
|
||||
|
||||
231
src/core/workorder_permissions.py
Normal file
231
src/core/workorder_permissions.py
Normal file
@@ -0,0 +1,231 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
工单权限管理模块
|
||||
实现基于角色的访问控制(RBAC)和工单分发流程
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import List, Dict, Optional, Set
|
||||
from enum import Enum
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class UserRole(Enum):
|
||||
"""用户角色枚举"""
|
||||
# 属地运维(海外/国内)
|
||||
OVERSEAS_OPS = "overseas_ops" # 海外属地运维
|
||||
DOMESTIC_OPS = "domestic_ops" # 国内属地运维
|
||||
|
||||
# 业务方接口人(各模块负责人)
|
||||
TBOX_OWNER = "tbox_owner" # TBOX模块负责人
|
||||
OTA_OWNER = "ota_owner" # OTA模块负责人
|
||||
DMC_OWNER = "dmc_owner" # DMC模块负责人
|
||||
MES_OWNER = "mes_owner" # MES模块负责人
|
||||
APP_OWNER = "app_owner" # APP模块负责人
|
||||
PKI_OWNER = "pki_owner" # PKI模块负责人
|
||||
TSP_OWNER = "tsp_owner" # TSP模块负责人
|
||||
|
||||
# 系统角色
|
||||
ADMIN = "admin" # 系统管理员
|
||||
VIEWER = "viewer" # 只读用户
|
||||
|
||||
class WorkOrderModule(Enum):
|
||||
"""工单模块枚举"""
|
||||
TBOX = "TBOX"
|
||||
OTA = "OTA"
|
||||
DMC = "DMC"
|
||||
MES = "MES"
|
||||
APP = "APP"
|
||||
PKI = "PKI"
|
||||
TSP = "TSP"
|
||||
LOCAL_OPS = "local_ops" # 属地运维处理
|
||||
UNASSIGNED = "unassigned" # 未分配
|
||||
|
||||
class WorkOrderStatus:
|
||||
"""工单状态常量"""
|
||||
PENDING = "pending" # 待处理
|
||||
ASSIGNED = "assigned" # 已分配
|
||||
IN_PROGRESS = "in_progress" # 处理中
|
||||
RESOLVED = "resolved" # 已解决
|
||||
CLOSED = "closed" # 已关闭
|
||||
|
||||
class WorkOrderPermissionManager:
|
||||
"""工单权限管理器"""
|
||||
|
||||
# 所有模块集合(供属地运维和管理员使用)
|
||||
ALL_MODULES = {
|
||||
WorkOrderModule.TBOX, WorkOrderModule.OTA, WorkOrderModule.DMC,
|
||||
WorkOrderModule.MES, WorkOrderModule.APP, WorkOrderModule.PKI,
|
||||
WorkOrderModule.TSP, WorkOrderModule.LOCAL_OPS
|
||||
}
|
||||
|
||||
# 角色到模块的映射
|
||||
ROLE_MODULE_MAP = {
|
||||
UserRole.TBOX_OWNER: {WorkOrderModule.TBOX},
|
||||
UserRole.OTA_OWNER: {WorkOrderModule.OTA},
|
||||
UserRole.DMC_OWNER: {WorkOrderModule.DMC},
|
||||
UserRole.MES_OWNER: {WorkOrderModule.MES},
|
||||
UserRole.APP_OWNER: {WorkOrderModule.APP},
|
||||
UserRole.PKI_OWNER: {WorkOrderModule.PKI},
|
||||
UserRole.TSP_OWNER: {WorkOrderModule.TSP},
|
||||
UserRole.OVERSEAS_OPS: ALL_MODULES, # 可访问所有模块
|
||||
UserRole.DOMESTIC_OPS: ALL_MODULES, # 可访问所有模块
|
||||
UserRole.ADMIN: ALL_MODULES, # 管理员可访问所有
|
||||
UserRole.VIEWER: set(), # 只读,由其他逻辑控制
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def can_view_all_workorders(role: UserRole) -> bool:
|
||||
"""判断角色是否可以查看所有工单(属地运维和管理员)"""
|
||||
return role in [UserRole.OVERSEAS_OPS, UserRole.DOMESTIC_OPS, UserRole.ADMIN]
|
||||
|
||||
@staticmethod
|
||||
def get_accessible_modules(role: UserRole) -> Set[WorkOrderModule]:
|
||||
"""获取角色可访问的模块列表"""
|
||||
return WorkOrderPermissionManager.ROLE_MODULE_MAP.get(role, set())
|
||||
|
||||
@staticmethod
|
||||
def can_access_module(role: UserRole, module: WorkOrderModule) -> bool:
|
||||
"""判断角色是否可以访问指定模块"""
|
||||
accessible_modules = WorkOrderPermissionManager.get_accessible_modules(role)
|
||||
|
||||
# 属地运维和管理员可以访问所有模块
|
||||
if WorkOrderPermissionManager.can_view_all_workorders(role):
|
||||
return True
|
||||
|
||||
# 业务方只能访问自己的模块
|
||||
return module in accessible_modules
|
||||
|
||||
@staticmethod
|
||||
def can_dispatch_workorder(role: UserRole) -> bool:
|
||||
"""判断角色是否可以进行工单分发(属地运维和管理员)"""
|
||||
return role in [UserRole.OVERSEAS_OPS, UserRole.DOMESTIC_OPS, UserRole.ADMIN]
|
||||
|
||||
@staticmethod
|
||||
def can_update_workorder(role: UserRole, workorder_module: Optional[WorkOrderModule],
|
||||
assigned_to_module: Optional[WorkOrderModule]) -> bool:
|
||||
"""判断角色是否可以更新工单"""
|
||||
# 管理员和属地运维可以更新所有工单
|
||||
if WorkOrderPermissionManager.can_view_all_workorders(role):
|
||||
return True
|
||||
|
||||
# 业务方只能更新分配给自己的模块的工单
|
||||
if workorder_module and assigned_to_module:
|
||||
accessible_modules = WorkOrderPermissionManager.get_accessible_modules(role)
|
||||
return workorder_module in accessible_modules and workorder_module == assigned_to_module
|
||||
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def filter_workorders_by_permission(role: UserRole, workorders: List[Dict]) -> List[Dict]:
|
||||
"""根据权限过滤工单列表"""
|
||||
if WorkOrderPermissionManager.can_view_all_workorders(role):
|
||||
# 属地运维和管理员可以看到所有工单
|
||||
return workorders
|
||||
|
||||
# 业务方只能看到自己模块的工单
|
||||
accessible_modules = WorkOrderPermissionManager.get_accessible_modules(role)
|
||||
filtered = []
|
||||
|
||||
for wo in workorders:
|
||||
module_str = wo.get("module") or wo.get("assigned_module")
|
||||
if module_str:
|
||||
try:
|
||||
module = WorkOrderModule(module_str)
|
||||
if module in accessible_modules:
|
||||
filtered.append(wo)
|
||||
except ValueError:
|
||||
# 如果模块值不在枚举中,跳过
|
||||
continue
|
||||
else:
|
||||
# 未分配的工单,业务方看不到
|
||||
pass
|
||||
|
||||
return filtered
|
||||
|
||||
class WorkOrderDispatchManager:
|
||||
"""工单分发管理器"""
|
||||
|
||||
# 模块到业务接口人的映射(可以动态配置)
|
||||
MODULE_OWNER_MAP = {
|
||||
WorkOrderModule.TBOX: "TBOX业务接口人",
|
||||
WorkOrderModule.OTA: "OTA业务接口人",
|
||||
WorkOrderModule.DMC: "DMC业务接口人",
|
||||
WorkOrderModule.MES: "MES业务接口人",
|
||||
WorkOrderModule.APP: "APP业务接口人",
|
||||
WorkOrderModule.PKI: "PKI业务接口人",
|
||||
WorkOrderModule.TSP: "TSP业务接口人",
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_module_owner(module: WorkOrderModule) -> str:
|
||||
"""获取模块的业务接口人"""
|
||||
return WorkOrderDispatchManager.MODULE_OWNER_MAP.get(module, "未指定")
|
||||
|
||||
@staticmethod
|
||||
def dispatch_workorder(workorder_id: int, target_module: WorkOrderModule,
|
||||
dispatcher_role: UserRole, dispatcher_name: str) -> Dict:
|
||||
"""
|
||||
分发工单到指定模块
|
||||
|
||||
Args:
|
||||
workorder_id: 工单ID
|
||||
target_module: 目标模块
|
||||
dispatcher_role: 分发者角色(必须是运维或管理员)
|
||||
dispatcher_name: 分发者姓名
|
||||
|
||||
Returns:
|
||||
分发结果
|
||||
"""
|
||||
# 检查分发权限
|
||||
if not WorkOrderPermissionManager.can_dispatch_workorder(dispatcher_role):
|
||||
return {
|
||||
"success": False,
|
||||
"error": "无权进行工单分发,只有属地运维和管理员可以分发工单"
|
||||
}
|
||||
|
||||
# 获取模块负责人
|
||||
module_owner = WorkOrderDispatchManager.get_module_owner(target_module)
|
||||
|
||||
# 这里应该更新数据库中的工单信息
|
||||
# 实际实现时需要调用数据库更新逻辑
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"工单已分发到{target_module.value}模块",
|
||||
"assigned_module": target_module.value,
|
||||
"module_owner": module_owner,
|
||||
"dispatcher": dispatcher_name,
|
||||
"dispatcher_role": dispatcher_role.value
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def suggest_module(description: str, title: str = "") -> Optional[WorkOrderModule]:
|
||||
"""
|
||||
根据工单描述建议分配模块(可以使用AI分析)
|
||||
|
||||
Args:
|
||||
description: 工单描述
|
||||
title: 工单标题
|
||||
|
||||
Returns:
|
||||
建议的模块
|
||||
"""
|
||||
# 简单的关键词匹配(实际可以使用AI分析)
|
||||
text = (title + " " + description).lower()
|
||||
|
||||
keyword_module_map = {
|
||||
WorkOrderModule.TBOX: ["tbox", "telematics", "车载", "车联网"],
|
||||
WorkOrderModule.OTA: ["ota", "over-the-air", "升级", "update"],
|
||||
WorkOrderModule.DMC: ["dmc", "device management", "设备管理"],
|
||||
WorkOrderModule.MES: ["mes", "manufacturing", "制造"],
|
||||
WorkOrderModule.APP: ["app", "application", "应用", "remote control"],
|
||||
WorkOrderModule.PKI: ["pki", "certificate", "证书"],
|
||||
WorkOrderModule.TSP: ["tsp", "service", "服务"],
|
||||
}
|
||||
|
||||
for module, keywords in keyword_module_map.items():
|
||||
for keyword in keywords:
|
||||
if keyword in text:
|
||||
return module
|
||||
|
||||
return WorkOrderModule.UNASSIGNED
|
||||
Binary file not shown.
Binary file not shown.
@@ -23,7 +23,7 @@ class AISuggestionService:
|
||||
self.llm_config = get_config().llm
|
||||
logger.info(f"使用LLM配置: {self.llm_config.provider} - {self.llm_config.model}")
|
||||
|
||||
def generate_suggestion(self, tr_description: str, process_history: Optional[str] = None, vin: Optional[str] = None) -> str:
|
||||
def generate_suggestion(self, tr_description: str, process_history: Optional[str] = None, vin: Optional[str] = None, existing_ai_suggestion: Optional[str] = None) -> str:
|
||||
"""
|
||||
生成AI建议 - 参考处理过程记录生成建议
|
||||
|
||||
@@ -31,6 +31,7 @@ class AISuggestionService:
|
||||
tr_description: TR描述
|
||||
process_history: 处理过程记录(可选,用于了解当前问题状态)
|
||||
vin: 车架号(可选)
|
||||
existing_ai_suggestion: 现有的AI建议(可选,用于判断是否是首次建议)
|
||||
|
||||
Returns:
|
||||
AI建议文本
|
||||
@@ -41,6 +42,11 @@ class AISuggestionService:
|
||||
|
||||
chat_manager = RealtimeChatManager()
|
||||
|
||||
# 判断是否是首次建议(通过检查现有AI建议)
|
||||
is_first_suggestion = True
|
||||
if existing_ai_suggestion and existing_ai_suggestion.strip():
|
||||
is_first_suggestion = False
|
||||
|
||||
# 构建上下文信息
|
||||
context_info = ""
|
||||
if process_history and process_history.strip():
|
||||
@@ -49,17 +55,29 @@ class AISuggestionService:
|
||||
已处理的步骤:
|
||||
{process_history}"""
|
||||
|
||||
# 根据是否为首次建议,设置不同的提示词
|
||||
if is_first_suggestion:
|
||||
# 首次建议:只给出一般性的排查步骤,不要提进站抓取日志
|
||||
suggestion_instruction = """要求:
|
||||
1. 首次给客户建议,只提供远程可操作的一般性排查步骤
|
||||
2. 如检查网络、重启系统、确认配置等常见操作
|
||||
3. 绝对不要提到"进站"、"抓取日志"等需要线下操作的内容
|
||||
4. 语言简洁精炼,用逗号连接,不要用序号或分行"""
|
||||
else:
|
||||
# 后续建议:如果已有处理记录但未解决,可以考虑更深入的方案
|
||||
suggestion_instruction = """要求:
|
||||
1. 基于已有处理步骤,给出下一步的排查建议
|
||||
2. 如果远程操作都无法解决,可以考虑更深入的诊断方案
|
||||
3. 语言简洁精炼,用逗号连接,不要用序号或分行"""
|
||||
|
||||
# 构建用户消息 - 要求生成简洁的简短建议
|
||||
user_message = f"""请为以下问题提供精炼的技术支持操作建议:
|
||||
|
||||
格式要求:
|
||||
1. 用逗号连接,一句话表达,不要用序号或分行
|
||||
2. 现状+步骤,语言精炼
|
||||
3. 总长度控制在150字以内
|
||||
1. 现状+步骤,语言精炼
|
||||
2. 总长度控制在150字以内
|
||||
|
||||
根据问题复杂程度选择结尾:
|
||||
- 简单问题:给出具体操作步骤即可,不需要提日志分析
|
||||
- 复杂问题:如远程操作无法解决,结尾才使用"建议邀请用户进站抓取日志分析"
|
||||
{suggestion_instruction}
|
||||
|
||||
问题描述:{tr_description}{context_info}"""
|
||||
|
||||
@@ -76,13 +94,13 @@ class AISuggestionService:
|
||||
logger.info(f"AI生成原始内容: {content[:100]}...")
|
||||
|
||||
# 二次处理:替换默认建议(在清理前先替换)
|
||||
content = self._post_process_suggestion(content)
|
||||
content = self._post_process_suggestion(content, is_first_suggestion)
|
||||
|
||||
# 清理并限制长度
|
||||
cleaned = self._clean_response(content)
|
||||
|
||||
# 再次检查,确保替换生效
|
||||
cleaned = self._post_process_suggestion(cleaned)
|
||||
cleaned = self._post_process_suggestion(cleaned, is_first_suggestion)
|
||||
|
||||
# 记录清理后的内容
|
||||
logger.info(f"AI建议清理后: {cleaned[:100]}...")
|
||||
@@ -178,12 +196,13 @@ class AISuggestionService:
|
||||
|
||||
return cleaned
|
||||
|
||||
def _post_process_suggestion(self, content: str) -> str:
|
||||
def _post_process_suggestion(self, content: str, is_first_suggestion: bool = True) -> str:
|
||||
"""
|
||||
二次处理建议内容:替换默认建议文案
|
||||
|
||||
Args:
|
||||
content: 清理后的内容
|
||||
is_first_suggestion: 是否是首次建议
|
||||
|
||||
Returns:
|
||||
处理后的内容
|
||||
@@ -191,22 +210,38 @@ class AISuggestionService:
|
||||
if not content or not content.strip():
|
||||
return content
|
||||
|
||||
# 替换各种形式的"联系售后技术支持"为"邀请用户进站抓取日志分析"
|
||||
replacements = [
|
||||
("建议联系售后技术支持进一步排查", "建议邀请用户进站抓取日志分析"),
|
||||
("联系售后技术支持进行进一步排查", "邀请用户进站抓取日志分析"),
|
||||
("建议联系售后技术支持", "建议邀请用户进站抓取日志分析"),
|
||||
("联系售后技术支持", "邀请用户进站抓取日志分析"),
|
||||
("如问题仍未解决,建议联系售后技术支持进行进一步排查", "如问题仍未解决,建议邀请用户进站抓取日志分析"),
|
||||
("若仍无效,建议联系售后技术支持进一步排查", "若仍无效,建议邀请用户进站抓取日志分析"),
|
||||
("仍无效,建议联系售后技术支持", "仍无效,建议邀请用户进站抓取日志分析"),
|
||||
]
|
||||
|
||||
result = content
|
||||
for old_text, new_text in replacements:
|
||||
if old_text in result:
|
||||
result = result.replace(old_text, new_text)
|
||||
logger.info(f"✓ 替换建议文案: '{old_text}' -> '{new_text}'")
|
||||
|
||||
# 如果是首次建议,移除所有"进站"、"抓取日志"相关的内容
|
||||
if is_first_suggestion:
|
||||
# 移除进站相关的文案
|
||||
station_keywords = [
|
||||
"进站", "抓取日志", "邀请用户进站", "建议邀请用户进站",
|
||||
"建议进站", "需要进站", "前往服务站", "联系售后", "售后技术支持"
|
||||
]
|
||||
for keyword in station_keywords:
|
||||
if keyword in result:
|
||||
# 找到包含关键词的句子并移除
|
||||
lines = result.split(',')
|
||||
new_lines = [line for line in lines if keyword not in line]
|
||||
result = ','.join(new_lines)
|
||||
logger.info(f"首次建议,移除包含'{keyword}'的内容")
|
||||
else:
|
||||
# 非首次建议:替换"联系售后技术支持"为"邀请用户进站抓取日志分析"
|
||||
replacements = [
|
||||
("建议联系售后技术支持进一步排查", "建议邀请用户进站抓取日志分析"),
|
||||
("联系售后技术支持进行进一步排查", "邀请用户进站抓取日志分析"),
|
||||
("建议联系售后技术支持", "建议邀请用户进站抓取日志分析"),
|
||||
("联系售后技术支持", "邀请用户进站抓取日志分析"),
|
||||
("如问题仍未解决,建议联系售后技术支持进行进一步排查", "如问题仍未解决,建议邀请用户进站抓取日志分析"),
|
||||
("若仍无效,建议联系售后技术支持进一步排查", "若仍无效,建议邀请用户进站抓取日志分析"),
|
||||
("仍无效,建议联系售后技术支持", "仍无效,建议邀请用户进站抓取日志分析"),
|
||||
]
|
||||
|
||||
for old_text, new_text in replacements:
|
||||
if old_text in result:
|
||||
result = result.replace(old_text, new_text)
|
||||
logger.info(f"✓ 替换建议文案: '{old_text}' -> '{new_text}'")
|
||||
|
||||
# 如果没有任何替换,记录一下
|
||||
if result == content:
|
||||
@@ -342,7 +377,7 @@ class AISuggestionService:
|
||||
logger.info(f"记录 {record.get('record_id', i)} - 现有AI建议前100字符: {existing_ai_suggestion[:100]}")
|
||||
|
||||
if tr_description:
|
||||
ai_suggestion = self.generate_suggestion(tr_description, process_history, vin)
|
||||
ai_suggestion = self.generate_suggestion(tr_description, process_history, vin, existing_ai_suggestion)
|
||||
# 处理同一天多次更新的情况
|
||||
new_suggestion = self._format_ai_suggestion_with_numbering(
|
||||
time_str, ai_suggestion, existing_ai_suggestion
|
||||
|
||||
@@ -53,13 +53,13 @@ class WorkOrderSyncService:
|
||||
self.field_mapping = {
|
||||
# 核心字段
|
||||
"TR Number": "order_id",
|
||||
"TR Description": "description",
|
||||
"TR Description": "description", # 问题描述
|
||||
"Type of problem": "category",
|
||||
"TR Level": "priority",
|
||||
"TR Status": "status",
|
||||
"Source": "source",
|
||||
"Date creation": "created_at",
|
||||
"处理过程": "solution",
|
||||
"处理过程": "resolution", # 处理过程历史记录(存储完整历史到resolution字段)
|
||||
"TR tracking": "resolution",
|
||||
|
||||
# 扩展字段
|
||||
@@ -194,18 +194,28 @@ class WorkOrderSyncService:
|
||||
workorder_data = self._convert_feishu_to_local(parsed_fields)
|
||||
workorder_data["feishu_record_id"] = feishu_id
|
||||
|
||||
# 过滤掉WorkOrder模型不支持的字段(防止dict参数错误)
|
||||
valid_fields = {}
|
||||
for key, value in workorder_data.items():
|
||||
if hasattr(WorkOrder, key):
|
||||
# 确保值不是dict、list等复杂类型
|
||||
if isinstance(value, (dict, list)):
|
||||
logger.warning(f"字段 '{key}' 包含复杂类型 {type(value).__name__},跳过")
|
||||
continue
|
||||
valid_fields[key] = value
|
||||
|
||||
if existing_workorder:
|
||||
# 更新现有记录
|
||||
for key, value in workorder_data.items():
|
||||
for key, value in valid_fields.items():
|
||||
if key != "feishu_record_id":
|
||||
setattr(existing_workorder, key, value)
|
||||
existing_workorder.updated_at = datetime.now()
|
||||
updated_count += 1
|
||||
else:
|
||||
# 创建新记录
|
||||
workorder_data["created_at"] = datetime.now()
|
||||
workorder_data["updated_at"] = datetime.now()
|
||||
new_workorder = WorkOrder(**workorder_data)
|
||||
valid_fields["created_at"] = datetime.now()
|
||||
valid_fields["updated_at"] = datetime.now()
|
||||
new_workorder = WorkOrder(**valid_fields)
|
||||
session.add(new_workorder)
|
||||
created_count += 1
|
||||
|
||||
@@ -337,21 +347,17 @@ class WorkOrderSyncService:
|
||||
"""创建新工单"""
|
||||
try:
|
||||
with db_manager.get_session() as session:
|
||||
workorder = WorkOrder(
|
||||
order_id=local_data.get("order_id"),
|
||||
title=local_data.get("title"),
|
||||
description=local_data.get("description"),
|
||||
category=local_data.get("category"),
|
||||
priority=local_data.get("priority"),
|
||||
status=local_data.get("status"),
|
||||
created_at=local_data.get("created_at"),
|
||||
updated_at=local_data.get("updated_at"),
|
||||
resolution=local_data.get("solution"),
|
||||
feishu_record_id=local_data.get("feishu_record_id"),
|
||||
assignee=local_data.get("assignee"),
|
||||
solution=local_data.get("solution"),
|
||||
ai_suggestion=local_data.get("ai_suggestion")
|
||||
)
|
||||
# 只使用WorkOrder模型支持的字段
|
||||
valid_data = {}
|
||||
for key, value in local_data.items():
|
||||
if hasattr(WorkOrder, key):
|
||||
# 确保值不是dict、list等复杂类型
|
||||
if isinstance(value, (dict, list)):
|
||||
logger.warning(f"字段 '{key}' 包含复杂类型 {type(value).__name__},跳过")
|
||||
continue
|
||||
valid_data[key] = value
|
||||
|
||||
workorder = WorkOrder(**valid_data)
|
||||
session.add(workorder)
|
||||
session.commit()
|
||||
session.refresh(workorder)
|
||||
@@ -432,15 +438,38 @@ class WorkOrderSyncService:
|
||||
logger.warning(f"时间字段转换失败: {e}, 使用当前时间")
|
||||
local_data[local_field] = datetime.now()
|
||||
|
||||
# 生成标题
|
||||
tr_number = feishu_fields.get("TR Number", "")
|
||||
problem_type = feishu_fields.get("Type of problem", "")
|
||||
if tr_number and problem_type:
|
||||
local_data["title"] = f"{tr_number} - {problem_type}"
|
||||
elif tr_number:
|
||||
local_data["title"] = f"{tr_number} - TR工单"
|
||||
# 生成标题:使用TR Description作为标题
|
||||
tr_description = feishu_fields.get("TR Description", "")
|
||||
if tr_description:
|
||||
# 标题直接使用问题描述,如果太长则截断
|
||||
if len(tr_description) > 200:
|
||||
local_data["title"] = tr_description[:197] + "..."
|
||||
else:
|
||||
local_data["title"] = tr_description
|
||||
else:
|
||||
local_data["title"] = "TR工单"
|
||||
# 如果没有描述,使用TR Number
|
||||
tr_number = feishu_fields.get("TR Number", "")
|
||||
if tr_number:
|
||||
local_data["title"] = f"{tr_number} - TR工单"
|
||||
else:
|
||||
local_data["title"] = "TR工单"
|
||||
|
||||
# 处理"处理过程"字段:提取最新一条作为solution
|
||||
# "处理过程"字段已映射到resolution,这里需要:
|
||||
# 1. resolution存储完整的"处理过程"历史
|
||||
# 2. solution存储"处理过程"的最新一条
|
||||
process_history = local_data.get("resolution", "")
|
||||
if process_history and isinstance(process_history, str):
|
||||
# 按换行分割,获取最后一行(最新一条)
|
||||
process_lines = [line.strip() for line in process_history.split('\n') if line.strip()]
|
||||
if process_lines:
|
||||
# 最新一条作为solution
|
||||
local_data["solution"] = process_lines[-1]
|
||||
# 完整历史保留在resolution(已在字段映射中设置)
|
||||
else:
|
||||
local_data["solution"] = ""
|
||||
else:
|
||||
local_data["solution"] = ""
|
||||
|
||||
# 设置默认值
|
||||
if "status" not in local_data:
|
||||
|
||||
BIN
src/utils/__pycache__/semantic_similarity.cpython-311.pyc
Normal file
BIN
src/utils/__pycache__/semantic_similarity.cpython-311.pyc
Normal file
Binary file not shown.
@@ -3,43 +3,59 @@
|
||||
|
||||
"""
|
||||
语义相似度计算服务
|
||||
使用sentence-transformers进行更准确的语义相似度计算
|
||||
使用LLM API进行更准确的语义相似度计算,提高理解力并节约服务端资源
|
||||
"""
|
||||
|
||||
import logging
|
||||
import numpy as np
|
||||
import re
|
||||
from typing import List, Tuple, Optional
|
||||
from sentence_transformers import SentenceTransformer
|
||||
import torch
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class SemanticSimilarityCalculator:
|
||||
"""语义相似度计算器"""
|
||||
"""语义相似度计算器 - 使用LLM API"""
|
||||
|
||||
def __init__(self, model_name: str = "all-MiniLM-L6-v2"):
|
||||
def __init__(self, use_llm: bool = True):
|
||||
"""
|
||||
初始化语义相似度计算器
|
||||
|
||||
Args:
|
||||
model_name: 使用的预训练模型名称
|
||||
- all-MiniLM-L6-v2: 英文模型,速度快,推荐用于生产环境
|
||||
- paraphrase-multilingual-MiniLM-L12-v2: 多语言模型,支持中文
|
||||
- paraphrase-multilingual-mpnet-base-v2: 多语言模型,精度高
|
||||
use_llm: 是否使用LLM API计算相似度(默认True,推荐)
|
||||
- True: 使用LLM API,理解力更强,无需加载本地模型
|
||||
- False: 使用本地模型(需要下载HuggingFace模型)
|
||||
"""
|
||||
self.model_name = model_name
|
||||
self.use_llm = use_llm
|
||||
self.model = None
|
||||
self._load_model()
|
||||
self.llm_client = None
|
||||
|
||||
if use_llm:
|
||||
self._init_llm_client()
|
||||
else:
|
||||
self._load_model()
|
||||
|
||||
def _init_llm_client(self):
|
||||
"""初始化LLM客户端"""
|
||||
try:
|
||||
from ..core.llm_client import QwenClient
|
||||
self.llm_client = QwenClient()
|
||||
logger.info("LLM客户端初始化成功,将使用LLM API计算语义相似度")
|
||||
except Exception as e:
|
||||
logger.error(f"初始化LLM客户端失败: {e}")
|
||||
self.llm_client = None
|
||||
# 回退到本地模型
|
||||
self.use_llm = False
|
||||
self._load_model()
|
||||
|
||||
def _load_model(self):
|
||||
"""加载预训练模型"""
|
||||
"""加载预训练模型(仅在use_llm=False时使用)"""
|
||||
try:
|
||||
logger.info(f"正在加载语义相似度模型: {self.model_name}")
|
||||
self.model = SentenceTransformer(self.model_name)
|
||||
logger.info("语义相似度模型加载成功")
|
||||
logger.info(f"正在加载本地语义相似度模型: all-MiniLM-L6-v2")
|
||||
self.model = SentenceTransformer("all-MiniLM-L6-v2")
|
||||
logger.info("本地语义相似度模型加载成功")
|
||||
except Exception as e:
|
||||
logger.error(f"加载语义相似度模型失败: {e}")
|
||||
# 回退到简单模型
|
||||
logger.error(f"加载本地语义相似度模型失败: {e}")
|
||||
self.model = None
|
||||
|
||||
def calculate_similarity(self, text1: str, text2: str, fast_mode: bool = True) -> float:
|
||||
@@ -49,7 +65,7 @@ class SemanticSimilarityCalculator:
|
||||
Args:
|
||||
text1: 第一个文本
|
||||
text2: 第二个文本
|
||||
fast_mode: 是否使用快速模式(结合传统方法)
|
||||
fast_mode: 是否使用快速模式(仅在使用本地模型时有效)
|
||||
|
||||
Returns:
|
||||
相似度分数 (0-1之间)
|
||||
@@ -58,27 +74,22 @@ class SemanticSimilarityCalculator:
|
||||
return 0.0
|
||||
|
||||
try:
|
||||
# 快速模式:先使用传统方法快速筛选
|
||||
if fast_mode:
|
||||
tfidf_sim = self._calculate_tfidf_similarity(text1, text2)
|
||||
|
||||
# 如果传统方法相似度很高或很低,直接返回
|
||||
if tfidf_sim >= 0.9:
|
||||
return tfidf_sim
|
||||
elif tfidf_sim <= 0.3:
|
||||
return tfidf_sim
|
||||
|
||||
# 中等相似度时,使用语义方法进行精确计算
|
||||
if self.model is not None:
|
||||
# 优先使用LLM API计算相似度
|
||||
if self.use_llm and self.llm_client:
|
||||
return self._calculate_llm_similarity(text1, text2)
|
||||
|
||||
# 回退到本地模型或TF-IDF
|
||||
if self.model is not None:
|
||||
if fast_mode:
|
||||
# 快速模式:先使用TF-IDF快速筛选
|
||||
tfidf_sim = self._calculate_tfidf_similarity(text1, text2)
|
||||
if tfidf_sim >= 0.9 or tfidf_sim <= 0.3:
|
||||
return tfidf_sim
|
||||
# 中等相似度时,使用语义方法进行精确计算
|
||||
semantic_sim = self._calculate_semantic_similarity(text1, text2)
|
||||
# 结合两种方法的结果
|
||||
return (tfidf_sim * 0.3 + semantic_sim * 0.7)
|
||||
else:
|
||||
return tfidf_sim
|
||||
|
||||
# 完整模式:直接使用语义相似度
|
||||
if self.model is not None:
|
||||
return self._calculate_semantic_similarity(text1, text2)
|
||||
return self._calculate_semantic_similarity(text1, text2)
|
||||
else:
|
||||
return self._calculate_tfidf_similarity(text1, text2)
|
||||
|
||||
@@ -86,6 +97,80 @@ class SemanticSimilarityCalculator:
|
||||
logger.error(f"计算语义相似度失败: {e}")
|
||||
return self._calculate_tfidf_similarity(text1, text2)
|
||||
|
||||
def _calculate_llm_similarity(self, text1: str, text2: str) -> float:
|
||||
"""使用LLM API计算语义相似度"""
|
||||
try:
|
||||
# 构建prompt,让LLM比较两个文本的相似度
|
||||
prompt = f"""请比较以下两个文本的语义相似度,并给出0-1之间的分数(保留2位小数),其中:
|
||||
- 1.0 表示完全相同
|
||||
- 0.8-0.9 表示非常相似
|
||||
- 0.6-0.7 表示较为相似
|
||||
- 0.4-0.5 表示部分相似
|
||||
- 0.0-0.3 表示差异很大
|
||||
|
||||
文本1: {text1}
|
||||
|
||||
文本2: {text2}
|
||||
|
||||
请只返回0-1之间的数字(保留2位小数),不要包含其他文字。例如:0.85"""
|
||||
|
||||
messages = [
|
||||
{"role": "system", "content": "你是一个专业的文本相似度评估专家,请准确评估两个文本的语义相似度。"},
|
||||
{"role": "user", "content": prompt}
|
||||
]
|
||||
|
||||
result = self.llm_client.chat_completion(
|
||||
messages=messages,
|
||||
temperature=0.1, # 低温度以获得更稳定的结果
|
||||
max_tokens=50
|
||||
)
|
||||
|
||||
if "error" in result:
|
||||
logger.error(f"LLM API调用失败: {result['error']}")
|
||||
# 回退到TF-IDF
|
||||
return self._calculate_tfidf_similarity(text1, text2)
|
||||
|
||||
# 提取响应中的数字
|
||||
response_content = result.get("choices", [{}])[0].get("message", {}).get("content", "")
|
||||
similarity = self._extract_similarity_from_response(response_content)
|
||||
|
||||
logger.debug(f"LLM计算语义相似度: {similarity:.4f}")
|
||||
return similarity
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"LLM语义相似度计算失败: {e}")
|
||||
# 回退到TF-IDF
|
||||
return self._calculate_tfidf_similarity(text1, text2)
|
||||
|
||||
def _extract_similarity_from_response(self, response: str) -> float:
|
||||
"""从LLM响应中提取相似度分数"""
|
||||
try:
|
||||
# 尝试提取0-1之间的浮点数
|
||||
patterns = [
|
||||
r'(\d+\.\d{1,2})', # 匹配两位小数的浮点数
|
||||
r'(\d+\.\d+)', # 匹配任意小数的浮点数
|
||||
r'(\d+)' # 匹配整数(可能是百分比形式)
|
||||
]
|
||||
|
||||
for pattern in patterns:
|
||||
matches = re.findall(pattern, response)
|
||||
if matches:
|
||||
value = float(matches[0])
|
||||
# 如果值大于1,可能是百分比形式,需要除以100
|
||||
if value > 1:
|
||||
value = value / 100.0
|
||||
# 确保在0-1范围内
|
||||
value = max(0.0, min(1.0, value))
|
||||
return value
|
||||
|
||||
# 如果没有找到数字,返回默认值
|
||||
logger.warning(f"无法从响应中提取相似度分数: {response}")
|
||||
return 0.5
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"提取相似度分数失败: {e}, 响应: {response}")
|
||||
return 0.5
|
||||
|
||||
def _calculate_semantic_similarity(self, text1: str, text2: str) -> float:
|
||||
"""使用sentence-transformers计算语义相似度"""
|
||||
try:
|
||||
@@ -159,6 +244,11 @@ class SemanticSimilarityCalculator:
|
||||
return []
|
||||
|
||||
try:
|
||||
# 优先使用LLM API
|
||||
if self.use_llm and self.llm_client:
|
||||
return [self._calculate_llm_similarity(t1, t2) for t1, t2 in text_pairs]
|
||||
|
||||
# 回退到本地模型或TF-IDF
|
||||
if self.model is not None:
|
||||
return self._batch_semantic_similarity(text_pairs)
|
||||
else:
|
||||
@@ -214,17 +304,24 @@ class SemanticSimilarityCalculator:
|
||||
return "语义差异较大,建议重新生成"
|
||||
|
||||
def is_model_available(self) -> bool:
|
||||
"""检查模型是否可用"""
|
||||
return self.model is not None
|
||||
"""检查模型是否可用(LLM或本地模型)"""
|
||||
if self.use_llm:
|
||||
return self.llm_client is not None
|
||||
else:
|
||||
return self.model is not None
|
||||
|
||||
# 全局实例
|
||||
_similarity_calculator = None
|
||||
|
||||
def get_similarity_calculator() -> SemanticSimilarityCalculator:
|
||||
"""获取全局相似度计算器实例"""
|
||||
def get_similarity_calculator(use_llm: bool = True) -> SemanticSimilarityCalculator:
|
||||
"""获取全局相似度计算器实例
|
||||
|
||||
Args:
|
||||
use_llm: 是否使用LLM API(默认True,推荐)
|
||||
"""
|
||||
global _similarity_calculator
|
||||
if _similarity_calculator is None:
|
||||
_similarity_calculator = SemanticSimilarityCalculator()
|
||||
_similarity_calculator = SemanticSimilarityCalculator(use_llm=use_llm)
|
||||
return _similarity_calculator
|
||||
|
||||
def calculate_semantic_similarity(text1: str, text2: str, fast_mode: bool = True) -> float:
|
||||
|
||||
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