fix: 业务层缺陷修复 5 项
1. process_message_stream 补齐 tenant_id 知识库搜索和流式生成都按租户隔离 2. 三个模型补齐 tenant_id WorkOrderSuggestion/WorkOrderProcessHistory/VehicleData 3. 删除 @resolve_tenant_id 死代码装饰器(未来在 Repository 层统一处理) 4. 删除前端死代码 app.js/app-new.js/main.js/chat.js/chat_http.js + HTML 引用清理 5. 飞书长连接 sender_id 调试日志删除
This commit is contained in:
@@ -95,6 +95,9 @@ class DatabaseManager:
|
|||||||
('users', 'tenant_id', "VARCHAR(50) DEFAULT 'default'"),
|
('users', 'tenant_id', "VARCHAR(50) DEFAULT 'default'"),
|
||||||
('alerts', 'tenant_id', "VARCHAR(50) DEFAULT 'default'"),
|
('alerts', 'tenant_id', "VARCHAR(50) DEFAULT 'default'"),
|
||||||
('analytics', 'tenant_id', "VARCHAR(50) DEFAULT 'default'"),
|
('analytics', 'tenant_id', "VARCHAR(50) DEFAULT 'default'"),
|
||||||
|
('work_order_suggestions', 'tenant_id', "VARCHAR(50) DEFAULT 'default'"),
|
||||||
|
('work_order_process_history', 'tenant_id', "VARCHAR(50) DEFAULT 'default'"),
|
||||||
|
('vehicle_data', 'tenant_id', "VARCHAR(50) DEFAULT 'default'"),
|
||||||
]
|
]
|
||||||
for table_name, col_name, col_type in migrations:
|
for table_name, col_name, col_type in migrations:
|
||||||
if table_name in inspector.get_table_names():
|
if table_name in inspector.get_table_names():
|
||||||
|
|||||||
@@ -183,6 +183,7 @@ class VehicleData(Base):
|
|||||||
__tablename__ = "vehicle_data"
|
__tablename__ = "vehicle_data"
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True)
|
id = Column(Integer, primary_key=True)
|
||||||
|
tenant_id = Column(String(50), nullable=False, default=DEFAULT_TENANT, index=True)
|
||||||
vehicle_id = Column(String(50), nullable=False) # 车辆ID
|
vehicle_id = Column(String(50), nullable=False) # 车辆ID
|
||||||
vehicle_vin = Column(String(17)) # 车架号
|
vehicle_vin = Column(String(17)) # 车架号
|
||||||
data_type = Column(String(50), nullable=False) # 数据类型(位置、状态、故障等)
|
data_type = Column(String(50), nullable=False) # 数据类型(位置、状态、故障等)
|
||||||
@@ -237,6 +238,7 @@ class WorkOrderSuggestion(Base):
|
|||||||
__tablename__ = "work_order_suggestions"
|
__tablename__ = "work_order_suggestions"
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True)
|
id = Column(Integer, primary_key=True)
|
||||||
|
tenant_id = Column(String(50), nullable=False, default=DEFAULT_TENANT, index=True)
|
||||||
work_order_id = Column(Integer, ForeignKey("work_orders.id"), nullable=False)
|
work_order_id = Column(Integer, ForeignKey("work_orders.id"), nullable=False)
|
||||||
ai_suggestion = Column(Text)
|
ai_suggestion = Column(Text)
|
||||||
human_resolution = Column(Text)
|
human_resolution = Column(Text)
|
||||||
@@ -251,6 +253,7 @@ class WorkOrderProcessHistory(Base):
|
|||||||
__tablename__ = "work_order_process_history"
|
__tablename__ = "work_order_process_history"
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True)
|
id = Column(Integer, primary_key=True)
|
||||||
|
tenant_id = Column(String(50), nullable=False, default=DEFAULT_TENANT, index=True)
|
||||||
work_order_id = Column(Integer, ForeignKey("work_orders.id"), nullable=False)
|
work_order_id = Column(Integer, ForeignKey("work_orders.id"), nullable=False)
|
||||||
|
|
||||||
# 处理人员信息
|
# 处理人员信息
|
||||||
|
|||||||
@@ -309,8 +309,9 @@ class RealtimeChatManager:
|
|||||||
)
|
)
|
||||||
self.message_history[session_id].append(user_msg)
|
self.message_history[session_id].append(user_msg)
|
||||||
|
|
||||||
# 搜索知识 + VIN
|
# 搜索知识 + VIN(按租户隔离)
|
||||||
knowledge_results = self._search_knowledge(user_message)
|
session_tenant = session.get("tenant_id")
|
||||||
|
knowledge_results = self._search_knowledge(user_message, tenant_id=session_tenant)
|
||||||
vin = self._extract_vin(user_message)
|
vin = self._extract_vin(user_message)
|
||||||
if vin:
|
if vin:
|
||||||
latest = self.vehicle_manager.get_latest_vehicle_data_by_vin(vin)
|
latest = self.vehicle_manager.get_latest_vehicle_data_by_vin(vin)
|
||||||
@@ -326,7 +327,7 @@ class RealtimeChatManager:
|
|||||||
# 流式生成
|
# 流式生成
|
||||||
full_content = []
|
full_content = []
|
||||||
for chunk in self._generate_response_stream(
|
for chunk in self._generate_response_stream(
|
||||||
user_message, knowledge_results, session["context"], session["work_order_id"]
|
user_message, knowledge_results, session["context"], session["work_order_id"], tenant_id=session_tenant
|
||||||
):
|
):
|
||||||
full_content.append(chunk)
|
full_content.append(chunk)
|
||||||
yield f"data: {json.dumps({'chunk': chunk}, ensure_ascii=False)}\n\n"
|
yield f"data: {json.dumps({'chunk': chunk}, ensure_ascii=False)}\n\n"
|
||||||
|
|||||||
@@ -97,17 +97,6 @@ class FeishuLongConnService:
|
|||||||
|
|
||||||
# 获取发送者ID和群信息
|
# 获取发送者ID和群信息
|
||||||
sender_id_obj = sender.sender_id
|
sender_id_obj = sender.sender_id
|
||||||
# 调试:打印 sender 对象的所有属性
|
|
||||||
try:
|
|
||||||
sender_attrs = {k: getattr(sender_id_obj, k, None) for k in ['user_id', 'open_id', 'union_id'] if getattr(sender_id_obj, k, None)}
|
|
||||||
if not sender_attrs:
|
|
||||||
# 尝试 __dict__ 或 dir
|
|
||||||
sender_attrs = {k: v for k, v in vars(sender_id_obj).items() if v and not k.startswith('_')} if hasattr(sender_id_obj, '__dict__') else {}
|
|
||||||
logger.info(f" sender_id 属性: {sender_attrs}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f" 无法解析 sender_id 属性: {e}, type={type(sender_id_obj)}")
|
|
||||||
sender_attrs = {}
|
|
||||||
|
|
||||||
sender_open_id = getattr(sender_id_obj, 'open_id', '') or ''
|
sender_open_id = getattr(sender_id_obj, 'open_id', '') or ''
|
||||||
sender_user_id = getattr(sender_id_obj, 'user_id', '') or ''
|
sender_user_id = getattr(sender_id_obj, 'user_id', '') or ''
|
||||||
sender_union_id = getattr(sender_id_obj, 'union_id', '') or ''
|
sender_union_id = getattr(sender_id_obj, 'union_id', '') or ''
|
||||||
|
|||||||
@@ -80,53 +80,8 @@ def cache_response(timeout=300):
|
|||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
def resolve_tenant_id(source='auto'):
|
# resolve_tenant_id 装饰器已移除 — 当前各端点手动处理 tenant_id,
|
||||||
"""
|
# 未来引入 Repository 层后可在 DAO 层统一拦截。
|
||||||
租户 ID 解析装饰器。
|
|
||||||
从请求中提取 tenant_id 并注入到 kwargs['tenant_id']。
|
|
||||||
|
|
||||||
source:
|
|
||||||
'auto' — 依次从 JSON body、query args、session 中查找
|
|
||||||
'query' — 仅从 query args
|
|
||||||
'body' — 仅从 JSON body
|
|
||||||
|
|
||||||
如果未找到,使用 DEFAULT_TENANT 并记录警告。
|
|
||||||
"""
|
|
||||||
import logging
|
|
||||||
_logger = logging.getLogger('tenant_resolver')
|
|
||||||
|
|
||||||
def decorator(f):
|
|
||||||
@wraps(f)
|
|
||||||
def decorated_function(*args, **kwargs):
|
|
||||||
from flask import request as req
|
|
||||||
from src.core.models import DEFAULT_TENANT
|
|
||||||
|
|
||||||
tenant_id = None
|
|
||||||
|
|
||||||
if source in ('auto', 'body'):
|
|
||||||
try:
|
|
||||||
data = req.get_json(silent=True)
|
|
||||||
if data:
|
|
||||||
tenant_id = data.get('tenant_id')
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if not tenant_id and source in ('auto', 'query'):
|
|
||||||
tenant_id = req.args.get('tenant_id')
|
|
||||||
|
|
||||||
if not tenant_id:
|
|
||||||
tenant_id = DEFAULT_TENANT
|
|
||||||
# 只在写操作时警告,读操作不警告(全局查询是合理的)
|
|
||||||
if req.method in ('POST', 'PUT', 'DELETE'):
|
|
||||||
_logger.warning(
|
|
||||||
f"⚠️ API {req.method} {req.path} 未指定 tenant_id,使用默认租户 '{DEFAULT_TENANT}'"
|
|
||||||
)
|
|
||||||
|
|
||||||
kwargs['tenant_id'] = tenant_id
|
|
||||||
return f(*args, **kwargs)
|
|
||||||
return decorated_function
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
|
|
||||||
# 简易内存限流器
|
# 简易内存限流器
|
||||||
_rate_limit_store = {}
|
_rate_limit_store = {}
|
||||||
|
|||||||
@@ -1,130 +0,0 @@
|
|||||||
/**
|
|
||||||
* 重构后的主应用文件
|
|
||||||
* 使用模块化架构整合所有功能
|
|
||||||
*/
|
|
||||||
|
|
||||||
// 全局变量声明
|
|
||||||
let alertManager;
|
|
||||||
let healthMonitor;
|
|
||||||
let agentMonitor;
|
|
||||||
|
|
||||||
// DOM加载完成后初始化应用
|
|
||||||
document.addEventListener('DOMContentLoaded', async () => {
|
|
||||||
try {
|
|
||||||
// 初始化各个管理器
|
|
||||||
alertManager = new AlertManager();
|
|
||||||
healthMonitor = new HealthMonitor();
|
|
||||||
agentMonitor = new AgentMonitor();
|
|
||||||
|
|
||||||
// 启动自动刷新
|
|
||||||
healthMonitor.startMonitoring();
|
|
||||||
|
|
||||||
console.log('TSP助手应用初始化完成');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('应用初始化失败:', error);
|
|
||||||
notificationManager.error('应用初始化失败,请刷新页面重试');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 健康监控组件
|
|
||||||
class HealthMonitor {
|
|
||||||
constructor() {
|
|
||||||
this.interval = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
startMonitoring() {
|
|
||||||
// 每5秒检查一次健康状态和监控状态
|
|
||||||
this.interval = setInterval(async () => {
|
|
||||||
try {
|
|
||||||
const [healthData, monitorData] = await Promise.all([
|
|
||||||
apiService.getHealth(),
|
|
||||||
apiService.getMonitorStatus()
|
|
||||||
]);
|
|
||||||
|
|
||||||
store.updateHealth(healthData);
|
|
||||||
store.updateMonitorStatus(monitorData.monitor_status);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('健康检查失败:', error);
|
|
||||||
}
|
|
||||||
}, 5000);
|
|
||||||
}
|
|
||||||
|
|
||||||
stopMonitoring() {
|
|
||||||
if (this.interval) {
|
|
||||||
clearInterval(this.interval);
|
|
||||||
this.interval = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Agent监控组件
|
|
||||||
class AgentMonitor {
|
|
||||||
constructor() {
|
|
||||||
this.interval = null;
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
// 监听Agent相关按钮
|
|
||||||
this.bindAgentControls();
|
|
||||||
this.loadAgentStatus();
|
|
||||||
}
|
|
||||||
|
|
||||||
bindAgentControls() {
|
|
||||||
const toggleBtn = document.getElementById('toggle-agent');
|
|
||||||
if (toggleBtn) {
|
|
||||||
toggleBtn.addEventListener('click', () => this.toggleAgent());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadAgentStatus() {
|
|
||||||
try {
|
|
||||||
const status = await apiService.getAgentStatus();
|
|
||||||
store.updateAgentStatus(status);
|
|
||||||
this.updateAgentDisplay();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('加载Agent状态失败:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async toggleAgent() {
|
|
||||||
try {
|
|
||||||
const currentStatus = store.getState().agentStatus;
|
|
||||||
const enabled = currentStatus.status === 'inactive';
|
|
||||||
|
|
||||||
const result = await apiService.toggleAgent(enabled);
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
notificationManager.success(`Agent已${enabled ? '启用' : '禁用'}`);
|
|
||||||
await this.loadAgentStatus();
|
|
||||||
} else {
|
|
||||||
notificationManager.error(result.message || '操作失败');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('切换Agent状态失败:', error);
|
|
||||||
notificationManager.error('操作失败');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateAgentDisplay() {
|
|
||||||
const status = store.getState().agentStatus;
|
|
||||||
const statusElement = document.getElementById('agent-status');
|
|
||||||
|
|
||||||
if (statusElement) {
|
|
||||||
const statusText = status.status === 'active' ? '运行中' : '未运行';
|
|
||||||
const statusClass = status.status === 'active' ? 'text-success' : 'text-secondary';
|
|
||||||
|
|
||||||
statusElement.innerHTML = `
|
|
||||||
<i class="fas fa-robot me-1 ${statusClass}"></i>
|
|
||||||
Agent: ${statusText}
|
|
||||||
<small class="text-muted">(${status.active_goals} 个活跃目标, ${status.available_tools} 个工具)</small>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 导出全局对象供HTML访问
|
|
||||||
window.alertManager = alertManager;
|
|
||||||
window.apiService = apiService;
|
|
||||||
window.store = store;
|
|
||||||
window.notificationManager = notificationManager;
|
|
||||||
@@ -1,556 +0,0 @@
|
|||||||
// TSP助手预警管理系统前端脚本
|
|
||||||
|
|
||||||
class AlertManager {
|
|
||||||
constructor() {
|
|
||||||
this.alerts = [];
|
|
||||||
this.rules = [];
|
|
||||||
this.health = {};
|
|
||||||
this.monitorStatus = 'unknown';
|
|
||||||
this.refreshInterval = null;
|
|
||||||
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
this.bindEvents();
|
|
||||||
this.loadInitialData();
|
|
||||||
this.startAutoRefresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
bindEvents() {
|
|
||||||
// 监控控制按钮
|
|
||||||
document.getElementById('start-monitor').addEventListener('click', () => this.startMonitoring());
|
|
||||||
document.getElementById('stop-monitor').addEventListener('click', () => this.stopMonitoring());
|
|
||||||
document.getElementById('check-alerts').addEventListener('click', () => this.checkAlerts());
|
|
||||||
document.getElementById('refresh-alerts').addEventListener('click', () => this.loadAlerts());
|
|
||||||
|
|
||||||
// 规则管理
|
|
||||||
document.getElementById('save-rule').addEventListener('click', () => this.saveRule());
|
|
||||||
document.getElementById('update-rule').addEventListener('click', () => this.updateRule());
|
|
||||||
|
|
||||||
// 预警过滤和排序
|
|
||||||
document.getElementById('alert-filter').addEventListener('change', () => this.updateAlertsDisplay());
|
|
||||||
document.getElementById('alert-sort').addEventListener('change', () => this.updateAlertsDisplay());
|
|
||||||
|
|
||||||
// 自动刷新
|
|
||||||
setInterval(() => {
|
|
||||||
this.loadHealth();
|
|
||||||
this.loadMonitorStatus();
|
|
||||||
}, 5000);
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadInitialData() {
|
|
||||||
await Promise.all([
|
|
||||||
this.loadHealth(),
|
|
||||||
this.loadAlerts(),
|
|
||||||
this.loadRules(),
|
|
||||||
this.loadMonitorStatus()
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
startAutoRefresh() {
|
|
||||||
this.refreshInterval = setInterval(() => {
|
|
||||||
this.loadAlerts();
|
|
||||||
}, 10000); // 每10秒刷新一次预警
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadHealth() {
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/health');
|
|
||||||
const data = await response.json();
|
|
||||||
this.health = data;
|
|
||||||
this.updateHealthDisplay();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('加载健康状态失败:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadAlerts() {
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/alerts');
|
|
||||||
const data = await response.json();
|
|
||||||
this.alerts = data;
|
|
||||||
this.updateAlertsDisplay();
|
|
||||||
this.updateAlertStatistics();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('加载预警失败:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadRules() {
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/rules');
|
|
||||||
const data = await response.json();
|
|
||||||
this.rules = data;
|
|
||||||
this.updateRulesDisplay();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('加载规则失败:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadMonitorStatus() {
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/monitor/status');
|
|
||||||
const data = await response.json();
|
|
||||||
this.monitorStatus = data.monitor_status;
|
|
||||||
this.updateMonitorStatusDisplay();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('加载监控状态失败:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateHealthDisplay() {
|
|
||||||
const healthScore = this.health.health_score || 0;
|
|
||||||
const healthStatus = this.health.status || 'unknown';
|
|
||||||
|
|
||||||
const scoreElement = document.getElementById('health-score-text');
|
|
||||||
const circleElement = document.getElementById('health-score-circle');
|
|
||||||
const statusElement = document.getElementById('health-status');
|
|
||||||
|
|
||||||
if (scoreElement) scoreElement.textContent = Math.round(healthScore);
|
|
||||||
if (statusElement) statusElement.textContent = this.getHealthStatusText(healthStatus);
|
|
||||||
|
|
||||||
if (circleElement) {
|
|
||||||
circleElement.className = `score-circle ${healthStatus}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateAlertsDisplay() {
|
|
||||||
const container = document.getElementById('alerts-container');
|
|
||||||
|
|
||||||
if (this.alerts.length === 0) {
|
|
||||||
container.innerHTML = `
|
|
||||||
<div class="empty-state">
|
|
||||||
<i class="fas fa-check-circle"></i>
|
|
||||||
<h5>暂无活跃预警</h5>
|
|
||||||
<p>系统运行正常,没有需要处理的预警</p>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 应用过滤和排序
|
|
||||||
let filteredAlerts = this.filterAndSortAlerts(this.alerts);
|
|
||||||
|
|
||||||
const alertsHtml = filteredAlerts.map(alert => {
|
|
||||||
const dataStr = alert.data ? JSON.stringify(alert.data, null, 2) : '无数据';
|
|
||||||
return `
|
|
||||||
<div class="alert-card ${alert.level}">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="d-flex justify-content-between align-items-start">
|
|
||||||
<div class="flex-grow-1">
|
|
||||||
<div class="d-flex align-items-center mb-2">
|
|
||||||
<span class="alert-level ${alert.level}">${this.getLevelText(alert.level)}</span>
|
|
||||||
<span class="ms-2 text-muted fw-bold">${alert.rule_name || '未知规则'}</span>
|
|
||||||
<span class="ms-auto text-muted small">${this.formatTime(alert.created_at)}</span>
|
|
||||||
</div>
|
|
||||||
<div class="alert-message">${alert.message}</div>
|
|
||||||
<div class="alert-meta">
|
|
||||||
类型: ${this.getTypeText(alert.alert_type)} |
|
|
||||||
级别: ${this.getLevelText(alert.level)}
|
|
||||||
</div>
|
|
||||||
<div class="alert-data">${dataStr}</div>
|
|
||||||
</div>
|
|
||||||
<div class="ms-3">
|
|
||||||
<button class="btn btn-sm btn-outline-success" onclick="alertManager.resolveAlert(${alert.id})">
|
|
||||||
<i class="fas fa-check me-1"></i>解决
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}).join('');
|
|
||||||
|
|
||||||
container.innerHTML = alertsHtml;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateRulesDisplay() {
|
|
||||||
const tbody = document.getElementById('rules-table');
|
|
||||||
|
|
||||||
if (this.rules.length === 0) {
|
|
||||||
tbody.innerHTML = `
|
|
||||||
<tr>
|
|
||||||
<td colspan="6" class="text-center text-muted">暂无规则</td>
|
|
||||||
</tr>
|
|
||||||
`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rulesHtml = this.rules.map(rule => `
|
|
||||||
<tr>
|
|
||||||
<td>${rule.name}</td>
|
|
||||||
<td>${this.getTypeText(rule.alert_type)}</td>
|
|
||||||
<td><span class="alert-level ${rule.level}">${this.getLevelText(rule.level)}</span></td>
|
|
||||||
<td>${rule.threshold}</td>
|
|
||||||
<td><span class="rule-status ${rule.enabled ? 'enabled' : 'disabled'}">${rule.enabled ? '启用' : '禁用'}</span></td>
|
|
||||||
<td>
|
|
||||||
<button class="btn btn-sm btn-outline-primary me-1" onclick="alertManager.editRule('${rule.name}')">
|
|
||||||
<i class="fas fa-edit"></i>
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-sm btn-outline-danger" onclick="alertManager.deleteRule('${rule.name}')">
|
|
||||||
<i class="fas fa-trash"></i>
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
`).join('');
|
|
||||||
|
|
||||||
tbody.innerHTML = rulesHtml;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateAlertStatistics() {
|
|
||||||
const stats = this.alerts.reduce((acc, alert) => {
|
|
||||||
acc[alert.level] = (acc[alert.level] || 0) + 1;
|
|
||||||
acc.total = (acc.total || 0) + 1;
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
document.getElementById('critical-alerts').textContent = stats.critical || 0;
|
|
||||||
document.getElementById('warning-alerts').textContent = stats.warning || 0;
|
|
||||||
document.getElementById('info-alerts').textContent = stats.info || 0;
|
|
||||||
document.getElementById('total-alerts').textContent = stats.total || 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateMonitorStatusDisplay() {
|
|
||||||
const statusElement = document.getElementById('monitor-status');
|
|
||||||
const icon = statusElement.querySelector('i');
|
|
||||||
const text = statusElement.querySelector('span') || statusElement;
|
|
||||||
|
|
||||||
let statusText = '';
|
|
||||||
let statusClass = '';
|
|
||||||
|
|
||||||
switch (this.monitorStatus) {
|
|
||||||
case 'running':
|
|
||||||
statusText = '监控运行中';
|
|
||||||
statusClass = 'text-success';
|
|
||||||
icon.className = 'fas fa-circle text-success';
|
|
||||||
break;
|
|
||||||
case 'stopped':
|
|
||||||
statusText = '监控已停止';
|
|
||||||
statusClass = 'text-danger';
|
|
||||||
icon.className = 'fas fa-circle text-danger';
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
statusText = '监控状态未知';
|
|
||||||
statusClass = 'text-warning';
|
|
||||||
icon.className = 'fas fa-circle text-warning';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (text.textContent) {
|
|
||||||
text.textContent = statusText;
|
|
||||||
} else {
|
|
||||||
statusElement.innerHTML = `<i class="fas fa-circle ${statusClass}"></i> ${statusText}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async startMonitoring() {
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/monitor/start', { method: 'POST' });
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (data.success) {
|
|
||||||
this.showNotification('监控服务已启动', 'success');
|
|
||||||
this.loadMonitorStatus();
|
|
||||||
} else {
|
|
||||||
this.showNotification(data.message || '启动监控失败', 'error');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('启动监控失败:', error);
|
|
||||||
this.showNotification('启动监控失败', 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async stopMonitoring() {
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/monitor/stop', { method: 'POST' });
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (data.success) {
|
|
||||||
this.showNotification('监控服务已停止', 'success');
|
|
||||||
this.loadMonitorStatus();
|
|
||||||
} else {
|
|
||||||
this.showNotification(data.message || '停止监控失败', 'error');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('停止监控失败:', error);
|
|
||||||
this.showNotification('停止监控失败', 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async checkAlerts() {
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/check-alerts', { method: 'POST' });
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (data.success) {
|
|
||||||
this.showNotification(`检查完成,发现 ${data.count} 个预警`, 'info');
|
|
||||||
this.loadAlerts();
|
|
||||||
} else {
|
|
||||||
this.showNotification('检查预警失败', 'error');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('检查预警失败:', error);
|
|
||||||
this.showNotification('检查预警失败', 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async resolveAlert(alertId) {
|
|
||||||
try {
|
|
||||||
const response = await fetch(`/api/alerts/${alertId}/resolve`, { method: 'POST' });
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (data.success) {
|
|
||||||
this.showNotification('预警已解决', 'success');
|
|
||||||
this.loadAlerts();
|
|
||||||
} else {
|
|
||||||
this.showNotification(data.message || '解决预警失败', 'error');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('解决预警失败:', error);
|
|
||||||
this.showNotification('解决预警失败', 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async saveRule() {
|
|
||||||
const formData = {
|
|
||||||
name: document.getElementById('rule-name').value,
|
|
||||||
description: document.getElementById('rule-description').value,
|
|
||||||
alert_type: document.getElementById('rule-type').value,
|
|
||||||
level: document.getElementById('rule-level').value,
|
|
||||||
threshold: parseFloat(document.getElementById('rule-threshold').value),
|
|
||||||
condition: document.getElementById('rule-condition').value,
|
|
||||||
enabled: document.getElementById('rule-enabled').checked,
|
|
||||||
check_interval: parseInt(document.getElementById('rule-interval').value),
|
|
||||||
cooldown: parseInt(document.getElementById('rule-cooldown').value)
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/rules', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify(formData)
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (data.success) {
|
|
||||||
this.showNotification('规则创建成功', 'success');
|
|
||||||
this.hideModal('ruleModal');
|
|
||||||
this.loadRules();
|
|
||||||
this.resetRuleForm();
|
|
||||||
} else {
|
|
||||||
this.showNotification(data.message || '创建规则失败', 'error');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('创建规则失败:', error);
|
|
||||||
this.showNotification('创建规则失败', 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteRule(ruleName) {
|
|
||||||
if (!confirm(`确定要删除规则 "${ruleName}" 吗?`)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(`/api/rules/${ruleName}`, { method: 'DELETE' });
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (data.success) {
|
|
||||||
this.showNotification('规则删除成功', 'success');
|
|
||||||
this.loadRules();
|
|
||||||
} else {
|
|
||||||
this.showNotification(data.message || '删除规则失败', 'error');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('删除规则失败:', error);
|
|
||||||
this.showNotification('删除规则失败', 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
filterAndSortAlerts(alerts) {
|
|
||||||
// 应用过滤
|
|
||||||
const filter = document.getElementById('alert-filter').value;
|
|
||||||
let filtered = alerts;
|
|
||||||
|
|
||||||
if (filter !== 'all') {
|
|
||||||
filtered = alerts.filter(alert => alert.level === filter);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 应用排序
|
|
||||||
const sort = document.getElementById('alert-sort').value;
|
|
||||||
filtered.sort((a, b) => {
|
|
||||||
switch (sort) {
|
|
||||||
case 'time-desc':
|
|
||||||
return new Date(b.created_at) - new Date(a.created_at);
|
|
||||||
case 'time-asc':
|
|
||||||
return new Date(a.created_at) - new Date(b.created_at);
|
|
||||||
case 'level-desc':
|
|
||||||
const levelOrder = { 'critical': 4, 'error': 3, 'warning': 2, 'info': 1 };
|
|
||||||
return (levelOrder[b.level] || 0) - (levelOrder[a.level] || 0);
|
|
||||||
case 'level-asc':
|
|
||||||
const levelOrderAsc = { 'critical': 4, 'error': 3, 'warning': 2, 'info': 1 };
|
|
||||||
return (levelOrderAsc[a.level] || 0) - (levelOrderAsc[b.level] || 0);
|
|
||||||
default:
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return filtered;
|
|
||||||
}
|
|
||||||
|
|
||||||
editRule(ruleName) {
|
|
||||||
// 查找规则数据
|
|
||||||
const rule = this.rules.find(r => r.name === ruleName);
|
|
||||||
if (!rule) {
|
|
||||||
this.showNotification('规则不存在', 'error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 填充编辑表单
|
|
||||||
document.getElementById('edit-rule-name-original').value = rule.name;
|
|
||||||
document.getElementById('edit-rule-name').value = rule.name;
|
|
||||||
document.getElementById('edit-rule-type').value = rule.alert_type;
|
|
||||||
document.getElementById('edit-rule-level').value = rule.level;
|
|
||||||
document.getElementById('edit-rule-threshold').value = rule.threshold;
|
|
||||||
document.getElementById('edit-rule-description').value = rule.description || '';
|
|
||||||
document.getElementById('edit-rule-condition').value = rule.condition;
|
|
||||||
document.getElementById('edit-rule-interval').value = rule.check_interval;
|
|
||||||
document.getElementById('edit-rule-cooldown').value = rule.cooldown;
|
|
||||||
document.getElementById('edit-rule-enabled').checked = rule.enabled;
|
|
||||||
|
|
||||||
// 显示编辑模态框
|
|
||||||
const modal = new bootstrap.Modal(document.getElementById('editRuleModal'));
|
|
||||||
modal.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateRule() {
|
|
||||||
const originalName = document.getElementById('edit-rule-name-original').value;
|
|
||||||
const formData = {
|
|
||||||
name: document.getElementById('edit-rule-name').value,
|
|
||||||
description: document.getElementById('edit-rule-description').value,
|
|
||||||
alert_type: document.getElementById('edit-rule-type').value,
|
|
||||||
level: document.getElementById('edit-rule-level').value,
|
|
||||||
threshold: parseFloat(document.getElementById('edit-rule-threshold').value),
|
|
||||||
condition: document.getElementById('edit-rule-condition').value,
|
|
||||||
enabled: document.getElementById('edit-rule-enabled').checked,
|
|
||||||
check_interval: parseInt(document.getElementById('edit-rule-interval').value),
|
|
||||||
cooldown: parseInt(document.getElementById('edit-rule-cooldown').value)
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(`/api/rules/${originalName}`, {
|
|
||||||
method: 'PUT',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify(formData)
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (data.success) {
|
|
||||||
this.showNotification('规则更新成功', 'success');
|
|
||||||
this.hideModal('editRuleModal');
|
|
||||||
this.loadRules();
|
|
||||||
} else {
|
|
||||||
this.showNotification(data.message || '更新规则失败', 'error');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('更新规则失败:', error);
|
|
||||||
this.showNotification('更新规则失败', 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resetRuleForm() {
|
|
||||||
document.getElementById('rule-form').reset();
|
|
||||||
document.getElementById('rule-interval').value = '300';
|
|
||||||
document.getElementById('rule-cooldown').value = '3600';
|
|
||||||
document.getElementById('rule-enabled').checked = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
hideModal(modalId) {
|
|
||||||
const modal = document.getElementById(modalId);
|
|
||||||
const bsModal = bootstrap.Modal.getInstance(modal);
|
|
||||||
if (bsModal) {
|
|
||||||
bsModal.hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
showNotification(message, type = 'info') {
|
|
||||||
// 创建通知元素
|
|
||||||
const notification = document.createElement('div');
|
|
||||||
notification.className = `alert alert-${type === 'error' ? 'danger' : type} alert-dismissible fade show position-fixed`;
|
|
||||||
notification.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 300px;';
|
|
||||||
notification.innerHTML = `
|
|
||||||
${message}
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
||||||
`;
|
|
||||||
|
|
||||||
document.body.appendChild(notification);
|
|
||||||
|
|
||||||
// 3秒后自动移除
|
|
||||||
setTimeout(() => {
|
|
||||||
if (notification.parentNode) {
|
|
||||||
notification.parentNode.removeChild(notification);
|
|
||||||
}
|
|
||||||
}, 3000);
|
|
||||||
}
|
|
||||||
|
|
||||||
getLevelText(level) {
|
|
||||||
const levelMap = {
|
|
||||||
'critical': '严重',
|
|
||||||
'error': '错误',
|
|
||||||
'warning': '警告',
|
|
||||||
'info': '信息'
|
|
||||||
};
|
|
||||||
return levelMap[level] || level;
|
|
||||||
}
|
|
||||||
|
|
||||||
getTypeText(type) {
|
|
||||||
const typeMap = {
|
|
||||||
'performance': '性能',
|
|
||||||
'quality': '质量',
|
|
||||||
'volume': '量级',
|
|
||||||
'system': '系统',
|
|
||||||
'business': '业务'
|
|
||||||
};
|
|
||||||
return typeMap[type] || type;
|
|
||||||
}
|
|
||||||
|
|
||||||
getHealthStatusText(status) {
|
|
||||||
const statusMap = {
|
|
||||||
'excellent': '优秀',
|
|
||||||
'good': '良好',
|
|
||||||
'fair': '一般',
|
|
||||||
'poor': '较差',
|
|
||||||
'critical': '严重',
|
|
||||||
'unknown': '未知'
|
|
||||||
};
|
|
||||||
return statusMap[status] || status;
|
|
||||||
}
|
|
||||||
|
|
||||||
formatTime(timestamp) {
|
|
||||||
const date = new Date(timestamp);
|
|
||||||
const now = new Date();
|
|
||||||
const diff = now - date;
|
|
||||||
|
|
||||||
if (diff < 60000) { // 1分钟内
|
|
||||||
return '刚刚';
|
|
||||||
} else if (diff < 3600000) { // 1小时内
|
|
||||||
return `${Math.floor(diff / 60000)}分钟前`;
|
|
||||||
} else if (diff < 86400000) { // 1天内
|
|
||||||
return `${Math.floor(diff / 3600000)}小时前`;
|
|
||||||
} else {
|
|
||||||
return date.toLocaleDateString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化应用
|
|
||||||
let alertManager;
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
alertManager = new AlertManager();
|
|
||||||
});
|
|
||||||
@@ -1,410 +0,0 @@
|
|||||||
// 实时对话前端脚本
|
|
||||||
|
|
||||||
class ChatClient {
|
|
||||||
constructor() {
|
|
||||||
this.websocket = null;
|
|
||||||
this.sessionId = null;
|
|
||||||
this.isConnected = false;
|
|
||||||
this.messageCount = 0;
|
|
||||||
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
this.bindEvents();
|
|
||||||
this.updateConnectionStatus(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
bindEvents() {
|
|
||||||
// 开始对话
|
|
||||||
document.getElementById('start-chat').addEventListener('click', () => this.startChat());
|
|
||||||
|
|
||||||
// 结束对话
|
|
||||||
document.getElementById('end-chat').addEventListener('click', () => this.endChat());
|
|
||||||
|
|
||||||
// 发送消息
|
|
||||||
document.getElementById('send-button').addEventListener('click', () => this.sendMessage());
|
|
||||||
|
|
||||||
// 回车发送
|
|
||||||
document.getElementById('message-input').addEventListener('keypress', (e) => {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
this.sendMessage();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 创建工单
|
|
||||||
document.getElementById('create-work-order').addEventListener('click', () => this.showWorkOrderModal());
|
|
||||||
document.getElementById('create-work-order-btn').addEventListener('click', () => this.createWorkOrder());
|
|
||||||
|
|
||||||
// 快速操作按钮
|
|
||||||
document.querySelectorAll('.quick-action-btn').forEach(btn => {
|
|
||||||
btn.addEventListener('click', (e) => {
|
|
||||||
const message = e.target.getAttribute('data-message');
|
|
||||||
document.getElementById('message-input').value = message;
|
|
||||||
this.sendMessage();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async startChat() {
|
|
||||||
try {
|
|
||||||
// 连接WebSocket
|
|
||||||
await this.connectWebSocket();
|
|
||||||
|
|
||||||
// 创建会话
|
|
||||||
const userId = document.getElementById('user-id').value || 'anonymous';
|
|
||||||
const workOrderId = document.getElementById('work-order-id').value || null;
|
|
||||||
|
|
||||||
const response = await this.sendWebSocketMessage({
|
|
||||||
type: 'create_session',
|
|
||||||
user_id: userId,
|
|
||||||
work_order_id: workOrderId ? parseInt(workOrderId) : null
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.type === 'session_created') {
|
|
||||||
this.sessionId = response.session_id;
|
|
||||||
this.updateSessionInfo();
|
|
||||||
this.enableChat();
|
|
||||||
this.addSystemMessage('对话已开始,请描述您的问题。');
|
|
||||||
} else {
|
|
||||||
this.showError('创建会话失败');
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('启动对话失败:', error);
|
|
||||||
this.showError('启动对话失败: ' + error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async endChat() {
|
|
||||||
try {
|
|
||||||
if (this.sessionId) {
|
|
||||||
await this.sendWebSocketMessage({
|
|
||||||
type: 'end_session',
|
|
||||||
session_id: this.sessionId
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.sessionId = null;
|
|
||||||
this.disableChat();
|
|
||||||
this.addSystemMessage('对话已结束。');
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('结束对话失败:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async sendMessage() {
|
|
||||||
const input = document.getElementById('message-input');
|
|
||||||
const message = input.value.trim();
|
|
||||||
|
|
||||||
if (!message || !this.sessionId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清空输入框
|
|
||||||
input.value = '';
|
|
||||||
|
|
||||||
// 添加用户消息
|
|
||||||
this.addMessage('user', message);
|
|
||||||
|
|
||||||
// 显示打字指示器
|
|
||||||
this.showTypingIndicator();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await this.sendWebSocketMessage({
|
|
||||||
type: 'send_message',
|
|
||||||
session_id: this.sessionId,
|
|
||||||
message: message
|
|
||||||
});
|
|
||||||
|
|
||||||
this.hideTypingIndicator();
|
|
||||||
|
|
||||||
if (response.type === 'message_response' && response.result.success) {
|
|
||||||
const result = response.result;
|
|
||||||
|
|
||||||
// 添加助手回复
|
|
||||||
this.addMessage('assistant', result.content, {
|
|
||||||
knowledge_used: result.knowledge_used,
|
|
||||||
confidence_score: result.confidence_score,
|
|
||||||
work_order_id: result.work_order_id
|
|
||||||
});
|
|
||||||
|
|
||||||
// 更新工单ID
|
|
||||||
if (result.work_order_id) {
|
|
||||||
document.getElementById('work-order-id').value = result.work_order_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
this.addMessage('assistant', '抱歉,我暂时无法处理您的问题。请稍后再试。');
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
this.hideTypingIndicator();
|
|
||||||
console.error('发送消息失败:', error);
|
|
||||||
this.addMessage('assistant', '发送消息失败,请检查网络连接。');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async createWorkOrder() {
|
|
||||||
const title = document.getElementById('wo-title').value;
|
|
||||||
const description = document.getElementById('wo-description').value;
|
|
||||||
const category = document.getElementById('wo-category').value;
|
|
||||||
const priority = document.getElementById('wo-priority').value;
|
|
||||||
|
|
||||||
if (!title || !description) {
|
|
||||||
this.showError('请填写工单标题和描述');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await this.sendWebSocketMessage({
|
|
||||||
type: 'create_work_order',
|
|
||||||
session_id: this.sessionId,
|
|
||||||
title: title,
|
|
||||||
description: description,
|
|
||||||
category: category,
|
|
||||||
priority: priority
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.type === 'work_order_created' && response.result.success) {
|
|
||||||
const workOrderId = response.result.work_order_id;
|
|
||||||
document.getElementById('work-order-id').value = workOrderId;
|
|
||||||
this.addSystemMessage(`工单创建成功!工单号: ${response.result.order_id}`);
|
|
||||||
|
|
||||||
// 关闭模态框
|
|
||||||
const modal = bootstrap.Modal.getInstance(document.getElementById('workOrderModal'));
|
|
||||||
modal.hide();
|
|
||||||
|
|
||||||
// 清空表单
|
|
||||||
document.getElementById('work-order-form').reset();
|
|
||||||
|
|
||||||
} else {
|
|
||||||
this.showError('创建工单失败: ' + (response.result.error || '未知错误'));
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('创建工单失败:', error);
|
|
||||||
this.showError('创建工单失败: ' + error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
connectWebSocket() {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
try {
|
|
||||||
this.websocket = new WebSocket('ws://localhost:8765');
|
|
||||||
|
|
||||||
// 设置连接超时
|
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
if (this.websocket.readyState !== WebSocket.OPEN) {
|
|
||||||
this.websocket.close();
|
|
||||||
reject(new Error('WebSocket连接超时,请检查服务器是否启动'));
|
|
||||||
}
|
|
||||||
}, 5000); // 5秒超时
|
|
||||||
|
|
||||||
this.websocket.onopen = () => {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
this.isConnected = true;
|
|
||||||
this.updateConnectionStatus(true);
|
|
||||||
resolve();
|
|
||||||
};
|
|
||||||
|
|
||||||
this.websocket.onclose = () => {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
this.isConnected = false;
|
|
||||||
this.updateConnectionStatus(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
this.websocket.onerror = (error) => {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
console.error('WebSocket错误:', error);
|
|
||||||
reject(new Error('WebSocket连接失败,请检查服务器是否启动'));
|
|
||||||
};
|
|
||||||
|
|
||||||
this.websocket.onmessage = (event) => {
|
|
||||||
try {
|
|
||||||
const data = JSON.parse(event.data);
|
|
||||||
this.handleWebSocketMessage(data);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('解析WebSocket消息失败:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
reject(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
sendWebSocketMessage(message) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
if (!this.websocket || this.websocket.readyState !== WebSocket.OPEN) {
|
|
||||||
reject(new Error('WebSocket未连接'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const messageId = 'msg_' + Date.now();
|
|
||||||
message.messageId = messageId;
|
|
||||||
|
|
||||||
// 设置超时
|
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
reject(new Error('请求超时'));
|
|
||||||
}, 10000);
|
|
||||||
|
|
||||||
// 监听响应
|
|
||||||
const handleResponse = (event) => {
|
|
||||||
try {
|
|
||||||
const data = JSON.parse(event.data);
|
|
||||||
if (data.messageId === messageId) {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
this.websocket.removeEventListener('message', handleResponse);
|
|
||||||
resolve(data);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// 忽略解析错误
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.websocket.addEventListener('message', handleResponse);
|
|
||||||
this.websocket.send(JSON.stringify(message));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
handleWebSocketMessage(data) {
|
|
||||||
// 处理WebSocket消息
|
|
||||||
console.log('收到WebSocket消息:', data);
|
|
||||||
}
|
|
||||||
|
|
||||||
addMessage(role, content, metadata = {}) {
|
|
||||||
const messagesContainer = document.getElementById('chat-messages');
|
|
||||||
|
|
||||||
// 如果是第一条消息,清空欢迎信息
|
|
||||||
if (this.messageCount === 0) {
|
|
||||||
messagesContainer.innerHTML = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
const messageDiv = document.createElement('div');
|
|
||||||
messageDiv.className = `message ${role}`;
|
|
||||||
|
|
||||||
const avatar = document.createElement('div');
|
|
||||||
avatar.className = 'message-avatar';
|
|
||||||
avatar.textContent = role === 'user' ? 'U' : 'A';
|
|
||||||
|
|
||||||
const contentDiv = document.createElement('div');
|
|
||||||
contentDiv.className = 'message-content';
|
|
||||||
contentDiv.innerHTML = content;
|
|
||||||
|
|
||||||
// 添加时间戳
|
|
||||||
const timeDiv = document.createElement('div');
|
|
||||||
timeDiv.className = 'message-time';
|
|
||||||
timeDiv.textContent = new Date().toLocaleTimeString();
|
|
||||||
contentDiv.appendChild(timeDiv);
|
|
||||||
|
|
||||||
// 添加元数据
|
|
||||||
if (metadata.knowledge_used && metadata.knowledge_used.length > 0) {
|
|
||||||
const knowledgeDiv = document.createElement('div');
|
|
||||||
knowledgeDiv.className = 'knowledge-info';
|
|
||||||
knowledgeDiv.innerHTML = `<i class="fas fa-lightbulb me-1"></i>基于 ${metadata.knowledge_used.length} 条知识库信息生成`;
|
|
||||||
contentDiv.appendChild(knowledgeDiv);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (metadata.confidence_score) {
|
|
||||||
const confidenceDiv = document.createElement('div');
|
|
||||||
confidenceDiv.className = 'confidence-score';
|
|
||||||
confidenceDiv.textContent = `置信度: ${(metadata.confidence_score * 100).toFixed(1)}%`;
|
|
||||||
contentDiv.appendChild(confidenceDiv);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (metadata.work_order_id) {
|
|
||||||
const workOrderDiv = document.createElement('div');
|
|
||||||
workOrderDiv.className = 'work-order-info';
|
|
||||||
workOrderDiv.innerHTML = `<i class="fas fa-ticket-alt me-1"></i>关联工单: ${metadata.work_order_id}`;
|
|
||||||
contentDiv.appendChild(workOrderDiv);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (role === 'user') {
|
|
||||||
messageDiv.appendChild(contentDiv);
|
|
||||||
messageDiv.appendChild(avatar);
|
|
||||||
} else {
|
|
||||||
messageDiv.appendChild(avatar);
|
|
||||||
messageDiv.appendChild(contentDiv);
|
|
||||||
}
|
|
||||||
|
|
||||||
messagesContainer.appendChild(messageDiv);
|
|
||||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
|
||||||
|
|
||||||
this.messageCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
addSystemMessage(content) {
|
|
||||||
const messagesContainer = document.getElementById('chat-messages');
|
|
||||||
|
|
||||||
const messageDiv = document.createElement('div');
|
|
||||||
messageDiv.className = 'text-center text-muted py-2';
|
|
||||||
messageDiv.innerHTML = `<small><i class="fas fa-info-circle me-1"></i>${content}</small>`;
|
|
||||||
|
|
||||||
messagesContainer.appendChild(messageDiv);
|
|
||||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
showTypingIndicator() {
|
|
||||||
document.getElementById('typing-indicator').classList.add('show');
|
|
||||||
}
|
|
||||||
|
|
||||||
hideTypingIndicator() {
|
|
||||||
document.getElementById('typing-indicator').classList.remove('show');
|
|
||||||
}
|
|
||||||
|
|
||||||
updateConnectionStatus(connected) {
|
|
||||||
const statusElement = document.getElementById('connection-status');
|
|
||||||
if (connected) {
|
|
||||||
statusElement.className = 'connection-status connected';
|
|
||||||
statusElement.innerHTML = '<i class="fas fa-circle me-1"></i>已连接';
|
|
||||||
} else {
|
|
||||||
statusElement.className = 'connection-status disconnected';
|
|
||||||
statusElement.innerHTML = '<i class="fas fa-circle me-1"></i>未连接';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateSessionInfo() {
|
|
||||||
const sessionInfo = document.getElementById('session-info');
|
|
||||||
sessionInfo.innerHTML = `
|
|
||||||
<div><strong>会话ID:</strong> ${this.sessionId}</div>
|
|
||||||
<div><strong>消息数:</strong> ${this.messageCount}</div>
|
|
||||||
<div><strong>状态:</strong> 活跃</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
enableChat() {
|
|
||||||
document.getElementById('start-chat').disabled = true;
|
|
||||||
document.getElementById('end-chat').disabled = false;
|
|
||||||
document.getElementById('message-input').disabled = false;
|
|
||||||
document.getElementById('send-button').disabled = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
disableChat() {
|
|
||||||
document.getElementById('start-chat').disabled = false;
|
|
||||||
document.getElementById('end-chat').disabled = true;
|
|
||||||
document.getElementById('message-input').disabled = true;
|
|
||||||
document.getElementById('send-button').disabled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
showWorkOrderModal() {
|
|
||||||
if (!this.sessionId) {
|
|
||||||
this.showError('请先开始对话');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const modal = new bootstrap.Modal(document.getElementById('workOrderModal'));
|
|
||||||
modal.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
showError(message) {
|
|
||||||
this.addSystemMessage(`<span class="text-danger">错误: ${message}</span>`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化聊天客户端
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
window.chatClient = new ChatClient();
|
|
||||||
});
|
|
||||||
@@ -1,382 +0,0 @@
|
|||||||
// HTTP版本实时对话前端脚本
|
|
||||||
|
|
||||||
class ChatHttpClient {
|
|
||||||
constructor() {
|
|
||||||
this.sessionId = null;
|
|
||||||
this.messageCount = 0;
|
|
||||||
this.apiBase = '/api/chat';
|
|
||||||
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
this.bindEvents();
|
|
||||||
this.updateConnectionStatus(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
bindEvents() {
|
|
||||||
// 开始对话
|
|
||||||
document.getElementById('start-chat').addEventListener('click', () => this.startChat());
|
|
||||||
|
|
||||||
// 结束对话
|
|
||||||
document.getElementById('end-chat').addEventListener('click', () => this.endChat());
|
|
||||||
|
|
||||||
// 发送消息
|
|
||||||
document.getElementById('send-button').addEventListener('click', () => this.sendMessage());
|
|
||||||
|
|
||||||
// 回车发送
|
|
||||||
document.getElementById('message-input').addEventListener('keypress', (e) => {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
this.sendMessage();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 创建工单
|
|
||||||
document.getElementById('create-work-order').addEventListener('click', () => this.showWorkOrderModal());
|
|
||||||
document.getElementById('create-work-order-btn').addEventListener('click', () => this.createWorkOrder());
|
|
||||||
|
|
||||||
// 快速操作按钮
|
|
||||||
document.querySelectorAll('.quick-action-btn').forEach(btn => {
|
|
||||||
btn.addEventListener('click', (e) => {
|
|
||||||
const message = e.target.getAttribute('data-message');
|
|
||||||
document.getElementById('message-input').value = message;
|
|
||||||
this.sendMessage();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async startChat() {
|
|
||||||
try {
|
|
||||||
// 创建会话
|
|
||||||
const userId = document.getElementById('user-id').value || 'anonymous';
|
|
||||||
const workOrderId = document.getElementById('work-order-id').value || null;
|
|
||||||
|
|
||||||
const response = await this.sendRequest('POST', '/session', {
|
|
||||||
user_id: userId,
|
|
||||||
work_order_id: workOrderId ? parseInt(workOrderId) : null
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.success) {
|
|
||||||
this.sessionId = response.session_id;
|
|
||||||
this.updateSessionInfo();
|
|
||||||
this.enableChat();
|
|
||||||
this.addSystemMessage('对话已开始,请描述您的问题。');
|
|
||||||
} else {
|
|
||||||
this.showError('创建会话失败');
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('启动对话失败:', error);
|
|
||||||
this.showError('启动对话失败: ' + error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async endChat() {
|
|
||||||
try {
|
|
||||||
if (this.sessionId) {
|
|
||||||
await this.sendRequest('DELETE', `/session/${this.sessionId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.sessionId = null;
|
|
||||||
this.disableChat();
|
|
||||||
this.addSystemMessage('对话已结束。');
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('结束对话失败:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async sendMessage() {
|
|
||||||
const input = document.getElementById('message-input');
|
|
||||||
const message = input.value.trim();
|
|
||||||
|
|
||||||
if (!message || !this.sessionId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清空输入框
|
|
||||||
input.value = '';
|
|
||||||
|
|
||||||
// 添加用户消息
|
|
||||||
this.addMessage('user', message);
|
|
||||||
|
|
||||||
// 显示打字指示器
|
|
||||||
this.showTypingIndicator();
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 使用流式接口
|
|
||||||
const response = await fetch('/api/chat/message/stream', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ session_id: this.sessionId, message: message })
|
|
||||||
});
|
|
||||||
|
|
||||||
this.hideTypingIndicator();
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
this.addMessage('assistant', '请求失败,请稍后再试。');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建一个空的助手消息容器用于流式填充
|
|
||||||
const msgEl = this.addMessage('assistant', '', {}, true);
|
|
||||||
const contentEl = msgEl.querySelector('.message-content') || msgEl;
|
|
||||||
let fullContent = '';
|
|
||||||
|
|
||||||
const reader = response.body.getReader();
|
|
||||||
const decoder = new TextDecoder();
|
|
||||||
let buffer = '';
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
const { done, value } = await reader.read();
|
|
||||||
if (done) break;
|
|
||||||
|
|
||||||
buffer += decoder.decode(value, { stream: true });
|
|
||||||
const lines = buffer.split('\n');
|
|
||||||
buffer = lines.pop(); // 保留不完整的行
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
if (!line.startsWith('data: ')) continue;
|
|
||||||
const dataStr = line.slice(6).trim();
|
|
||||||
if (dataStr === '[DONE]') continue;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const data = JSON.parse(dataStr);
|
|
||||||
if (data.chunk) {
|
|
||||||
fullContent += data.chunk;
|
|
||||||
contentEl.textContent = fullContent;
|
|
||||||
// 自动滚动
|
|
||||||
const chatMessages = document.getElementById('chat-messages');
|
|
||||||
if (chatMessages) chatMessages.scrollTop = chatMessages.scrollHeight;
|
|
||||||
}
|
|
||||||
if (data.done) {
|
|
||||||
// 流结束,可以拿到 confidence_score 等元数据
|
|
||||||
if (data.confidence_score != null) {
|
|
||||||
msgEl.dataset.confidence = data.confidence_score;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (data.error) {
|
|
||||||
fullContent += `\n[错误: ${data.error}]`;
|
|
||||||
contentEl.textContent = fullContent;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// 忽略解析错误
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!fullContent) {
|
|
||||||
contentEl.textContent = '抱歉,我暂时无法处理您的问题。请稍后再试。';
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
this.hideTypingIndicator();
|
|
||||||
console.error('发送消息失败:', error);
|
|
||||||
this.addMessage('assistant', '发送消息失败,请检查网络连接。');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async createWorkOrder() {
|
|
||||||
const title = document.getElementById('wo-title').value;
|
|
||||||
const description = document.getElementById('wo-description').value;
|
|
||||||
const category = document.getElementById('wo-category').value;
|
|
||||||
const priority = document.getElementById('wo-priority').value;
|
|
||||||
|
|
||||||
if (!title || !description) {
|
|
||||||
this.showError('请填写工单标题和描述');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await this.sendRequest('POST', '/work-order', {
|
|
||||||
session_id: this.sessionId,
|
|
||||||
title: title,
|
|
||||||
description: description,
|
|
||||||
category: category,
|
|
||||||
priority: priority
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.success) {
|
|
||||||
const workOrderId = response.work_order_id;
|
|
||||||
document.getElementById('work-order-id').value = workOrderId;
|
|
||||||
this.addSystemMessage(`工单创建成功!工单号: ${response.order_id}`);
|
|
||||||
|
|
||||||
// 关闭模态框
|
|
||||||
const modal = bootstrap.Modal.getInstance(document.getElementById('workOrderModal'));
|
|
||||||
modal.hide();
|
|
||||||
|
|
||||||
// 清空表单
|
|
||||||
document.getElementById('work-order-form').reset();
|
|
||||||
|
|
||||||
} else {
|
|
||||||
this.showError('创建工单失败: ' + (response.error || '未知错误'));
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('创建工单失败:', error);
|
|
||||||
this.showError('创建工单失败: ' + error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async sendRequest(method, endpoint, data = null) {
|
|
||||||
const url = this.apiBase + endpoint;
|
|
||||||
const options = {
|
|
||||||
method: method,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (data) {
|
|
||||||
options.body = JSON.stringify(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(url, options);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP错误: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await response.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
addMessage(role, content, metadata = {}, streaming = false) {
|
|
||||||
const messagesContainer = document.getElementById('chat-messages');
|
|
||||||
|
|
||||||
// 如果是第一条消息,清空欢迎信息
|
|
||||||
if (this.messageCount === 0) {
|
|
||||||
messagesContainer.innerHTML = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
const messageDiv = document.createElement('div');
|
|
||||||
messageDiv.className = `message ${role}`;
|
|
||||||
|
|
||||||
const avatar = document.createElement('div');
|
|
||||||
avatar.className = 'message-avatar';
|
|
||||||
avatar.textContent = role === 'user' ? 'U' : 'A';
|
|
||||||
|
|
||||||
const contentDiv = document.createElement('div');
|
|
||||||
contentDiv.className = 'message-content';
|
|
||||||
if (!streaming) {
|
|
||||||
contentDiv.innerHTML = content;
|
|
||||||
} else {
|
|
||||||
contentDiv.textContent = content;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加时间戳
|
|
||||||
const timeDiv = document.createElement('div');
|
|
||||||
timeDiv.className = 'message-time';
|
|
||||||
timeDiv.textContent = new Date().toLocaleTimeString();
|
|
||||||
if (!streaming) {
|
|
||||||
contentDiv.appendChild(timeDiv);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加元数据
|
|
||||||
if (metadata.knowledge_used && metadata.knowledge_used.length > 0) {
|
|
||||||
const knowledgeDiv = document.createElement('div');
|
|
||||||
knowledgeDiv.className = 'knowledge-info';
|
|
||||||
knowledgeDiv.innerHTML = `<i class="fas fa-lightbulb me-1"></i>基于 ${metadata.knowledge_used.length} 条知识库信息生成`;
|
|
||||||
contentDiv.appendChild(knowledgeDiv);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (metadata.confidence_score) {
|
|
||||||
const confidenceDiv = document.createElement('div');
|
|
||||||
confidenceDiv.className = 'confidence-score';
|
|
||||||
confidenceDiv.textContent = `置信度: ${(metadata.confidence_score * 100).toFixed(1)}%`;
|
|
||||||
contentDiv.appendChild(confidenceDiv);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (metadata.work_order_id) {
|
|
||||||
const workOrderDiv = document.createElement('div');
|
|
||||||
workOrderDiv.className = 'work-order-info';
|
|
||||||
workOrderDiv.innerHTML = `<i class="fas fa-ticket-alt me-1"></i>关联工单: ${metadata.work_order_id}`;
|
|
||||||
contentDiv.appendChild(workOrderDiv);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (role === 'user') {
|
|
||||||
messageDiv.appendChild(contentDiv);
|
|
||||||
messageDiv.appendChild(avatar);
|
|
||||||
} else {
|
|
||||||
messageDiv.appendChild(avatar);
|
|
||||||
messageDiv.appendChild(contentDiv);
|
|
||||||
}
|
|
||||||
|
|
||||||
messagesContainer.appendChild(messageDiv);
|
|
||||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
|
||||||
|
|
||||||
this.messageCount++;
|
|
||||||
return messageDiv;
|
|
||||||
}
|
|
||||||
|
|
||||||
addSystemMessage(content) {
|
|
||||||
const messagesContainer = document.getElementById('chat-messages');
|
|
||||||
|
|
||||||
const messageDiv = document.createElement('div');
|
|
||||||
messageDiv.className = 'text-center text-muted py-2';
|
|
||||||
messageDiv.innerHTML = `<small><i class="fas fa-info-circle me-1"></i>${content}</small>`;
|
|
||||||
|
|
||||||
messagesContainer.appendChild(messageDiv);
|
|
||||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
showTypingIndicator() {
|
|
||||||
document.getElementById('typing-indicator').classList.add('show');
|
|
||||||
}
|
|
||||||
|
|
||||||
hideTypingIndicator() {
|
|
||||||
document.getElementById('typing-indicator').classList.remove('show');
|
|
||||||
}
|
|
||||||
|
|
||||||
updateConnectionStatus(connected) {
|
|
||||||
const statusElement = document.getElementById('connection-status');
|
|
||||||
if (connected) {
|
|
||||||
statusElement.className = 'connection-status connected';
|
|
||||||
statusElement.innerHTML = '<i class="fas fa-circle me-1"></i>HTTP连接';
|
|
||||||
} else {
|
|
||||||
statusElement.className = 'connection-status disconnected';
|
|
||||||
statusElement.innerHTML = '<i class="fas fa-circle me-1"></i>连接断开';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateSessionInfo() {
|
|
||||||
const sessionInfo = document.getElementById('session-info');
|
|
||||||
sessionInfo.innerHTML = `
|
|
||||||
<div><strong>会话ID:</strong> ${this.sessionId}</div>
|
|
||||||
<div><strong>消息数:</strong> ${this.messageCount}</div>
|
|
||||||
<div><strong>状态:</strong> 活跃</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
enableChat() {
|
|
||||||
document.getElementById('start-chat').disabled = true;
|
|
||||||
document.getElementById('end-chat').disabled = false;
|
|
||||||
document.getElementById('message-input').disabled = false;
|
|
||||||
document.getElementById('send-button').disabled = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
disableChat() {
|
|
||||||
document.getElementById('start-chat').disabled = false;
|
|
||||||
document.getElementById('end-chat').disabled = true;
|
|
||||||
document.getElementById('message-input').disabled = true;
|
|
||||||
document.getElementById('send-button').disabled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
showWorkOrderModal() {
|
|
||||||
if (!this.sessionId) {
|
|
||||||
this.showError('请先开始对话');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const modal = new bootstrap.Modal(document.getElementById('workOrderModal'));
|
|
||||||
modal.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
showError(message) {
|
|
||||||
this.addSystemMessage(`<span class="text-danger">错误: ${message}</span>`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化聊天客户端
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
window.chatClient = new ChatHttpClient();
|
|
||||||
});
|
|
||||||
@@ -1,407 +0,0 @@
|
|||||||
/**
|
|
||||||
* 主入口文件
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { ready, storage } from './core/utils.js';
|
|
||||||
import store from './core/store.js';
|
|
||||||
import router from './core/router.js';
|
|
||||||
import { initWebSocket } from './core/websocket.js';
|
|
||||||
import Navbar from './components/navbar.js';
|
|
||||||
import Sidebar from './components/sidebar.js';
|
|
||||||
import { showToast } from './components/modal.js';
|
|
||||||
|
|
||||||
// 应用主类
|
|
||||||
class App {
|
|
||||||
constructor() {
|
|
||||||
this.components = {};
|
|
||||||
this.currentRoute = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化应用
|
|
||||||
async init() {
|
|
||||||
try {
|
|
||||||
// 显示加载状态
|
|
||||||
this.showLoading();
|
|
||||||
|
|
||||||
// 初始化路由
|
|
||||||
router.start();
|
|
||||||
|
|
||||||
// 初始化UI组件
|
|
||||||
this.initComponents();
|
|
||||||
|
|
||||||
// 恢复应用状态
|
|
||||||
this.restoreAppState();
|
|
||||||
|
|
||||||
// 初始化WebSocket
|
|
||||||
initWebSocket();
|
|
||||||
|
|
||||||
// 绑定全局事件
|
|
||||||
this.bindGlobalEvents();
|
|
||||||
|
|
||||||
// 注册服务工作者(PWA支持)
|
|
||||||
this.registerServiceWorker();
|
|
||||||
|
|
||||||
// 隐藏加载状态
|
|
||||||
this.hideLoading();
|
|
||||||
|
|
||||||
console.log('App initialized successfully');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('App initialization failed:', error);
|
|
||||||
this.handleInitError(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化组件
|
|
||||||
initComponents() {
|
|
||||||
// 初始化导航栏
|
|
||||||
const navbarContainer = document.querySelector('#navbar');
|
|
||||||
if (navbarContainer) {
|
|
||||||
this.components.navbar = new Navbar(navbarContainer);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化侧边栏
|
|
||||||
const sidebarContainer = document.querySelector('#sidebar-container');
|
|
||||||
if (sidebarContainer) {
|
|
||||||
this.components.sidebar = new Sidebar(sidebarContainer);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化其他组件...
|
|
||||||
}
|
|
||||||
|
|
||||||
// 恢复应用状态
|
|
||||||
restoreAppState() {
|
|
||||||
// 恢复主题
|
|
||||||
const savedTheme = storage.get('app.theme', 'light');
|
|
||||||
store.commit('SET_THEME', savedTheme);
|
|
||||||
|
|
||||||
// 恢复用户信息(如果有)
|
|
||||||
const userInfo = storage.get('userInfo');
|
|
||||||
if (userInfo) {
|
|
||||||
store.commit('SET_USER', userInfo);
|
|
||||||
store.commit('SET_LOGIN', true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 恢复其他设置...
|
|
||||||
}
|
|
||||||
|
|
||||||
// 绑定全局事件
|
|
||||||
bindGlobalEvents() {
|
|
||||||
// 监听路由变化
|
|
||||||
router.afterEach((to) => {
|
|
||||||
this.handleRouteChange(to);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 监听网络状态
|
|
||||||
window.addEventListener('online', () => {
|
|
||||||
showToast('网络已连接', 'success');
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener('offline', () => {
|
|
||||||
showToast('网络已断开', 'warning');
|
|
||||||
});
|
|
||||||
|
|
||||||
// 监听页面可见性变化
|
|
||||||
document.addEventListener('visibilitychange', () => {
|
|
||||||
if (document.hidden) {
|
|
||||||
store.commit('SET_APP_ACTIVE', false);
|
|
||||||
} else {
|
|
||||||
store.commit('SET_APP_ACTIVE', true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 监听存储变化(多标签页同步)
|
|
||||||
window.addEventListener('storage', (e) => {
|
|
||||||
this.handleStorageChange(e);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 监听未捕获的错误
|
|
||||||
window.addEventListener('error', (e) => {
|
|
||||||
this.handleError(e.error);
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener('unhandledrejection', (e) => {
|
|
||||||
this.handleError(e.reason);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理路由变化
|
|
||||||
async handleRouteChange(to) {
|
|
||||||
this.currentRoute = to;
|
|
||||||
|
|
||||||
// 更新页面标题
|
|
||||||
if (to.meta.title) {
|
|
||||||
document.title = `${to.meta.title} - TSP智能助手`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载页面组件
|
|
||||||
await this.loadPage(to);
|
|
||||||
|
|
||||||
// 更新导航状态
|
|
||||||
this.updateNavigation(to);
|
|
||||||
|
|
||||||
// 滚动到顶部
|
|
||||||
window.scrollTo(0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载页面组件
|
|
||||||
async loadPage(route) {
|
|
||||||
const pageContainer = document.querySelector('#page-content');
|
|
||||||
if (!pageContainer) return;
|
|
||||||
|
|
||||||
// 显示加载状态
|
|
||||||
pageContainer.innerHTML = this.createLoadingHTML();
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 映射路由到页面文件
|
|
||||||
const pageFile = this.getPageFile(route.name);
|
|
||||||
|
|
||||||
// 动态导入页面组件 (添加版本号防止缓存)
|
|
||||||
const version = '1.0.2';
|
|
||||||
const pageModule = await import(`./pages/${pageFile}.js?v=${version}`);
|
|
||||||
const PageComponent = pageModule.default;
|
|
||||||
|
|
||||||
// 实例化页面组件
|
|
||||||
const page = new PageComponent(pageContainer, route);
|
|
||||||
|
|
||||||
// 保存页面实例
|
|
||||||
this.components.currentPage = page;
|
|
||||||
|
|
||||||
// 将页面实例暴露到全局(供内联事件使用)
|
|
||||||
if (route.name === 'alerts') {
|
|
||||||
window.alertsPage = page;
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load page:', error);
|
|
||||||
pageContainer.innerHTML = this.createErrorHTML(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取页面文件名
|
|
||||||
getPageFile(routeName) {
|
|
||||||
const pageMap = {
|
|
||||||
'dashboard': 'dashboard',
|
|
||||||
'workorders': 'workorders',
|
|
||||||
'workorder-detail': 'workorders',
|
|
||||||
'alerts': 'alerts',
|
|
||||||
'knowledge': 'knowledge',
|
|
||||||
'knowledge-detail': 'knowledge',
|
|
||||||
'chat': 'chat',
|
|
||||||
'chat-http': 'chat',
|
|
||||||
'monitoring': 'monitoring',
|
|
||||||
'settings': 'settings',
|
|
||||||
'profile': 'settings',
|
|
||||||
'login': 'login',
|
|
||||||
'feishu': 'feishu',
|
|
||||||
'agent': 'agent',
|
|
||||||
'vehicle': 'vehicle'
|
|
||||||
};
|
|
||||||
|
|
||||||
return pageMap[routeName] || 'dashboard';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新导航状态
|
|
||||||
updateNavigation(route) {
|
|
||||||
// 更新侧边栏激活状态
|
|
||||||
if (this.components.sidebar) {
|
|
||||||
this.components.sidebar.updateActiveMenu(route.path);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新导航栏面包屑
|
|
||||||
this.updateBreadcrumb(route);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新面包屑
|
|
||||||
updateBreadcrumb(route) {
|
|
||||||
const breadcrumbContainer = document.querySelector('#breadcrumb');
|
|
||||||
if (!breadcrumbContainer) return;
|
|
||||||
|
|
||||||
const items = [
|
|
||||||
{ text: '首页', link: '/' }
|
|
||||||
];
|
|
||||||
|
|
||||||
// 根据路由构建面包屑
|
|
||||||
if (route.path !== '/') {
|
|
||||||
const pathSegments = route.path.split('/').filter(Boolean);
|
|
||||||
let currentPath = '';
|
|
||||||
|
|
||||||
pathSegments.forEach((segment, index) => {
|
|
||||||
currentPath += `/${segment}`;
|
|
||||||
const isLast = index === pathSegments.length - 1;
|
|
||||||
|
|
||||||
// 这里可以根据路由配置获取更友好的名称
|
|
||||||
const name = this.getPathName(segment);
|
|
||||||
|
|
||||||
items.push({
|
|
||||||
text: name,
|
|
||||||
link: isLast ? null : currentPath
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
breadcrumbContainer.innerHTML = this.createBreadcrumbHTML(items);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取路径名称
|
|
||||||
getPathName(segment) {
|
|
||||||
const names = {
|
|
||||||
workorders: '工单管理',
|
|
||||||
alerts: '预警管理',
|
|
||||||
knowledge: '知识库',
|
|
||||||
chat: '智能对话',
|
|
||||||
'chat-http': 'HTTP对话',
|
|
||||||
monitoring: '系统监控',
|
|
||||||
settings: '系统设置'
|
|
||||||
};
|
|
||||||
return names[segment] || segment;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理存储变化
|
|
||||||
handleStorageChange(e) {
|
|
||||||
// 处理多标签页之间的状态同步
|
|
||||||
if (e.key === 'tsp_assistant_store') {
|
|
||||||
const newState = JSON.parse(e.newValue);
|
|
||||||
store.setState(newState, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理错误
|
|
||||||
handleError(error) {
|
|
||||||
console.error('Application error:', error);
|
|
||||||
|
|
||||||
// 显示错误提示
|
|
||||||
showToast('发生错误,请刷新页面重试', 'error');
|
|
||||||
|
|
||||||
// 发送错误报告(如果配置了且有report方法)
|
|
||||||
if (window.errorReporting && window.errorReporting.enabled && window.errorReporting.report) {
|
|
||||||
try {
|
|
||||||
window.errorReporting.report(error);
|
|
||||||
} catch (reportError) {
|
|
||||||
console.error('Error reporting failed:', reportError);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理初始化错误
|
|
||||||
handleInitError(error) {
|
|
||||||
this.hideLoading();
|
|
||||||
|
|
||||||
const pageContainer = document.querySelector('#page-content');
|
|
||||||
if (pageContainer) {
|
|
||||||
pageContainer.innerHTML = `
|
|
||||||
<div class="container-fluid">
|
|
||||||
<div class="row justify-content-center">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-body text-center">
|
|
||||||
<i class="fas fa-exclamation-triangle fa-3x text-danger mb-3"></i>
|
|
||||||
<h4>应用初始化失败</h4>
|
|
||||||
<p class="text-muted">${error.message || '未知错误'}</p>
|
|
||||||
<button class="btn btn-primary" onclick="location.reload()">
|
|
||||||
<i class="fas fa-redo me-2"></i>重新加载
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 注册服务工作者
|
|
||||||
registerServiceWorker() {
|
|
||||||
if ('serviceWorker' in navigator) {
|
|
||||||
navigator.serviceWorker.register('/sw.js')
|
|
||||||
.then(registration => {
|
|
||||||
console.log('ServiceWorker registered:', registration);
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.log('ServiceWorker registration failed:', error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示加载状态
|
|
||||||
showLoading() {
|
|
||||||
const loadingHTML = `
|
|
||||||
<div id="loading-overlay" class="loading-overlay">
|
|
||||||
<div class="loading-content">
|
|
||||||
<div class="spinner"></div>
|
|
||||||
<p class="mt-3">加载中...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
document.body.insertAdjacentHTML('beforeend', loadingHTML);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 隐藏加载状态
|
|
||||||
hideLoading() {
|
|
||||||
const overlay = document.getElementById('loading-overlay');
|
|
||||||
if (overlay) {
|
|
||||||
overlay.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建加载HTML
|
|
||||||
createLoadingHTML() {
|
|
||||||
return `
|
|
||||||
<div class="d-flex justify-content-center align-items-center" style="min-height: 400px;">
|
|
||||||
<div class="text-center">
|
|
||||||
<div class="spinner"></div>
|
|
||||||
<p class="mt-3">加载中...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建错误HTML
|
|
||||||
createErrorHTML(error) {
|
|
||||||
return `
|
|
||||||
<div class="container-fluid">
|
|
||||||
<div class="row justify-content-center">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-body text-center">
|
|
||||||
<i class="fas fa-exclamation-triangle fa-3x text-danger mb-3"></i>
|
|
||||||
<h4>页面加载失败</h4>
|
|
||||||
<p class="text-muted">${error.message || '未知错误'}</p>
|
|
||||||
<button class="btn btn-primary" onclick="location.reload()">
|
|
||||||
<i class="fas fa-redo me-2"></i>重新加载
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建面包屑HTML
|
|
||||||
createBreadcrumbHTML(items) {
|
|
||||||
return `
|
|
||||||
<nav aria-label="breadcrumb">
|
|
||||||
<ol class="breadcrumb">
|
|
||||||
${items.map((item, index) => `
|
|
||||||
<li class="breadcrumb-item ${index === items.length - 1 ? 'active' : ''}">
|
|
||||||
${item.link ? `<a href="${item.link}">${item.text}</a>` : item.text}
|
|
||||||
</li>
|
|
||||||
`).join('')}
|
|
||||||
</ol>
|
|
||||||
</nav>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建应用实例
|
|
||||||
const app = new App();
|
|
||||||
|
|
||||||
// DOM加载完成后初始化应用
|
|
||||||
ready(() => {
|
|
||||||
app.init();
|
|
||||||
});
|
|
||||||
|
|
||||||
// 暴露到全局(便于调试)
|
|
||||||
window.app = app;
|
|
||||||
window.store = store;
|
|
||||||
window.router = router;
|
|
||||||
@@ -2666,17 +2666,6 @@
|
|||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.min.js"></script>
|
||||||
|
|
||||||
<!-- 核心模块 -->
|
|
||||||
<script src="{{ url_for('static', filename='js/core/store.js') }}"></script>
|
|
||||||
<script src="{{ url_for('static', filename='js/services/api.js') }}"></script>
|
|
||||||
<script src="{{ url_for('static', filename='js/components/NotificationManager.js') }}"></script>
|
|
||||||
|
|
||||||
<!-- 功能组件 -->
|
|
||||||
<script src="{{ url_for('static', filename='js/components/AlertManager.js') }}"></script>
|
|
||||||
|
|
||||||
<!-- 主应用文件 -->
|
|
||||||
<script src="{{ url_for('static', filename='js/app-new.js') }}"></script>
|
|
||||||
|
|
||||||
<!-- 原有dashboard.js保持兼容 -->
|
<!-- 原有dashboard.js保持兼容 -->
|
||||||
<script src="{{ url_for('static', filename='js/dashboard.js') }}?v=2.0.0"></script>
|
<script src="{{ url_for('static', filename='js/dashboard.js') }}?v=2.0.0"></script>
|
||||||
<!-- 功能模块 -->
|
<!-- 功能模块 -->
|
||||||
|
|||||||
Reference in New Issue
Block a user