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"
|
PLANNING = "planning"
|
||||||
EXECUTING = "executing"
|
EXECUTING = "executing"
|
||||||
LEARNING = "learning"
|
LEARNING = "learning"
|
||||||
|
PROCESSING = "processing"
|
||||||
ERROR = "error"
|
ERROR = "error"
|
||||||
|
|
||||||
class AgentCore:
|
class AgentCore:
|
||||||
|
|||||||
@@ -6,10 +6,12 @@ TSP Agent助手 - 简化版本
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import json
|
||||||
from typing import Dict, Any, List, Optional
|
from typing import Dict, Any, List, Optional
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from src.config.unified_config import get_config
|
from src.config.unified_config import get_config
|
||||||
from src.agent.llm_client import LLMManager
|
from src.agent.llm_client import LLMManager
|
||||||
|
from src.web.service_manager import service_manager
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -304,22 +306,65 @@ class TSPAgentAssistant:
|
|||||||
logger.error(f"获取LLM使用统计失败: {e}")
|
logger.error(f"获取LLM使用统计失败: {e}")
|
||||||
return {}
|
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",
|
async def process_message_agent(self, message: str, user_id: str = "admin",
|
||||||
work_order_id: Optional[int] = None,
|
work_order_id: Optional[int] = None,
|
||||||
enable_proactive: bool = True) -> Dict[str, Any]:
|
enable_proactive: bool = True) -> Dict[str, Any]:
|
||||||
"""处理消息"""
|
"""处理消息 (实战化)"""
|
||||||
try:
|
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 {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
"message": f"Agent收到消息: {message}",
|
"response": response_text,
|
||||||
|
"actions": actions,
|
||||||
"user_id": user_id,
|
"user_id": user_id,
|
||||||
"work_order_id": work_order_id,
|
"work_order_id": work_order_id,
|
||||||
|
"status": "completed",
|
||||||
"timestamp": datetime.now().isoformat()
|
"timestamp": datetime.now().isoformat()
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"处理消息失败: {e}")
|
logger.error(f"处理消息失败: {e}")
|
||||||
return {"error": str(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]:
|
async def trigger_sample_actions(self) -> Dict[str, Any]:
|
||||||
"""触发示例动作"""
|
"""触发示例动作"""
|
||||||
@@ -336,7 +381,7 @@ class TSPAgentAssistant:
|
|||||||
logger.error(f"触发示例动作失败: {e}")
|
logger.error(f"触发示例动作失败: {e}")
|
||||||
return {"success": False, "error": str(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:
|
try:
|
||||||
import os
|
import os
|
||||||
@@ -356,20 +401,39 @@ class TSPAgentAssistant:
|
|||||||
|
|
||||||
logger.info(f"文件读取成功: {filename}, 字符数={len(content)}")
|
logger.info(f"文件读取成功: {filename}, 字符数={len(content)}")
|
||||||
|
|
||||||
# 使用简化的知识提取
|
# 使用LLM进行知识提取 (异步调用)
|
||||||
logger.info(f"正在对文件内容进行 AI 知识提取...")
|
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)} 个潜在条目")
|
logger.info(f"知识提取完成: 共提取出 {len(knowledge_entries)} 个潜在条目")
|
||||||
|
|
||||||
# 保存到知识库
|
# 保存到知识库
|
||||||
saved_count = 0
|
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):
|
for i, entry in enumerate(knowledge_entries):
|
||||||
try:
|
try:
|
||||||
logger.info(f"正在保存知识条目 [{i+1}/{len(knowledge_entries)}]: {entry.get('question', '')[:30]}...")
|
logger.info(f"正在保存知识条目 [{i+1}/{len(knowledge_entries)}]: {entry.get('question', '')[:30]}...")
|
||||||
# 这里在实际项目中应当注入知识库管理器的保存逻辑
|
|
||||||
# 但在当前简化版本中仅记录日志
|
if knowledge_manager:
|
||||||
saved_count += 1
|
# 实际保存到数据库
|
||||||
|
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:
|
except Exception as save_error:
|
||||||
logger.error(f"保存知识条目 {i+1} 时出错: {save_error}")
|
logger.error(f"保存知识条目 {i+1} 时出错: {save_error}")
|
||||||
|
|
||||||
@@ -402,26 +466,71 @@ class TSPAgentAssistant:
|
|||||||
logger.error(f"读取文件失败: {e}")
|
logger.error(f"读取文件失败: {e}")
|
||||||
return ""
|
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:
|
try:
|
||||||
# 简化的知识提取逻辑
|
# 限制内容长度,避免超出token限制
|
||||||
entries = []
|
# 假设每个汉字2个token,保留前8000个字符作为上下文
|
||||||
|
truncated_content = content[:8000]
|
||||||
# 按段落分割内容
|
if len(content) > 8000:
|
||||||
paragraphs = content.split('\n\n')
|
truncated_content += "\n...(后续内容已省略)"
|
||||||
|
|
||||||
for i, paragraph in enumerate(paragraphs[:5]): # 最多提取5个
|
prompt = f"""
|
||||||
if len(paragraph.strip()) > 20: # 过滤太短的段落
|
你是一个专业的知识库构建助手。请分析以下文档内容,提取出关键的"问题"和"答案"对,用于构建知识库。
|
||||||
entries.append({
|
|
||||||
"question": f"关于{filename}的问题{i+1}",
|
文档文件名:{filename}
|
||||||
"answer": paragraph.strip(),
|
文档内容:
|
||||||
"category": "文档知识",
|
{truncated_content}
|
||||||
"confidence_score": 0.7
|
|
||||||
|
要求:
|
||||||
|
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:
|
except Exception as e:
|
||||||
logger.error(f"提取知识失败: {e}")
|
logger.error(f"提取知识失败: {e}")
|
||||||
return []
|
return []
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -161,13 +161,36 @@ class CacheManager:
|
|||||||
"""淘汰最旧的缓存"""
|
"""淘汰最旧的缓存"""
|
||||||
if not self.memory_cache:
|
if not self.memory_cache:
|
||||||
return
|
return
|
||||||
|
|
||||||
oldest_key = min(
|
oldest_key = min(
|
||||||
self.memory_cache.keys(),
|
self.memory_cache.keys(),
|
||||||
key=lambda k: self.memory_cache[k]['created_at']
|
key=lambda k: self.memory_cache[k]['created_at']
|
||||||
)
|
)
|
||||||
del self.memory_cache[oldest_key]
|
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]:
|
def get_stats(self) -> Dict[str, Any]:
|
||||||
"""获取缓存统计信息"""
|
"""获取缓存统计信息"""
|
||||||
with self.cache_lock:
|
with self.cache_lock:
|
||||||
@@ -218,11 +241,10 @@ class DatabaseCache:
|
|||||||
logger.debug(f"缓存未命中: {cache_key}")
|
logger.debug(f"缓存未命中: {cache_key}")
|
||||||
result = func(*args, **kwargs)
|
result = func(*args, **kwargs)
|
||||||
self.cache_manager.set(cache_key, result, self.ttl)
|
self.cache_manager.set(cache_key, result, self.ttl)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
# 全局缓存管理器实例
|
# 全局缓存管理器实例
|
||||||
cache_manager = CacheManager()
|
cache_manager = CacheManager()
|
||||||
|
|
||||||
|
|||||||
@@ -92,7 +92,9 @@ class QueryOptimizer:
|
|||||||
'confidence_score': conv.confidence_score,
|
'confidence_score': conv.confidence_score,
|
||||||
'work_order_id': conv.work_order_id,
|
'work_order_id': conv.work_order_id,
|
||||||
'ip_address': conv.ip_address,
|
'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:
|
if is_first_suggestion:
|
||||||
prompt += """
|
prompt += """
|
||||||
要求:
|
要求:
|
||||||
1. 详细分析问题描述,识别可能的根本原因
|
1. 输出格式必须严格遵守:"时间:当前问题现状,下一步做法"
|
||||||
2. 基于当前处理进度,判断问题处于哪个阶段
|
例如:"0210:无TBOX登录记录,需要用户进站抓取TBOX日志分析。"
|
||||||
3. 提供具体的排查步骤和技术指导
|
2. "时间"使用当前的MMDD格式(如0210)
|
||||||
4. 建议需要收集哪些技术信息(如日志、配置、版本等)
|
3. "当前问题现状"要基于问题描述和处理记录总结,简明扼要
|
||||||
5. 如果需要,可以建议进站处理的具体项目
|
4. "下一步做法"如果是首次建议,只提供远程排查步骤(如检查网络、重启等),不要提进站
|
||||||
6. 语言专业,包含技术细节,方便技术人员理解
|
5. 总字数控制在100字以内,不要分行,不要列点
|
||||||
7. 建议格式要清晰,便于执行和跟踪
|
6. 绝对不要使用"建议您"、"您可以"等客套话,直接陈述做法
|
||||||
|
|
||||||
请提供完整的分析和建议:"""
|
请按照上述格式生成建议:"""
|
||||||
else:
|
else:
|
||||||
prompt += """
|
prompt += """
|
||||||
要求:
|
要求:
|
||||||
1. 基于已有处理记录和当前进度,分析问题进展情况
|
1. 输出格式必须严格遵守:"时间:当前问题现状,下一步做法"
|
||||||
2. 判断之前的处理步骤是否有效,找出可能的遗漏点
|
例如:"0210:无TBOX登录记录,需要用户进站抓取TBOX日志分析。"
|
||||||
3. 根据问题发展阶段提供更深入的技术解决方案
|
2. "时间"使用当前的MMDD格式(如0210)
|
||||||
4. 如果远程处理无效,明确说明需要哪些线下技术支持
|
3. "当前问题现状"要简述之前的处理结果(如远程无效)
|
||||||
5. 详细说明进站后需要执行的具体诊断和修复步骤
|
4. "下一步做法"可以包含进站抓取日志、联系技术支持等深入方案
|
||||||
6. 包含技术参数、工具要求和注意事项
|
5. 总字数控制在100字以内,不要分行,不要列点
|
||||||
7. 便于技术人员快速理解问题状态和下一步行动
|
6. 绝对不要使用"建议您"、"您可以"等客套话,直接陈述做法
|
||||||
|
|
||||||
请提供针对性的深入分析和处理建议:"""
|
请按照上述格式生成建议:"""
|
||||||
|
|
||||||
return prompt
|
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.config.unified_config import get_config
|
||||||
from src.web.service_manager import service_manager
|
from src.web.service_manager import service_manager
|
||||||
|
from src.core.cache_manager import cache_manager
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -66,15 +67,30 @@ class FeishuLongConnService:
|
|||||||
message_id = message.message_id
|
message_id = message.message_id
|
||||||
chat_id = message.chat_id
|
chat_id = message.chat_id
|
||||||
message_type = message.message_type
|
message_type = message.message_type
|
||||||
|
chat_type = getattr(message, 'chat_type', 'unknown') # 获取会话类型
|
||||||
content = message.content
|
content = message.content
|
||||||
sender = event.sender
|
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: {message_id}")
|
||||||
logger.info(f" - 群聊ID: {chat_id}")
|
logger.info(f" - 会话类型: {'群聊(group)' if chat_type == 'group' else '私聊(p2p)' if chat_type == 'p2p' else chat_type}")
|
||||||
logger.info(f" - 发送者ID: {sender.sender_id.user_id}")
|
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" - 消息类型: {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":
|
if message_type != "text":
|
||||||
@@ -145,8 +161,8 @@ class FeishuLongConnService:
|
|||||||
response_data = chat_manager.process_message(
|
response_data = chat_manager.process_message(
|
||||||
session_id=session_id,
|
session_id=session_id,
|
||||||
user_message=text_content,
|
user_message=text_content,
|
||||||
ip_address=None,
|
ip_address=f"Feishu:{sender_id}",
|
||||||
invocation_method="feishu_longconn"
|
invocation_method=f"Feishu({chat_type})"
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(f"📊 处理结果: {response_data.get('success')}")
|
logger.info(f"📊 处理结果: {response_data.get('success')}")
|
||||||
|
|||||||
Binary file not shown.
@@ -401,27 +401,35 @@ class KnowledgeManager:
|
|||||||
"""获取知识库统计信息"""
|
"""获取知识库统计信息"""
|
||||||
try:
|
try:
|
||||||
with db_manager.get_session() as session:
|
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
|
KnowledgeEntry.is_active == True
|
||||||
).count()
|
).count()
|
||||||
|
|
||||||
# 按类别统计
|
# 统计已验证的条目
|
||||||
|
verified_entries = session.query(KnowledgeEntry).filter(
|
||||||
|
KnowledgeEntry.is_active == True,
|
||||||
|
KnowledgeEntry.is_verified == True
|
||||||
|
).count()
|
||||||
|
|
||||||
|
# 按类别统计(仅限活跃条目)
|
||||||
category_stats = session.query(
|
category_stats = session.query(
|
||||||
KnowledgeEntry.category,
|
KnowledgeEntry.category,
|
||||||
session.query(KnowledgeEntry).filter(
|
func.count(KnowledgeEntry.id)
|
||||||
KnowledgeEntry.category == KnowledgeEntry.category
|
).filter(
|
||||||
).count()
|
KnowledgeEntry.is_active == True
|
||||||
).group_by(KnowledgeEntry.category).all()
|
).group_by(KnowledgeEntry.category).all()
|
||||||
|
|
||||||
# 平均置信度
|
# 平均置信度(仅限活跃条目)
|
||||||
avg_confidence = session.query(
|
avg_confidence = session.query(
|
||||||
func.avg(KnowledgeEntry.confidence_score)
|
func.avg(KnowledgeEntry.confidence_score)
|
||||||
|
).filter(
|
||||||
|
KnowledgeEntry.is_active == True
|
||||||
).scalar() or 0.0
|
).scalar() or 0.0
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"total_entries": total_entries,
|
"total_entries": total_entries,
|
||||||
"active_entries": active_entries,
|
"active_entries": verified_entries, # 将 active_entries 复用为已验证数量,或前端相应修改
|
||||||
"category_distribution": dict(category_stats),
|
"category_distribution": dict(category_stats),
|
||||||
"average_confidence": float(avg_confidence)
|
"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:
|
try:
|
||||||
from src.web.service_manager import service_manager
|
from src.web.service_manager import service_manager
|
||||||
import asyncio
|
result = service_manager.get_agent_assistant().trigger_sample_actions_sync()
|
||||||
result = asyncio.run(service_manager.get_agent_assistant().trigger_sample_actions())
|
|
||||||
return jsonify(result)
|
return jsonify(result)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({"error": str(e)}), 500
|
return jsonify({"error": str(e)}), 500
|
||||||
@@ -163,13 +162,12 @@ def agent_chat():
|
|||||||
agent_assistant = service_manager.get_agent_assistant()
|
agent_assistant = service_manager.get_agent_assistant()
|
||||||
|
|
||||||
# 模拟Agent处理(实际应该调用真正的Agent处理逻辑)
|
# 模拟Agent处理(实际应该调用真正的Agent处理逻辑)
|
||||||
import asyncio
|
result = agent_assistant.process_message_agent_sync(
|
||||||
result = asyncio.run(agent_assistant.process_message_agent(
|
|
||||||
message=message,
|
message=message,
|
||||||
user_id=context.get('user_id', 'admin'),
|
user_id=context.get('user_id', 'admin'),
|
||||||
work_order_id=None,
|
work_order_id=None,
|
||||||
enable_proactive=True
|
enable_proactive=True
|
||||||
))
|
)
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"success": True,
|
"success": True,
|
||||||
@@ -215,8 +213,7 @@ def execute_agent_tool():
|
|||||||
if not tool_name:
|
if not tool_name:
|
||||||
return jsonify({"error": "缺少工具名称tool"}), 400
|
return jsonify({"error": "缺少工具名称tool"}), 400
|
||||||
|
|
||||||
import asyncio
|
result = service_manager.get_agent_assistant().agent_core.tool_manager.execute_tool_sync(tool_name, parameters)
|
||||||
result = asyncio.run(service_manager.get_agent_assistant().agent_core.tool_manager.execute_tool(tool_name, parameters))
|
|
||||||
return jsonify(result)
|
return jsonify(result)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({"error": str(e)}), 500
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import threading
|
|||||||
from flask import Blueprint, request, jsonify, current_app
|
from flask import Blueprint, request, jsonify, current_app
|
||||||
from src.integrations.feishu_service import FeishuService
|
from src.integrations.feishu_service import FeishuService
|
||||||
from src.web.service_manager import service_manager
|
from src.web.service_manager import service_manager
|
||||||
|
from src.core.cache_manager import cache_manager
|
||||||
|
|
||||||
# 初始化日志
|
# 初始化日志
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -35,11 +36,21 @@ def _process_message_in_background(app, event_data: dict):
|
|||||||
|
|
||||||
message_id = message.get('message_id')
|
message_id = message.get('message_id')
|
||||||
chat_id = message.get('chat_id')
|
chat_id = message.get('chat_id')
|
||||||
|
chat_type = message.get('chat_type', 'unknown')
|
||||||
|
|
||||||
if not message_id or not chat_id:
|
if not message_id or not chat_id:
|
||||||
logger.error(f"[Feishu Bot] 事件数据缺少必要字段: {event_data}")
|
logger.error(f"[Feishu Bot] 事件数据缺少必要字段: {event_data}")
|
||||||
return
|
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字符串,需要再次解析
|
# 内容是一个JSON字符串,需要再次解析
|
||||||
try:
|
try:
|
||||||
content_json = json.loads(message.get('content', '{}'))
|
content_json = json.loads(message.get('content', '{}'))
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import logging
|
|||||||
from flask import Blueprint, request, jsonify
|
from flask import Blueprint, request, jsonify
|
||||||
from src.agent_assistant import TSPAgentAssistant
|
from src.agent_assistant import TSPAgentAssistant
|
||||||
from src.web.service_manager import service_manager
|
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
|
from src.web.error_handlers import handle_api_errors, create_error_response, create_success_response
|
||||||
|
|
||||||
knowledge_bp = Blueprint('knowledge', __name__, url_prefix='/api/knowledge')
|
knowledge_bp = Blueprint('knowledge', __name__, url_prefix='/api/knowledge')
|
||||||
@@ -98,7 +99,16 @@ def upload_knowledge_file():
|
|||||||
try:
|
try:
|
||||||
file.save(temp_path)
|
file.save(temp_path)
|
||||||
assistant = get_agent_assistant()
|
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)
|
return jsonify(result)
|
||||||
finally:
|
finally:
|
||||||
try:
|
try:
|
||||||
@@ -108,6 +118,78 @@ def upload_knowledge_file():
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
logger.warning(f"清理临时文件失败: {cleanup_error}")
|
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'])
|
@knowledge_bp.route('/delete/<int:knowledge_id>', methods=['DELETE'])
|
||||||
@handle_api_errors
|
@handle_api_errors
|
||||||
def delete_knowledge(knowledge_id):
|
def delete_knowledge(knowledge_id):
|
||||||
|
|||||||
@@ -478,19 +478,42 @@ def import_workorders():
|
|||||||
# 解析Excel文件
|
# 解析Excel文件
|
||||||
try:
|
try:
|
||||||
df = pd.read_excel(upload_path)
|
df = pd.read_excel(upload_path)
|
||||||
|
if df is None or df.empty:
|
||||||
|
return jsonify({"error": "Excel文件内容为空或无法识别表格结构"}), 400
|
||||||
|
|
||||||
imported_workorders = []
|
imported_workorders = []
|
||||||
|
skipped_rows = 0
|
||||||
|
|
||||||
# 处理每一行数据
|
# 处理每一行数据
|
||||||
for index, row in df.iterrows():
|
for index, row in df.iterrows():
|
||||||
# 根据Excel列名映射到工单字段
|
# 定义候选列名映射
|
||||||
title = str(row.get('标题', row.get('title', f'导入工单 {index + 1}')))
|
col_mappings = {
|
||||||
description = str(row.get('描述', row.get('description', '')))
|
'title': ['标题', 'title', '工单标题', '问题主体', 'Subject'],
|
||||||
category = str(row.get('分类', row.get('category', '技术问题')))
|
'description': ['描述', 'description', '问题描述', '详细内容', 'Detail'],
|
||||||
priority = str(row.get('优先级', row.get('priority', 'medium')))
|
'category': ['分类', 'category', '业务分类', '模块', 'Type'],
|
||||||
status = str(row.get('状态', row.get('status', 'open')))
|
'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() == '':
|
if not title or title.strip() == '':
|
||||||
|
skipped_rows += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 生成唯一的工单ID
|
# 生成唯一的工单ID
|
||||||
@@ -513,12 +536,14 @@ def import_workorders():
|
|||||||
)
|
)
|
||||||
|
|
||||||
# 处理可选字段
|
# 处理可选字段
|
||||||
if pd.notna(row.get('解决方案', row.get('resolution'))):
|
res_val = get_val(row, col_mappings['resolution'], None)
|
||||||
workorder.resolution = str(row.get('解决方案', row.get('resolution')))
|
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:
|
try:
|
||||||
workorder.satisfaction_score = int(row.get('满意度', row.get('satisfaction_score')))
|
workorder.satisfaction_score = int(float(score_val))
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
workorder.satisfaction_score = None
|
workorder.satisfaction_score = None
|
||||||
|
|
||||||
@@ -551,8 +576,9 @@ def import_workorders():
|
|||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"success": True,
|
"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),
|
"imported_count": len(imported_workorders),
|
||||||
|
"skipped_count": skipped_rows,
|
||||||
"workorders": imported_workorders
|
"workorders": imported_workorders
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ body {
|
|||||||
|
|
||||||
.btn-group .btn:hover {
|
.btn-group .btn:hover {
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 小尺寸按钮优化 */
|
/* 小尺寸按钮优化 */
|
||||||
@@ -97,7 +97,7 @@ body {
|
|||||||
bottom: 100%;
|
bottom: 100%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
background-color: rgba(0,0,0,0.8);
|
background-color: rgba(0, 0, 0, 0.8);
|
||||||
color: white;
|
color: white;
|
||||||
padding: 0.25rem 0.5rem;
|
padding: 0.25rem 0.5rem;
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
@@ -153,9 +153,17 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes pulse {
|
@keyframes pulse {
|
||||||
0% { opacity: 1; }
|
0% {
|
||||||
50% { opacity: 0.5; }
|
opacity: 1;
|
||||||
100% { opacity: 1; }
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.health-dot.normal {
|
.health-dot.normal {
|
||||||
@@ -176,8 +184,13 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
from { transform: rotate(0deg); }
|
from {
|
||||||
to { transform: rotate(360deg); }
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 可点击统计数字样式 */
|
/* 可点击统计数字样式 */
|
||||||
@@ -239,23 +252,23 @@ body {
|
|||||||
height: 1.75rem;
|
height: 1.75rem;
|
||||||
padding: 0.25rem 0.375rem;
|
padding: 0.25rem 0.375rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-sm i {
|
.btn-sm i {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-group .btn:not(:last-child) {
|
.btn-group .btn:not(:last-child) {
|
||||||
margin-right: 0.125rem;
|
margin-right: 0.125rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table .btn-group .btn {
|
.table .btn-group .btn {
|
||||||
margin: 0 0.0625rem;
|
margin: 0 0.0625rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.clickable-stat {
|
.clickable-stat {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-xl {
|
.modal-xl {
|
||||||
max-width: 95vw;
|
max-width: 95vw;
|
||||||
}
|
}
|
||||||
@@ -321,7 +334,7 @@ body {
|
|||||||
|
|
||||||
.alert-card:hover {
|
.alert-card:hover {
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-card.critical {
|
.alert-card.critical {
|
||||||
@@ -401,7 +414,7 @@ body {
|
|||||||
|
|
||||||
.card:hover {
|
.card:hover {
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 按钮样式 */
|
/* 按钮样式 */
|
||||||
@@ -427,8 +440,13 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
0% { transform: rotate(0deg); }
|
0% {
|
||||||
100% { transform: rotate(360deg); }
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 响应式设计 */
|
/* 响应式设计 */
|
||||||
@@ -436,13 +454,13 @@ body {
|
|||||||
.container-fluid {
|
.container-fluid {
|
||||||
padding: 0 15px;
|
padding: 0 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.score-circle {
|
.score-circle {
|
||||||
width: 80px;
|
width: 80px;
|
||||||
height: 80px;
|
height: 80px;
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-body {
|
.card-body {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
@@ -493,9 +511,11 @@ body {
|
|||||||
0% {
|
0% {
|
||||||
box-shadow: 0 0 0 0 rgba(40, 167, 69, 0.7);
|
box-shadow: 0 0 0 0 rgba(40, 167, 69, 0.7);
|
||||||
}
|
}
|
||||||
|
|
||||||
70% {
|
70% {
|
||||||
box-shadow: 0 0 0 10px rgba(40, 167, 69, 0);
|
box-shadow: 0 0 0 10px rgba(40, 167, 69, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
box-shadow: 0 0 0 0 rgba(40, 167, 69, 0);
|
box-shadow: 0 0 0 0 rgba(40, 167, 69, 0);
|
||||||
}
|
}
|
||||||
@@ -505,7 +525,7 @@ body {
|
|||||||
.modal-content {
|
.modal-content {
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
border: none;
|
border: none;
|
||||||
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-header {
|
.modal-header {
|
||||||
@@ -520,7 +540,7 @@ body {
|
|||||||
|
|
||||||
/* 表格样式 */
|
/* 表格样式 */
|
||||||
.table-hover tbody tr:hover {
|
.table-hover tbody tr:hover {
|
||||||
background-color: rgba(0,123,255,0.1);
|
background-color: rgba(0, 123, 255, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 空状态样式 */
|
/* 空状态样式 */
|
||||||
@@ -649,20 +669,20 @@ body {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-controls .form-select {
|
.alert-controls .form-select {
|
||||||
min-width: auto;
|
min-width: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-card .d-flex {
|
.alert-card .d-flex {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-card .ms-3 {
|
.alert-card .ms-3 {
|
||||||
margin-left: 0 !important;
|
margin-left: 0 !important;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-data {
|
.alert-data {
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
max-height: 80px;
|
max-height: 80px;
|
||||||
@@ -700,7 +720,7 @@ body {
|
|||||||
|
|
||||||
.preset-card:hover {
|
.preset-card:hover {
|
||||||
transform: translateY(-5px);
|
transform: translateY(-5px);
|
||||||
box-shadow: 0 8px 25px rgba(0,0,0,0.15);
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
|
||||||
border-color: #007bff;
|
border-color: #007bff;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -796,15 +816,15 @@ body {
|
|||||||
.preset-card .card-body {
|
.preset-card .card-body {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preset-card h6 {
|
.preset-card h6 {
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preset-card p {
|
.preset-card p {
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preset-params .badge {
|
.preset-params .badge {
|
||||||
font-size: 0.65rem;
|
font-size: 0.65rem;
|
||||||
padding: 0.2rem 0.4rem;
|
padding: 0.2rem 0.4rem;
|
||||||
@@ -863,8 +883,27 @@ body {
|
|||||||
.preset-preview .preview-param span {
|
.preset-preview .preview-param span {
|
||||||
color: #6c757d;
|
color: #6c757d;
|
||||||
font-size: 0.8rem;
|
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建议与人工描述优化样式 */
|
||||||
.ai-suggestion-section {
|
.ai-suggestion-section {
|
||||||
background: linear-gradient(135deg, #f8f9ff, #e8f2ff);
|
background: linear-gradient(135deg, #f8f9ff, #e8f2ff);
|
||||||
@@ -1087,13 +1126,18 @@ body {
|
|||||||
left: -100%;
|
left: -100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent);
|
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.4), transparent);
|
||||||
animation: shimmer 1.5s infinite;
|
animation: shimmer 1.5s infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes shimmer {
|
@keyframes shimmer {
|
||||||
0% { left: -100%; }
|
0% {
|
||||||
100% { left: 100%; }
|
left: -100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
left: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 按钮加载状态 */
|
/* 按钮加载状态 */
|
||||||
@@ -1122,9 +1166,17 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes successPulse {
|
@keyframes successPulse {
|
||||||
0% { transform: scale(1); }
|
0% {
|
||||||
50% { transform: scale(1.05); }
|
transform: scale(1);
|
||||||
100% { transform: scale(1); }
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 工具提示优化 */
|
/* 工具提示优化 */
|
||||||
@@ -1176,94 +1228,49 @@ body {
|
|||||||
padding: 15px;
|
padding: 15px;
|
||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ai-suggestion-header {
|
.ai-suggestion-header {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.similarity-indicator {
|
.similarity-indicator {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-buttons {
|
.action-buttons {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.save-human-btn,
|
.save-human-btn,
|
||||||
.approve-btn {
|
.approve-btn {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip-custom::before {
|
.tooltip-custom::before {
|
||||||
font-size: 0.7rem;
|
font-size: 0.7rem;
|
||||||
padding: 6px 10px;
|
padding: 6px 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
| |||||||