feat: 优化飞书集成、知识库、Agent、工单管理及AI建议功能,统一前端对话字体样式并移除工单模板文件。
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -29,6 +29,7 @@ class AgentState(Enum):
|
||||
PLANNING = "planning"
|
||||
EXECUTING = "executing"
|
||||
LEARNING = "learning"
|
||||
PROCESSING = "processing"
|
||||
ERROR = "error"
|
||||
|
||||
class AgentCore:
|
||||
|
||||
@@ -6,10 +6,12 @@ TSP Agent助手 - 简化版本
|
||||
|
||||
import logging
|
||||
import asyncio
|
||||
import json
|
||||
from typing import Dict, Any, List, Optional
|
||||
from datetime import datetime
|
||||
from src.config.unified_config import get_config
|
||||
from src.agent.llm_client import LLMManager
|
||||
from src.web.service_manager import service_manager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -304,23 +306,66 @@ class TSPAgentAssistant:
|
||||
logger.error(f"获取LLM使用统计失败: {e}")
|
||||
return {}
|
||||
|
||||
def process_message_agent_sync(self, message: str, user_id: str = "admin",
|
||||
work_order_id: Optional[int] = None,
|
||||
enable_proactive: bool = True) -> Dict[str, Any]:
|
||||
"""处理消息(同步桥接)"""
|
||||
try:
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
return loop.run_until_complete(self.process_message_agent(message, user_id, work_order_id, enable_proactive))
|
||||
except Exception as e:
|
||||
logger.error(f"同步处理消息失败: {e}")
|
||||
return {"error": str(e)}
|
||||
|
||||
async def process_message_agent(self, message: str, user_id: str = "admin",
|
||||
work_order_id: Optional[int] = None,
|
||||
enable_proactive: bool = True) -> Dict[str, Any]:
|
||||
"""处理消息"""
|
||||
"""处理消息 (实战化)"""
|
||||
try:
|
||||
# 简化的消息处理
|
||||
logger.info(f"Agent收到消息: {message}")
|
||||
|
||||
# 1. 识别意图和推荐工具
|
||||
prompt = f"用户消息: {message}\n请分析用户意图,并从工具列表中选择最合适的工具。工具列表: {json.dumps(self.get_available_tools())}\n请直接返回你的分析和建议响应。"
|
||||
|
||||
response_text = await self.llm_manager.generate(prompt)
|
||||
|
||||
# 2. 模拟动作生成
|
||||
actions = []
|
||||
if "工单" in message or "查询" in message:
|
||||
actions.append({"type": "tool_call", "tool": "search_work_order", "status": "suggested"})
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Agent收到消息: {message}",
|
||||
"response": response_text,
|
||||
"actions": actions,
|
||||
"user_id": user_id,
|
||||
"work_order_id": work_order_id,
|
||||
"status": "completed",
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"处理消息失败: {e}")
|
||||
return {"error": str(e)}
|
||||
|
||||
def execute_tool_sync(self, tool_name: str, parameters: Dict[str, Any] = None) -> Dict[str, Any]:
|
||||
"""执行工具(同步桥接)"""
|
||||
try:
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
return loop.run_until_complete(self.execute_tool(tool_name, parameters))
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
def trigger_sample_actions_sync(self) -> Dict[str, Any]:
|
||||
"""触发示例动作(同步桥接)"""
|
||||
try:
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
return loop.run_until_complete(self.trigger_sample_actions())
|
||||
except Exception as e:
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
async def trigger_sample_actions(self) -> Dict[str, Any]:
|
||||
"""触发示例动作"""
|
||||
try:
|
||||
@@ -336,7 +381,7 @@ class TSPAgentAssistant:
|
||||
logger.error(f"触发示例动作失败: {e}")
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
def process_file_to_knowledge(self, file_path: str, filename: str) -> Dict[str, Any]:
|
||||
async def process_file_to_knowledge(self, file_path: str, filename: str) -> Dict[str, Any]:
|
||||
"""处理文件并生成知识库"""
|
||||
try:
|
||||
import os
|
||||
@@ -356,20 +401,39 @@ class TSPAgentAssistant:
|
||||
|
||||
logger.info(f"文件读取成功: {filename}, 字符数={len(content)}")
|
||||
|
||||
# 使用简化的知识提取
|
||||
# 使用LLM进行知识提取 (异步调用)
|
||||
logger.info(f"正在对文件内容进行 AI 知识提取...")
|
||||
knowledge_entries = self._extract_knowledge_from_content(content, filename)
|
||||
knowledge_entries = await self._extract_knowledge_from_content(content, filename)
|
||||
|
||||
logger.info(f"知识提取完成: 共提取出 {len(knowledge_entries)} 个潜在条目")
|
||||
|
||||
# 保存到知识库
|
||||
saved_count = 0
|
||||
|
||||
# 获取知识库管理器
|
||||
try:
|
||||
knowledge_manager = service_manager.get_assistant().knowledge_manager
|
||||
except Exception as e:
|
||||
logger.error(f"无法获取知识库管理器: {e}")
|
||||
knowledge_manager = None
|
||||
|
||||
for i, entry in enumerate(knowledge_entries):
|
||||
try:
|
||||
logger.info(f"正在保存知识条目 [{i+1}/{len(knowledge_entries)}]: {entry.get('question', '')[:30]}...")
|
||||
# 这里在实际项目中应当注入知识库管理器的保存逻辑
|
||||
# 但在当前简化版本中仅记录日志
|
||||
|
||||
if knowledge_manager:
|
||||
# 实际保存到数据库
|
||||
knowledge_manager.add_knowledge_entry(
|
||||
question=entry.get('question'),
|
||||
answer=entry.get('answer'),
|
||||
category=entry.get('category', '文档导入'),
|
||||
confidence_score=entry.get('confidence_score', 0.8)
|
||||
)
|
||||
saved_count += 1
|
||||
else:
|
||||
# 如果无法获取管理器,仅记录日志(降级处理)
|
||||
logger.warning("知识库管理器不可用,跳过保存")
|
||||
|
||||
except Exception as save_error:
|
||||
logger.error(f"保存知识条目 {i+1} 时出错: {save_error}")
|
||||
|
||||
@@ -402,25 +466,70 @@ class TSPAgentAssistant:
|
||||
logger.error(f"读取文件失败: {e}")
|
||||
return ""
|
||||
|
||||
def _extract_knowledge_from_content(self, content: str, filename: str) -> List[Dict[str, Any]]:
|
||||
"""从内容中提取知识"""
|
||||
async def _extract_knowledge_from_content(self, content: str, filename: str) -> List[Dict[str, Any]]:
|
||||
"""从内容中提取知识 - 使用LLM"""
|
||||
try:
|
||||
# 简化的知识提取逻辑
|
||||
entries = []
|
||||
# 限制内容长度,避免超出token限制
|
||||
# 假设每个汉字2个token,保留前8000个字符作为上下文
|
||||
truncated_content = content[:8000]
|
||||
if len(content) > 8000:
|
||||
truncated_content += "\n...(后续内容已省略)"
|
||||
|
||||
# 按段落分割内容
|
||||
paragraphs = content.split('\n\n')
|
||||
prompt = f"""
|
||||
你是一个专业的知识库构建助手。请分析以下文档内容,提取出关键的"问题"和"答案"对,用于构建知识库。
|
||||
|
||||
for i, paragraph in enumerate(paragraphs[:5]): # 最多提取5个
|
||||
if len(paragraph.strip()) > 20: # 过滤太短的段落
|
||||
entries.append({
|
||||
"question": f"关于{filename}的问题{i+1}",
|
||||
"answer": paragraph.strip(),
|
||||
"category": "文档知识",
|
||||
"confidence_score": 0.7
|
||||
文档文件名:{filename}
|
||||
文档内容:
|
||||
{truncated_content}
|
||||
|
||||
要求:
|
||||
1. 提取文档中的核心知识点,转化为"问题(question)"和"答案(answer)"的形式。
|
||||
2. "问题"应该清晰明确,方便用户搜索。
|
||||
3. "答案"应该准确、完整,直接回答问题。
|
||||
4. "分类(category)"请根据内容自动归类(如:故障排查、操作指南、系统配置、业务流程等)。
|
||||
5. 输出格式必须是合法的 JSON 数组,不要包含Markdown标记。
|
||||
|
||||
JSON格式示例:
|
||||
[
|
||||
{{"question": "如何重置密码?", "answer": "请访问设置页面,点击重置密码按钮...", "category": "操作指南"}},
|
||||
{{"question": "系统支持哪些浏览器?", "answer": "支持Chrome, Edge, Firefox...", "category": "系统配置"}}
|
||||
]
|
||||
"""
|
||||
# 调用LLM生成
|
||||
logger.info("正在调用LLM进行知识提取...")
|
||||
response_text = await self.llm_manager.generate(prompt, temperature=0.3)
|
||||
|
||||
# 清理响应中的Markdown标记(如果存在)
|
||||
cleaned_text = response_text.strip()
|
||||
if cleaned_text.startswith("```json"):
|
||||
cleaned_text = cleaned_text[7:]
|
||||
if cleaned_text.startswith("```"):
|
||||
cleaned_text = cleaned_text[3:]
|
||||
if cleaned_text.endswith("```"):
|
||||
cleaned_text = cleaned_text[:-3]
|
||||
cleaned_text = cleaned_text.strip()
|
||||
|
||||
# 解析JSON
|
||||
try:
|
||||
entries = json.loads(cleaned_text)
|
||||
except json.JSONDecodeError:
|
||||
# 尝试修复常见的JSON错误
|
||||
logger.warning(f"JSON解析失败,尝试简单修复: {cleaned_text[:100]}...")
|
||||
# 这里可以添加更复杂的修复逻辑,或者直接记录错误
|
||||
return []
|
||||
|
||||
# 验证和标准化
|
||||
valid_entries = []
|
||||
for entry in entries:
|
||||
if isinstance(entry, dict) and "question" in entry and "answer" in entry:
|
||||
valid_entries.append({
|
||||
"question": entry["question"],
|
||||
"answer": entry["answer"],
|
||||
"category": entry.get("category", "文档导入"),
|
||||
"confidence_score": 0.9 # LLM生成的置信度较高
|
||||
})
|
||||
|
||||
return entries
|
||||
return valid_entries
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"提取知识失败: {e}")
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -168,6 +168,29 @@ class CacheManager:
|
||||
)
|
||||
del self.memory_cache[oldest_key]
|
||||
|
||||
def check_and_set_message_processed(self, message_id: str, ttl: int = 300) -> bool:
|
||||
"""
|
||||
检查消息是否已处理,如果未处理则标记为已处理
|
||||
|
||||
Args:
|
||||
message_id: 消息ID
|
||||
ttl: 过期时间(秒),默认5分钟
|
||||
|
||||
Returns:
|
||||
bool: True 表示已处理(重复消息),False 表示未处理(新消息)
|
||||
"""
|
||||
key = f"msg_processed:{message_id}"
|
||||
|
||||
# 使用锁确保原子性(针对内存缓存)
|
||||
with self.cache_lock:
|
||||
# 1. 检查是否存在
|
||||
if self.get(key):
|
||||
return True
|
||||
|
||||
# 2. 如果不存在,则标记为已处理
|
||||
self.set(key, 1, ttl)
|
||||
return False
|
||||
|
||||
def get_stats(self) -> Dict[str, Any]:
|
||||
"""获取缓存统计信息"""
|
||||
with self.cache_lock:
|
||||
@@ -222,7 +245,6 @@ class DatabaseCache:
|
||||
return result
|
||||
return wrapper
|
||||
|
||||
|
||||
# 全局缓存管理器实例
|
||||
cache_manager = CacheManager()
|
||||
|
||||
|
||||
@@ -92,7 +92,9 @@ class QueryOptimizer:
|
||||
'confidence_score': conv.confidence_score,
|
||||
'work_order_id': conv.work_order_id,
|
||||
'ip_address': conv.ip_address,
|
||||
'invocation_method': conv.invocation_method
|
||||
'invocation_method': conv.invocation_method,
|
||||
# 构造用户显示名称:如果有IP则显示IP,否则显示匿名
|
||||
'user_id': f"{conv.ip_address} ({conv.invocation_method})" if conv.ip_address else "匿名"
|
||||
})
|
||||
|
||||
# 记录查询时间
|
||||
|
||||
Binary file not shown.
@@ -293,27 +293,27 @@ class AISuggestionService:
|
||||
if is_first_suggestion:
|
||||
prompt += """
|
||||
要求:
|
||||
1. 详细分析问题描述,识别可能的根本原因
|
||||
2. 基于当前处理进度,判断问题处于哪个阶段
|
||||
3. 提供具体的排查步骤和技术指导
|
||||
4. 建议需要收集哪些技术信息(如日志、配置、版本等)
|
||||
5. 如果需要,可以建议进站处理的具体项目
|
||||
6. 语言专业,包含技术细节,方便技术人员理解
|
||||
7. 建议格式要清晰,便于执行和跟踪
|
||||
1. 输出格式必须严格遵守:"时间:当前问题现状,下一步做法"
|
||||
例如:"0210:无TBOX登录记录,需要用户进站抓取TBOX日志分析。"
|
||||
2. "时间"使用当前的MMDD格式(如0210)
|
||||
3. "当前问题现状"要基于问题描述和处理记录总结,简明扼要
|
||||
4. "下一步做法"如果是首次建议,只提供远程排查步骤(如检查网络、重启等),不要提进站
|
||||
5. 总字数控制在100字以内,不要分行,不要列点
|
||||
6. 绝对不要使用"建议您"、"您可以"等客套话,直接陈述做法
|
||||
|
||||
请提供完整的分析和建议:"""
|
||||
请按照上述格式生成建议:"""
|
||||
else:
|
||||
prompt += """
|
||||
要求:
|
||||
1. 基于已有处理记录和当前进度,分析问题进展情况
|
||||
2. 判断之前的处理步骤是否有效,找出可能的遗漏点
|
||||
3. 根据问题发展阶段提供更深入的技术解决方案
|
||||
4. 如果远程处理无效,明确说明需要哪些线下技术支持
|
||||
5. 详细说明进站后需要执行的具体诊断和修复步骤
|
||||
6. 包含技术参数、工具要求和注意事项
|
||||
7. 便于技术人员快速理解问题状态和下一步行动
|
||||
1. 输出格式必须严格遵守:"时间:当前问题现状,下一步做法"
|
||||
例如:"0210:无TBOX登录记录,需要用户进站抓取TBOX日志分析。"
|
||||
2. "时间"使用当前的MMDD格式(如0210)
|
||||
3. "当前问题现状"要简述之前的处理结果(如远程无效)
|
||||
4. "下一步做法"可以包含进站抓取日志、联系技术支持等深入方案
|
||||
5. 总字数控制在100字以内,不要分行,不要列点
|
||||
6. 绝对不要使用"建议您"、"您可以"等客套话,直接陈述做法
|
||||
|
||||
请提供针对性的深入分析和处理建议:"""
|
||||
请按照上述格式生成建议:"""
|
||||
|
||||
return prompt
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ from lark_oapi.api.im.v1 import P2ImMessageReceiveV1, ReplyMessageRequest, Reply
|
||||
|
||||
from src.config.unified_config import get_config
|
||||
from src.web.service_manager import service_manager
|
||||
from src.core.cache_manager import cache_manager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -66,15 +67,30 @@ class FeishuLongConnService:
|
||||
message_id = message.message_id
|
||||
chat_id = message.chat_id
|
||||
message_type = message.message_type
|
||||
chat_type = getattr(message, 'chat_type', 'unknown') # 获取会话类型
|
||||
content = message.content
|
||||
sender = event.sender
|
||||
|
||||
logger.info(f"📋 消息详情:")
|
||||
# 获取发送者ID和群信息
|
||||
sender_id = sender.sender_id.user_id
|
||||
try:
|
||||
tenant_key = sender.sender_id.tenant_key
|
||||
except:
|
||||
tenant_key = "unknown"
|
||||
|
||||
# 详细日志记录
|
||||
logger.info(f"📋 消息详情 [长连接]:")
|
||||
logger.info(f" - 消息ID: {message_id}")
|
||||
logger.info(f" - 群聊ID: {chat_id}")
|
||||
logger.info(f" - 发送者ID: {sender.sender_id.user_id}")
|
||||
logger.info(f" - 会话类型: {'群聊(group)' if chat_type == 'group' else '私聊(p2p)' if chat_type == 'p2p' else chat_type}")
|
||||
logger.info(f" - 会话ID: {chat_id}")
|
||||
logger.info(f" - 发送者ID: {sender_id}")
|
||||
logger.info(f" - 租户Key: {tenant_key}")
|
||||
logger.info(f" - 消息类型: {message_type}")
|
||||
logger.info(f" - 原始内容: {content}")
|
||||
|
||||
# 消息去重检查
|
||||
if cache_manager.check_and_set_message_processed(message_id):
|
||||
logger.warning(f"🔁 消息 {message_id} 已被处理过(可能是Webhook已处理),跳过")
|
||||
return
|
||||
|
||||
# 只处理文本消息
|
||||
if message_type != "text":
|
||||
@@ -145,8 +161,8 @@ class FeishuLongConnService:
|
||||
response_data = chat_manager.process_message(
|
||||
session_id=session_id,
|
||||
user_message=text_content,
|
||||
ip_address=None,
|
||||
invocation_method="feishu_longconn"
|
||||
ip_address=f"Feishu:{sender_id}",
|
||||
invocation_method=f"Feishu({chat_type})"
|
||||
)
|
||||
|
||||
logger.info(f"📊 处理结果: {response_data.get('success')}")
|
||||
|
||||
Binary file not shown.
@@ -401,27 +401,35 @@ class KnowledgeManager:
|
||||
"""获取知识库统计信息"""
|
||||
try:
|
||||
with db_manager.get_session() as session:
|
||||
total_entries = session.query(KnowledgeEntry).count()
|
||||
active_entries = session.query(KnowledgeEntry).filter(
|
||||
# 只统计活跃(未删除)的条目
|
||||
total_entries = session.query(KnowledgeEntry).filter(
|
||||
KnowledgeEntry.is_active == True
|
||||
).count()
|
||||
|
||||
# 按类别统计
|
||||
# 统计已验证的条目
|
||||
verified_entries = session.query(KnowledgeEntry).filter(
|
||||
KnowledgeEntry.is_active == True,
|
||||
KnowledgeEntry.is_verified == True
|
||||
).count()
|
||||
|
||||
# 按类别统计(仅限活跃条目)
|
||||
category_stats = session.query(
|
||||
KnowledgeEntry.category,
|
||||
session.query(KnowledgeEntry).filter(
|
||||
KnowledgeEntry.category == KnowledgeEntry.category
|
||||
).count()
|
||||
func.count(KnowledgeEntry.id)
|
||||
).filter(
|
||||
KnowledgeEntry.is_active == True
|
||||
).group_by(KnowledgeEntry.category).all()
|
||||
|
||||
# 平均置信度
|
||||
# 平均置信度(仅限活跃条目)
|
||||
avg_confidence = session.query(
|
||||
func.avg(KnowledgeEntry.confidence_score)
|
||||
).filter(
|
||||
KnowledgeEntry.is_active == True
|
||||
).scalar() or 0.0
|
||||
|
||||
return {
|
||||
"total_entries": total_entries,
|
||||
"active_entries": active_entries,
|
||||
"active_entries": verified_entries, # 将 active_entries 复用为已验证数量,或前端相应修改
|
||||
"category_distribution": dict(category_stats),
|
||||
"average_confidence": float(avg_confidence)
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -49,8 +49,7 @@ def trigger_sample_action():
|
||||
"""触发示例动作"""
|
||||
try:
|
||||
from src.web.service_manager import service_manager
|
||||
import asyncio
|
||||
result = asyncio.run(service_manager.get_agent_assistant().trigger_sample_actions())
|
||||
result = service_manager.get_agent_assistant().trigger_sample_actions_sync()
|
||||
return jsonify(result)
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
@@ -163,13 +162,12 @@ def agent_chat():
|
||||
agent_assistant = service_manager.get_agent_assistant()
|
||||
|
||||
# 模拟Agent处理(实际应该调用真正的Agent处理逻辑)
|
||||
import asyncio
|
||||
result = asyncio.run(agent_assistant.process_message_agent(
|
||||
result = agent_assistant.process_message_agent_sync(
|
||||
message=message,
|
||||
user_id=context.get('user_id', 'admin'),
|
||||
work_order_id=None,
|
||||
enable_proactive=True
|
||||
))
|
||||
)
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
@@ -215,8 +213,7 @@ def execute_agent_tool():
|
||||
if not tool_name:
|
||||
return jsonify({"error": "缺少工具名称tool"}), 400
|
||||
|
||||
import asyncio
|
||||
result = asyncio.run(service_manager.get_agent_assistant().agent_core.tool_manager.execute_tool(tool_name, parameters))
|
||||
result = service_manager.get_agent_assistant().agent_core.tool_manager.execute_tool_sync(tool_name, parameters)
|
||||
return jsonify(result)
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@@ -9,6 +9,7 @@ import threading
|
||||
from flask import Blueprint, request, jsonify, current_app
|
||||
from src.integrations.feishu_service import FeishuService
|
||||
from src.web.service_manager import service_manager
|
||||
from src.core.cache_manager import cache_manager
|
||||
|
||||
# 初始化日志
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -35,11 +36,21 @@ def _process_message_in_background(app, event_data: dict):
|
||||
|
||||
message_id = message.get('message_id')
|
||||
chat_id = message.get('chat_id')
|
||||
chat_type = message.get('chat_type', 'unknown')
|
||||
|
||||
if not message_id or not chat_id:
|
||||
logger.error(f"[Feishu Bot] 事件数据缺少必要字段: {event_data}")
|
||||
return
|
||||
|
||||
# 记录会话类型
|
||||
chat_type_desc = '群聊(group)' if chat_type == 'group' else '私聊(p2p)' if chat_type == 'p2p' else chat_type
|
||||
logger.info(f"[Feishu Bot] 收到 {chat_type_desc} 消息, ChatID: {chat_id}")
|
||||
|
||||
# 消息去重检查
|
||||
if cache_manager.check_and_set_message_processed(message_id):
|
||||
logger.warning(f"[Feishu Bot] 🔁 消息 {message_id} 已被处理过(可能是长连接已处理),跳过")
|
||||
return
|
||||
|
||||
# 内容是一个JSON字符串,需要再次解析
|
||||
try:
|
||||
content_json = json.loads(message.get('content', '{}'))
|
||||
|
||||
@@ -11,6 +11,7 @@ import logging
|
||||
from flask import Blueprint, request, jsonify
|
||||
from src.agent_assistant import TSPAgentAssistant
|
||||
from src.web.service_manager import service_manager
|
||||
from src.core.cache_manager import cache_manager
|
||||
from src.web.error_handlers import handle_api_errors, create_error_response, create_success_response
|
||||
|
||||
knowledge_bp = Blueprint('knowledge', __name__, url_prefix='/api/knowledge')
|
||||
@@ -98,7 +99,16 @@ def upload_knowledge_file():
|
||||
try:
|
||||
file.save(temp_path)
|
||||
assistant = get_agent_assistant()
|
||||
result = assistant.process_file_to_knowledge(temp_path, file.filename)
|
||||
# 由于process_file_to_knowledge现在是异步的,我们需要同步调用它
|
||||
# 或者将整个视图函数改为异步(Flask 2.0+支持)
|
||||
import asyncio
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
result = loop.run_until_complete(assistant.process_file_to_knowledge(temp_path, file.filename))
|
||||
loop.close()
|
||||
|
||||
cache_manager.clear()
|
||||
|
||||
return jsonify(result)
|
||||
finally:
|
||||
try:
|
||||
@@ -108,6 +118,78 @@ def upload_knowledge_file():
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.warning(f"清理临时文件失败: {cleanup_error}")
|
||||
|
||||
@knowledge_bp.route('/batch_delete', methods=['POST'])
|
||||
@handle_api_errors
|
||||
def batch_delete_knowledge():
|
||||
"""批量删除知识库条目"""
|
||||
data = request.get_json()
|
||||
if not data or 'ids' not in data:
|
||||
return create_error_response("缺少 ids 参数", 400)
|
||||
|
||||
ids = data['ids']
|
||||
if not isinstance(ids, list):
|
||||
return create_error_response("ids 必须是列表", 400)
|
||||
|
||||
knowledge_manager = service_manager.get_assistant().knowledge_manager
|
||||
success_count = 0
|
||||
fail_count = 0
|
||||
|
||||
for knowledge_id in ids:
|
||||
if knowledge_manager.delete_knowledge_entry(knowledge_id):
|
||||
success_count += 1
|
||||
else:
|
||||
fail_count += 1
|
||||
|
||||
return create_success_response(f"批量删除完成: 成功 {success_count} 条,失败 {fail_count} 条")
|
||||
|
||||
@knowledge_bp.route('/batch_verify', methods=['POST'])
|
||||
@handle_api_errors
|
||||
def batch_verify_knowledge():
|
||||
"""批量验证知识库条目"""
|
||||
data = request.get_json()
|
||||
if not data or 'ids' not in data:
|
||||
return create_error_response("缺少 ids 参数", 400)
|
||||
|
||||
ids = data['ids']
|
||||
if not isinstance(ids, list):
|
||||
return create_error_response("ids 必须是列表", 400)
|
||||
|
||||
knowledge_manager = service_manager.get_assistant().knowledge_manager
|
||||
success_count = 0
|
||||
fail_count = 0
|
||||
|
||||
for knowledge_id in ids:
|
||||
if knowledge_manager.verify_knowledge_entry(knowledge_id, verified_by="admin"):
|
||||
success_count += 1
|
||||
else:
|
||||
fail_count += 1
|
||||
|
||||
return create_success_response(f"批量验证完成: 成功 {success_count} 条,失败 {fail_count} 条")
|
||||
|
||||
@knowledge_bp.route('/batch_unverify', methods=['POST'])
|
||||
@handle_api_errors
|
||||
def batch_unverify_knowledge():
|
||||
"""批量取消验证知识库条目"""
|
||||
data = request.get_json()
|
||||
if not data or 'ids' not in data:
|
||||
return create_error_response("缺少 ids 参数", 400)
|
||||
|
||||
ids = data['ids']
|
||||
if not isinstance(ids, list):
|
||||
return create_error_response("ids 必须是列表", 400)
|
||||
|
||||
knowledge_manager = service_manager.get_assistant().knowledge_manager
|
||||
success_count = 0
|
||||
fail_count = 0
|
||||
|
||||
for knowledge_id in ids:
|
||||
if knowledge_manager.unverify_knowledge_entry(knowledge_id):
|
||||
success_count += 1
|
||||
else:
|
||||
fail_count += 1
|
||||
|
||||
return create_success_response(f"批量取消验证完成: 成功 {success_count} 条,失败 {fail_count} 条")
|
||||
|
||||
@knowledge_bp.route('/delete/<int:knowledge_id>', methods=['DELETE'])
|
||||
@handle_api_errors
|
||||
def delete_knowledge(knowledge_id):
|
||||
|
||||
@@ -478,19 +478,42 @@ def import_workorders():
|
||||
# 解析Excel文件
|
||||
try:
|
||||
df = pd.read_excel(upload_path)
|
||||
if df is None or df.empty:
|
||||
return jsonify({"error": "Excel文件内容为空或无法识别表格结构"}), 400
|
||||
|
||||
imported_workorders = []
|
||||
skipped_rows = 0
|
||||
|
||||
# 处理每一行数据
|
||||
for index, row in df.iterrows():
|
||||
# 根据Excel列名映射到工单字段
|
||||
title = str(row.get('标题', row.get('title', f'导入工单 {index + 1}')))
|
||||
description = str(row.get('描述', row.get('description', '')))
|
||||
category = str(row.get('分类', row.get('category', '技术问题')))
|
||||
priority = str(row.get('优先级', row.get('priority', 'medium')))
|
||||
status = str(row.get('状态', row.get('status', 'open')))
|
||||
# 定义候选列名映射
|
||||
col_mappings = {
|
||||
'title': ['标题', 'title', '工单标题', '问题主体', 'Subject'],
|
||||
'description': ['描述', 'description', '问题描述', '详细内容', 'Detail'],
|
||||
'category': ['分类', 'category', '业务分类', '模块', 'Type'],
|
||||
'priority': ['优先级', 'priority', '紧急程度', 'Level', 'Priority'],
|
||||
'status': ['状态', 'status', '工单状态', 'State'],
|
||||
'resolution': ['解决方案', 'resolution', '处理结果', 'Solution'],
|
||||
'satisfaction': ['满意度', 'satisfaction_score', 'rating', 'Score']
|
||||
}
|
||||
|
||||
def get_val(row_data, keys, default=''):
|
||||
for k in keys:
|
||||
if k in row_data:
|
||||
val = row_data[k]
|
||||
if pd.notna(val):
|
||||
return str(val)
|
||||
return default
|
||||
|
||||
title = get_val(row, col_mappings['title'], f'导入工单 {index + 1}')
|
||||
description = get_val(row, col_mappings['description'], '')
|
||||
category = get_val(row, col_mappings['category'], '技术问题')
|
||||
priority = get_val(row, col_mappings['priority'], 'medium')
|
||||
status = get_val(row, col_mappings['status'], 'open')
|
||||
|
||||
# 验证必填字段
|
||||
if not title or title.strip() == '':
|
||||
skipped_rows += 1
|
||||
continue
|
||||
|
||||
# 生成唯一的工单ID
|
||||
@@ -513,12 +536,14 @@ def import_workorders():
|
||||
)
|
||||
|
||||
# 处理可选字段
|
||||
if pd.notna(row.get('解决方案', row.get('resolution'))):
|
||||
workorder.resolution = str(row.get('解决方案', row.get('resolution')))
|
||||
res_val = get_val(row, col_mappings['resolution'], None)
|
||||
if res_val:
|
||||
workorder.resolution = res_val
|
||||
|
||||
if pd.notna(row.get('满意度', row.get('satisfaction_score'))):
|
||||
score_val = get_val(row, col_mappings['satisfaction'], None)
|
||||
if score_val:
|
||||
try:
|
||||
workorder.satisfaction_score = int(row.get('满意度', row.get('satisfaction_score')))
|
||||
workorder.satisfaction_score = int(float(score_val))
|
||||
except (ValueError, TypeError):
|
||||
workorder.satisfaction_score = None
|
||||
|
||||
@@ -551,8 +576,9 @@ def import_workorders():
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"message": f"成功导入 {len(imported_workorders)} 个工单",
|
||||
"message": f"成功导入 {len(imported_workorders)} 个工单" + (f",跳过 {skipped_rows} 行无效数据" if skipped_rows > 0 else ""),
|
||||
"imported_count": len(imported_workorders),
|
||||
"skipped_count": skipped_rows,
|
||||
"workorders": imported_workorders
|
||||
})
|
||||
|
||||
|
||||
@@ -153,9 +153,17 @@ body {
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
100% { opacity: 1; }
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.health-dot.normal {
|
||||
@@ -176,8 +184,13 @@ body {
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* 可点击统计数字样式 */
|
||||
@@ -427,8 +440,13 @@ body {
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@@ -493,9 +511,11 @@ body {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 rgba(40, 167, 69, 0.7);
|
||||
}
|
||||
|
||||
70% {
|
||||
box-shadow: 0 0 0 10px rgba(40, 167, 69, 0);
|
||||
}
|
||||
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 rgba(40, 167, 69, 0);
|
||||
}
|
||||
@@ -863,8 +883,27 @@ body {
|
||||
.preset-preview .preview-param span {
|
||||
color: #6c757d;
|
||||
font-size: 0.8rem;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* 历史对话字体深度统一补丁 */
|
||||
.conversation-item,
|
||||
.conversation-item h6,
|
||||
.conversation-item p,
|
||||
.conversation-item span,
|
||||
.conversation-item small,
|
||||
.conversation-item strong,
|
||||
#conversation-list * {
|
||||
font-family: var(--font-family-primary) !important;
|
||||
}
|
||||
|
||||
.conversation-preview p {
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: var(--line-height-relaxed);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
|
||||
/* AI建议与人工描述优化样式 */
|
||||
.ai-suggestion-section {
|
||||
background: linear-gradient(135deg, #f8f9ff, #e8f2ff);
|
||||
@@ -1092,8 +1131,13 @@ body {
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% { left: -100%; }
|
||||
100% { left: 100%; }
|
||||
0% {
|
||||
left: -100%;
|
||||
}
|
||||
|
||||
100% {
|
||||
left: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* 按钮加载状态 */
|
||||
@@ -1122,9 +1166,17 @@ body {
|
||||
}
|
||||
|
||||
@keyframes successPulse {
|
||||
0% { transform: scale(1); }
|
||||
50% { transform: scale(1.05); }
|
||||
100% { transform: scale(1); }
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* 工具提示优化 */
|
||||
@@ -1206,64 +1258,19 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
| ||||