feat: 优化飞书集成、知识库、Agent、工单管理及AI建议功能,统一前端对话字体样式并移除工单模板文件。
This commit is contained in:
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
|
||||
})
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ body {
|
||||
|
||||
.btn-group .btn:hover {
|
||||
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%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background-color: rgba(0,0,0,0.8);
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
color: white;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
/* 可点击统计数字样式 */
|
||||
@@ -239,23 +252,23 @@ body {
|
||||
height: 1.75rem;
|
||||
padding: 0.25rem 0.375rem;
|
||||
}
|
||||
|
||||
|
||||
.btn-sm i {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
|
||||
.btn-group .btn:not(:last-child) {
|
||||
margin-right: 0.125rem;
|
||||
}
|
||||
|
||||
|
||||
.table .btn-group .btn {
|
||||
margin: 0 0.0625rem;
|
||||
}
|
||||
|
||||
|
||||
.clickable-stat {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
|
||||
.modal-xl {
|
||||
max-width: 95vw;
|
||||
}
|
||||
@@ -321,7 +334,7 @@ body {
|
||||
|
||||
.alert-card:hover {
|
||||
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 {
|
||||
@@ -401,7 +414,7 @@ body {
|
||||
|
||||
.card:hover {
|
||||
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 {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@@ -436,13 +454,13 @@ body {
|
||||
.container-fluid {
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
|
||||
.score-circle {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
|
||||
.card-body {
|
||||
padding: 1rem;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -505,7 +525,7 @@ body {
|
||||
.modal-content {
|
||||
border-radius: 0.5rem;
|
||||
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 {
|
||||
@@ -520,7 +540,7 @@ body {
|
||||
|
||||
/* 表格样式 */
|
||||
.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;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
|
||||
.alert-controls .form-select {
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
|
||||
.alert-card .d-flex {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
|
||||
.alert-card .ms-3 {
|
||||
margin-left: 0 !important;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
|
||||
.alert-data {
|
||||
font-size: 10px;
|
||||
max-height: 80px;
|
||||
@@ -700,7 +720,7 @@ body {
|
||||
|
||||
.preset-card:hover {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -796,15 +816,15 @@ body {
|
||||
.preset-card .card-body {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
|
||||
.preset-card h6 {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
|
||||
.preset-card p {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
|
||||
.preset-params .badge {
|
||||
font-size: 0.65rem;
|
||||
padding: 0.2rem 0.4rem;
|
||||
@@ -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);
|
||||
@@ -1087,13 +1126,18 @@ body {
|
||||
left: -100%;
|
||||
width: 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;
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
|
||||
/* 工具提示优化 */
|
||||
@@ -1176,94 +1228,49 @@ body {
|
||||
padding: 15px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
|
||||
.ai-suggestion-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
|
||||
.similarity-indicator {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
|
||||
.action-buttons {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
|
||||
.save-human-btn,
|
||||
.approve-btn {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
.tooltip-custom::before {
|
||||
font-size: 0.7rem;
|
||||
padding: 6px 10px;
|
||||
}
|
||||
}
|
||||
|
||||
| ||||