修复AI建议逻辑和字段映射问题

- 修复AI建议基于问题描述而不是处理过程生成
- 修复工单详情页面显示逻辑
- 修复飞书时间字段处理(毫秒时间戳转换)
- 优化字段映射和转换逻辑
- 添加飞书集成功能
- 改进对话历史合并功能
- 优化系统优化反馈机制
This commit is contained in:
赵杰 Jie Zhao (雄狮汽车科技)
2025-09-19 17:29:33 +01:00
parent 66f44143d9
commit 79cf316c63
20 changed files with 2648 additions and 86 deletions

View File

@@ -32,6 +32,7 @@ from src.web.blueprints.conversations import conversations_bp
from src.web.blueprints.knowledge import knowledge_bp
from src.web.blueprints.monitoring import monitoring_bp
from src.web.blueprints.system import system_bp
from src.web.blueprints.feishu_sync import feishu_sync_bp
# 配置日志
logger = logging.getLogger(__name__)
@@ -103,6 +104,7 @@ app.register_blueprint(conversations_bp)
app.register_blueprint(knowledge_bp)
app.register_blueprint(monitoring_bp)
app.register_blueprint(system_bp)
app.register_blueprint(feishu_sync_bp)
# 页面路由
@app.route('/')
@@ -384,7 +386,7 @@ def get_active_sessions():
def get_agent_status():
"""获取Agent状态"""
try:
status = agent_assistant.get_agent_status()
status = get_agent_assistant().get_agent_status()
return jsonify(status)
except Exception as e:
return jsonify({"error": str(e)}), 500
@@ -394,7 +396,7 @@ def get_agent_action_history():
"""获取Agent动作执行历史"""
try:
limit = request.args.get('limit', 50, type=int)
history = agent_assistant.get_action_history(limit)
history = get_agent_assistant().get_action_history(limit)
return jsonify({
"success": True,
"history": history,
@@ -408,7 +410,7 @@ def trigger_sample_action():
"""触发示例动作"""
try:
import asyncio
result = asyncio.run(agent_assistant.trigger_sample_actions())
result = asyncio.run(get_agent_assistant().trigger_sample_actions())
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
@@ -417,7 +419,7 @@ def trigger_sample_action():
def clear_agent_history():
"""清空Agent执行历史"""
try:
result = agent_assistant.clear_execution_history()
result = get_agent_assistant().clear_execution_history()
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
@@ -426,7 +428,7 @@ def clear_agent_history():
def get_llm_stats():
"""获取LLM使用统计"""
try:
stats = agent_assistant.get_llm_usage_stats()
stats = get_agent_assistant().get_llm_usage_stats()
return jsonify({
"success": True,
"stats": stats
@@ -440,7 +442,7 @@ def toggle_agent_mode():
try:
data = request.get_json()
enabled = data.get('enabled', True)
success = agent_assistant.toggle_agent_mode(enabled)
success = get_agent_assistant().toggle_agent_mode(enabled)
return jsonify({
"success": success,
"message": f"Agent模式已{'启用' if enabled else '禁用'}"
@@ -452,7 +454,7 @@ def toggle_agent_mode():
def start_agent_monitoring():
"""启动Agent监控"""
try:
success = agent_assistant.start_proactive_monitoring()
success = get_agent_assistant().start_proactive_monitoring()
return jsonify({
"success": success,
"message": "Agent监控已启动" if success else "启动失败"
@@ -464,7 +466,7 @@ def start_agent_monitoring():
def stop_agent_monitoring():
"""停止Agent监控"""
try:
success = agent_assistant.stop_proactive_monitoring()
success = get_agent_assistant().stop_proactive_monitoring()
return jsonify({
"success": success,
"message": "Agent监控已停止" if success else "停止失败"
@@ -476,7 +478,7 @@ def stop_agent_monitoring():
def proactive_monitoring():
"""主动监控检查"""
try:
result = agent_assistant.run_proactive_monitoring()
result = get_agent_assistant().run_proactive_monitoring()
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
@@ -526,8 +528,8 @@ def agent_chat():
@app.route('/api/agent/tools/stats')
def get_agent_tools_stats():
try:
tools = agent_assistant.agent_core.tool_manager.get_available_tools()
performance = agent_assistant.agent_core.tool_manager.get_tool_performance_report()
tools = get_agent_assistant().agent_core.tool_manager.get_available_tools()
performance = get_agent_assistant().agent_core.tool_manager.get_tool_performance_report()
return jsonify({
"success": True,
"tools": tools,
@@ -536,6 +538,22 @@ def get_agent_tools_stats():
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/api/agent/tools/execute', methods=['POST'])
def execute_agent_tool():
"""执行指定的Agent工具"""
try:
data = request.get_json() or {}
tool_name = data.get('tool') or data.get('name')
parameters = data.get('parameters') or {}
if not tool_name:
return jsonify({"error": "缺少工具名称tool"}), 400
import asyncio
result = asyncio.run(get_agent_assistant().agent_core.tool_manager.execute_tool(tool_name, parameters))
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/api/agent/tools/register', methods=['POST'])
def register_custom_tool():
"""注册自定义工具(仅登记元数据,函数为占位符)"""
@@ -549,7 +567,7 @@ def register_custom_tool():
def _placeholder_tool(**kwargs):
return {"message": f"自定义工具 {name} 已登记(占位),当前不可执行", "params": kwargs}
agent_assistant.agent_core.tool_manager.register_tool(
get_agent_assistant().agent_core.tool_manager.register_tool(
name,
_placeholder_tool,
metadata={"description": description, "custom": True}
@@ -561,7 +579,7 @@ def register_custom_tool():
@app.route('/api/agent/tools/unregister/<name>', methods=['DELETE'])
def unregister_custom_tool(name):
try:
success = agent_assistant.agent_core.tool_manager.unregister_tool(name)
success = get_agent_assistant().agent_core.tool_manager.unregister_tool(name)
return jsonify({"success": success})
except Exception as e:
return jsonify({"error": str(e)}), 500
@@ -737,6 +755,11 @@ def test_model_response():
except Exception as e:
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/feishu-sync')
def feishu_sync():
"""飞书同步管理页面"""
return render_template('feishu_sync.html')
if __name__ == '__main__':
import time
app.config['START_TIME'] = time.time()

View File

@@ -8,6 +8,7 @@ from flask import Blueprint, request, jsonify
from src.core.database import db_manager
from src.core.models import Conversation
from src.core.query_optimizer import query_optimizer
from datetime import timedelta
conversations_bp = Blueprint('conversations', __name__, url_prefix='/api/conversations')
@@ -27,6 +28,10 @@ def get_conversations():
user_id=user_id, date_filter=date_filter
)
# 规范化移除不存在的user_id字段避免前端误用
for conv in result.get('conversations', []):
if 'user_id' in conv and conv['user_id'] is None:
conv.pop('user_id', None)
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
@@ -40,10 +45,11 @@ def get_conversation_detail(conversation_id):
if not conv:
return jsonify({"error": "对话不存在"}), 404
# Conversation模型没有user_id字段这里用占位或由外层推断
return jsonify({
'success': True,
'id': conv.id,
'user_id': conv.user_id,
'user_id': None,
'user_message': conv.user_message,
'assistant_response': conv.assistant_response,
'timestamp': conv.timestamp.isoformat() if conv.timestamp else None,
@@ -88,3 +94,106 @@ def clear_all_conversations():
return jsonify({"success": True, "message": "对话历史已清空"})
except Exception as e:
return jsonify({"error": str(e)}), 500
@conversations_bp.route('/migrate-merge', methods=['POST'])
def migrate_merge_conversations():
"""一次性迁移:将历史上拆分存储的用户/助手两条记录合并为一条
规则:
- 只处理一端为空的记录user_only 或 assistant_only
- 优先将 user_only 与其后最近的 assistant_only 合并同工单且5分钟内
- 若当前为 assistant_only 且前一条是 user_only 也合并到前一条
- 合并后删除被吸收的那条记录
- 可重复执行(幂等):已合并的不再满足“一端为空”的条件
"""
try:
merged_pairs = 0
deleted_rows = 0
time_threshold_seconds = 300
to_delete_ids = []
with db_manager.get_session() as session:
conversations = session.query(Conversation).order_by(Conversation.timestamp.asc(), Conversation.id.asc()).all()
total = len(conversations)
i = 0
def is_empty(text: str) -> bool:
return (text is None) or (str(text).strip() == '')
while i < total:
c = conversations[i]
user_only = (not is_empty(c.user_message)) and is_empty(c.assistant_response)
assistant_only = (not is_empty(c.assistant_response)) and is_empty(c.user_message)
if user_only:
# 向后寻找匹配的assistant_only
j = i + 1
while j < total:
n = conversations[j]
# 跳过已经标记删除的
if n.id in to_delete_ids:
j += 1
continue
# 超过阈值不再尝试
if c.timestamp and n.timestamp and (n.timestamp - c.timestamp).total_seconds() > time_threshold_seconds:
break
# 同工单或两者都为空均可
same_wo = (c.work_order_id == n.work_order_id) or (c.work_order_id is None and n.work_order_id is None)
if same_wo and (not is_empty(n.assistant_response)) and is_empty(n.user_message):
# 合并
c.assistant_response = n.assistant_response
if c.response_time is None and c.timestamp and n.timestamp:
try:
c.response_time = max(0.0, (n.timestamp - c.timestamp).total_seconds() * 1000.0)
except Exception:
pass
# 继承辅助信息
if (not c.confidence_score) and n.confidence_score is not None:
c.confidence_score = n.confidence_score
if (not c.knowledge_used) and n.knowledge_used:
c.knowledge_used = n.knowledge_used
session.add(c)
to_delete_ids.append(n.id)
merged_pairs += 1
break
j += 1
elif assistant_only:
# 向前与最近的 user_only 合并(如果尚未被其他合并吸收)
j = i - 1
while j >= 0:
p = conversations[j]
if p.id in to_delete_ids:
j -= 1
continue
if p.timestamp and c.timestamp and (c.timestamp - p.timestamp).total_seconds() > time_threshold_seconds:
break
same_wo = (c.work_order_id == p.work_order_id) or (c.work_order_id is None and p.work_order_id is None)
if same_wo and (not is_empty(p.user_message)) and is_empty(p.assistant_response):
p.assistant_response = c.assistant_response
if p.response_time is None and p.timestamp and c.timestamp:
try:
p.response_time = max(0.0, (c.timestamp - p.timestamp).total_seconds() * 1000.0)
except Exception:
pass
if (not p.confidence_score) and c.confidence_score is not None:
p.confidence_score = c.confidence_score
if (not p.knowledge_used) and c.knowledge_used:
p.knowledge_used = c.knowledge_used
session.add(p)
to_delete_ids.append(c.id)
merged_pairs += 1
break
j -= 1
i += 1
if to_delete_ids:
deleted_rows = session.query(Conversation).filter(Conversation.id.in_(to_delete_ids)).delete(synchronize_session=False)
session.commit()
return jsonify({
'success': True,
'merged_pairs': merged_pairs,
'deleted_rows': deleted_rows
})
except Exception as e:
return jsonify({"error": str(e)}), 500

View File

@@ -0,0 +1,304 @@
# -*- coding: utf-8 -*-
"""
飞书同步蓝图
处理飞书多维表格与工单系统的同步
"""
from flask import Blueprint, request, jsonify
from src.integrations.feishu_client import FeishuClient
from src.integrations.workorder_sync import WorkOrderSyncService
from src.integrations.config_manager import config_manager
import logging
logger = logging.getLogger(__name__)
feishu_sync_bp = Blueprint('feishu_sync', __name__, url_prefix='/api/feishu-sync')
# 全局同步服务实例
sync_service = None
def get_sync_service():
"""获取同步服务实例"""
global sync_service
if sync_service is None:
# 从配置管理器读取飞书配置
feishu_config = config_manager.get_feishu_config()
if not all([feishu_config.get("app_id"), feishu_config.get("app_secret"),
feishu_config.get("app_token"), feishu_config.get("table_id")]):
raise Exception("飞书配置不完整,请先配置飞书应用信息")
feishu_client = FeishuClient(feishu_config["app_id"], feishu_config["app_secret"])
sync_service = WorkOrderSyncService(feishu_client, feishu_config["app_token"], feishu_config["table_id"])
return sync_service
@feishu_sync_bp.route('/config', methods=['GET', 'POST'])
def manage_config():
"""管理飞书同步配置"""
if request.method == 'GET':
# 返回当前配置
try:
config_summary = config_manager.get_config_summary()
return jsonify({
"success": True,
"config": config_summary
})
except Exception as e:
logger.error(f"获取配置失败: {e}")
return jsonify({"error": str(e)}), 500
elif request.method == 'POST':
# 更新配置
try:
data = request.get_json()
app_id = data.get('app_id')
app_secret = data.get('app_secret')
app_token = data.get('app_token')
table_id = data.get('table_id')
if not all([app_id, app_secret, app_token, table_id]):
return jsonify({"error": "缺少必要配置参数"}), 400
# 更新配置管理器
success = config_manager.update_feishu_config(
app_id=app_id,
app_secret=app_secret,
app_token=app_token,
table_id=table_id
)
if success:
# 重新初始化同步服务
global sync_service
sync_service = None # 强制重新创建
return jsonify({
"success": True,
"message": "配置更新成功"
})
else:
return jsonify({"error": "配置更新失败"}), 500
except Exception as e:
logger.error(f"更新飞书配置失败: {e}")
return jsonify({"error": str(e)}), 500
@feishu_sync_bp.route('/sync-from-feishu', methods=['POST'])
def sync_from_feishu():
"""从飞书同步数据到本地"""
try:
data = request.get_json() or {}
generate_ai = data.get('generate_ai_suggestions', True)
limit = data.get('limit', 10)
sync_service = get_sync_service()
result = sync_service.sync_from_feishu(generate_ai_suggestions=generate_ai, limit=limit)
if result.get("success"):
message = f"同步完成:创建 {result['created_count']} 条,更新 {result['updated_count']}"
if result.get('ai_suggestions_generated'):
message += "AI建议已生成并更新到飞书表格"
return jsonify({
"success": True,
"message": message,
"details": result
})
else:
return jsonify({"error": result.get("error")}), 500
except Exception as e:
logger.error(f"从飞书同步失败: {e}")
return jsonify({"error": str(e)}), 500
@feishu_sync_bp.route('/sync-to-feishu/<int:workorder_id>', methods=['POST'])
def sync_to_feishu(workorder_id):
"""将本地工单同步到飞书"""
try:
sync_service = get_sync_service()
result = sync_service.sync_to_feishu(workorder_id)
if result.get("success"):
return jsonify({
"success": True,
"message": "同步到飞书成功"
})
else:
return jsonify({"error": result.get("error")}), 500
except Exception as e:
logger.error(f"同步到飞书失败: {e}")
return jsonify({"error": str(e)}), 500
@feishu_sync_bp.route('/status')
def get_sync_status():
"""获取同步状态"""
try:
sync_service = get_sync_service()
status = sync_service.get_sync_status()
return jsonify({
"success": True,
"status": status
})
except Exception as e:
logger.error(f"获取同步状态失败: {e}")
return jsonify({"error": str(e)}), 500
@feishu_sync_bp.route('/test-connection')
def test_connection():
"""测试飞书连接"""
try:
# 使用配置管理器测试连接
result = config_manager.test_feishu_connection()
if result.get("success"):
# 如果连接成功,尝试获取表格字段信息
try:
sync_service = get_sync_service()
# 使用新的测试连接方法
connection_test = sync_service.feishu_client.test_connection()
if not connection_test.get("success"):
return jsonify({
"success": False,
"message": f"飞书连接测试失败: {connection_test.get('message')}"
}), 400
fields_info = sync_service.feishu_client.get_table_fields(
sync_service.app_token, sync_service.table_id
)
if fields_info.get("code") == 0:
result["fields"] = fields_info.get("data", {}).get("items", [])
except Exception as e:
logger.warning(f"获取表格字段信息失败: {e}")
return jsonify(result)
except Exception as e:
logger.error(f"测试飞书连接失败: {e}")
return jsonify({"error": str(e)}), 500
@feishu_sync_bp.route('/create-workorder', methods=['POST'])
def create_workorder_from_feishu():
"""从飞书记录创建工单"""
try:
data = request.get_json()
record_id = data.get('record_id')
if not record_id:
return jsonify({"success": False, "message": "缺少记录ID"}), 400
sync_service = get_sync_service()
result = sync_service.create_workorder_from_feishu_record(record_id)
if result.get("success"):
return jsonify(result)
else:
return jsonify(result), 400
except Exception as e:
logger.error(f"创建工单失败: {e}")
return jsonify({"success": False, "message": str(e)}), 500
@feishu_sync_bp.route('/preview-feishu-data')
def preview_feishu_data():
"""预览飞书数据"""
try:
sync_service = get_sync_service()
# 获取前10条记录进行预览
records = sync_service.feishu_client.get_table_records(
sync_service.app_token, sync_service.table_id, page_size=10
)
if records.get("code") == 0:
items = records.get("data", {}).get("items", [])
preview_data = []
for record in items:
parsed_fields = sync_service.feishu_client.parse_record_fields(record)
preview_data.append({
"record_id": record.get("record_id"),
"fields": parsed_fields
})
return jsonify({
"success": True,
"preview_data": preview_data,
"total_count": len(preview_data)
})
else:
return jsonify({
"success": False,
"error": records.get("msg", "获取数据失败")
}), 500
except Exception as e:
logger.error(f"预览飞书数据失败: {e}")
return jsonify({"error": str(e)}), 500
@feishu_sync_bp.route('/config/export', methods=['GET'])
def export_config():
"""导出配置"""
try:
config_json = config_manager.export_config()
return jsonify({
"success": True,
"config": config_json
})
except Exception as e:
logger.error(f"导出配置失败: {e}")
return jsonify({"error": str(e)}), 500
@feishu_sync_bp.route('/config/import', methods=['POST'])
def import_config():
"""导入配置"""
try:
data = request.get_json()
config_json = data.get('config')
if not config_json:
return jsonify({"error": "缺少配置数据"}), 400
success = config_manager.import_config(config_json)
if success:
# 重新初始化同步服务
global sync_service
sync_service = None
return jsonify({
"success": True,
"message": "配置导入成功"
})
else:
return jsonify({"error": "配置导入失败"}), 500
except Exception as e:
logger.error(f"导入配置失败: {e}")
return jsonify({"error": str(e)}), 500
@feishu_sync_bp.route('/config/reset', methods=['POST'])
def reset_config():
"""重置配置"""
try:
success = config_manager.reset_config()
if success:
# 重新初始化同步服务
global sync_service
sync_service = None
return jsonify({
"success": True,
"message": "配置重置成功"
})
else:
return jsonify({"error": "配置重置失败"}), 500
except Exception as e:
logger.error(f"重置配置失败: {e}")
return jsonify({"error": str(e)}), 500

View File

@@ -306,6 +306,108 @@ def optimize_disk():
except Exception as e:
return jsonify({"error": str(e)}), 500
@system_bp.route('/system-optimizer/clear-cache', methods=['POST'])
def clear_cache():
"""清理应用缓存(内存/Redis均尝试"""
try:
cleared = False
try:
from src.core.cache_manager import cache_manager
cache_manager.clear()
cleared = True
except Exception:
pass
return jsonify({
'success': True,
'message': '缓存已清理' if cleared else '缓存清理已尝试(可能未启用缓存模块)',
'progress': 100
})
except Exception as e:
return jsonify({"error": str(e)}), 500
@system_bp.route('/system-optimizer/optimize-all', methods=['POST'])
def optimize_all():
"""一键优化CPU/内存/磁盘 + 缓存清理 + 轻量数据库维护"""
try:
import gc
import time
actions = []
start_time = time.time()
# 垃圾回收 & 缓存
try:
collected = gc.collect()
actions.append(f"垃圾回收:{collected}")
except Exception:
actions.append("垃圾回收:跳过")
try:
from src.core.cache_manager import cache_manager
cache_manager.clear()
actions.append("缓存清理:完成")
except Exception:
actions.append("缓存清理:跳过")
# 临时文件与日志清理(沿用磁盘优化逻辑的子集)
temp_files_cleaned = 0
log_files_cleaned = 0
try:
import os, tempfile
temp_dir = tempfile.gettempdir()
for filename in os.listdir(temp_dir):
if filename.startswith('tsp_') or filename.startswith('tmp_'):
file_path = os.path.join(temp_dir, filename)
try:
if os.path.isfile(file_path):
os.remove(file_path)
temp_files_cleaned += 1
except Exception:
pass
except Exception:
pass
actions.append(f"临时文件:{temp_files_cleaned}")
try:
import os, glob
from datetime import datetime, timedelta
log_dir = 'logs'
if os.path.exists(log_dir):
cutoff_date = datetime.now() - timedelta(days=7)
for log_file in glob.glob(os.path.join(log_dir, '*.log')):
try:
file_time = datetime.fromtimestamp(os.path.getmtime(log_file))
if file_time < cutoff_date:
os.remove(log_file)
log_files_cleaned += 1
except Exception:
pass
except Exception:
pass
actions.append(f"日志清理:{log_files_cleaned}")
# 轻量数据库维护尽力而为SQLite时执行VACUUM其他数据库跳过
try:
engine = db_manager.engine
if str(engine.url).startswith('sqlite'):
with engine.begin() as conn:
conn.exec_driver_sql('VACUUM')
actions.append("SQLite VACUUM:完成")
else:
actions.append("DB维护:跳过(非SQLite)")
except Exception:
actions.append("DB维护:失败")
optimization_time = round((time.time() - start_time) * 1000, 1)
return jsonify({
'success': True,
'message': '一键优化完成: ' + ''.join(actions) + f',耗时{optimization_time}ms',
'progress': 100,
'actions': actions,
'optimization_time': optimization_time
})
except Exception as e:
return jsonify({"error": str(e)}), 500
@system_bp.route('/system-optimizer/security-settings', methods=['GET', 'POST'])
def security_settings():
"""安全设置"""

View File

@@ -31,15 +31,23 @@ def _ensure_workorder_template_file() -> str:
# 确保目录存在
os.makedirs('uploads', exist_ok=True)
if not os.path.exists(template_path):
# 如果运行目录不存在模板,尝试从项目根相对路径拷贝一份
repo_template = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), 'uploads', 'workorder_template.xlsx')
repo_template = os.path.abspath(repo_template)
# 优先从项目根目录的 uploads 拷贝(仓库自带模板)
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..'))
repo_template = os.path.join(project_root, 'uploads', 'workorder_template.xlsx')
try:
if os.path.exists(repo_template):
import shutil
shutil.copyfile(repo_template, template_path)
else:
raise FileNotFoundError('模板文件缺失uploads/workorder_template.xlsx')
# 仓库模板不存在时,自动生成一个最小可用模板
try:
import pandas as pd
from pandas import DataFrame
columns = ['标题', '描述', '分类', '优先级', '状态', '解决方案', '满意度']
df: DataFrame = pd.DataFrame(columns=columns)
df.to_excel(template_path, index=False)
except Exception as gen_err:
raise FileNotFoundError('模板文件缺失且自动生成失败请检查依赖openpyxl/pandas') from gen_err
except Exception as copy_err:
raise copy_err
return template_path
@@ -199,14 +207,15 @@ def generate_workorder_ai_suggestion(workorder_id):
if not w:
return jsonify({"error": "工单不存在"}), 404
# 调用知识库搜索与LLM生成
query = f"{w.title} {w.description}"
# 使用问题描述title而不是处理过程description)作为主要查询依据
query = f"{w.title}"
kb_results = get_assistant().search_knowledge(query, top_k=3)
kb_list = kb_results.get('results', []) if isinstance(kb_results, dict) else []
# 组装提示词
context = "\n".join([f"Q: {k.get('question','')}\nA: {k.get('answer','')}" for k in kb_list])
from src.core.llm_client import QwenClient
llm = QwenClient()
prompt = f"请基于以下工单描述与知识库片段,给出简洁、可执行的处理建议。\n工单描述:\n{w.description}\n\n知识库片段:\n{context}\n\n请直接输出建议文本:"
prompt = f"请基于以下工单问题描述与知识库片段,给出简洁、可执行的处理建议。\n\n问题描述:\n{w.title}\n\n处理过程(仅供参考):\n{w.description}\n\n知识库片段:\n{context}\n\n请直接输出建议文本:"
llm_resp = llm.chat_completion(messages=[{"role":"user","content":prompt}], temperature=0.3, max_tokens=800)
suggestion = ""
if llm_resp and 'choices' in llm_resp:
@@ -404,6 +413,11 @@ def download_import_template_file():
"""直接返回工单导入模板文件(下载)"""
try:
template_path = _ensure_workorder_template_file()
return send_file(template_path, as_attachment=True, download_name='工单导入模板.xlsx')
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:
return jsonify({"error": str(e)}), 500

View File

@@ -924,6 +924,7 @@ class TSPDashboard {
</div>
<div>
<span class="badge ${success >= 80 ? 'bg-success' : success >= 50 ? 'bg-warning' : 'bg-secondary'}">${success}%</span>
<button class="btn btn-sm btn-outline-primary ms-2" data-tool="${tool.name}">执行</button>
</div>
</div>
`;
@@ -931,6 +932,39 @@ class TSPDashboard {
toolsList.innerHTML = toolsHtml;
// 绑定执行事件
toolsList.querySelectorAll('button[data-tool]').forEach(btn => {
btn.addEventListener('click', async () => {
const tool = btn.getAttribute('data-tool');
// 简单参数输入(可扩展为动态表单)
let params = {};
try {
const input = prompt('请输入执行参数(JSON)', '{}');
if (input) params = JSON.parse(input);
} catch (e) {
this.showNotification('参数格式错误应为JSON', 'warning');
return;
}
try {
const resp = await fetch('/api/agent/tools/execute', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tool, parameters: params })
});
const res = await resp.json();
if (res.success) {
this.showNotification(`工具 ${tool} 执行成功`, 'success');
await this.loadAgentData();
} else {
this.showNotification(res.error || `工具 ${tool} 执行失败`, 'error');
}
} catch (err) {
console.error('执行工具失败:', err);
this.showNotification('执行工具失败: ' + err.message, 'error');
}
});
});
// 追加自定义工具注册入口
const addDiv = document.createElement('div');
addDiv.className = 'mt-3';
@@ -1508,7 +1542,7 @@ class TSPDashboard {
<div class="d-flex justify-content-between align-items-start">
<div class="flex-grow-1">
<h6 class="mb-1">${workorder.title}</h6>
<p class="text-muted mb-2">${workorder.description}</p>
<p class="text-muted mb-2">${workorder.description ? workorder.description.substring(0, 100) + (workorder.description.length > 100 ? '...' : '') : '无处理过程'}</p>
<div class="d-flex gap-3">
<span class="badge bg-${this.getPriorityColor(workorder.priority)}">${this.getPriorityText(workorder.priority)}</span>
<span class="badge bg-${this.getStatusColor(workorder.status)}">${this.getStatusText(workorder.status)}</span>
@@ -1656,8 +1690,14 @@ class TSPDashboard {
<div class="col-md-6">
<h6>问题描述</h6>
<div class="border p-3 rounded">
${workorder.description}
${workorder.title || '无问题描述'}
</div>
${workorder.description ? `
<h6 class="mt-3">处理过程</h6>
<div class="border p-3 rounded bg-light">
${workorder.description}
</div>
` : ''}
${workorder.resolution ? `
<h6 class="mt-3">解决方案</h6>
<div class="border p-3 rounded bg-light">
@@ -1857,7 +1897,7 @@ class TSPDashboard {
</div>
<div class="mb-3">
<label for="editDescription" class="form-label">描述 *</label>
<label for="editDescription" class="form-label">处理过程 *</label>
<textarea class="form-control" id="editDescription" rows="4" required>${workorder.description}</textarea>
</div>
@@ -2154,6 +2194,10 @@ class TSPDashboard {
}
async refreshConversationHistory() {
// 先尝试触发一次合并迁移(幂等,重复调用也安全)
try {
await fetch('/api/conversations/migrate-merge', { method: 'POST' });
} catch (e) { /* 忽略迁移失败 */ }
await this.loadConversationHistory();
this.showNotification('对话历史已刷新', 'success');
}
@@ -2206,6 +2250,7 @@ class TSPDashboard {
const data = await response.json();
if (data.success) {
data.user_id = data.user_id || '匿名';
this.showConversationModal(data);
} else {
throw new Error(data.error || '获取对话详情失败');
@@ -2760,8 +2805,11 @@ class TSPDashboard {
const data = await response.json();
if (data.success) {
this.showNotification('CPU优化完成', 'success');
this.showNotification(data.message || 'CPU优化完成', 'success');
this.updateOptimizationProgress('cpu-optimization', data.progress || 100);
// 刷新状态并回落进度条
await this.loadSystemOptimizer();
setTimeout(() => this.updateOptimizationProgress('cpu-optimization', 0), 1500);
} else {
throw new Error(data.error || 'CPU优化失败');
}
@@ -2777,8 +2825,10 @@ class TSPDashboard {
const data = await response.json();
if (data.success) {
this.showNotification('内存优化完成', 'success');
this.showNotification(data.message || '内存优化完成', 'success');
this.updateOptimizationProgress('memory-optimization', data.progress || 100);
await this.loadSystemOptimizer();
setTimeout(() => this.updateOptimizationProgress('memory-optimization', 0), 1500);
} else {
throw new Error(data.error || '内存优化失败');
}
@@ -2794,8 +2844,10 @@ class TSPDashboard {
const data = await response.json();
if (data.success) {
this.showNotification('磁盘优化完成', 'success');
this.showNotification(data.message || '磁盘优化完成', 'success');
this.updateOptimizationProgress('disk-optimization', data.progress || 100);
await this.loadSystemOptimizer();
setTimeout(() => this.updateOptimizationProgress('disk-optimization', 0), 1500);
} else {
throw new Error(data.error || '磁盘优化失败');
}
@@ -2916,6 +2968,40 @@ class TSPDashboard {
this.showNotification('系统状态已刷新', 'success');
}
async clearCache() {
try {
const response = await fetch('/api/system-optimizer/clear-cache', { method: 'POST' });
const data = await response.json();
if (data.success) {
this.showNotification(data.message || '缓存已清理', 'success');
await this.loadSystemOptimizer();
} else {
throw new Error(data.error || '清理缓存失败');
}
} catch (error) {
console.error('清理缓存失败:', error);
this.showNotification('清理缓存失败: ' + error.message, 'error');
}
}
async optimizeAll() {
try {
const response = await fetch('/api/system-optimizer/optimize-all', { method: 'POST' });
const data = await response.json();
if (data.success) {
this.showNotification(data.message || '一键优化完成', 'success');
await this.loadSystemOptimizer();
['cpu-optimization','memory-optimization','disk-optimization'].forEach(id => this.updateOptimizationProgress(id, 100));
setTimeout(() => ['cpu-optimization','memory-optimization','disk-optimization'].forEach(id => this.updateOptimizationProgress(id, 0)), 1500);
} else {
throw new Error(data.error || '一键优化失败');
}
} catch (error) {
console.error('一键优化失败:', error);
this.showNotification('一键优化失败: ' + error.message, 'error');
}
}
async loadSecuritySettings() {
try {
const response = await fetch('/api/system-optimizer/security-settings');

View File

@@ -450,25 +450,25 @@
<!-- 仪表板标签页 -->
<div id="dashboard-tab" class="tab-content">
<div class="row mb-4">
<div class="col-md-3">
<div class="col-6 col-md-3">
<div class="stat-card success">
<div class="stat-number" id="total-sessions">0</div>
<div class="stat-label">活跃会话</div>
</div>
</div>
<div class="col-md-3">
<div class="col-6 col-md-3">
<div class="stat-card warning">
<div class="stat-number" id="total-alerts">0</div>
<div class="stat-label">活跃预警</div>
</div>
</div>
<div class="col-md-3">
<div class="col-6 col-md-3">
<div class="stat-card danger">
<div class="stat-number" id="total-workorders">0</div>
<div class="stat-label">待处理工单</div>
</div>
</div>
<div class="col-md-3">
<div class="col-6 col-md-3">
<div class="stat-card info">
<div class="stat-number" id="knowledge-count">0</div>
<div class="stat-label">知识条目</div>
@@ -1380,25 +1380,25 @@
<!-- 系统优化标签页 -->
<div id="system-optimizer-tab" class="tab-content" style="display: none;">
<div class="row mb-4">
<div class="col-md-3">
<div class="col-6 col-md-3">
<div class="stat-card success">
<div class="stat-number" id="cpu-usage">0%</div>
<div class="stat-label">CPU使用率</div>
</div>
</div>
<div class="col-md-3">
<div class="col-6 col-md-3">
<div class="stat-card warning">
<div class="stat-number" id="memory-usage-percent">0%</div>
<div class="stat-label">内存使用率</div>
</div>
</div>
<div class="col-md-3">
<div class="col-6 col-md-3">
<div class="stat-card danger">
<div class="stat-number" id="disk-usage">0%</div>
<div class="stat-label">磁盘使用率</div>
</div>
</div>
<div class="col-md-3">
<div class="col-6 col-md-3">
<div class="stat-card info">
<div class="stat-number" id="network-latency">0ms</div>
<div class="stat-label">网络延迟</div>
@@ -1440,6 +1440,14 @@
<i class="fas fa-hdd me-1"></i>优化磁盘
</button>
</div>
<div class="mt-3 d-flex gap-2">
<button class="btn btn-success btn-sm" onclick="dashboard.optimizeAll()">
<i class="fas fa-magic me-1"></i>一键优化
</button>
<button class="btn btn-outline-secondary btn-sm" onclick="dashboard.clearCache()">
<i class="fas fa-broom me-1"></i>清理缓存
</button>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,662 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>飞书同步管理 - TSP助手</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<link href="/static/css/style.css" rel="stylesheet">
</head>
<body>
<div class="container-fluid">
<div class="row">
<!-- 侧边栏 -->
<nav class="col-md-3 col-lg-2 d-md-block bg-light sidebar">
<div class="position-sticky pt-3">
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link" href="/">
<i class="fas fa-tachometer-alt me-2"></i>仪表板
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/alerts">
<i class="fas fa-exclamation-triangle me-2"></i>预警管理
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/workorders">
<i class="fas fa-tasks me-2"></i>工单管理
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/conversations">
<i class="fas fa-comments me-2"></i>对话历史
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/knowledge">
<i class="fas fa-book me-2"></i>知识库
</a>
</li>
<li class="nav-item">
<a class="nav-link active" href="/feishu-sync">
<i class="fas fa-sync me-2"></i>飞书同步
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/monitoring">
<i class="fas fa-chart-line me-2"></i>系统监控
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/settings">
<i class="fas fa-cog me-2"></i>系统设置
</a>
</li>
</ul>
</div>
</nav>
<!-- 主内容区 -->
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4">
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2">
<i class="fas fa-sync me-2"></i>飞书同步管理
</h1>
</div>
<!-- 配置区域 -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-cog me-2"></i>飞书配置
</h5>
</div>
<div class="card-body">
<form id="feishuConfigForm">
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="appId" class="form-label">应用ID</label>
<input type="text" class="form-control" id="appId" placeholder="cli_xxxxxxxxxx">
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="appSecret" class="form-label">应用密钥</label>
<input type="password" class="form-control" id="appSecret" placeholder="输入应用密钥">
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="appToken" class="form-label">应用Token</label>
<input type="text" class="form-control" id="appToken" placeholder="XXnEbiCmEaMblSs6FDJcFCqsnIg">
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="tableId" class="form-label">表格ID</label>
<input type="text" class="form-control" id="tableId" placeholder="tblnl3vJPpgMTSiP">
</div>
</div>
</div>
<div class="d-flex gap-2 flex-wrap">
<button type="button" class="btn btn-primary" onclick="feishuSync.saveConfig()">
<i class="fas fa-save me-1"></i>保存配置
</button>
<button type="button" class="btn btn-outline-secondary" onclick="feishuSync.testConnection()">
<i class="fas fa-plug me-1"></i>测试连接
</button>
<button type="button" class="btn btn-outline-info" onclick="feishuSync.exportConfig()">
<i class="fas fa-download me-1"></i>导出配置
</button>
<button type="button" class="btn btn-outline-warning" onclick="feishuSync.showImportModal()">
<i class="fas fa-upload me-1"></i>导入配置
</button>
<button type="button" class="btn btn-outline-danger" onclick="feishuSync.resetConfig()">
<i class="fas fa-undo me-1"></i>重置配置
</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- 同步状态 -->
<div class="row mb-4">
<div class="col-md-4">
<div class="card text-center">
<div class="card-body">
<h5 class="card-title text-primary" id="totalLocalWorkorders">0</h5>
<p class="card-text">本地工单总数</p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card text-center">
<div class="card-body">
<h5 class="card-title text-success" id="syncedWorkorders">0</h5>
<p class="card-text">已同步工单</p>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card text-center">
<div class="card-body">
<h5 class="card-title text-warning" id="unsyncedWorkorders">0</h5>
<p class="card-text">未同步工单</p>
</div>
</div>
</div>
</div>
<!-- 同步操作 -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-sync-alt me-2"></i>同步操作
</h5>
</div>
<div class="card-body">
<div class="d-flex gap-2 mb-3">
<button class="btn btn-success" onclick="feishuSync.syncFromFeishu()">
<i class="fas fa-download me-1"></i>从飞书同步
</button>
<button class="btn btn-primary" onclick="feishuSync.syncWithAI()">
<i class="fas fa-robot me-1"></i>同步+AI建议
</button>
<button class="btn btn-info" onclick="feishuSync.previewFeishuData()">
<i class="fas fa-eye me-1"></i>预览飞书数据
</button>
<button class="btn btn-secondary" onclick="feishuSync.refreshStatus()">
<i class="fas fa-refresh me-1"></i>刷新状态
</button>
</div>
<div class="mb-3">
<label for="syncLimit" class="form-label">同步数量限制:</label>
<select class="form-select" id="syncLimit" style="width: auto; display: inline-block;">
<option value="10">前10条</option>
<option value="20">前20条</option>
<option value="50">前50条</option>
<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"
role="progressbar" style="width: 0%"></div>
</div>
<!-- 同步日志 -->
<div class="mt-3">
<h6>同步日志</h6>
<div id="syncLog" class="bg-light p-3 rounded" style="max-height: 300px; overflow-y: auto;">
<div class="text-muted">暂无同步记录</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 预览数据 -->
<div class="row" id="previewSection" style="display: none;">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-table me-2"></i>飞书数据预览
</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped" id="previewTable">
<thead>
<tr>
<th>记录ID</th>
<th>TR编号</th>
<th>TR描述</th>
<th>问题类型</th>
<th>来源</th>
<th>优先级/状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</main>
</div>
</div>
<!-- 导入配置模态框 -->
<div class="modal fade" id="importConfigModal" tabindex="-1" aria-labelledby="importConfigModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="importConfigModalLabel">导入配置</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="configJson" class="form-label">配置JSON数据:</label>
<textarea class="form-control" id="configJson" rows="10" placeholder="粘贴导出的配置JSON数据..."></textarea>
</div>
<div class="alert alert-warning">
<i class="fas fa-exclamation-triangle me-2"></i>
导入配置将覆盖当前所有配置,请谨慎操作!
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" onclick="feishuSync.importConfig()">导入</button>
</div>
</div>
</div>
</div>
<!-- 通知容器 -->
<div id="notificationContainer" class="position-fixed top-0 end-0 p-3" style="z-index: 1050;"></div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
class FeishuSyncManager {
constructor() {
this.loadConfig();
this.refreshStatus();
}
async loadConfig() {
try {
const response = await fetch('/api/feishu-sync/config');
const data = await response.json();
if (data.success) {
const config = data.config;
document.getElementById('appId').value = config.feishu.app_id || '';
document.getElementById('appSecret').value = '';
document.getElementById('appToken').value = config.feishu.app_token || '';
document.getElementById('tableId').value = config.feishu.table_id || '';
// 显示配置状态
const statusBadge = config.feishu.status === 'active' ?
'<span class="badge bg-success">已配置</span>' :
'<span class="badge bg-warning">未配置</span>';
// 可以在这里添加状态显示
}
} catch (error) {
console.error('加载配置失败:', error);
}
}
async saveConfig() {
const config = {
app_id: document.getElementById('appId').value,
app_secret: document.getElementById('appSecret').value,
app_token: document.getElementById('appToken').value,
table_id: document.getElementById('tableId').value
};
if (!config.app_id || !config.app_secret || !config.app_token || !config.table_id) {
this.showNotification('请填写完整的配置信息', 'error');
return;
}
try {
const response = await fetch('/api/feishu-sync/config', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(config)
});
const data = await response.json();
if (data.success) {
this.showNotification('配置保存成功', 'success');
} else {
this.showNotification('配置保存失败: ' + data.error, 'error');
}
} catch (error) {
this.showNotification('配置保存失败: ' + error.message, 'error');
}
}
async testConnection() {
try {
this.showNotification('正在测试连接...', 'info');
const response = await fetch('/api/feishu-sync/test-connection');
const data = await response.json();
if (data.success) {
this.showNotification('飞书连接正常', 'success');
} else {
this.showNotification('连接失败: ' + data.error, 'error');
}
} catch (error) {
this.showNotification('连接测试失败: ' + error.message, 'error');
}
}
async syncFromFeishu() {
try {
const limit = document.getElementById('syncLimit').value;
this.showNotification('开始从飞书同步数据...', 'info');
this.showProgress(true);
const response = await fetch('/api/feishu-sync/sync-from-feishu', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
generate_ai_suggestions: false,
limit: parseInt(limit)
})
});
const data = await response.json();
if (data.success) {
this.showNotification(data.message, 'success');
this.addSyncLog(data.message);
this.refreshStatus();
} else {
this.showNotification('同步失败: ' + data.error, 'error');
this.addSyncLog('同步失败: ' + data.error);
}
} catch (error) {
this.showNotification('同步失败: ' + error.message, 'error');
this.addSyncLog('同步失败: ' + error.message);
} finally {
this.showProgress(false);
}
}
async syncWithAI() {
try {
const limit = document.getElementById('syncLimit').value;
this.showNotification('开始同步数据并生成AI建议...', 'info');
this.showProgress(true);
const response = await fetch('/api/feishu-sync/sync-from-feishu', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
generate_ai_suggestions: true,
limit: parseInt(limit)
})
});
const data = await response.json();
if (data.success) {
this.showNotification(data.message, 'success');
this.addSyncLog(data.message);
this.refreshStatus();
} else {
this.showNotification('同步失败: ' + data.error, 'error');
this.addSyncLog('同步失败: ' + data.error);
}
} catch (error) {
this.showNotification('同步失败: ' + error.message, 'error');
this.addSyncLog('同步失败: ' + error.message);
} finally {
this.showProgress(false);
}
}
async previewFeishuData() {
try {
this.showNotification('正在获取飞书数据预览...', 'info');
const response = await fetch('/api/feishu-sync/preview-feishu-data');
const data = await response.json();
if (data.success) {
this.displayPreviewData(data.preview_data);
this.showNotification(`获取到 ${data.total_count} 条预览数据`, 'success');
} else {
this.showNotification('获取预览数据失败: ' + data.error, 'error');
}
} catch (error) {
this.showNotification('获取预览数据失败: ' + error.message, 'error');
}
}
displayPreviewData(data) {
const tbody = document.querySelector('#previewTable tbody');
tbody.innerHTML = '';
data.forEach(item => {
const row = document.createElement('tr');
row.innerHTML = `
<td>${item.record_id}</td>
<td>${item.fields['TR Number'] || '-'}</td>
<td>${item.fields['TR Description'] || '-'}</td>
<td>${item.fields['Type of problem'] || '-'}</td>
<td>${item.fields['Source'] || '-'}</td>
<td>${item.fields['TR (Priority/Status)'] || '-'}</td>
<td>
<button class="btn btn-sm btn-primary" onclick="feishuSync.createWorkorder('${item.record_id}')">
<i class="fas fa-plus"></i> 创建工单
</button>
</td>
`;
tbody.appendChild(row);
});
document.getElementById('previewSection').style.display = 'block';
}
async refreshStatus() {
try {
const response = await fetch('/api/feishu-sync/status');
const data = await response.json();
if (data.success) {
const status = data.status;
document.getElementById('totalLocalWorkorders').textContent = status.total_local_workorders || 0;
document.getElementById('syncedWorkorders').textContent = status.synced_workorders || 0;
document.getElementById('unsyncedWorkorders').textContent = status.unsynced_workorders || 0;
}
} catch (error) {
console.error('刷新状态失败:', error);
}
}
showProgress(show) {
const progress = document.getElementById('syncProgress');
if (show) {
progress.style.display = 'block';
const bar = progress.querySelector('.progress-bar');
bar.style.width = '100%';
} else {
setTimeout(() => {
progress.style.display = 'none';
const bar = progress.querySelector('.progress-bar');
bar.style.width = '0%';
}, 1000);
}
}
addSyncLog(message) {
const log = document.getElementById('syncLog');
const timestamp = new Date().toLocaleString();
const logEntry = document.createElement('div');
logEntry.innerHTML = `<small class="text-muted">[${timestamp}]</small> ${message}`;
if (log.querySelector('.text-muted')) {
log.innerHTML = '';
}
log.appendChild(logEntry);
log.scrollTop = log.scrollHeight;
}
async exportConfig() {
try {
const response = await fetch('/api/feishu-sync/config/export');
const data = await response.json();
if (data.success) {
// 创建下载链接
const blob = new Blob([data.config], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `feishu_config_${new Date().toISOString().split('T')[0]}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
this.showNotification('配置导出成功', 'success');
} else {
this.showNotification('配置导出失败: ' + data.error, 'error');
}
} catch (error) {
this.showNotification('配置导出失败: ' + error.message, 'error');
}
}
showImportModal() {
const modal = new bootstrap.Modal(document.getElementById('importConfigModal'));
modal.show();
}
async importConfig() {
try {
const configJson = document.getElementById('configJson').value.trim();
if (!configJson) {
this.showNotification('请输入配置JSON数据', 'warning');
return;
}
const response = await fetch('/api/feishu-sync/config/import', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ config: configJson })
});
const data = await response.json();
if (data.success) {
this.showNotification('配置导入成功', 'success');
this.loadConfig();
this.refreshStatus();
// 关闭模态框
const modal = bootstrap.Modal.getInstance(document.getElementById('importConfigModal'));
modal.hide();
document.getElementById('configJson').value = '';
} else {
this.showNotification('配置导入失败: ' + data.error, 'error');
}
} catch (error) {
this.showNotification('配置导入失败: ' + error.message, 'error');
}
}
async resetConfig() {
if (confirm('确定要重置所有配置吗?此操作不可撤销!')) {
try {
const response = await fetch('/api/feishu-sync/config/reset', {
method: 'POST'
});
const data = await response.json();
if (data.success) {
this.showNotification('配置重置成功', 'success');
this.loadConfig();
this.refreshStatus();
} else {
this.showNotification('配置重置失败: ' + data.error, 'error');
}
} catch (error) {
this.showNotification('配置重置失败: ' + error.message, 'error');
}
}
}
async createWorkorder(recordId) {
if (confirm(`确定要从飞书记录 ${recordId} 创建工单吗?`)) {
try {
this.showNotification('正在创建工单...', 'info');
const response = await fetch('/api/feishu-sync/create-workorder', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
record_id: recordId
})
});
const data = await response.json();
if (data.success) {
this.showNotification(data.message, 'success');
// 刷新工单列表(如果用户在工单页面)
if (typeof window.refreshWorkOrders === 'function') {
window.refreshWorkOrders();
}
} else {
this.showNotification('创建工单失败: ' + data.message, 'error');
}
} catch (error) {
this.showNotification('创建工单失败: ' + error.message, 'error');
}
}
}
showNotification(message, type = 'info') {
const container = document.getElementById('notificationContainer');
const alert = document.createElement('div');
alert.className = `alert alert-${type === 'error' ? 'danger' : type} alert-dismissible fade show`;
alert.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
container.appendChild(alert);
setTimeout(() => {
if (alert.parentNode) {
alert.parentNode.removeChild(alert);
}
}, 5000);
}
}
// 初始化
const feishuSync = new FeishuSyncManager();
</script>
</body>
</html>