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

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

View File

@@ -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:

View File

@@ -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")

View 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

View File

@@ -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

View File

@@ -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:

View File

@@ -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:

View File

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

Binary file not shown.

View File

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

View File

@@ -6,7 +6,7 @@ class ChatClient {
this.sessionId = null;
this.isConnected = false;
this.messageCount = 0;
this.init();
}
@@ -18,24 +18,24 @@ class ChatClient {
bindEvents() {
// 开始对话
document.getElementById('start-chat').addEventListener('click', () => this.startChat());
// 结束对话
document.getElementById('end-chat').addEventListener('click', () => this.endChat());
// 发送消息
document.getElementById('send-button').addEventListener('click', () => this.sendMessage());
// 回车发送
document.getElementById('message-input').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
this.sendMessage();
}
});
// 创建工单
document.getElementById('create-work-order').addEventListener('click', () => this.showWorkOrderModal());
document.getElementById('create-work-order-btn').addEventListener('click', () => this.createWorkOrder());
// 快速操作按钮
document.querySelectorAll('.quick-action-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
@@ -50,17 +50,17 @@ class ChatClient {
try {
// 连接WebSocket
await this.connectWebSocket();
// 创建会话
const userId = document.getElementById('user-id').value || 'anonymous';
const workOrderId = document.getElementById('work-order-id').value || null;
const response = await this.sendWebSocketMessage({
type: 'create_session',
user_id: userId,
work_order_id: workOrderId ? parseInt(workOrderId) : null
});
if (response.type === 'session_created') {
this.sessionId = response.session_id;
this.updateSessionInfo();
@@ -69,7 +69,7 @@ class ChatClient {
} else {
this.showError('创建会话失败');
}
} catch (error) {
console.error('启动对话失败:', error);
this.showError('启动对话失败: ' + error.message);
@@ -84,11 +84,11 @@ class ChatClient {
session_id: this.sessionId
});
}
this.sessionId = null;
this.disableChat();
this.addSystemMessage('对话已结束。');
} catch (error) {
console.error('结束对话失败:', error);
}
@@ -97,48 +97,48 @@ class ChatClient {
async sendMessage() {
const input = document.getElementById('message-input');
const message = input.value.trim();
if (!message || !this.sessionId) {
return;
}
// 清空输入框
input.value = '';
// 添加用户消息
this.addMessage('user', message);
// 显示打字指示器
this.showTypingIndicator();
try {
const response = await this.sendWebSocketMessage({
type: 'send_message',
session_id: this.sessionId,
message: message
});
this.hideTypingIndicator();
if (response.type === 'message_response' && response.result.success) {
const result = response.result;
// 添加助手回复
this.addMessage('assistant', result.content, {
knowledge_used: result.knowledge_used,
confidence_score: result.confidence_score,
work_order_id: result.work_order_id
});
// 更新工单ID
if (result.work_order_id) {
document.getElementById('work-order-id').value = result.work_order_id;
}
} else {
this.addMessage('assistant', '抱歉,我暂时无法处理您的问题。请稍后再试。');
}
} catch (error) {
this.hideTypingIndicator();
console.error('发送消息失败:', error);
@@ -151,12 +151,12 @@ class ChatClient {
const description = document.getElementById('wo-description').value;
const category = document.getElementById('wo-category').value;
const priority = document.getElementById('wo-priority').value;
if (!title || !description) {
this.showError('请填写工单标题和描述');
return;
}
try {
const response = await this.sendWebSocketMessage({
type: 'create_work_order',
@@ -166,23 +166,23 @@ class ChatClient {
category: category,
priority: priority
});
if (response.type === 'work_order_created' && response.result.success) {
const workOrderId = response.result.work_order_id;
document.getElementById('work-order-id').value = workOrderId;
this.addSystemMessage(`工单创建成功!工单号: ${response.result.order_id}`);
// 关闭模态框
const modal = bootstrap.Modal.getInstance(document.getElementById('workOrderModal'));
modal.hide();
// 清空表单
document.getElementById('work-order-form').reset();
} else {
this.showError('创建工单失败: ' + (response.result.error || '未知错误'));
}
} catch (error) {
console.error('创建工单失败:', error);
this.showError('创建工单失败: ' + error.message);
@@ -193,7 +193,7 @@ class ChatClient {
return new Promise((resolve, reject) => {
try {
this.websocket = new WebSocket('ws://localhost:8765');
// 设置连接超时
const timeout = setTimeout(() => {
if (this.websocket.readyState !== WebSocket.OPEN) {
@@ -201,26 +201,26 @@ class ChatClient {
reject(new Error('WebSocket连接超时请检查服务器是否启动'));
}
}, 5000); // 5秒超时
this.websocket.onopen = () => {
clearTimeout(timeout);
this.isConnected = true;
this.updateConnectionStatus(true);
resolve();
};
this.websocket.onclose = () => {
clearTimeout(timeout);
this.isConnected = false;
this.updateConnectionStatus(false);
};
this.websocket.onerror = (error) => {
clearTimeout(timeout);
console.error('WebSocket错误:', error);
reject(new Error('WebSocket连接失败请检查服务器是否启动'));
};
this.websocket.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
@@ -229,7 +229,7 @@ class ChatClient {
console.error('解析WebSocket消息失败:', error);
}
};
} catch (error) {
reject(error);
}
@@ -242,15 +242,15 @@ class ChatClient {
reject(new Error('WebSocket未连接'));
return;
}
const messageId = 'msg_' + Date.now();
message.messageId = messageId;
// 设置超时
const timeout = setTimeout(() => {
reject(new Error('请求超时'));
}, 10000);
// 监听响应
const handleResponse = (event) => {
try {
@@ -264,7 +264,7 @@ class ChatClient {
// 忽略解析错误
}
};
this.websocket.addEventListener('message', handleResponse);
this.websocket.send(JSON.stringify(message));
});
@@ -277,29 +277,29 @@ class ChatClient {
addMessage(role, content, metadata = {}) {
const messagesContainer = document.getElementById('chat-messages');
// 如果是第一条消息,清空欢迎信息
if (this.messageCount === 0) {
messagesContainer.innerHTML = '';
}
const messageDiv = document.createElement('div');
messageDiv.className = `message ${role}`;
const avatar = document.createElement('div');
avatar.className = 'message-avatar';
avatar.textContent = role === 'user' ? 'U' : 'A';
const contentDiv = document.createElement('div');
contentDiv.className = 'message-content';
contentDiv.innerHTML = content;
// 添加时间戳
const timeDiv = document.createElement('div');
timeDiv.className = 'message-time';
timeDiv.textContent = new Date().toLocaleTimeString();
contentDiv.appendChild(timeDiv);
// 添加元数据
if (metadata.knowledge_used && metadata.knowledge_used.length > 0) {
const knowledgeDiv = document.createElement('div');
@@ -307,21 +307,21 @@ class ChatClient {
knowledgeDiv.innerHTML = `<i class="fas fa-lightbulb me-1"></i>基于 ${metadata.knowledge_used.length} 条知识库信息生成`;
contentDiv.appendChild(knowledgeDiv);
}
if (metadata.confidence_score) {
const confidenceDiv = document.createElement('div');
confidenceDiv.className = 'confidence-score';
confidenceDiv.textContent = `置信度: ${(metadata.confidence_score * 100).toFixed(1)}%`;
contentDiv.appendChild(confidenceDiv);
}
if (metadata.work_order_id) {
const workOrderDiv = document.createElement('div');
workOrderDiv.className = 'work-order-info';
workOrderDiv.innerHTML = `<i class="fas fa-ticket-alt me-1"></i>关联工单: ${metadata.work_order_id}`;
contentDiv.appendChild(workOrderDiv);
}
if (role === 'user') {
messageDiv.appendChild(contentDiv);
messageDiv.appendChild(avatar);
@@ -329,20 +329,20 @@ class ChatClient {
messageDiv.appendChild(avatar);
messageDiv.appendChild(contentDiv);
}
messagesContainer.appendChild(messageDiv);
messagesContainer.scrollTop = messagesContainer.scrollHeight;
this.messageCount++;
}
addSystemMessage(content) {
const messagesContainer = document.getElementById('chat-messages');
const messageDiv = document.createElement('div');
messageDiv.className = 'text-center text-muted py-2';
messageDiv.innerHTML = `<small><i class="fas fa-info-circle me-1"></i>${content}</small>`;
messagesContainer.appendChild(messageDiv);
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}
@@ -394,7 +394,7 @@ class ChatClient {
this.showError('请先开始对话');
return;
}
const modal = new bootstrap.Modal(document.getElementById('workOrderModal'));
modal.show();
}

File diff suppressed because it is too large Load Diff

View File

@@ -572,12 +572,12 @@
<label class="form-label">用户ID</label>
<input type="text" class="form-control" id="user-id" value="user_001">
</div>
<div class="mb-3">
<label class="form-label">工单ID (可选)</label>
<input type="number" class="form-control" id="work-order-id" placeholder="留空则自动创建">
</div>
<div class="d-grid gap-2">
<button class="btn btn-primary" id="start-chat">
<i class="fas fa-play me-2"></i>开始对话
@@ -589,9 +589,9 @@
<i class="fas fa-plus me-2"></i>创建工单
</button>
</div>
<hr>
<div class="mb-3">
<h6>快速操作</h6>
<div class="quick-actions">
@@ -601,7 +601,7 @@
<button class="quick-action-btn" data-message="如何解绑车辆">解绑车辆</button>
</div>
</div>
<div class="mb-3">
<h6>会话信息</h6>
<div id="session-info" class="text-muted">
@@ -611,7 +611,7 @@
</div>
</div>
</div>
<div class="col-md-9">
<div class="card chat-container">
<div class="chat-header">
@@ -625,7 +625,7 @@
</div>
</div>
</div>
<div class="chat-messages" id="chat-messages">
<div class="text-center text-muted py-5">
<i class="fas fa-comments fa-3x mb-3"></i>
@@ -633,10 +633,10 @@
<p>请点击"开始对话"按钮开始聊天</p>
</div>
</div>
<div class="chat-input">
<div class="input-group">
<input type="text" class="form-control" id="message-input"
<input type="text" class="form-control" id="message-input"
placeholder="请输入您的问题..." disabled>
<button class="btn btn-primary" id="send-button" disabled>
<i class="fas fa-paper-plane"></i>
@@ -721,7 +721,7 @@
</button>
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<button class="btn btn-agent w-100" id="proactive-monitoring">
@@ -1155,7 +1155,7 @@
<i class="fas fa-refresh me-1"></i>刷新状态
</button>
</div>
<!-- 字段映射管理区域 -->
<div class="row mb-4" id="fieldMappingSection" style="display: none;">
<div class="col-12">
@@ -1196,13 +1196,13 @@
<option value="100">前100条</option>
</select>
</div>
<!-- 同步进度 -->
<div class="progress mb-3" id="syncProgress" style="display: none;">
<div class="progress-bar progress-bar-striped progress-bar-animated"
<div class="progress-bar progress-bar-striped progress-bar-animated"
role="progressbar" style="width: 0%"></div>
</div>
<!-- 同步日志 -->
<div class="mt-3">
<h6>同步日志</h6>
@@ -1327,7 +1327,7 @@
</div>
</div>
</div>
<div class="card mt-3">
<div class="card-header">
<h5><i class="fas fa-memory me-2"></i>对话记忆</h5>
@@ -2230,13 +2230,13 @@
<i class="fas fa-info-circle me-2"></i>
请先下载模板文件按照模板格式填写工单信息然后上传Excel文件进行导入。
</div>
<div class="mb-3">
<label class="form-label">选择Excel文件</label>
<input type="file" class="form-control" id="excel-file-input" accept=".xlsx,.xls">
<div class="form-text">支持 .xlsx 和 .xls 格式文件大小不超过16MB</div>
</div>
<div class="mb-3">
<div class="d-flex justify-content-between align-items-center">
<span>Excel文件列名说明</span>
@@ -2301,7 +2301,7 @@
</table>
</div>
</div>
<div id="import-progress" class="d-none">
<div class="progress mb-3">
<div class="progress-bar" role="progressbar" style="width: 0%"></div>
@@ -2311,7 +2311,7 @@
<span id="import-status">正在导入工单...</span>
</div>
</div>
<div id="import-result" class="d-none">
<div class="alert alert-success">
<i class="fas fa-check-circle me-2"></i>

View File

@@ -213,18 +213,22 @@ class WebSocketServer:
async def handle_client(self, websocket: WebSocketServerProtocol, path: str):
"""处理客户端连接"""
# 检查连接头
headers = websocket.request_headers
connection = headers.get("Connection", "").lower()
# 处理不同的连接头格式
if "upgrade" not in connection and "keep-alive" in connection:
logger.warning(f"收到非标准连接头: {connection}")
# 对于keep-alive连接头我们仍然接受连接
elif "upgrade" not in connection:
logger.warning(f"连接头不包含upgrade: {connection}")
await websocket.close(code=1002, reason="Invalid connection header")
return
# 检查连接头(如果可用)
try:
if hasattr(websocket, 'request_headers'):
headers = websocket.request_headers
connection = headers.get("Connection", "").lower()
# 处理不同的连接头格式
if "upgrade" not in connection and "keep-alive" in connection:
logger.warning(f"收到非标准连接头: {connection}")
# 对于keep-alive连接头我们仍然接受连接
elif "upgrade" not in connection:
logger.warning(f"连接头不包含upgrade: {connection}")
# 在websockets 15.x中连接已经在serve时验证所以这里只记录警告
except AttributeError:
# websockets 15.x版本可能没有request_headers属性跳过检查
pass
await self.register_client(websocket)
@@ -243,19 +247,15 @@ class WebSocketServer:
logger.info(f"启动WebSocket服务器: ws://{self.host}:{self.port}")
# 添加CORS支持
async def handle_client_with_cors(websocket: WebSocketServerProtocol, path: str):
# 设置CORS
if websocket.request_headers.get("Origin"):
# 允许跨域连接
pass
await self.handle_client(websocket, path)
async def handle_client_with_cors(websocket: WebSocketServerProtocol, path: str = None):
# CORS处理websockets库默认允许所有来源连接
# 如果需要限制可以在serve时使用additional_headers参数
await self.handle_client(websocket, path or "")
async with websockets.serve(
handle_client_with_cors,
self.host,
self.port,
# 添加额外的服务器选项
process_request=self._process_request
self.port
):
await asyncio.Future() # 保持服务器运行