refactor: 清理不需要的代码文件,添加.gitignore,优化项目结构
This commit is contained in:
713
src/web/app.py
713
src/web/app.py
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
TSP助手预警管理Web应用
|
||||
@@ -8,6 +8,7 @@ TSP助手预警管理Web应用
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
import pandas as pd
|
||||
from datetime import datetime, timedelta
|
||||
from openpyxl import Workbook
|
||||
@@ -24,10 +25,25 @@ from src.agent_assistant import TSPAgentAssistant
|
||||
from src.analytics.alert_system import AlertRule, AlertLevel, AlertType
|
||||
from src.dialogue.realtime_chat import RealtimeChatManager
|
||||
from src.vehicle.vehicle_data_manager import VehicleDataManager
|
||||
from src.core.database import db_manager
|
||||
from src.core.models import WorkOrder, Alert, Conversation, KnowledgeEntry, WorkOrderSuggestion
|
||||
|
||||
app = Flask(__name__)
|
||||
CORS(app)
|
||||
|
||||
# 抑制 /api/health 的访问日志
|
||||
werkzeug_logger = logging.getLogger('werkzeug')
|
||||
|
||||
class HealthLogFilter(logging.Filter):
|
||||
def filter(self, record):
|
||||
try:
|
||||
msg = record.getMessage()
|
||||
return '/api/health' not in msg
|
||||
except Exception:
|
||||
return True
|
||||
|
||||
werkzeug_logger.addFilter(HealthLogFilter())
|
||||
|
||||
# 配置上传文件夹
|
||||
UPLOAD_FOLDER = 'uploads'
|
||||
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
|
||||
@@ -39,6 +55,26 @@ agent_assistant = TSPAgentAssistant()
|
||||
chat_manager = RealtimeChatManager()
|
||||
vehicle_manager = VehicleDataManager()
|
||||
|
||||
# 工具函数:确保工单模板文件存在
|
||||
def _ensure_workorder_template_file() -> str:
|
||||
"""返回已有的模板xlsx路径;不做动态生成,避免运行时依赖问题"""
|
||||
template_path = os.path.join(app.config['UPLOAD_FOLDER'], 'workorder_template.xlsx')
|
||||
# 确保目录存在
|
||||
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
|
||||
if not os.path.exists(template_path):
|
||||
# 如果运行目录不存在模板,尝试从项目根相对路径拷贝一次
|
||||
repo_template = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), '..', 'uploads', 'workorder_template.xlsx')
|
||||
repo_template = os.path.abspath(repo_template)
|
||||
try:
|
||||
if os.path.exists(repo_template):
|
||||
import shutil
|
||||
shutil.copyfile(repo_template, template_path)
|
||||
else:
|
||||
raise FileNotFoundError('模板文件缺失:uploads/workorder_template.xlsx')
|
||||
except Exception as copy_err:
|
||||
raise copy_err
|
||||
return template_path
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
"""主页 - 综合管理平台"""
|
||||
@@ -51,10 +87,28 @@ def alerts():
|
||||
|
||||
@app.route('/api/health')
|
||||
def get_health():
|
||||
"""获取系统健康状态"""
|
||||
"""获取系统健康状态(附加近1小时业务指标)"""
|
||||
try:
|
||||
health = assistant.get_system_health()
|
||||
return jsonify(health)
|
||||
base = assistant.get_system_health() or {}
|
||||
# 追加数据库近1小时指标
|
||||
from datetime import datetime, timedelta
|
||||
with db_manager.get_session() as session:
|
||||
since = datetime.now() - timedelta(hours=1)
|
||||
conv_count = session.query(Conversation).filter(Conversation.timestamp >= since).count()
|
||||
resp_times = [c.response_time for c in session.query(Conversation).filter(Conversation.timestamp >= since).all() if c.response_time]
|
||||
avg_resp = round(sum(resp_times)/len(resp_times), 2) if resp_times else 0
|
||||
open_wos = session.query(WorkOrder).filter(WorkOrder.status == 'open').count()
|
||||
levels = session.query(Alert.level).filter(Alert.is_active == True).all()
|
||||
level_map = {}
|
||||
for (lvl,) in levels:
|
||||
level_map[lvl] = level_map.get(lvl, 0) + 1
|
||||
base.update({
|
||||
"throughput_1h": conv_count,
|
||||
"avg_response_time_1h": avg_resp,
|
||||
"open_workorders": open_wos,
|
||||
"active_alerts_by_level": level_map
|
||||
})
|
||||
return jsonify(base)
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@@ -571,94 +625,34 @@ def unverify_knowledge(knowledge_id):
|
||||
# 工单相关API
|
||||
@app.route('/api/workorders')
|
||||
def get_workorders():
|
||||
"""获取工单列表"""
|
||||
"""获取工单列表(来自数据库)"""
|
||||
try:
|
||||
status_filter = request.args.get('status')
|
||||
priority_filter = request.args.get('priority')
|
||||
|
||||
# 这里应该调用工单管理器的获取方法
|
||||
workorders = [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "车辆无法远程启动",
|
||||
"description": "用户反映APP中远程启动功能无法使用",
|
||||
"category": "远程控制",
|
||||
"priority": "high",
|
||||
"status": "open",
|
||||
"created_at": "2024-01-01T10:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"title": "APP显示异常",
|
||||
"description": "APP中车辆信息显示不正确",
|
||||
"category": "APP功能",
|
||||
"priority": "medium",
|
||||
"status": "in_progress",
|
||||
"created_at": "2024-01-01T11:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"title": "蓝牙连接失败",
|
||||
"description": "用户无法通过蓝牙连接车辆",
|
||||
"category": "蓝牙功能",
|
||||
"priority": "high",
|
||||
"status": "open",
|
||||
"created_at": "2024-01-01T12:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"title": "车辆定位不准确",
|
||||
"description": "APP中显示的车辆位置与实际位置不符",
|
||||
"category": "定位功能",
|
||||
"priority": "medium",
|
||||
"status": "resolved",
|
||||
"created_at": "2024-01-01T13:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"title": "远程解锁失败",
|
||||
"description": "用户无法通过APP远程解锁车辆",
|
||||
"category": "远程控制",
|
||||
"priority": "urgent",
|
||||
"status": "open",
|
||||
"created_at": "2024-01-01T14:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"title": "APP闪退问题",
|
||||
"description": "用户反映APP在使用过程中频繁闪退",
|
||||
"category": "APP功能",
|
||||
"priority": "high",
|
||||
"status": "in_progress",
|
||||
"created_at": "2024-01-01T15:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"title": "车辆状态更新延迟",
|
||||
"description": "车辆状态信息更新不及时,存在延迟",
|
||||
"category": "数据同步",
|
||||
"priority": "low",
|
||||
"status": "open",
|
||||
"created_at": "2024-01-01T16:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"title": "用户认证失败",
|
||||
"description": "部分用户无法正常登录APP",
|
||||
"category": "用户认证",
|
||||
"priority": "high",
|
||||
"status": "resolved",
|
||||
"created_at": "2024-01-01T17:00:00Z"
|
||||
}
|
||||
]
|
||||
|
||||
# 应用过滤
|
||||
with db_manager.get_session() as session:
|
||||
q = session.query(WorkOrder)
|
||||
if status_filter and status_filter != 'all':
|
||||
workorders = [w for w in workorders if w['status'] == status_filter]
|
||||
q = q.filter(WorkOrder.status == status_filter)
|
||||
if priority_filter and priority_filter != 'all':
|
||||
workorders = [w for w in workorders if w['priority'] == priority_filter]
|
||||
|
||||
return jsonify(workorders)
|
||||
q = q.filter(WorkOrder.priority == priority_filter)
|
||||
q = q.order_by(WorkOrder.created_at.desc())
|
||||
rows = q.all()
|
||||
result = []
|
||||
for w in rows:
|
||||
result.append({
|
||||
"id": w.id,
|
||||
"order_id": w.order_id,
|
||||
"title": w.title,
|
||||
"description": w.description,
|
||||
"category": w.category,
|
||||
"priority": w.priority,
|
||||
"status": w.status,
|
||||
"created_at": w.created_at.isoformat() if w.created_at else None,
|
||||
"updated_at": w.updated_at.isoformat() if w.updated_at else None,
|
||||
"resolution": w.resolution,
|
||||
"satisfaction_score": w.satisfaction_score
|
||||
})
|
||||
return jsonify(result)
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@@ -679,36 +673,34 @@ def create_workorder():
|
||||
|
||||
@app.route('/api/workorders/<int:workorder_id>')
|
||||
def get_workorder_details(workorder_id):
|
||||
"""获取工单详情"""
|
||||
"""获取工单详情(含数据库对话记录)"""
|
||||
try:
|
||||
# 这里应该从数据库获取工单详情
|
||||
# 暂时返回模拟数据
|
||||
with db_manager.get_session() as session:
|
||||
w = session.query(WorkOrder).filter(WorkOrder.id == workorder_id).first()
|
||||
if not w:
|
||||
return jsonify({"error": "工单不存在"}), 404
|
||||
convs = session.query(Conversation).filter(Conversation.work_order_id == w.id).order_by(Conversation.timestamp.asc()).all()
|
||||
conv_list = []
|
||||
for c in convs:
|
||||
conv_list.append({
|
||||
"id": c.id,
|
||||
"user_message": c.user_message,
|
||||
"assistant_response": c.assistant_response,
|
||||
"timestamp": c.timestamp.isoformat() if c.timestamp else None
|
||||
})
|
||||
workorder = {
|
||||
"id": workorder_id,
|
||||
"order_id": f"WO{workorder_id:06d}",
|
||||
"title": "车辆无法远程启动",
|
||||
"description": "用户反映APP中远程启动功能无法使用,点击启动按钮后没有任何反应,车辆也没有响应。",
|
||||
"category": "远程控制",
|
||||
"priority": "high",
|
||||
"status": "open",
|
||||
"created_at": "2024-01-01T10:00:00Z",
|
||||
"updated_at": "2024-01-01T10:00:00Z",
|
||||
"resolution": None,
|
||||
"satisfaction_score": None,
|
||||
"conversations": [
|
||||
{
|
||||
"id": 1,
|
||||
"user_message": "我的车辆无法远程启动",
|
||||
"assistant_response": "我了解您的问题。让我帮您排查一下远程启动功能的问题。",
|
||||
"timestamp": "2024-01-01T10:05:00Z"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"user_message": "点击启动按钮后没有任何反应",
|
||||
"assistant_response": "这种情况通常是由于网络连接或车辆状态问题导致的。请检查车辆是否处于可启动状态。",
|
||||
"timestamp": "2024-01-01T10:10:00Z"
|
||||
}
|
||||
]
|
||||
"id": w.id,
|
||||
"order_id": w.order_id,
|
||||
"title": w.title,
|
||||
"description": w.description,
|
||||
"category": w.category,
|
||||
"priority": w.priority,
|
||||
"status": w.status,
|
||||
"created_at": w.created_at.isoformat() if w.created_at else None,
|
||||
"updated_at": w.updated_at.isoformat() if w.updated_at else None,
|
||||
"resolution": w.resolution,
|
||||
"satisfaction_score": w.satisfaction_score,
|
||||
"conversations": conv_list
|
||||
}
|
||||
return jsonify(workorder)
|
||||
except Exception as e:
|
||||
@@ -716,33 +708,135 @@ def get_workorder_details(workorder_id):
|
||||
|
||||
@app.route('/api/workorders/<int:workorder_id>', methods=['PUT'])
|
||||
def update_workorder(workorder_id):
|
||||
"""更新工单"""
|
||||
"""更新工单(写入数据库)"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
|
||||
# 验证必填字段
|
||||
if not data.get('title') or not data.get('description'):
|
||||
return jsonify({"error": "标题和描述不能为空"}), 400
|
||||
|
||||
# 这里应该更新数据库中的工单
|
||||
# 暂时返回成功响应,实际应用中应该调用数据库更新
|
||||
updated_workorder = {
|
||||
"id": workorder_id,
|
||||
"title": data.get('title'),
|
||||
"description": data.get('description'),
|
||||
"category": data.get('category', '技术问题'),
|
||||
"priority": data.get('priority', 'medium'),
|
||||
"status": data.get('status', 'open'),
|
||||
"resolution": data.get('resolution'),
|
||||
"satisfaction_score": data.get('satisfaction_score'),
|
||||
"updated_at": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"message": "工单更新成功",
|
||||
"workorder": updated_workorder
|
||||
})
|
||||
with db_manager.get_session() as session:
|
||||
w = session.query(WorkOrder).filter(WorkOrder.id == workorder_id).first()
|
||||
if not w:
|
||||
return jsonify({"error": "工单不存在"}), 404
|
||||
w.title = data.get('title', w.title)
|
||||
w.description = data.get('description', w.description)
|
||||
w.category = data.get('category', w.category)
|
||||
w.priority = data.get('priority', w.priority)
|
||||
w.status = data.get('status', w.status)
|
||||
w.resolution = data.get('resolution', w.resolution)
|
||||
w.satisfaction_score = data.get('satisfaction_score', w.satisfaction_score)
|
||||
w.updated_at = datetime.now()
|
||||
session.commit()
|
||||
updated = {
|
||||
"id": w.id,
|
||||
"title": w.title,
|
||||
"description": w.description,
|
||||
"category": w.category,
|
||||
"priority": w.priority,
|
||||
"status": w.status,
|
||||
"resolution": w.resolution,
|
||||
"satisfaction_score": w.satisfaction_score,
|
||||
"updated_at": w.updated_at.isoformat() if w.updated_at else None
|
||||
}
|
||||
return jsonify({"success": True, "message": "工单更新成功", "workorder": updated})
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
# 工单AI建议:生成、保存人工描述、审批入库
|
||||
@app.route('/api/workorders/<int:workorder_id>/ai-suggestion', methods=['POST'])
|
||||
def generate_workorder_ai_suggestion(workorder_id):
|
||||
"""根据工单描述与知识库生成AI建议草稿"""
|
||||
try:
|
||||
with db_manager.get_session() as session:
|
||||
w = session.query(WorkOrder).filter(WorkOrder.id == workorder_id).first()
|
||||
if not w:
|
||||
return jsonify({"error": "工单不存在"}), 404
|
||||
# 调用知识库搜索与LLM生成
|
||||
query = f"{w.title} {w.description}"
|
||||
kb_results = 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请直接输出建议文本:"
|
||||
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:
|
||||
suggestion = llm_resp['choices'][0]['message']['content']
|
||||
# 保存/更新草稿记录
|
||||
rec = session.query(WorkOrderSuggestion).filter(WorkOrderSuggestion.work_order_id == w.id).first()
|
||||
if not rec:
|
||||
rec = WorkOrderSuggestion(work_order_id=w.id, ai_suggestion=suggestion)
|
||||
session.add(rec)
|
||||
else:
|
||||
rec.ai_suggestion = suggestion
|
||||
rec.updated_at = datetime.now()
|
||||
session.commit()
|
||||
return jsonify({"success": True, "ai_suggestion": suggestion})
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route('/api/workorders/<int:workorder_id>/human-resolution', methods=['POST'])
|
||||
def save_workorder_human_resolution(workorder_id):
|
||||
"""保存人工描述,并计算与AI建议相似度;若≥95%可自动审批入库"""
|
||||
try:
|
||||
data = request.get_json() or {}
|
||||
human_text = data.get('human_resolution','').strip()
|
||||
if not human_text:
|
||||
return jsonify({"error":"人工描述不能为空"}), 400
|
||||
with db_manager.get_session() as session:
|
||||
w = session.query(WorkOrder).filter(WorkOrder.id == workorder_id).first()
|
||||
if not w:
|
||||
return jsonify({"error": "工单不存在"}), 404
|
||||
rec = session.query(WorkOrderSuggestion).filter(WorkOrderSuggestion.work_order_id == w.id).first()
|
||||
if not rec:
|
||||
rec = WorkOrderSuggestion(work_order_id=w.id)
|
||||
session.add(rec)
|
||||
rec.human_resolution = human_text
|
||||
# 计算相似度(使用简单cosine TF-IDF,避免外部服务依赖)
|
||||
try:
|
||||
from sklearn.feature_extraction.text import TfidfVectorizer
|
||||
from sklearn.metrics.pairwise import cosine_similarity
|
||||
texts = [rec.ai_suggestion or "", human_text]
|
||||
vec = TfidfVectorizer(max_features=1000)
|
||||
mat = vec.fit_transform(texts)
|
||||
sim = float(cosine_similarity(mat[0:1], mat[1:2])[0][0])
|
||||
except Exception:
|
||||
sim = 0.0
|
||||
rec.ai_similarity = sim
|
||||
# 自动审批条件≥0.95
|
||||
approved = sim >= 0.95
|
||||
rec.approved = approved
|
||||
session.commit()
|
||||
return jsonify({"success": True, "similarity": sim, "approved": approved})
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route('/api/workorders/<int:workorder_id>/approve-to-knowledge', methods=['POST'])
|
||||
def approve_workorder_to_knowledge(workorder_id):
|
||||
"""将已审批的AI建议入库为知识条目"""
|
||||
try:
|
||||
with db_manager.get_session() as session:
|
||||
w = session.query(WorkOrder).filter(WorkOrder.id == workorder_id).first()
|
||||
if not w:
|
||||
return jsonify({"error": "工单不存在"}), 404
|
||||
rec = session.query(WorkOrderSuggestion).filter(WorkOrderSuggestion.work_order_id == w.id).first()
|
||||
if not rec or not rec.approved or not rec.ai_suggestion:
|
||||
return jsonify({"error": "未找到可入库的已审批AI建议"}), 400
|
||||
# 入库为知识条目(问=工单标题;答=AI建议;类目用工单分类)
|
||||
entry = KnowledgeEntry(
|
||||
question=w.title or (w.description[:20] if w.description else '工单问题'),
|
||||
answer=rec.ai_suggestion,
|
||||
category=w.category or '其他',
|
||||
confidence_score=0.95,
|
||||
is_active=True,
|
||||
is_verified=True,
|
||||
verified_by='auto_approve',
|
||||
verified_at=datetime.now()
|
||||
)
|
||||
session.add(entry)
|
||||
session.commit()
|
||||
return jsonify({"success": True, "knowledge_id": entry.id})
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@@ -751,98 +845,112 @@ def update_workorder(workorder_id):
|
||||
def get_analytics():
|
||||
"""获取分析数据"""
|
||||
try:
|
||||
time_range = request.args.get('timeRange', '30')
|
||||
# 支持多种参数名
|
||||
time_range = request.args.get('timeRange', request.args.get('days', '30'))
|
||||
dimension = request.args.get('dimension', 'workorders')
|
||||
|
||||
# 生成模拟分析数据
|
||||
analytics = generate_analytics_data(int(time_range), dimension)
|
||||
analytics = generate_db_analytics(int(time_range), dimension)
|
||||
return jsonify(analytics)
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
def generate_analytics_data(days, dimension):
|
||||
"""生成分析数据"""
|
||||
import random
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
# 生成时间序列数据
|
||||
trend_data = []
|
||||
for i in range(days):
|
||||
date = (datetime.now() - timedelta(days=days-i-1)).strftime('%Y-%m-%d')
|
||||
workorders = random.randint(5, 25)
|
||||
alerts = random.randint(0, 10)
|
||||
trend_data.append({
|
||||
'date': date,
|
||||
'workorders': workorders,
|
||||
'alerts': alerts
|
||||
})
|
||||
def generate_db_analytics(days: int, dimension: str) -> dict:
|
||||
"""基于数据库生成真实分析数据"""
|
||||
from collections import defaultdict, Counter
|
||||
end_time = datetime.now()
|
||||
start_time = end_time - timedelta(days=days-1)
|
||||
|
||||
with db_manager.get_session() as session:
|
||||
# 拉取数据
|
||||
workorders = session.query(WorkOrder).filter(WorkOrder.created_at >= start_time).all()
|
||||
alerts = session.query(Alert).filter(Alert.created_at >= start_time).all()
|
||||
conversations = session.query(Conversation).filter(Conversation.timestamp >= start_time).all()
|
||||
knowledge_entries = session.query(KnowledgeEntry).all()
|
||||
|
||||
# 趋势数据(按天)
|
||||
day_keys = [(start_time + timedelta(days=i)).strftime('%Y-%m-%d') for i in range(days)]
|
||||
wo_by_day = Counter([(wo.created_at.strftime('%Y-%m-%d') if wo.created_at else end_time.strftime('%Y-%m-%d')) for wo in workorders])
|
||||
alert_by_day = Counter([(al.created_at.strftime('%Y-%m-%d') if al.created_at else end_time.strftime('%Y-%m-%d')) for al in alerts])
|
||||
trend = [{
|
||||
'date': d,
|
||||
'workorders': int(wo_by_day.get(d, 0)),
|
||||
'alerts': int(alert_by_day.get(d, 0))
|
||||
} for d in day_keys]
|
||||
|
||||
# 工单统计
|
||||
total = len(workorders)
|
||||
status_counts = Counter([wo.status for wo in workorders])
|
||||
category_counts = Counter([wo.category for wo in workorders])
|
||||
priority_counts = Counter([wo.priority for wo in workorders])
|
||||
resolved_count = status_counts.get('resolved', 0)
|
||||
workorders_stats = {
|
||||
'total': random.randint(100, 500),
|
||||
'open': random.randint(10, 50),
|
||||
'in_progress': random.randint(5, 30),
|
||||
'resolved': random.randint(50, 200),
|
||||
'closed': random.randint(20, 100),
|
||||
'by_category': {
|
||||
'技术问题': random.randint(20, 80),
|
||||
'业务问题': random.randint(15, 60),
|
||||
'系统故障': random.randint(10, 40),
|
||||
'功能需求': random.randint(5, 30),
|
||||
'其他': random.randint(5, 20)
|
||||
},
|
||||
'by_priority': {
|
||||
'low': random.randint(20, 60),
|
||||
'medium': random.randint(30, 80),
|
||||
'high': random.randint(10, 40),
|
||||
'urgent': random.randint(5, 20)
|
||||
'total': total,
|
||||
'open': status_counts.get('open', 0),
|
||||
'in_progress': status_counts.get('in_progress', 0),
|
||||
'resolved': resolved_count,
|
||||
'closed': status_counts.get('closed', 0),
|
||||
'by_category': dict(category_counts),
|
||||
'by_priority': dict(priority_counts)
|
||||
}
|
||||
}
|
||||
|
||||
# 满意度分析
|
||||
|
||||
# 满意度
|
||||
scores = []
|
||||
for wo in workorders:
|
||||
if wo.satisfaction_score not in (None, ''):
|
||||
try:
|
||||
score = float(wo.satisfaction_score)
|
||||
scores.append(score)
|
||||
except (ValueError, TypeError):
|
||||
continue
|
||||
avg_satisfaction = round(sum(scores)/len(scores), 1) if scores else 0
|
||||
dist = Counter([str(int(round(s))) for s in scores]) if scores else {}
|
||||
satisfaction_stats = {
|
||||
'average': round(random.uniform(3.5, 4.8), 1),
|
||||
'distribution': {
|
||||
'1': random.randint(0, 5),
|
||||
'2': random.randint(0, 10),
|
||||
'3': random.randint(5, 20),
|
||||
'4': random.randint(20, 50),
|
||||
'5': random.randint(30, 80)
|
||||
}
|
||||
'average': avg_satisfaction,
|
||||
'distribution': {k: int(v) for k, v in dist.items()}
|
||||
}
|
||||
|
||||
# 预警统计
|
||||
level_counts = Counter([al.level for al in alerts])
|
||||
active_alerts = len([al for al in alerts if al.is_active])
|
||||
resolved_alerts = len([al for al in alerts if not al.is_active and al.resolved_at])
|
||||
alerts_stats = {
|
||||
'total': random.randint(50, 200),
|
||||
'active': random.randint(5, 30),
|
||||
'resolved': random.randint(20, 100),
|
||||
'by_level': {
|
||||
'low': random.randint(10, 40),
|
||||
'medium': random.randint(15, 50),
|
||||
'high': random.randint(5, 25),
|
||||
'critical': random.randint(2, 10)
|
||||
'total': len(alerts),
|
||||
'active': active_alerts,
|
||||
'resolved': resolved_alerts,
|
||||
'by_level': {k: int(v) for k, v in level_counts.items()}
|
||||
}
|
||||
}
|
||||
|
||||
# 性能指标
|
||||
|
||||
# 性能指标(基于对话响应时间粗略估计)
|
||||
resp_times = []
|
||||
for c in conversations:
|
||||
if c.response_time not in (None, ''):
|
||||
try:
|
||||
resp_time = float(c.response_time)
|
||||
resp_times.append(resp_time)
|
||||
except (ValueError, TypeError):
|
||||
continue
|
||||
avg_resp = round(sum(resp_times)/len(resp_times), 2) if resp_times else 0
|
||||
throughput = len(conversations) # 期间内的对话数量
|
||||
# 错误率:用严重预警比例粗估
|
||||
critical = level_counts.get('critical', 0)
|
||||
error_rate = round((critical / alerts_stats['total']) * 100, 2) if alerts_stats['total'] > 0 else 0
|
||||
performance_stats = {
|
||||
'response_time': round(random.uniform(0.5, 2.0), 2),
|
||||
'uptime': round(random.uniform(95, 99.9), 1),
|
||||
'error_rate': round(random.uniform(0.1, 2.0), 2),
|
||||
'throughput': random.randint(1000, 5000)
|
||||
'response_time': avg_resp,
|
||||
'uptime': 99.0, # 可接入真实监控后更新
|
||||
'error_rate': error_rate,
|
||||
'throughput': throughput
|
||||
}
|
||||
|
||||
return {
|
||||
'trend': trend_data,
|
||||
'trend': trend,
|
||||
'workorders': workorders_stats,
|
||||
'satisfaction': satisfaction_stats,
|
||||
'alerts': alerts_stats,
|
||||
'performance': performance_stats,
|
||||
'summary': {
|
||||
'total_workorders': workorders_stats['total'],
|
||||
'resolution_rate': round((workorders_stats['resolved'] / workorders_stats['total']) * 100, 1) if workorders_stats['total'] > 0 else 0,
|
||||
'avg_satisfaction': satisfaction_stats['average'],
|
||||
'active_alerts': alerts_stats['active']
|
||||
'total_workorders': total,
|
||||
'resolution_rate': round((resolved_count/total)*100, 1) if total > 0 else 0,
|
||||
'avg_satisfaction': avg_satisfaction,
|
||||
'active_alerts': active_alerts
|
||||
}
|
||||
}
|
||||
|
||||
@@ -850,8 +958,8 @@ def generate_analytics_data(days, dimension):
|
||||
def export_analytics():
|
||||
"""导出分析报告"""
|
||||
try:
|
||||
# 生成Excel报告
|
||||
analytics = generate_analytics_data(30, 'workorders')
|
||||
# 生成Excel报告(使用数据库真实数据)
|
||||
analytics = generate_db_analytics(30, 'workorders')
|
||||
|
||||
# 创建工作簿
|
||||
wb = Workbook()
|
||||
@@ -882,6 +990,50 @@ def export_analytics():
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
# Agent 工具统计与自定义工具
|
||||
@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()
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"tools": tools,
|
||||
"performance": performance
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route('/api/agent/tools/register', methods=['POST'])
|
||||
def register_custom_tool():
|
||||
"""注册自定义工具(仅登记元数据,函数为占位)"""
|
||||
try:
|
||||
data = request.get_json() or {}
|
||||
name = data.get('name')
|
||||
description = data.get('description', '')
|
||||
if not name:
|
||||
return jsonify({"error": "缺少工具名称"}), 400
|
||||
|
||||
def _placeholder_tool(**kwargs):
|
||||
return {"message": f"自定义工具 {name} 已登记(占位),当前不可执行", "params": kwargs}
|
||||
|
||||
agent_assistant.agent_core.tool_manager.register_tool(
|
||||
name,
|
||||
_placeholder_tool,
|
||||
metadata={"description": description, "custom": True}
|
||||
)
|
||||
return jsonify({"success": True, "message": "工具已注册"})
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@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)
|
||||
return jsonify({"success": success})
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
# 工单导入相关API
|
||||
@app.route('/api/workorders/import', methods=['POST'])
|
||||
def import_workorders():
|
||||
@@ -953,24 +1105,7 @@ def import_workorders():
|
||||
def download_import_template():
|
||||
"""下载工单导入模板"""
|
||||
try:
|
||||
# 创建模板数据
|
||||
template_data = {
|
||||
'标题': ['车辆无法启动', '空调不制冷', '导航系统故障'],
|
||||
'描述': ['用户反映车辆无法正常启动', '空调系统无法制冷', '导航系统显示异常'],
|
||||
'分类': ['技术问题', '技术问题', '技术问题'],
|
||||
'优先级': ['high', 'medium', 'low'],
|
||||
'状态': ['open', 'in_progress', 'resolved'],
|
||||
'解决方案': ['检查电池和启动系统', '检查制冷剂和压缩机', '更新导航软件'],
|
||||
'满意度': [5, 4, 5]
|
||||
}
|
||||
|
||||
df = pd.DataFrame(template_data)
|
||||
|
||||
# 保存为Excel文件
|
||||
template_path = 'uploads/workorder_template.xlsx'
|
||||
os.makedirs('uploads', exist_ok=True)
|
||||
df.to_excel(template_path, index=False)
|
||||
|
||||
template_path = _ensure_workorder_template_file()
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"template_url": f"/uploads/workorder_template.xlsx"
|
||||
@@ -979,6 +1114,15 @@ def download_import_template():
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route('/api/workorders/import/template/file')
|
||||
def download_import_template_file():
|
||||
"""直接返回工单导入模板文件(下载)"""
|
||||
try:
|
||||
template_path = _ensure_workorder_template_file()
|
||||
return send_file(template_path, as_attachment=True, download_name='工单导入模板.xlsx')
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route('/uploads/<filename>')
|
||||
def uploaded_file(filename):
|
||||
"""提供上传文件的下载服务"""
|
||||
@@ -989,13 +1133,46 @@ def uploaded_file(filename):
|
||||
def get_settings():
|
||||
"""获取系统设置"""
|
||||
try:
|
||||
import json
|
||||
settings_path = os.path.join('data', 'system_settings.json')
|
||||
os.makedirs('data', exist_ok=True)
|
||||
if os.path.exists(settings_path):
|
||||
with open(settings_path, 'r', encoding='utf-8') as f:
|
||||
settings = json.load(f)
|
||||
# 掩码API Key
|
||||
if settings.get('api_key'):
|
||||
settings['api_key'] = '******'
|
||||
settings['api_key_masked'] = True
|
||||
else:
|
||||
settings = {
|
||||
"api_timeout": 30,
|
||||
"max_history": 10,
|
||||
"refresh_interval": 10,
|
||||
"auto_monitoring": True,
|
||||
"agent_mode": True
|
||||
}
|
||||
"agent_mode": True,
|
||||
# LLM与API配置(仅持久化,不直接热更新LLM客户端)
|
||||
"api_provider": "openai",
|
||||
"api_base_url": "",
|
||||
"api_key": "",
|
||||
"model_name": "qwen-turbo",
|
||||
"model_temperature": 0.7,
|
||||
"model_max_tokens": 1000,
|
||||
# 服务配置
|
||||
"server_port": 5000,
|
||||
"websocket_port": 8765,
|
||||
"log_level": "INFO"
|
||||
}
|
||||
with open(settings_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(settings, f, ensure_ascii=False, indent=2)
|
||||
# 添加当前服务状态信息
|
||||
import time
|
||||
import psutil
|
||||
settings['current_server_port'] = app.config.get('SERVER_PORT', 5000)
|
||||
settings['current_websocket_port'] = app.config.get('WEBSOCKET_PORT', 8765)
|
||||
settings['uptime_seconds'] = int(time.time() - app.config.get('START_TIME', time.time()))
|
||||
settings['memory_usage_percent'] = psutil.virtual_memory().percent
|
||||
settings['cpu_usage_percent'] = psutil.cpu_percent()
|
||||
|
||||
return jsonify(settings)
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
@@ -1005,7 +1182,26 @@ def save_settings():
|
||||
"""保存系统设置"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
# 这里应该保存设置到配置文件
|
||||
import json
|
||||
os.makedirs('data', exist_ok=True)
|
||||
settings_path = os.path.join('data', 'system_settings.json')
|
||||
# 读取旧值,处理api_key掩码
|
||||
old = {}
|
||||
if os.path.exists(settings_path):
|
||||
try:
|
||||
with open(settings_path, 'r', encoding='utf-8') as f:
|
||||
old = json.load(f)
|
||||
except Exception:
|
||||
old = {}
|
||||
# 如果前端传回掩码或空,则保留旧的api_key
|
||||
if 'api_key' in data:
|
||||
if not data['api_key'] or data['api_key'] == '******':
|
||||
data['api_key'] = old.get('api_key', '')
|
||||
# 移除mask标志
|
||||
if 'api_key_masked' in data:
|
||||
data.pop('api_key_masked')
|
||||
with open(settings_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||
return jsonify({"success": True, "message": "设置保存成功"})
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
@@ -1033,10 +1229,13 @@ def get_vehicle_data():
|
||||
"""获取车辆数据"""
|
||||
try:
|
||||
vehicle_id = request.args.get('vehicle_id')
|
||||
vehicle_vin = request.args.get('vehicle_vin')
|
||||
data_type = request.args.get('data_type')
|
||||
limit = request.args.get('limit', 10, type=int)
|
||||
|
||||
if vehicle_id:
|
||||
if vehicle_vin:
|
||||
data = vehicle_manager.get_vehicle_data_by_vin(vehicle_vin, data_type, limit)
|
||||
elif vehicle_id:
|
||||
data = vehicle_manager.get_vehicle_data(vehicle_id, data_type, limit)
|
||||
else:
|
||||
data = vehicle_manager.search_vehicle_data(limit=limit)
|
||||
@@ -1045,6 +1244,15 @@ def get_vehicle_data():
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route('/api/vehicle/data/vin/<vehicle_vin>/latest')
|
||||
def get_latest_vehicle_data_by_vin(vehicle_vin):
|
||||
"""按VIN获取车辆最新数据"""
|
||||
try:
|
||||
data = vehicle_manager.get_latest_vehicle_data_by_vin(vehicle_vin)
|
||||
return jsonify(data)
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route('/api/vehicle/data/<vehicle_id>/latest')
|
||||
def get_latest_vehicle_data(vehicle_id):
|
||||
"""获取车辆最新数据"""
|
||||
@@ -1087,5 +1295,50 @@ def init_sample_vehicle_data():
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
# API测试相关接口
|
||||
@app.route('/api/test/connection', methods=['POST'])
|
||||
def test_api_connection():
|
||||
"""测试API连接"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
api_provider = data.get('api_provider', 'openai')
|
||||
api_base_url = data.get('api_base_url', '')
|
||||
api_key = data.get('api_key', '')
|
||||
model_name = data.get('model_name', 'qwen-turbo')
|
||||
|
||||
# 这里可以调用LLM客户端进行连接测试
|
||||
# 暂时返回模拟结果
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"message": f"API连接测试成功 - {api_provider}",
|
||||
"response_time": "150ms",
|
||||
"model_status": "可用"
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
|
||||
@app.route('/api/test/model', methods=['POST'])
|
||||
def test_model_response():
|
||||
"""测试模型回答"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
test_message = data.get('test_message', '你好,请简单介绍一下你自己')
|
||||
|
||||
# 这里可以调用LLM客户端进行回答测试
|
||||
# 暂时返回模拟结果
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"test_message": test_message,
|
||||
"response": "你好!我是TSP智能助手,基于大语言模型构建的智能客服系统。我可以帮助您解决车辆相关问题,提供技术支持和服务。",
|
||||
"response_time": "1.2s",
|
||||
"tokens_used": 45
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
|
||||
if __name__ == '__main__':
|
||||
import time
|
||||
app.config['START_TIME'] = time.time()
|
||||
app.config['SERVER_PORT'] = 5000
|
||||
app.config['WEBSOCKET_PORT'] = 8765
|
||||
app.run(debug=True, host='0.0.0.0', port=5000)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -13,6 +13,65 @@ class TSPDashboard {
|
||||
this.restorePageState();
|
||||
}
|
||||
|
||||
async generateAISuggestion(workorderId) {
|
||||
try {
|
||||
const resp = await fetch(`/api/workorders/${workorderId}/ai-suggestion`, { method: 'POST' });
|
||||
const data = await resp.json();
|
||||
if (data.success) {
|
||||
const ta = document.getElementById(`aiSuggestion_${workorderId}`);
|
||||
if (ta) ta.value = data.ai_suggestion || '';
|
||||
this.showNotification('AI建议已生成', 'success');
|
||||
} else {
|
||||
throw new Error(data.error || '生成失败');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('生成AI建议失败:', e);
|
||||
this.showNotification('生成AI建议失败: ' + e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async saveHumanResolution(workorderId) {
|
||||
try {
|
||||
const text = document.getElementById(`humanResolution_${workorderId}`).value.trim();
|
||||
if (!text) { this.showNotification('请输入人工描述', 'warning'); return; }
|
||||
const resp = await fetch(`/api/workorders/${workorderId}/human-resolution`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ human_resolution: text })
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (data.success) {
|
||||
const simEl = document.getElementById(`aiSim_${workorderId}`);
|
||||
const apprEl = document.getElementById(`aiApproved_${workorderId}`);
|
||||
const approveBtn = document.getElementById(`approveBtn_${workorderId}`);
|
||||
const percent = Math.round((data.similarity || 0) * 100);
|
||||
if (simEl) { simEl.textContent = `相似度: ${percent}%`; simEl.className = `badge ${percent>=95?'bg-success':percent>=70?'bg-warning':'bg-secondary'}`; }
|
||||
if (apprEl) { apprEl.textContent = data.approved ? '已自动审批' : '未审批'; apprEl.className = `badge ${data.approved?'bg-success':'bg-secondary'}`; }
|
||||
if (approveBtn) approveBtn.disabled = !data.approved;
|
||||
this.showNotification('人工描述已保存并评估完成', 'success');
|
||||
} else {
|
||||
throw new Error(data.error || '保存失败');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('保存人工描述失败:', e);
|
||||
this.showNotification('保存人工描述失败: ' + e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async approveToKnowledge(workorderId) {
|
||||
try {
|
||||
const resp = await fetch(`/api/workorders/${workorderId}/approve-to-knowledge`, { method: 'POST' });
|
||||
const data = await resp.json();
|
||||
if (data.success) {
|
||||
this.showNotification('已入库为知识条目', 'success');
|
||||
} else {
|
||||
throw new Error(data.error || '入库失败');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('入库失败:', e);
|
||||
this.showNotification('入库失败: ' + e.message, 'error');
|
||||
}
|
||||
}
|
||||
init() {
|
||||
this.bindEvents();
|
||||
this.loadInitialData();
|
||||
@@ -99,6 +158,27 @@ class TSPDashboard {
|
||||
e.preventDefault();
|
||||
this.saveSystemSettings();
|
||||
});
|
||||
|
||||
// API测试按钮事件
|
||||
const testApiBtn = document.getElementById('test-api-connection');
|
||||
if (testApiBtn) {
|
||||
testApiBtn.addEventListener('click', () => this.testApiConnection());
|
||||
}
|
||||
|
||||
const testModelBtn = document.getElementById('test-model-response');
|
||||
if (testModelBtn) {
|
||||
testModelBtn.addEventListener('click', () => this.testModelResponse());
|
||||
}
|
||||
|
||||
// 重启服务按钮事件
|
||||
const restartBtn = document.getElementById('restart-service');
|
||||
if (restartBtn) {
|
||||
restartBtn.addEventListener('click', () => {
|
||||
if (confirm('确定要重启服务吗?这将中断当前连接。')) {
|
||||
this.showNotification('重启服务功能待实现', 'info');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
switchTab(tabName) {
|
||||
@@ -180,10 +260,10 @@ class TSPDashboard {
|
||||
}
|
||||
|
||||
startAutoRefresh() {
|
||||
// 每5秒刷新健康状态
|
||||
// 每15秒刷新健康状态(减少 /api/health 日志)
|
||||
this.refreshIntervals.health = setInterval(() => {
|
||||
this.loadHealth();
|
||||
}, 5000);
|
||||
}, 15000);
|
||||
|
||||
// 每10秒刷新当前标签页数据
|
||||
this.refreshIntervals.currentTab = setInterval(() => {
|
||||
@@ -305,7 +385,10 @@ class TSPDashboard {
|
||||
document.getElementById('knowledge-confidence').textContent = `${confidencePercent}%`;
|
||||
|
||||
// 更新性能图表
|
||||
this.updatePerformanceChart(sessions, alerts, workorders);
|
||||
await this.updatePerformanceChart(sessions, alerts, workorders);
|
||||
|
||||
// 更新系统健康状态
|
||||
await this.updateSystemHealth();
|
||||
|
||||
} catch (error) {
|
||||
console.error('加载仪表板数据失败:', error);
|
||||
@@ -323,13 +406,13 @@ class TSPDashboard {
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [{
|
||||
label: '活跃会话',
|
||||
label: '工单数量',
|
||||
data: [],
|
||||
borderColor: '#007bff',
|
||||
backgroundColor: 'rgba(0, 123, 255, 0.1)',
|
||||
tension: 0.4
|
||||
}, {
|
||||
label: '活跃预警',
|
||||
label: '预警数量',
|
||||
data: [],
|
||||
borderColor: '#dc3545',
|
||||
backgroundColor: 'rgba(220, 53, 69, 0.1)',
|
||||
@@ -407,26 +490,96 @@ class TSPDashboard {
|
||||
}
|
||||
}
|
||||
|
||||
updatePerformanceChart(sessions, alerts, workorders) {
|
||||
async updatePerformanceChart(sessions, alerts, workorders) {
|
||||
if (!this.charts.performance) return;
|
||||
|
||||
const now = new Date();
|
||||
const labels = [];
|
||||
const sessionData = [];
|
||||
const alertData = [];
|
||||
|
||||
// 生成过去24小时的数据点
|
||||
for (let i = 23; i >= 0; i--) {
|
||||
const time = new Date(now.getTime() - i * 60 * 60 * 1000);
|
||||
labels.push(time.getHours() + ':00');
|
||||
sessionData.push(Math.floor(Math.random() * 10) + 5); // 模拟数据
|
||||
alertData.push(Math.floor(Math.random() * 5)); // 模拟数据
|
||||
try {
|
||||
// 获取真实的分析数据
|
||||
const response = await fetch('/api/analytics?days=7&dimension=performance');
|
||||
const analyticsData = await response.json();
|
||||
|
||||
if (analyticsData.trend && analyticsData.trend.length > 0) {
|
||||
// 使用真实数据
|
||||
const labels = analyticsData.trend.map(item => {
|
||||
const date = new Date(item.date);
|
||||
return `${date.getMonth() + 1}/${date.getDate()}`;
|
||||
});
|
||||
|
||||
const workorderData = analyticsData.trend.map(item => item.workorders || 0);
|
||||
const alertData = analyticsData.trend.map(item => item.alerts || 0);
|
||||
|
||||
this.charts.performance.data.labels = labels;
|
||||
this.charts.performance.data.datasets[0].data = workorderData;
|
||||
this.charts.performance.data.datasets[1].data = alertData;
|
||||
this.charts.performance.update();
|
||||
} else {
|
||||
// 如果没有真实数据,显示提示
|
||||
this.charts.performance.data.labels = ['暂无数据'];
|
||||
this.charts.performance.data.datasets[0].data = [0];
|
||||
this.charts.performance.data.datasets[1].data = [0];
|
||||
this.charts.performance.update();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取性能趋势数据失败:', error);
|
||||
// 出错时显示空数据
|
||||
this.charts.performance.data.labels = ['数据加载失败'];
|
||||
this.charts.performance.data.datasets[0].data = [0];
|
||||
this.charts.performance.data.datasets[1].data = [0];
|
||||
this.charts.performance.update();
|
||||
}
|
||||
}
|
||||
|
||||
this.charts.performance.data.labels = labels;
|
||||
this.charts.performance.data.datasets[0].data = sessionData;
|
||||
this.charts.performance.data.datasets[1].data = alertData;
|
||||
this.charts.performance.update();
|
||||
// 更新系统健康状态显示
|
||||
async updateSystemHealth() {
|
||||
try {
|
||||
const response = await fetch('/api/settings');
|
||||
const settings = await response.json();
|
||||
|
||||
// 更新健康分数
|
||||
const healthScore = Math.max(0, 100 - (settings.memory_usage_percent || 0) - (settings.cpu_usage_percent || 0));
|
||||
const healthProgress = document.getElementById('health-progress');
|
||||
const healthDot = document.getElementById('system-health-dot');
|
||||
const healthText = document.getElementById('system-health-text');
|
||||
|
||||
if (healthProgress) {
|
||||
healthProgress.style.width = `${healthScore}%`;
|
||||
healthProgress.setAttribute('aria-valuenow', healthScore);
|
||||
}
|
||||
|
||||
if (healthDot) {
|
||||
healthDot.className = 'health-dot';
|
||||
if (healthScore >= 80) healthDot.classList.add('excellent');
|
||||
else if (healthScore >= 60) healthDot.classList.add('good');
|
||||
else if (healthScore >= 40) healthDot.classList.add('fair');
|
||||
else if (healthScore >= 20) healthDot.classList.add('poor');
|
||||
else healthDot.classList.add('critical');
|
||||
}
|
||||
|
||||
if (healthText) {
|
||||
const statusText = healthScore >= 80 ? '优秀' :
|
||||
healthScore >= 60 ? '良好' :
|
||||
healthScore >= 40 ? '一般' :
|
||||
healthScore >= 20 ? '较差' : '严重';
|
||||
healthText.textContent = `${statusText} (${healthScore}%)`;
|
||||
}
|
||||
|
||||
// 更新内存使用
|
||||
const memoryProgress = document.getElementById('memory-progress');
|
||||
if (memoryProgress && settings.memory_usage_percent !== undefined) {
|
||||
memoryProgress.style.width = `${settings.memory_usage_percent}%`;
|
||||
memoryProgress.setAttribute('aria-valuenow', settings.memory_usage_percent);
|
||||
}
|
||||
|
||||
// 更新CPU使用
|
||||
const cpuProgress = document.getElementById('cpu-progress');
|
||||
if (cpuProgress && settings.cpu_usage_percent !== undefined) {
|
||||
cpuProgress.style.width = `${settings.cpu_usage_percent}%`;
|
||||
cpuProgress.setAttribute('aria-valuenow', settings.cpu_usage_percent);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('更新系统健康状态失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 对话功能
|
||||
@@ -506,6 +659,9 @@ class TSPDashboard {
|
||||
this.addMessage('user', message);
|
||||
messageInput.value = '';
|
||||
|
||||
// 显示占位提示:小奇正在查询中
|
||||
const typingId = this.showTypingIndicator();
|
||||
|
||||
// 发送消息到服务器
|
||||
try {
|
||||
const response = await fetch('/api/chat/message', {
|
||||
@@ -521,16 +677,70 @@ class TSPDashboard {
|
||||
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
this.addMessage('assistant', data.response, data.knowledge_used);
|
||||
this.updateTypingIndicator(typingId, data.response, data.knowledge_used);
|
||||
} else {
|
||||
this.addMessage('assistant', '抱歉,处理您的消息时出现了错误。', null, true);
|
||||
this.updateTypingIndicator(typingId, '抱歉,处理您的消息时出现了错误。', null, true);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('发送消息失败:', error);
|
||||
this.addMessage('assistant', '网络连接错误,请稍后重试。', null, true);
|
||||
this.updateTypingIndicator(typingId, '网络连接错误,请稍后重试。', null, true);
|
||||
}
|
||||
}
|
||||
|
||||
showTypingIndicator() {
|
||||
const messagesContainer = document.getElementById('chat-messages');
|
||||
const id = `typing-${Date.now()}`;
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = 'message assistant';
|
||||
messageDiv.id = id;
|
||||
const avatar = document.createElement('div');
|
||||
avatar.className = 'message-avatar';
|
||||
avatar.innerHTML = '<i class="fas fa-robot"></i>';
|
||||
const contentDiv = document.createElement('div');
|
||||
contentDiv.className = 'message-content';
|
||||
contentDiv.innerHTML = `
|
||||
<div>小奇正在查询中,请稍后…</div>
|
||||
<div class="message-time">${new Date().toLocaleTimeString()}</div>
|
||||
`;
|
||||
messageDiv.appendChild(avatar);
|
||||
messageDiv.appendChild(contentDiv);
|
||||
messagesContainer.appendChild(messageDiv);
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
return id;
|
||||
}
|
||||
|
||||
updateTypingIndicator(typingId, content, knowledgeUsed = null, isError = false) {
|
||||
const node = document.getElementById(typingId);
|
||||
if (!node) {
|
||||
// 回退:若占位不存在则直接追加
|
||||
this.addMessage('assistant', content, knowledgeUsed, isError);
|
||||
return;
|
||||
}
|
||||
const contentDiv = node.querySelector('.message-content');
|
||||
if (contentDiv) {
|
||||
contentDiv.innerHTML = `
|
||||
<div>${content}</div>
|
||||
<div class="message-time">${new Date().toLocaleTimeString()}</div>
|
||||
`;
|
||||
if (knowledgeUsed && knowledgeUsed.length > 0) {
|
||||
const knowledgeDiv = document.createElement('div');
|
||||
knowledgeDiv.className = 'knowledge-info';
|
||||
knowledgeDiv.innerHTML = `
|
||||
<i class="fas fa-lightbulb me-1"></i>
|
||||
使用了知识库: ${knowledgeUsed.map(k => k.question || k.source || '实时数据').join(', ')}
|
||||
`;
|
||||
contentDiv.appendChild(knowledgeDiv);
|
||||
}
|
||||
if (isError) {
|
||||
contentDiv.style.borderLeft = '4px solid #dc3545';
|
||||
} else {
|
||||
contentDiv.style.borderLeft = '';
|
||||
}
|
||||
}
|
||||
const messagesContainer = document.getElementById('chat-messages');
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
}
|
||||
|
||||
addMessage(role, content, knowledgeUsed = null, isError = false) {
|
||||
const messagesContainer = document.getElementById('chat-messages');
|
||||
|
||||
@@ -603,16 +813,21 @@ class TSPDashboard {
|
||||
|
||||
async loadAgentData() {
|
||||
try {
|
||||
const response = await fetch('/api/agent/status');
|
||||
const data = await response.json();
|
||||
const [statusResp, toolsResp] = await Promise.all([
|
||||
fetch('/api/agent/status'),
|
||||
fetch('/api/agent/tools/stats')
|
||||
]);
|
||||
const data = await statusResp.json();
|
||||
const toolsData = await toolsResp.json();
|
||||
|
||||
if (data.success) {
|
||||
document.getElementById('agent-current-state').textContent = data.status || '未知';
|
||||
document.getElementById('agent-active-goals').textContent = data.active_goals || 0;
|
||||
document.getElementById('agent-available-tools').textContent = data.available_tools || 0;
|
||||
const tools = (toolsData.success ? toolsData.tools : (data.tools || [])) || [];
|
||||
document.getElementById('agent-available-tools').textContent = tools.length || 0;
|
||||
|
||||
// 更新工具列表
|
||||
this.updateToolsList(data.tools || []);
|
||||
// 更新工具列表(使用真实统计)
|
||||
this.updateToolsList(tools);
|
||||
|
||||
// 更新执行历史
|
||||
this.updateAgentExecutionHistory(data.execution_history || []);
|
||||
@@ -624,27 +839,64 @@ class TSPDashboard {
|
||||
|
||||
updateToolsList(tools) {
|
||||
const toolsList = document.getElementById('tools-list');
|
||||
if (tools.length === 0) {
|
||||
if (!tools || tools.length === 0) {
|
||||
toolsList.innerHTML = '<div class="empty-state"><i class="fas fa-tools"></i><p>暂无工具</p></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const toolsHtml = tools.map(tool => `
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<div>
|
||||
<strong>${tool.name}</strong>
|
||||
<br>
|
||||
<small class="text-muted">使用次数: ${tool.usage_count || 0}</small>
|
||||
const toolsHtml = tools.map(tool => {
|
||||
const usage = tool.usage_count || 0;
|
||||
const success = Math.round((tool.success_rate || 0) * 100);
|
||||
const meta = tool.metadata || {};
|
||||
return `
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<div>
|
||||
<strong>${tool.name}</strong>
|
||||
${meta.description ? `<div class="text-muted small">${meta.description}</div>` : ''}
|
||||
<small class="text-muted">使用次数: ${usage}</small>
|
||||
</div>
|
||||
<div>
|
||||
<span class="badge ${success >= 80 ? 'bg-success' : success >= 50 ? 'bg-warning' : 'bg-secondary'}">${success}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span class="badge ${tool.success_rate >= 0.8 ? 'bg-success' : 'bg-warning'}">
|
||||
${Math.round((tool.success_rate || 0) * 100)}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
toolsList.innerHTML = toolsHtml;
|
||||
|
||||
// 追加自定义工具注册入口
|
||||
const addDiv = document.createElement('div');
|
||||
addDiv.className = 'mt-3';
|
||||
addDiv.innerHTML = `
|
||||
<div class="input-group input-group-sm">
|
||||
<input type="text" id="custom-tool-name" class="form-control" placeholder="自定义工具名称">
|
||||
<input type="text" id="custom-tool-desc" class="form-control" placeholder="描述(可选)">
|
||||
<button class="btn btn-outline-primary" id="register-tool-btn">注册</button>
|
||||
</div>
|
||||
`;
|
||||
toolsList.appendChild(addDiv);
|
||||
document.getElementById('register-tool-btn').addEventListener('click', async () => {
|
||||
const name = document.getElementById('custom-tool-name').value.trim();
|
||||
const description = document.getElementById('custom-tool-desc').value.trim();
|
||||
if (!name) { this.showNotification('请输入工具名称', 'warning'); return; }
|
||||
try {
|
||||
const resp = await fetch('/api/agent/tools/register', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name, description })
|
||||
});
|
||||
const res = await resp.json();
|
||||
if (res.success) {
|
||||
this.showNotification('工具注册成功', 'success');
|
||||
this.loadAgentData();
|
||||
} else {
|
||||
this.showNotification(res.error || '工具注册失败', 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('注册工具失败:', e);
|
||||
this.showNotification('注册工具失败', 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateExecutionHistory(history) {
|
||||
@@ -1349,6 +1601,32 @@ class TSPDashboard {
|
||||
<small class="text-muted">${workorder.satisfaction_score}/5.0</small>
|
||||
</div>
|
||||
` : ''}
|
||||
<h6 class="mt-3">AI建议与人工描述</h6>
|
||||
<div class="border p-3 rounded">
|
||||
<div class="mb-2">
|
||||
<button class="btn btn-sm btn-outline-primary" onclick="dashboard.generateAISuggestion(${workorder.id})">
|
||||
<i class="fas fa-magic me-1"></i>生成AI建议
|
||||
</button>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label">AI建议</label>
|
||||
<textarea id="aiSuggestion_${workorder.id}" class="form-control" rows="4" placeholder="点击上方按钮生成..." readonly></textarea>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label">人工描述</label>
|
||||
<textarea id="humanResolution_${workorder.id}" class="form-control" rows="3" placeholder="请填写人工处理描述..."></textarea>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2 mb-2">
|
||||
<button class="btn btn-sm btn-outline-success" onclick="dashboard.saveHumanResolution(${workorder.id})">
|
||||
<i class="fas fa-save me-1"></i>保存人工描述并评估
|
||||
</button>
|
||||
<span id="aiSim_${workorder.id}" class="badge bg-secondary">相似度: --</span>
|
||||
<span id="aiApproved_${workorder.id}" class="badge bg-secondary">未审批</span>
|
||||
<button id="approveBtn_${workorder.id}" class="btn btn-sm btn-outline-primary" onclick="dashboard.approveToKnowledge(${workorder.id})" disabled>
|
||||
<i class="fas fa-check me-1"></i>入库
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1582,22 +1860,19 @@ class TSPDashboard {
|
||||
|
||||
async downloadTemplate() {
|
||||
try {
|
||||
const response = await fetch('/api/workorders/import/template');
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
// 创建下载链接
|
||||
const link = document.createElement('a');
|
||||
link.href = result.template_url;
|
||||
link.download = '工单导入模板.xlsx';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
this.showNotification('模板下载成功', 'success');
|
||||
} else {
|
||||
throw new Error(result.error || '下载模板失败');
|
||||
}
|
||||
// 直接请求文件接口,避免浏览器跨源/权限限制
|
||||
const resp = await fetch('/api/workorders/import/template/file');
|
||||
if (!resp.ok) throw new Error('下载接口返回错误');
|
||||
const blob = await resp.blob();
|
||||
const blobUrl = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = blobUrl;
|
||||
a.download = '工单导入模板.xlsx';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
window.URL.revokeObjectURL(blobUrl);
|
||||
this.showNotification('模板下载成功', 'success');
|
||||
} catch (error) {
|
||||
console.error('下载模板失败:', error);
|
||||
this.showNotification('下载模板失败: ' + error.message, 'error');
|
||||
@@ -2348,11 +2623,40 @@ class TSPDashboard {
|
||||
}
|
||||
|
||||
updateSettingsDisplay(settings) {
|
||||
if (settings.api_timeout) document.getElementById('api-timeout').value = settings.api_timeout;
|
||||
if (settings.max_history) document.getElementById('max-history').value = settings.max_history;
|
||||
if (settings.refresh_interval) document.getElementById('refresh-interval').value = settings.refresh_interval;
|
||||
if (settings.api_timeout !== undefined) document.getElementById('api-timeout').value = settings.api_timeout;
|
||||
if (settings.max_history !== undefined) document.getElementById('max-history').value = settings.max_history;
|
||||
if (settings.refresh_interval !== undefined) document.getElementById('refresh-interval').value = settings.refresh_interval;
|
||||
if (settings.auto_monitoring !== undefined) document.getElementById('auto-monitoring').checked = settings.auto_monitoring;
|
||||
if (settings.agent_mode !== undefined) document.getElementById('agent-mode').checked = settings.agent_mode;
|
||||
// 新增:API与模型、端口、日志级别(如页面存在对应输入框则填充)
|
||||
const map = [
|
||||
['api-provider','api_provider'],
|
||||
['api-base-url','api_base_url'],
|
||||
['api-key','api_key'],
|
||||
['model-name','model_name'],
|
||||
['model-temperature','model_temperature'],
|
||||
['model-max-tokens','model_max_tokens'],
|
||||
['server-port','server_port'],
|
||||
['websocket-port','websocket_port'],
|
||||
['log-level','log_level']
|
||||
];
|
||||
map.forEach(([id, key]) => {
|
||||
const el = document.getElementById(id);
|
||||
if (el && settings[key] !== undefined) el.value = settings[key];
|
||||
});
|
||||
|
||||
// 更新温度滑块显示值
|
||||
const tempSlider = document.getElementById('model-temperature');
|
||||
const tempValue = document.getElementById('temperature-value');
|
||||
if (tempSlider && tempValue) {
|
||||
tempSlider.addEventListener('input', function() {
|
||||
tempValue.textContent = this.value;
|
||||
});
|
||||
tempValue.textContent = tempSlider.value;
|
||||
}
|
||||
|
||||
// 更新服务状态显示
|
||||
this.updateServiceStatus(settings);
|
||||
}
|
||||
|
||||
async saveSystemSettings() {
|
||||
@@ -2361,7 +2665,16 @@ class TSPDashboard {
|
||||
max_history: parseInt(document.getElementById('max-history').value),
|
||||
refresh_interval: parseInt(document.getElementById('refresh-interval').value),
|
||||
auto_monitoring: document.getElementById('auto-monitoring').checked,
|
||||
agent_mode: document.getElementById('agent-mode').checked
|
||||
agent_mode: document.getElementById('agent-mode').checked,
|
||||
api_provider: document.getElementById('api-provider')?.value || '',
|
||||
api_base_url: document.getElementById('api-base-url')?.value || '',
|
||||
api_key: document.getElementById('api-key')?.value || '',
|
||||
model_name: document.getElementById('model-name')?.value || '',
|
||||
model_temperature: parseFloat(document.getElementById('model-temperature')?.value || 0.7),
|
||||
model_max_tokens: parseInt(document.getElementById('model-max-tokens')?.value || 1000),
|
||||
server_port: parseInt(document.getElementById('server-port')?.value || 5000),
|
||||
websocket_port: parseInt(document.getElementById('websocket-port')?.value || 8765),
|
||||
log_level: document.getElementById('log-level')?.value || 'INFO'
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -2385,6 +2698,113 @@ class TSPDashboard {
|
||||
}
|
||||
}
|
||||
|
||||
// 更新服务状态显示
|
||||
updateServiceStatus(settings) {
|
||||
// 更新仪表板服务状态卡片
|
||||
if (settings.current_server_port !== undefined) {
|
||||
const webPortEl = document.getElementById('web-port-status');
|
||||
if (webPortEl) webPortEl.textContent = settings.current_server_port;
|
||||
}
|
||||
if (settings.current_websocket_port !== undefined) {
|
||||
const wsPortEl = document.getElementById('ws-port-status');
|
||||
if (wsPortEl) wsPortEl.textContent = settings.current_websocket_port;
|
||||
}
|
||||
if (settings.log_level !== undefined) {
|
||||
const logLevelEl = document.getElementById('log-level-status');
|
||||
if (logLevelEl) logLevelEl.textContent = settings.log_level;
|
||||
}
|
||||
if (settings.uptime_seconds !== undefined) {
|
||||
const uptimeEl = document.getElementById('uptime-status');
|
||||
if (uptimeEl) {
|
||||
const hours = Math.floor(settings.uptime_seconds / 3600);
|
||||
const minutes = Math.floor((settings.uptime_seconds % 3600) / 60);
|
||||
uptimeEl.textContent = `${hours}小时${minutes}分钟`;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新系统设置页面的当前端口显示
|
||||
const currentPortEl = document.getElementById('current-server-port');
|
||||
if (currentPortEl && settings.current_server_port !== undefined) {
|
||||
currentPortEl.textContent = settings.current_server_port;
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新服务状态
|
||||
async refreshServiceStatus() {
|
||||
try {
|
||||
const response = await fetch('/api/settings');
|
||||
const settings = await response.json();
|
||||
this.updateServiceStatus(settings);
|
||||
this.showNotification('服务状态已刷新', 'success');
|
||||
} catch (error) {
|
||||
console.error('刷新服务状态失败:', error);
|
||||
this.showNotification('刷新服务状态失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 测试API连接
|
||||
async testApiConnection() {
|
||||
try {
|
||||
const apiProvider = document.getElementById('api-provider').value;
|
||||
const apiBaseUrl = document.getElementById('api-base-url').value;
|
||||
const apiKey = document.getElementById('api-key').value;
|
||||
const modelName = document.getElementById('model-name').value;
|
||||
|
||||
const response = await fetch('/api/test/connection', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
api_provider: apiProvider,
|
||||
api_base_url: apiBaseUrl,
|
||||
api_key: apiKey,
|
||||
model_name: modelName
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
this.showNotification(`API连接测试成功: ${result.message}`, 'success');
|
||||
} else {
|
||||
this.showNotification(`API连接测试失败: ${result.error}`, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('API连接测试失败:', error);
|
||||
this.showNotification('API连接测试失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 测试模型回答
|
||||
async testModelResponse() {
|
||||
try {
|
||||
const testMessage = prompt('请输入测试消息:', '你好,请简单介绍一下你自己');
|
||||
if (!testMessage) return;
|
||||
|
||||
const response = await fetch('/api/test/model', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
test_message: testMessage
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
const message = `模型回答测试成功:\n\n问题: ${result.test_message}\n\n回答: ${result.response}\n\n响应时间: ${result.response_time}`;
|
||||
alert(message);
|
||||
this.showNotification('模型回答测试成功', 'success');
|
||||
} else {
|
||||
this.showNotification(`模型回答测试失败: ${result.error}`, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('模型回答测试失败:', error);
|
||||
this.showNotification('模型回答测试失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async loadSystemInfo() {
|
||||
try {
|
||||
const response = await fetch('/api/system/info');
|
||||
|
||||
@@ -461,7 +461,7 @@
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5><i class="fas fa-chart-line me-2"></i>系统性能趋势</h5>
|
||||
@@ -473,7 +473,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="col-md-3">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5><i class="fas fa-heartbeat me-2"></i>系统健康状态</h5>
|
||||
@@ -504,6 +504,44 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5><i class="fas fa-server me-2"></i>服务运行状态</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span class="text-muted">Web服务端口</span>
|
||||
<span class="badge bg-success" id="web-port-status">5000</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span class="text-muted">WebSocket端口</span>
|
||||
<span class="badge bg-info" id="ws-port-status">8765</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span class="text-muted">日志级别</span>
|
||||
<span class="badge bg-secondary" id="log-level-status">INFO</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<span class="text-muted">运行时间</span>
|
||||
<span class="text-primary" id="uptime-status">计算中...</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-grid">
|
||||
<button class="btn btn-outline-primary btn-sm" onclick="dashboard.refreshServiceStatus()">
|
||||
<i class="fas fa-sync-alt me-1"></i>刷新状态
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1148,10 +1186,11 @@
|
||||
<!-- 系统设置标签页 -->
|
||||
<div id="settings-tab" class="tab-content" style="display: none;">
|
||||
<div class="row">
|
||||
<!-- 基础系统配置 -->
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5><i class="fas fa-cog me-2"></i>系统配置</h5>
|
||||
<h5><i class="fas fa-cog me-2"></i>基础系统配置</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="system-settings-form">
|
||||
@@ -1186,10 +1225,108 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- API与模型配置 -->
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5><i class="fas fa-info-circle me-2"></i>系统信息</h5>
|
||||
<h5><i class="fas fa-brain me-2"></i>API与模型配置</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="api-model-settings-form">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">API提供商</label>
|
||||
<select class="form-select" id="api-provider">
|
||||
<option value="openai">OpenAI</option>
|
||||
<option value="qwen">通义千问</option>
|
||||
<option value="custom">自定义</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">API基础URL</label>
|
||||
<input type="url" class="form-control" id="api-base-url" placeholder="https://api.openai.com/v1">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">API密钥</label>
|
||||
<input type="password" class="form-control" id="api-key" placeholder="输入API密钥">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">模型名称</label>
|
||||
<input type="text" class="form-control" id="model-name" value="qwen-turbo">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">温度参数</label>
|
||||
<input type="range" class="form-range" id="model-temperature" min="0" max="2" step="0.1" value="0.7">
|
||||
<div class="d-flex justify-content-between">
|
||||
<small>0 (确定性)</small>
|
||||
<small id="temperature-value">0.7</small>
|
||||
<small>2 (创造性)</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">最大令牌数</label>
|
||||
<input type="number" class="form-control" id="model-max-tokens" value="1000">
|
||||
</div>
|
||||
<div class="d-grid gap-2">
|
||||
<button type="button" class="btn btn-success" id="test-api-connection">
|
||||
<i class="fas fa-plug me-2"></i>测试API连接
|
||||
</button>
|
||||
<button type="button" class="btn btn-info" id="test-model-response">
|
||||
<i class="fas fa-comment me-2"></i>测试模型回答
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-4">
|
||||
<!-- 服务端口配置 -->
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5><i class="fas fa-server me-2"></i>服务端口配置</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="port-settings-form">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Web服务端口</label>
|
||||
<input type="number" class="form-control" id="server-port" value="5000">
|
||||
<div class="form-text">当前运行端口: <span id="current-server-port">5000</span></div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">WebSocket端口</label>
|
||||
<input type="number" class="form-control" id="websocket-port" value="8765">
|
||||
<div class="form-text">实时通信端口</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">日志级别</label>
|
||||
<select class="form-select" id="log-level">
|
||||
<option value="DEBUG">DEBUG</option>
|
||||
<option value="INFO" selected>INFO</option>
|
||||
<option value="WARNING">WARNING</option>
|
||||
<option value="ERROR">ERROR</option>
|
||||
<option value="CRITICAL">CRITICAL</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="alert alert-warning">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||
修改端口后需要重启服务才能生效
|
||||
</div>
|
||||
<button type="button" class="btn btn-warning" id="restart-service">
|
||||
<i class="fas fa-redo me-2"></i>重启服务
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 系统信息与状态 -->
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5><i class="fas fa-info-circle me-2"></i>系统信息与状态</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="system-info">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
WebSocket实时通信服务器
|
||||
|
||||
Reference in New Issue
Block a user