2025-09-06 21:06:18 +08:00
|
|
|
|
// TSP智能助手综合管理平台前端脚本
|
|
|
|
|
|
|
|
|
|
|
|
class TSPDashboard {
|
|
|
|
|
|
constructor() {
|
|
|
|
|
|
this.currentTab = 'dashboard';
|
|
|
|
|
|
this.charts = {};
|
|
|
|
|
|
this.refreshIntervals = {};
|
|
|
|
|
|
this.websocket = null;
|
|
|
|
|
|
this.sessionId = null;
|
|
|
|
|
|
this.isAgentMode = true;
|
|
|
|
|
|
|
2025-09-18 19:37:14 +01:00
|
|
|
|
// 优化:添加前端缓存
|
|
|
|
|
|
this.cache = new Map();
|
|
|
|
|
|
this.cacheTimeout = 30000; // 30秒缓存
|
|
|
|
|
|
|
2025-09-06 21:06:18 +08:00
|
|
|
|
this.init();
|
|
|
|
|
|
this.restorePageState();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-16 17:05:50 +01:00
|
|
|
|
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');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-09-06 21:06:18 +08:00
|
|
|
|
init() {
|
|
|
|
|
|
this.bindEvents();
|
2025-09-18 19:37:14 +01:00
|
|
|
|
// 优化:并行加载初始数据,提高响应速度
|
|
|
|
|
|
this.loadInitialDataAsync();
|
2025-09-06 21:06:18 +08:00
|
|
|
|
this.startAutoRefresh();
|
|
|
|
|
|
this.initCharts();
|
|
|
|
|
|
}
|
2025-09-18 19:37:14 +01:00
|
|
|
|
|
|
|
|
|
|
async loadInitialDataAsync() {
|
|
|
|
|
|
// 并行加载多个数据源
|
|
|
|
|
|
try {
|
|
|
|
|
|
await Promise.all([
|
|
|
|
|
|
this.loadDashboard(),
|
|
|
|
|
|
this.loadWorkOrders(),
|
|
|
|
|
|
this.loadConversationHistory(),
|
|
|
|
|
|
this.loadKnowledgeBase()
|
|
|
|
|
|
]);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('并行加载数据失败:', error);
|
|
|
|
|
|
// 回退到串行加载
|
|
|
|
|
|
await this.loadInitialData();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 优化:添加缓存方法
|
|
|
|
|
|
getCachedData(key) {
|
|
|
|
|
|
const cached = this.cache.get(key);
|
|
|
|
|
|
if (cached && Date.now() - cached.timestamp < this.cacheTimeout) {
|
|
|
|
|
|
return cached.data;
|
|
|
|
|
|
}
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setCachedData(key, data) {
|
|
|
|
|
|
this.cache.set(key, {
|
|
|
|
|
|
data: data,
|
|
|
|
|
|
timestamp: Date.now()
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
clearCache() {
|
|
|
|
|
|
this.cache.clear();
|
|
|
|
|
|
}
|
2025-09-06 21:06:18 +08:00
|
|
|
|
|
|
|
|
|
|
bindEvents() {
|
|
|
|
|
|
// 标签页切换
|
|
|
|
|
|
document.querySelectorAll('[data-tab]').forEach(tab => {
|
|
|
|
|
|
tab.addEventListener('click', (e) => {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
this.switchTab(tab.dataset.tab);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 对话控制
|
|
|
|
|
|
document.getElementById('start-chat').addEventListener('click', () => this.startChat());
|
|
|
|
|
|
document.getElementById('end-chat').addEventListener('click', () => this.endChat());
|
|
|
|
|
|
document.getElementById('create-work-order').addEventListener('click', () => this.showCreateWorkOrderModal());
|
|
|
|
|
|
document.getElementById('send-button').addEventListener('click', () => this.sendMessage());
|
|
|
|
|
|
document.getElementById('message-input').addEventListener('keypress', (e) => {
|
|
|
|
|
|
if (e.key === 'Enter') this.sendMessage();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 快速操作按钮
|
|
|
|
|
|
document.querySelectorAll('.quick-action-btn').forEach(btn => {
|
|
|
|
|
|
btn.addEventListener('click', () => {
|
|
|
|
|
|
const message = btn.dataset.message;
|
|
|
|
|
|
document.getElementById('message-input').value = message;
|
|
|
|
|
|
this.sendMessage();
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Agent控制
|
|
|
|
|
|
document.getElementById('agent-mode-toggle').addEventListener('change', (e) => {
|
|
|
|
|
|
this.toggleAgentMode(e.target.checked);
|
|
|
|
|
|
});
|
2025-09-18 19:37:14 +01:00
|
|
|
|
|
|
|
|
|
|
// Agent对话功能
|
|
|
|
|
|
document.getElementById('send-agent-message').addEventListener('click', () => this.sendAgentMessage());
|
|
|
|
|
|
document.getElementById('clear-agent-chat').addEventListener('click', () => this.clearAgentChat());
|
|
|
|
|
|
document.getElementById('agent-message-input').addEventListener('keypress', (e) => {
|
|
|
|
|
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
this.sendAgentMessage();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Agent控制按钮
|
|
|
|
|
|
document.getElementById('trigger-sample-action').addEventListener('click', () => this.triggerSampleAction());
|
|
|
|
|
|
document.getElementById('clear-agent-history').addEventListener('click', () => this.clearAgentHistory());
|
2025-09-06 21:06:18 +08:00
|
|
|
|
document.getElementById('start-agent-monitoring').addEventListener('click', () => this.startAgentMonitoring());
|
|
|
|
|
|
document.getElementById('stop-agent-monitoring').addEventListener('click', () => this.stopAgentMonitoring());
|
|
|
|
|
|
document.getElementById('proactive-monitoring').addEventListener('click', () => this.proactiveMonitoring());
|
|
|
|
|
|
document.getElementById('intelligent-analysis').addEventListener('click', () => this.intelligentAnalysis());
|
|
|
|
|
|
|
|
|
|
|
|
// 预警管理
|
|
|
|
|
|
document.getElementById('refresh-alerts').addEventListener('click', () => this.loadAlerts());
|
|
|
|
|
|
document.getElementById('alert-filter').addEventListener('change', () => this.updateAlertsDisplay());
|
|
|
|
|
|
|
|
|
|
|
|
// 知识库管理
|
|
|
|
|
|
document.getElementById('search-knowledge').addEventListener('click', () => this.searchKnowledge());
|
|
|
|
|
|
document.getElementById('knowledge-search').addEventListener('keypress', (e) => {
|
|
|
|
|
|
if (e.key === 'Enter') this.searchKnowledge();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 工单管理
|
|
|
|
|
|
document.getElementById('refresh-workorders').addEventListener('click', () => this.loadWorkOrders());
|
|
|
|
|
|
document.getElementById('workorder-status-filter').addEventListener('change', () => this.loadWorkOrders());
|
|
|
|
|
|
document.getElementById('workorder-priority-filter').addEventListener('change', () => this.loadWorkOrders());
|
|
|
|
|
|
|
|
|
|
|
|
// 模态框
|
|
|
|
|
|
document.getElementById('create-work-order-btn').addEventListener('click', () => this.createWorkOrder());
|
|
|
|
|
|
document.getElementById('add-knowledge-btn').addEventListener('click', () => this.addKnowledge());
|
|
|
|
|
|
document.getElementById('upload-file-btn').addEventListener('click', () => this.uploadFile());
|
|
|
|
|
|
|
|
|
|
|
|
// 置信度滑块
|
|
|
|
|
|
document.getElementById('knowledge-confidence').addEventListener('input', (e) => {
|
|
|
|
|
|
document.getElementById('confidence-value').textContent = e.target.value;
|
|
|
|
|
|
});
|
|
|
|
|
|
document.getElementById('file-confidence').addEventListener('input', (e) => {
|
|
|
|
|
|
document.getElementById('file-confidence-value').textContent = e.target.value;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 处理方式选择
|
|
|
|
|
|
document.getElementById('process-method').addEventListener('change', (e) => {
|
|
|
|
|
|
const manualDiv = document.getElementById('manual-question-div');
|
|
|
|
|
|
if (e.target.value === 'manual') {
|
|
|
|
|
|
manualDiv.style.display = 'block';
|
|
|
|
|
|
} else {
|
|
|
|
|
|
manualDiv.style.display = 'none';
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 系统设置
|
|
|
|
|
|
document.getElementById('system-settings-form').addEventListener('submit', (e) => {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
this.saveSystemSettings();
|
|
|
|
|
|
});
|
2025-09-16 17:05:50 +01:00
|
|
|
|
|
|
|
|
|
|
// 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');
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2025-09-06 21:06:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
switchTab(tabName) {
|
|
|
|
|
|
// 更新导航状态
|
|
|
|
|
|
document.querySelectorAll('.nav-link').forEach(link => {
|
|
|
|
|
|
link.classList.remove('active');
|
|
|
|
|
|
});
|
|
|
|
|
|
document.querySelector(`[data-tab="${tabName}"]`).classList.add('active');
|
|
|
|
|
|
|
|
|
|
|
|
// 显示对应内容
|
|
|
|
|
|
document.querySelectorAll('.tab-content').forEach(content => {
|
|
|
|
|
|
content.style.display = 'none';
|
|
|
|
|
|
});
|
|
|
|
|
|
document.getElementById(`${tabName}-tab`).style.display = 'block';
|
|
|
|
|
|
|
|
|
|
|
|
this.currentTab = tabName;
|
|
|
|
|
|
|
|
|
|
|
|
// 保存当前页面状态
|
|
|
|
|
|
this.savePageState();
|
|
|
|
|
|
|
|
|
|
|
|
// 加载对应数据
|
|
|
|
|
|
switch(tabName) {
|
|
|
|
|
|
case 'dashboard':
|
|
|
|
|
|
this.loadDashboardData();
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'chat':
|
|
|
|
|
|
this.loadChatData();
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'agent':
|
|
|
|
|
|
this.loadAgentData();
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'alerts':
|
|
|
|
|
|
this.loadAlerts();
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'knowledge':
|
|
|
|
|
|
this.loadKnowledge();
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'workorders':
|
|
|
|
|
|
this.loadWorkOrders();
|
|
|
|
|
|
break;
|
2025-09-18 19:37:14 +01:00
|
|
|
|
case 'conversation-history':
|
|
|
|
|
|
this.loadConversationHistory();
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'token-monitor':
|
|
|
|
|
|
this.loadTokenMonitor();
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'ai-monitor':
|
|
|
|
|
|
this.loadAIMonitor();
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'system-optimizer':
|
|
|
|
|
|
this.loadSystemOptimizer();
|
|
|
|
|
|
break;
|
2025-09-06 21:06:18 +08:00
|
|
|
|
case 'analytics':
|
|
|
|
|
|
this.loadAnalytics();
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'settings':
|
|
|
|
|
|
this.loadSettings();
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
savePageState() {
|
|
|
|
|
|
const state = {
|
|
|
|
|
|
currentTab: this.currentTab,
|
|
|
|
|
|
timestamp: Date.now()
|
|
|
|
|
|
};
|
|
|
|
|
|
localStorage.setItem('tsp_dashboard_state', JSON.stringify(state));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
restorePageState() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const savedState = localStorage.getItem('tsp_dashboard_state');
|
|
|
|
|
|
if (savedState) {
|
|
|
|
|
|
const state = JSON.parse(savedState);
|
|
|
|
|
|
// 如果状态保存时间不超过1小时,则恢复
|
|
|
|
|
|
if (Date.now() - state.timestamp < 3600000) {
|
|
|
|
|
|
this.switchTab(state.currentTab);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.warn('恢复页面状态失败:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async loadInitialData() {
|
|
|
|
|
|
await Promise.all([
|
|
|
|
|
|
this.loadHealth(),
|
|
|
|
|
|
this.loadDashboardData(),
|
|
|
|
|
|
this.loadSystemInfo()
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
startAutoRefresh() {
|
2025-09-16 17:05:50 +01:00
|
|
|
|
// 每15秒刷新健康状态(减少 /api/health 日志)
|
2025-09-06 21:06:18 +08:00
|
|
|
|
this.refreshIntervals.health = setInterval(() => {
|
|
|
|
|
|
this.loadHealth();
|
2025-09-16 17:05:50 +01:00
|
|
|
|
}, 15000);
|
2025-09-06 21:06:18 +08:00
|
|
|
|
|
|
|
|
|
|
// 每10秒刷新当前标签页数据
|
|
|
|
|
|
this.refreshIntervals.currentTab = setInterval(() => {
|
|
|
|
|
|
this.refreshCurrentTab();
|
|
|
|
|
|
}, 10000);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
refreshCurrentTab() {
|
|
|
|
|
|
switch(this.currentTab) {
|
|
|
|
|
|
case 'dashboard':
|
|
|
|
|
|
this.loadDashboardData();
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'alerts':
|
|
|
|
|
|
this.loadAlerts();
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'agent':
|
|
|
|
|
|
this.loadAgentData();
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async loadHealth() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/api/health');
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
this.updateHealthDisplay(data);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('加载健康状态失败:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
updateHealthDisplay(health) {
|
|
|
|
|
|
const healthScore = health.health_score || 0;
|
2025-09-08 15:27:22 +08:00
|
|
|
|
const healthStatus = health.status || 'unknown';
|
2025-09-06 21:06:18 +08:00
|
|
|
|
|
|
|
|
|
|
// 更新健康指示器
|
|
|
|
|
|
const healthDot = document.getElementById('health-dot');
|
|
|
|
|
|
const healthStatusText = document.getElementById('health-status');
|
|
|
|
|
|
const systemHealthDot = document.getElementById('system-health-dot');
|
|
|
|
|
|
const systemHealthText = document.getElementById('system-health-text');
|
|
|
|
|
|
const healthProgress = document.getElementById('health-progress');
|
|
|
|
|
|
|
|
|
|
|
|
if (healthDot) {
|
|
|
|
|
|
healthDot.className = `health-dot ${healthStatus}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (healthStatusText) {
|
|
|
|
|
|
healthStatusText.textContent = this.getHealthStatusText(healthStatus);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (systemHealthDot) {
|
|
|
|
|
|
systemHealthDot.className = `health-dot ${healthStatus}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (systemHealthText) {
|
|
|
|
|
|
systemHealthText.textContent = this.getHealthStatusText(healthStatus);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (healthProgress) {
|
|
|
|
|
|
healthProgress.style.width = `${healthScore * 100}%`;
|
|
|
|
|
|
healthProgress.className = `progress-bar ${this.getHealthColor(healthScore)}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 更新内存和CPU使用率
|
|
|
|
|
|
const memoryUsage = health.memory_usage || 0;
|
|
|
|
|
|
const memoryProgress = document.getElementById('memory-progress');
|
|
|
|
|
|
if (memoryProgress) {
|
|
|
|
|
|
memoryProgress.style.width = `${memoryUsage}%`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const cpuUsage = health.cpu_usage || 0;
|
|
|
|
|
|
const cpuProgress = document.getElementById('cpu-progress');
|
|
|
|
|
|
if (cpuProgress) {
|
|
|
|
|
|
cpuProgress.style.width = `${cpuUsage}%`;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
getHealthStatusText(status) {
|
|
|
|
|
|
const statusMap = {
|
|
|
|
|
|
'excellent': '优秀',
|
|
|
|
|
|
'good': '良好',
|
|
|
|
|
|
'fair': '一般',
|
|
|
|
|
|
'poor': '较差',
|
|
|
|
|
|
'critical': '严重',
|
|
|
|
|
|
'unknown': '未知'
|
|
|
|
|
|
};
|
|
|
|
|
|
return statusMap[status] || status;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
getHealthColor(score) {
|
|
|
|
|
|
if (score >= 0.8) return 'bg-success';
|
|
|
|
|
|
if (score >= 0.6) return 'bg-info';
|
|
|
|
|
|
if (score >= 0.4) return 'bg-warning';
|
|
|
|
|
|
return 'bg-danger';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async loadDashboardData() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const [sessionsResponse, alertsResponse, workordersResponse, knowledgeResponse] = await Promise.all([
|
|
|
|
|
|
fetch('/api/chat/sessions'),
|
|
|
|
|
|
fetch('/api/alerts'),
|
|
|
|
|
|
fetch('/api/workorders'),
|
|
|
|
|
|
fetch('/api/knowledge/stats')
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
const sessions = await sessionsResponse.json();
|
|
|
|
|
|
const alerts = await alertsResponse.json();
|
|
|
|
|
|
const workorders = await workordersResponse.json();
|
|
|
|
|
|
const knowledge = await knowledgeResponse.json();
|
|
|
|
|
|
|
|
|
|
|
|
// 更新统计卡片
|
|
|
|
|
|
document.getElementById('total-sessions').textContent = sessions.sessions?.length || 0;
|
|
|
|
|
|
document.getElementById('total-alerts').textContent = alerts.length || 0;
|
|
|
|
|
|
document.getElementById('total-workorders').textContent = workorders.filter(w => w.status === 'open').length || 0;
|
|
|
|
|
|
document.getElementById('knowledge-count').textContent = knowledge.total_entries || 0;
|
|
|
|
|
|
|
|
|
|
|
|
// 更新知识库详细统计
|
|
|
|
|
|
document.getElementById('knowledge-total').textContent = knowledge.total_entries || 0;
|
|
|
|
|
|
document.getElementById('knowledge-active').textContent = knowledge.active_entries || 0;
|
|
|
|
|
|
const confidencePercent = Math.round((knowledge.average_confidence || 0) * 100);
|
|
|
|
|
|
document.getElementById('knowledge-confidence').style.width = `${confidencePercent}%`;
|
|
|
|
|
|
document.getElementById('knowledge-confidence').setAttribute('aria-valuenow', confidencePercent);
|
|
|
|
|
|
document.getElementById('knowledge-confidence').textContent = `${confidencePercent}%`;
|
|
|
|
|
|
|
|
|
|
|
|
// 更新性能图表
|
2025-09-16 17:05:50 +01:00
|
|
|
|
await this.updatePerformanceChart(sessions, alerts, workorders);
|
|
|
|
|
|
|
|
|
|
|
|
// 更新系统健康状态
|
|
|
|
|
|
await this.updateSystemHealth();
|
2025-09-06 21:06:18 +08:00
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('加载仪表板数据失败:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-08 15:27:22 +08:00
|
|
|
|
|
|
|
|
|
|
|
2025-09-06 21:06:18 +08:00
|
|
|
|
initCharts() {
|
|
|
|
|
|
// 性能趋势图
|
|
|
|
|
|
const performanceCtx = document.getElementById('performanceChart');
|
|
|
|
|
|
if (performanceCtx) {
|
|
|
|
|
|
this.charts.performance = new Chart(performanceCtx, {
|
|
|
|
|
|
type: 'line',
|
|
|
|
|
|
data: {
|
|
|
|
|
|
labels: [],
|
|
|
|
|
|
datasets: [{
|
2025-09-16 17:05:50 +01:00
|
|
|
|
label: '工单数量',
|
2025-09-06 21:06:18 +08:00
|
|
|
|
data: [],
|
|
|
|
|
|
borderColor: '#007bff',
|
|
|
|
|
|
backgroundColor: 'rgba(0, 123, 255, 0.1)',
|
|
|
|
|
|
tension: 0.4
|
|
|
|
|
|
}, {
|
2025-09-16 17:05:50 +01:00
|
|
|
|
label: '预警数量',
|
2025-09-06 21:06:18 +08:00
|
|
|
|
data: [],
|
|
|
|
|
|
borderColor: '#dc3545',
|
|
|
|
|
|
backgroundColor: 'rgba(220, 53, 69, 0.1)',
|
|
|
|
|
|
tension: 0.4
|
|
|
|
|
|
}]
|
|
|
|
|
|
},
|
|
|
|
|
|
options: {
|
|
|
|
|
|
responsive: true,
|
|
|
|
|
|
maintainAspectRatio: false,
|
|
|
|
|
|
scales: {
|
|
|
|
|
|
y: {
|
|
|
|
|
|
beginAtZero: true
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 分析图表
|
|
|
|
|
|
const analyticsCtx = document.getElementById('analyticsChart');
|
|
|
|
|
|
if (analyticsCtx) {
|
|
|
|
|
|
this.charts.analytics = new Chart(analyticsCtx, {
|
|
|
|
|
|
type: 'line',
|
|
|
|
|
|
data: {
|
|
|
|
|
|
labels: [],
|
|
|
|
|
|
datasets: [{
|
|
|
|
|
|
label: '满意度',
|
|
|
|
|
|
data: [],
|
|
|
|
|
|
borderColor: '#28a745',
|
|
|
|
|
|
backgroundColor: 'rgba(40, 167, 69, 0.1)',
|
|
|
|
|
|
tension: 0.4
|
|
|
|
|
|
}, {
|
|
|
|
|
|
label: '解决时间(小时)',
|
|
|
|
|
|
data: [],
|
|
|
|
|
|
borderColor: '#ffc107',
|
|
|
|
|
|
backgroundColor: 'rgba(255, 193, 7, 0.1)',
|
|
|
|
|
|
tension: 0.4
|
|
|
|
|
|
}]
|
|
|
|
|
|
},
|
|
|
|
|
|
options: {
|
|
|
|
|
|
responsive: true,
|
|
|
|
|
|
maintainAspectRatio: false,
|
|
|
|
|
|
scales: {
|
|
|
|
|
|
y: {
|
|
|
|
|
|
beginAtZero: true
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 类别分布图
|
|
|
|
|
|
const categoryCtx = document.getElementById('categoryChart');
|
|
|
|
|
|
if (categoryCtx) {
|
|
|
|
|
|
this.charts.category = new Chart(categoryCtx, {
|
|
|
|
|
|
type: 'doughnut',
|
|
|
|
|
|
data: {
|
|
|
|
|
|
labels: [],
|
|
|
|
|
|
datasets: [{
|
|
|
|
|
|
data: [],
|
|
|
|
|
|
backgroundColor: [
|
|
|
|
|
|
'#007bff',
|
|
|
|
|
|
'#28a745',
|
|
|
|
|
|
'#ffc107',
|
|
|
|
|
|
'#dc3545',
|
|
|
|
|
|
'#17a2b8'
|
|
|
|
|
|
]
|
|
|
|
|
|
}]
|
|
|
|
|
|
},
|
|
|
|
|
|
options: {
|
|
|
|
|
|
responsive: true,
|
|
|
|
|
|
maintainAspectRatio: false
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-16 17:05:50 +01:00
|
|
|
|
async updatePerformanceChart(sessions, alerts, workorders) {
|
2025-09-06 21:06:18 +08:00
|
|
|
|
if (!this.charts.performance) return;
|
|
|
|
|
|
|
2025-09-16 17:05:50 +01:00
|
|
|
|
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();
|
2025-09-06 21:06:18 +08:00
|
|
|
|
}
|
2025-09-16 17:05:50 +01:00
|
|
|
|
}
|
2025-09-06 21:06:18 +08:00
|
|
|
|
|
2025-09-16 17:05:50 +01:00
|
|
|
|
// 更新系统健康状态显示
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
2025-09-06 21:06:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 对话功能
|
|
|
|
|
|
async startChat() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const userId = document.getElementById('user-id').value;
|
|
|
|
|
|
const workOrderId = document.getElementById('work-order-id').value;
|
|
|
|
|
|
|
|
|
|
|
|
const response = await fetch('/api/chat/session', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
|
|
},
|
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
|
user_id: userId,
|
|
|
|
|
|
work_order_id: workOrderId ? parseInt(workOrderId) : null
|
|
|
|
|
|
})
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
this.sessionId = data.session_id;
|
|
|
|
|
|
document.getElementById('start-chat').disabled = true;
|
|
|
|
|
|
document.getElementById('end-chat').disabled = false;
|
|
|
|
|
|
document.getElementById('message-input').disabled = false;
|
|
|
|
|
|
document.getElementById('send-button').disabled = false;
|
|
|
|
|
|
document.getElementById('session-info').textContent = `会话ID: ${this.sessionId}`;
|
|
|
|
|
|
document.getElementById('connection-status').className = 'badge bg-success';
|
|
|
|
|
|
document.getElementById('connection-status').innerHTML = '<i class="fas fa-circle me-1"></i>已连接';
|
|
|
|
|
|
|
|
|
|
|
|
this.showNotification('对话已开始', 'success');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.showNotification('开始对话失败', 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('开始对话失败:', error);
|
|
|
|
|
|
this.showNotification('开始对话失败', 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async endChat() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (!this.sessionId) return;
|
|
|
|
|
|
|
|
|
|
|
|
const response = await fetch(`/api/chat/session/${this.sessionId}`, {
|
|
|
|
|
|
method: 'DELETE'
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
this.sessionId = null;
|
|
|
|
|
|
document.getElementById('start-chat').disabled = false;
|
|
|
|
|
|
document.getElementById('end-chat').disabled = true;
|
|
|
|
|
|
document.getElementById('message-input').disabled = true;
|
|
|
|
|
|
document.getElementById('send-button').disabled = true;
|
|
|
|
|
|
document.getElementById('session-info').textContent = '未开始对话';
|
|
|
|
|
|
document.getElementById('connection-status').className = 'badge bg-secondary';
|
|
|
|
|
|
document.getElementById('connection-status').innerHTML = '<i class="fas fa-circle me-1"></i>未连接';
|
|
|
|
|
|
|
|
|
|
|
|
this.showNotification('对话已结束', 'info');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.showNotification('结束对话失败', 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('结束对话失败:', error);
|
|
|
|
|
|
this.showNotification('结束对话失败', 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async sendMessage() {
|
|
|
|
|
|
const messageInput = document.getElementById('message-input');
|
|
|
|
|
|
const message = messageInput.value.trim();
|
|
|
|
|
|
|
|
|
|
|
|
if (!message || !this.sessionId) return;
|
|
|
|
|
|
|
|
|
|
|
|
// 显示用户消息
|
|
|
|
|
|
this.addMessage('user', message);
|
|
|
|
|
|
messageInput.value = '';
|
|
|
|
|
|
|
2025-09-16 17:05:50 +01:00
|
|
|
|
// 显示占位提示:小奇正在查询中
|
|
|
|
|
|
const typingId = this.showTypingIndicator();
|
|
|
|
|
|
|
2025-09-06 21:06:18 +08:00
|
|
|
|
// 发送消息到服务器
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/api/chat/message', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
|
|
},
|
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
|
session_id: this.sessionId,
|
|
|
|
|
|
message: message
|
|
|
|
|
|
})
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
if (data.success) {
|
2025-09-16 17:05:50 +01:00
|
|
|
|
this.updateTypingIndicator(typingId, data.response, data.knowledge_used);
|
2025-09-06 21:06:18 +08:00
|
|
|
|
} else {
|
2025-09-16 17:05:50 +01:00
|
|
|
|
this.updateTypingIndicator(typingId, '抱歉,处理您的消息时出现了错误。', null, true);
|
2025-09-06 21:06:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('发送消息失败:', error);
|
2025-09-16 17:05:50 +01:00
|
|
|
|
this.updateTypingIndicator(typingId, '网络连接错误,请稍后重试。', null, true);
|
2025-09-06 21:06:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-16 17:05:50 +01:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-06 21:06:18 +08:00
|
|
|
|
addMessage(role, content, knowledgeUsed = null, isError = false) {
|
|
|
|
|
|
const messagesContainer = document.getElementById('chat-messages');
|
|
|
|
|
|
|
|
|
|
|
|
// 移除欢迎消息
|
|
|
|
|
|
const welcomeMsg = messagesContainer.querySelector('.text-center');
|
|
|
|
|
|
if (welcomeMsg) {
|
|
|
|
|
|
welcomeMsg.remove();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const messageDiv = document.createElement('div');
|
|
|
|
|
|
messageDiv.className = `message ${role}`;
|
|
|
|
|
|
|
|
|
|
|
|
const avatar = document.createElement('div');
|
|
|
|
|
|
avatar.className = 'message-avatar';
|
|
|
|
|
|
avatar.innerHTML = role === 'user' ? '<i class="fas fa-user"></i>' : '<i class="fas fa-robot"></i>';
|
|
|
|
|
|
|
|
|
|
|
|
const contentDiv = document.createElement('div');
|
|
|
|
|
|
contentDiv.className = 'message-content';
|
|
|
|
|
|
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).join(', ')}
|
|
|
|
|
|
`;
|
|
|
|
|
|
contentDiv.appendChild(knowledgeDiv);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (isError) {
|
|
|
|
|
|
contentDiv.style.borderLeft = '4px solid #dc3545';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
messageDiv.appendChild(avatar);
|
|
|
|
|
|
messageDiv.appendChild(contentDiv);
|
|
|
|
|
|
messagesContainer.appendChild(messageDiv);
|
|
|
|
|
|
|
|
|
|
|
|
// 滚动到底部
|
|
|
|
|
|
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Agent功能
|
|
|
|
|
|
async toggleAgentMode(enabled) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/api/agent/toggle', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
|
|
},
|
|
|
|
|
|
body: JSON.stringify({ enabled })
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
this.isAgentMode = enabled;
|
|
|
|
|
|
this.showNotification(`Agent模式已${enabled ? '启用' : '禁用'}`, 'success');
|
|
|
|
|
|
this.loadAgentData();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.showNotification('切换Agent模式失败', 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('切换Agent模式失败:', error);
|
|
|
|
|
|
this.showNotification('切换Agent模式失败', 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async loadAgentData() {
|
|
|
|
|
|
try {
|
2025-09-16 17:05:50 +01:00
|
|
|
|
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();
|
2025-09-06 21:06:18 +08:00
|
|
|
|
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
document.getElementById('agent-current-state').textContent = data.status || '未知';
|
|
|
|
|
|
document.getElementById('agent-active-goals').textContent = data.active_goals || 0;
|
2025-09-16 17:05:50 +01:00
|
|
|
|
const tools = (toolsData.success ? toolsData.tools : (data.tools || [])) || [];
|
|
|
|
|
|
document.getElementById('agent-available-tools').textContent = tools.length || 0;
|
2025-09-06 21:06:18 +08:00
|
|
|
|
|
2025-09-16 17:05:50 +01:00
|
|
|
|
// 更新工具列表(使用真实统计)
|
|
|
|
|
|
this.updateToolsList(tools);
|
2025-09-06 21:06:18 +08:00
|
|
|
|
|
|
|
|
|
|
// 更新执行历史
|
2025-09-11 00:01:12 +08:00
|
|
|
|
this.updateAgentExecutionHistory(data.execution_history || []);
|
2025-09-06 21:06:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('加载Agent数据失败:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
updateToolsList(tools) {
|
|
|
|
|
|
const toolsList = document.getElementById('tools-list');
|
2025-09-16 17:05:50 +01:00
|
|
|
|
if (!tools || tools.length === 0) {
|
2025-09-06 21:06:18 +08:00
|
|
|
|
toolsList.innerHTML = '<div class="empty-state"><i class="fas fa-tools"></i><p>暂无工具</p></div>';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-16 17:05:50 +01:00
|
|
|
|
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>
|
2025-09-06 21:06:18 +08:00
|
|
|
|
</div>
|
2025-09-16 17:05:50 +01:00
|
|
|
|
`;
|
|
|
|
|
|
}).join('');
|
2025-09-06 21:06:18 +08:00
|
|
|
|
|
|
|
|
|
|
toolsList.innerHTML = toolsHtml;
|
2025-09-16 17:05:50 +01:00
|
|
|
|
|
|
|
|
|
|
// 追加自定义工具注册入口
|
|
|
|
|
|
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');
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2025-09-06 21:06:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
updateExecutionHistory(history) {
|
|
|
|
|
|
const historyContainer = document.getElementById('agent-execution-history');
|
|
|
|
|
|
if (history.length === 0) {
|
|
|
|
|
|
historyContainer.innerHTML = '<div class="empty-state"><i class="fas fa-history"></i><p>暂无执行历史</p></div>';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const historyHtml = history.slice(-5).map(item => `
|
|
|
|
|
|
<div class="border-bottom pb-2 mb-2">
|
|
|
|
|
|
<div class="d-flex justify-content-between">
|
|
|
|
|
|
<strong>${item.type || '未知任务'}</strong>
|
|
|
|
|
|
<small class="text-muted">${new Date(item.timestamp).toLocaleString()}</small>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="text-muted small">${item.description || '无描述'}</div>
|
|
|
|
|
|
<div class="mt-1">
|
|
|
|
|
|
<span class="badge ${item.success ? 'bg-success' : 'bg-danger'}">
|
|
|
|
|
|
${item.success ? '成功' : '失败'}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`).join('');
|
|
|
|
|
|
|
|
|
|
|
|
historyContainer.innerHTML = historyHtml;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async startAgentMonitoring() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/api/agent/monitoring/start', { method: 'POST' });
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
this.showNotification('Agent监控已启动', 'success');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.showNotification('启动Agent监控失败', 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('启动Agent监控失败:', error);
|
|
|
|
|
|
this.showNotification('启动Agent监控失败', 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async stopAgentMonitoring() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/api/agent/monitoring/stop', { method: 'POST' });
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
this.showNotification('Agent监控已停止', 'success');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.showNotification('停止Agent监控失败', 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('停止Agent监控失败:', error);
|
|
|
|
|
|
this.showNotification('停止Agent监控失败', 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async proactiveMonitoring() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/api/agent/proactive-monitoring', { method: 'POST' });
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
this.showNotification(`主动监控完成,发现 ${data.proactive_actions?.length || 0} 个行动机会`, 'info');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.showNotification('主动监控失败', 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('主动监控失败:', error);
|
|
|
|
|
|
this.showNotification('主动监控失败', 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async intelligentAnalysis() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/api/agent/intelligent-analysis', { method: 'POST' });
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
this.showNotification('智能分析完成', 'success');
|
|
|
|
|
|
// 更新分析图表
|
|
|
|
|
|
this.updateAnalyticsChart(data.analysis);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.showNotification('智能分析失败', 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('智能分析失败:', error);
|
|
|
|
|
|
this.showNotification('智能分析失败', 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
updateAnalyticsChart(analysis) {
|
|
|
|
|
|
if (!this.charts.analytics || !analysis) return;
|
|
|
|
|
|
|
|
|
|
|
|
// 更新分析图表数据
|
|
|
|
|
|
const labels = analysis.trends?.dates || [];
|
|
|
|
|
|
const satisfactionData = analysis.trends?.satisfaction || [];
|
|
|
|
|
|
const resolutionTimeData = analysis.trends?.resolution_time || [];
|
|
|
|
|
|
|
|
|
|
|
|
this.charts.analytics.data.labels = labels;
|
|
|
|
|
|
this.charts.analytics.data.datasets[0].data = satisfactionData;
|
|
|
|
|
|
this.charts.analytics.data.datasets[1].data = resolutionTimeData;
|
|
|
|
|
|
this.charts.analytics.update();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 预警管理
|
|
|
|
|
|
async loadAlerts() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/api/alerts');
|
|
|
|
|
|
const alerts = await response.json();
|
|
|
|
|
|
this.updateAlertsDisplay(alerts);
|
|
|
|
|
|
this.updateAlertStatistics(alerts);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('加载预警失败:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
updateAlertsDisplay(alerts) {
|
|
|
|
|
|
const container = document.getElementById('alerts-container');
|
|
|
|
|
|
|
|
|
|
|
|
if (alerts.length === 0) {
|
|
|
|
|
|
container.innerHTML = `
|
|
|
|
|
|
<div class="empty-state">
|
|
|
|
|
|
<i class="fas fa-check-circle"></i>
|
|
|
|
|
|
<h5>暂无活跃预警</h5>
|
|
|
|
|
|
<p>系统运行正常,没有需要处理的预警</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const alertsHtml = alerts.map(alert => `
|
|
|
|
|
|
<div class="alert-item ${alert.level}">
|
|
|
|
|
|
<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="badge bg-${this.getAlertColor(alert.level)} me-2">${this.getLevelText(alert.level)}</span>
|
|
|
|
|
|
<span class="fw-bold">${alert.rule_name || '未知规则'}</span>
|
|
|
|
|
|
<span class="ms-auto text-muted small">${this.formatTime(alert.created_at)}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="alert-message mb-2">${alert.message}</div>
|
|
|
|
|
|
<div class="alert-meta text-muted small">
|
|
|
|
|
|
类型: ${this.getTypeText(alert.alert_type)} |
|
|
|
|
|
|
级别: ${this.getLevelText(alert.level)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="ms-3">
|
|
|
|
|
|
<button class="btn btn-sm btn-outline-success" onclick="dashboard.resolveAlert(${alert.id})">
|
|
|
|
|
|
<i class="fas fa-check me-1"></i>解决
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`).join('');
|
|
|
|
|
|
|
|
|
|
|
|
container.innerHTML = alertsHtml;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
updateAlertStatistics(alerts) {
|
|
|
|
|
|
const stats = 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-count').textContent = stats.total || 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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('解决预警失败', 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('解决预警失败:', error);
|
|
|
|
|
|
this.showNotification('解决预警失败', 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 知识库管理
|
|
|
|
|
|
async loadKnowledge(page = 1) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch(`/api/knowledge?page=${page}&per_page=10`);
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (data.knowledge) {
|
|
|
|
|
|
this.updateKnowledgeDisplay(data.knowledge);
|
|
|
|
|
|
this.updateKnowledgePagination(data);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 兼容旧格式
|
|
|
|
|
|
this.updateKnowledgeDisplay(data);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('加载知识库失败:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
updateKnowledgeDisplay(knowledge) {
|
|
|
|
|
|
const container = document.getElementById('knowledge-list');
|
|
|
|
|
|
|
|
|
|
|
|
if (knowledge.length === 0) {
|
|
|
|
|
|
container.innerHTML = '<div class="empty-state"><i class="fas fa-database"></i><p>暂无知识条目</p></div>';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const knowledgeHtml = knowledge.map(item => `
|
|
|
|
|
|
<div class="knowledge-item">
|
|
|
|
|
|
<div class="d-flex justify-content-between align-items-start">
|
|
|
|
|
|
<div class="flex-grow-1">
|
|
|
|
|
|
<h6 class="mb-1">${item.question}</h6>
|
|
|
|
|
|
<p class="text-muted mb-2">${item.answer}</p>
|
|
|
|
|
|
<div class="d-flex gap-3">
|
|
|
|
|
|
<small class="text-muted">分类: ${item.category}</small>
|
|
|
|
|
|
<small class="text-muted">置信度: ${Math.round(item.confidence_score * 100)}%</small>
|
|
|
|
|
|
<small class="text-muted">使用次数: ${item.usage_count || 0}</small>
|
|
|
|
|
|
<span class="badge ${item.is_verified ? 'bg-success' : 'bg-warning'}">
|
|
|
|
|
|
${item.is_verified ? '已验证' : '未验证'}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="ms-3">
|
|
|
|
|
|
<div class="btn-group" role="group">
|
|
|
|
|
|
${item.is_verified ?
|
|
|
|
|
|
`<button class="btn btn-sm btn-outline-warning" onclick="dashboard.unverifyKnowledge(${item.id})" title="取消验证">
|
|
|
|
|
|
<i class="fas fa-times-circle"></i>
|
|
|
|
|
|
</button>` :
|
|
|
|
|
|
`<button class="btn btn-sm btn-outline-success" onclick="dashboard.verifyKnowledge(${item.id})" title="验证">
|
|
|
|
|
|
<i class="fas fa-check-circle"></i>
|
|
|
|
|
|
</button>`
|
|
|
|
|
|
}
|
|
|
|
|
|
<button class="btn btn-sm btn-outline-danger" onclick="dashboard.deleteKnowledge(${item.id})" title="删除">
|
|
|
|
|
|
<i class="fas fa-trash"></i>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`).join('');
|
|
|
|
|
|
|
|
|
|
|
|
container.innerHTML = knowledgeHtml;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
updateKnowledgePagination(data) {
|
|
|
|
|
|
const paginationContainer = document.getElementById('knowledge-pagination');
|
|
|
|
|
|
if (!paginationContainer) return;
|
|
|
|
|
|
|
|
|
|
|
|
const { page, total_pages, total } = data;
|
|
|
|
|
|
|
|
|
|
|
|
if (total_pages <= 1) {
|
|
|
|
|
|
paginationContainer.innerHTML = '';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let paginationHtml = `
|
|
|
|
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<small class="text-muted">共 ${total} 条知识,第 ${page} / ${total_pages} 页</small>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<nav>
|
|
|
|
|
|
<ul class="pagination pagination-sm mb-0">
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
// 上一页
|
|
|
|
|
|
if (page > 1) {
|
|
|
|
|
|
paginationHtml += `<li class="page-item"><a class="page-link" href="#" onclick="dashboard.loadKnowledge(${page - 1})">上一页</a></li>`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 页码
|
|
|
|
|
|
const startPage = Math.max(1, page - 2);
|
|
|
|
|
|
const endPage = Math.min(total_pages, page + 2);
|
|
|
|
|
|
|
|
|
|
|
|
for (let i = startPage; i <= endPage; i++) {
|
|
|
|
|
|
const activeClass = i === page ? 'active' : '';
|
|
|
|
|
|
paginationHtml += `<li class="page-item ${activeClass}"><a class="page-link" href="#" onclick="dashboard.loadKnowledge(${i})">${i}</a></li>`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 下一页
|
|
|
|
|
|
if (page < total_pages) {
|
|
|
|
|
|
paginationHtml += `<li class="page-item"><a class="page-link" href="#" onclick="dashboard.loadKnowledge(${page + 1})">下一页</a></li>`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
paginationHtml += `
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
</nav>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
paginationContainer.innerHTML = paginationHtml;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async searchKnowledge() {
|
|
|
|
|
|
const query = document.getElementById('knowledge-search').value.trim();
|
|
|
|
|
|
if (!query) {
|
|
|
|
|
|
this.loadKnowledge();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch(`/api/knowledge/search?q=${encodeURIComponent(query)}`);
|
|
|
|
|
|
const results = await response.json();
|
|
|
|
|
|
this.updateKnowledgeDisplay(results);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('搜索知识库失败:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async addKnowledge() {
|
|
|
|
|
|
const question = document.getElementById('knowledge-question').value.trim();
|
|
|
|
|
|
const answer = document.getElementById('knowledge-answer').value.trim();
|
|
|
|
|
|
const category = document.getElementById('knowledge-category').value;
|
|
|
|
|
|
const confidence = parseFloat(document.getElementById('knowledge-confidence').value);
|
|
|
|
|
|
|
|
|
|
|
|
if (!question || !answer) {
|
|
|
|
|
|
this.showNotification('请填写完整信息', 'warning');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/api/knowledge', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
|
|
},
|
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
|
question,
|
|
|
|
|
|
answer,
|
|
|
|
|
|
category,
|
|
|
|
|
|
confidence_score: confidence
|
|
|
|
|
|
})
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
this.showNotification('知识添加成功', 'success');
|
|
|
|
|
|
bootstrap.Modal.getInstance(document.getElementById('addKnowledgeModal')).hide();
|
|
|
|
|
|
document.getElementById('knowledge-form').reset();
|
|
|
|
|
|
this.loadKnowledge();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.showNotification('添加知识失败', 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('添加知识失败:', error);
|
|
|
|
|
|
this.showNotification('添加知识失败', 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async uploadFile() {
|
|
|
|
|
|
const fileInput = document.getElementById('file-input');
|
|
|
|
|
|
const processMethod = document.getElementById('process-method').value;
|
|
|
|
|
|
const category = document.getElementById('file-category').value;
|
|
|
|
|
|
const confidence = parseFloat(document.getElementById('file-confidence').value);
|
|
|
|
|
|
const manualQuestion = document.getElementById('manual-question').value.trim();
|
|
|
|
|
|
|
|
|
|
|
|
if (!fileInput.files[0]) {
|
|
|
|
|
|
this.showNotification('请选择文件', 'warning');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (processMethod === 'manual' && !manualQuestion) {
|
|
|
|
|
|
this.showNotification('请指定问题', 'warning');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 显示进度条
|
|
|
|
|
|
const progressDiv = document.getElementById('upload-progress');
|
|
|
|
|
|
const progressBar = progressDiv.querySelector('.progress-bar');
|
|
|
|
|
|
const statusText = document.getElementById('upload-status');
|
|
|
|
|
|
|
|
|
|
|
|
progressDiv.style.display = 'block';
|
|
|
|
|
|
progressBar.style.width = '0%';
|
|
|
|
|
|
statusText.textContent = '正在上传文件...';
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const formData = new FormData();
|
|
|
|
|
|
formData.append('file', fileInput.files[0]);
|
|
|
|
|
|
formData.append('process_method', processMethod);
|
|
|
|
|
|
formData.append('category', category);
|
|
|
|
|
|
formData.append('confidence_score', confidence);
|
|
|
|
|
|
if (manualQuestion) {
|
|
|
|
|
|
formData.append('manual_question', manualQuestion);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 模拟进度更新
|
|
|
|
|
|
let progress = 0;
|
|
|
|
|
|
const progressInterval = setInterval(() => {
|
|
|
|
|
|
progress += Math.random() * 20;
|
|
|
|
|
|
if (progress > 90) progress = 90;
|
|
|
|
|
|
progressBar.style.width = progress + '%';
|
|
|
|
|
|
|
|
|
|
|
|
if (progress < 30) {
|
|
|
|
|
|
statusText.textContent = '正在上传文件...';
|
|
|
|
|
|
} else if (progress < 60) {
|
|
|
|
|
|
statusText.textContent = '正在解析文件内容...';
|
|
|
|
|
|
} else if (progress < 90) {
|
|
|
|
|
|
statusText.textContent = '正在生成知识库...';
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 500);
|
|
|
|
|
|
|
|
|
|
|
|
const response = await fetch('/api/knowledge/upload', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
body: formData
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
clearInterval(progressInterval);
|
|
|
|
|
|
progressBar.style.width = '100%';
|
|
|
|
|
|
statusText.textContent = '处理完成!';
|
|
|
|
|
|
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
progressDiv.style.display = 'none';
|
|
|
|
|
|
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
this.showNotification(`文件处理成功,生成了 ${data.knowledge_count || 0} 条知识`, 'success');
|
|
|
|
|
|
bootstrap.Modal.getInstance(document.getElementById('uploadFileModal')).hide();
|
|
|
|
|
|
document.getElementById('file-upload-form').reset();
|
|
|
|
|
|
this.loadKnowledge();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.showNotification(data.error || '文件处理失败', 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 1000);
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('文件上传失败:', error);
|
|
|
|
|
|
progressDiv.style.display = 'none';
|
|
|
|
|
|
this.showNotification('文件上传失败', 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async verifyKnowledge(knowledgeId) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch(`/api/knowledge/verify/${knowledgeId}`, {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
|
|
},
|
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
|
verified_by: 'admin'
|
|
|
|
|
|
})
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
this.showNotification('知识库验证成功', 'success');
|
|
|
|
|
|
this.loadKnowledge();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.showNotification('知识库验证失败', 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('验证知识库失败:', error);
|
|
|
|
|
|
this.showNotification('验证知识库失败', 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async unverifyKnowledge(knowledgeId) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch(`/api/knowledge/unverify/${knowledgeId}`, {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
this.showNotification('取消验证成功', 'success');
|
|
|
|
|
|
this.loadKnowledge();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.showNotification('取消验证失败', 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('取消验证失败:', error);
|
|
|
|
|
|
this.showNotification('取消验证失败', 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async deleteKnowledge(knowledgeId) {
|
|
|
|
|
|
if (!confirm('确定要删除这条知识库条目吗?')) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch(`/api/knowledge/delete/${knowledgeId}`, {
|
|
|
|
|
|
method: 'DELETE'
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
this.showNotification('知识库删除成功', 'success');
|
|
|
|
|
|
this.loadKnowledge();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.showNotification('知识库删除失败', 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('删除知识库失败:', error);
|
|
|
|
|
|
this.showNotification('删除知识库失败', 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 工单管理
|
|
|
|
|
|
async loadWorkOrders() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const statusFilter = document.getElementById('workorder-status-filter').value;
|
|
|
|
|
|
const priorityFilter = document.getElementById('workorder-priority-filter').value;
|
|
|
|
|
|
|
|
|
|
|
|
let url = '/api/workorders';
|
|
|
|
|
|
const params = new URLSearchParams();
|
|
|
|
|
|
if (statusFilter !== 'all') params.append('status', statusFilter);
|
|
|
|
|
|
if (priorityFilter !== 'all') params.append('priority', priorityFilter);
|
|
|
|
|
|
if (params.toString()) url += '?' + params.toString();
|
|
|
|
|
|
|
|
|
|
|
|
const response = await fetch(url);
|
|
|
|
|
|
const workorders = await response.json();
|
|
|
|
|
|
this.updateWorkOrdersDisplay(workorders);
|
|
|
|
|
|
this.updateWorkOrderStatistics(workorders);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('加载工单失败:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
updateWorkOrdersDisplay(workorders) {
|
|
|
|
|
|
const container = document.getElementById('workorders-list');
|
|
|
|
|
|
|
|
|
|
|
|
if (workorders.length === 0) {
|
|
|
|
|
|
container.innerHTML = '<div class="empty-state"><i class="fas fa-tasks"></i><p>暂无工单</p></div>';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const workordersHtml = workorders.map(workorder => `
|
|
|
|
|
|
<div class="work-order-item">
|
|
|
|
|
|
<div class="d-flex justify-content-between align-items-start">
|
|
|
|
|
|
<div class="flex-grow-1">
|
|
|
|
|
|
<h6 class="mb-1">${workorder.title}</h6>
|
|
|
|
|
|
<p class="text-muted mb-2">${workorder.description}</p>
|
|
|
|
|
|
<div class="d-flex gap-3">
|
|
|
|
|
|
<span class="badge bg-${this.getPriorityColor(workorder.priority)}">${this.getPriorityText(workorder.priority)}</span>
|
|
|
|
|
|
<span class="badge bg-${this.getStatusColor(workorder.status)}">${this.getStatusText(workorder.status)}</span>
|
|
|
|
|
|
<small class="text-muted">分类: ${workorder.category}</small>
|
|
|
|
|
|
<small class="text-muted">创建时间: ${new Date(workorder.created_at).toLocaleString()}</small>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="ms-3">
|
2025-09-10 23:13:08 +08:00
|
|
|
|
<div class="btn-group" role="group">
|
|
|
|
|
|
<button class="btn btn-sm btn-outline-info" onclick="dashboard.viewWorkOrderDetails(${workorder.id})" title="查看详情">
|
|
|
|
|
|
<i class="fas fa-eye"></i>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button class="btn btn-sm btn-outline-primary" onclick="dashboard.updateWorkOrder(${workorder.id})" title="编辑">
|
|
|
|
|
|
<i class="fas fa-edit"></i>
|
|
|
|
|
|
</button>
|
2025-09-18 19:37:14 +01:00
|
|
|
|
<button class="btn btn-sm btn-outline-danger" onclick="dashboard.deleteWorkOrder(${workorder.id})" title="删除">
|
|
|
|
|
|
<i class="fas fa-trash"></i>
|
|
|
|
|
|
</button>
|
2025-09-10 23:13:08 +08:00
|
|
|
|
</div>
|
2025-09-06 21:06:18 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`).join('');
|
|
|
|
|
|
|
|
|
|
|
|
container.innerHTML = workordersHtml;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
updateWorkOrderStatistics(workorders) {
|
|
|
|
|
|
const stats = workorders.reduce((acc, wo) => {
|
|
|
|
|
|
acc.total = (acc.total || 0) + 1;
|
|
|
|
|
|
acc[wo.status] = (acc[wo.status] || 0) + 1;
|
|
|
|
|
|
return acc;
|
|
|
|
|
|
}, {});
|
|
|
|
|
|
|
|
|
|
|
|
document.getElementById('workorders-total').textContent = stats.total || 0;
|
|
|
|
|
|
document.getElementById('workorders-open').textContent = stats.open || 0;
|
|
|
|
|
|
document.getElementById('workorders-progress').textContent = stats.in_progress || 0;
|
|
|
|
|
|
document.getElementById('workorders-resolved').textContent = stats.resolved || 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async createWorkOrder() {
|
|
|
|
|
|
const title = document.getElementById('wo-title').value.trim();
|
|
|
|
|
|
const description = document.getElementById('wo-description').value.trim();
|
|
|
|
|
|
const category = document.getElementById('wo-category').value;
|
|
|
|
|
|
const priority = document.getElementById('wo-priority').value;
|
|
|
|
|
|
|
|
|
|
|
|
if (!title || !description) {
|
|
|
|
|
|
this.showNotification('请填写完整信息', 'warning');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/api/workorders', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
|
|
},
|
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
|
title,
|
|
|
|
|
|
description,
|
|
|
|
|
|
category,
|
|
|
|
|
|
priority
|
|
|
|
|
|
})
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
this.showNotification('工单创建成功', 'success');
|
|
|
|
|
|
bootstrap.Modal.getInstance(document.getElementById('createWorkOrderModal')).hide();
|
|
|
|
|
|
document.getElementById('work-order-form').reset();
|
2025-09-18 19:37:14 +01:00
|
|
|
|
// 立即刷新工单列表和统计
|
|
|
|
|
|
await this.loadWorkOrders();
|
|
|
|
|
|
await this.loadAnalytics();
|
2025-09-06 21:06:18 +08:00
|
|
|
|
} else {
|
2025-09-18 19:37:14 +01:00
|
|
|
|
this.showNotification('创建工单失败: ' + (data.error || '未知错误'), 'error');
|
2025-09-06 21:06:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('创建工单失败:', error);
|
|
|
|
|
|
this.showNotification('创建工单失败', 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-10 23:13:08 +08:00
|
|
|
|
async viewWorkOrderDetails(workorderId) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch(`/api/workorders/${workorderId}`);
|
|
|
|
|
|
const workorder = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (workorder.error) {
|
|
|
|
|
|
this.showNotification('获取工单详情失败', 'error');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this.showWorkOrderDetailsModal(workorder);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('获取工单详情失败:', error);
|
|
|
|
|
|
this.showNotification('获取工单详情失败', 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
showWorkOrderDetailsModal(workorder) {
|
|
|
|
|
|
// 创建模态框HTML
|
|
|
|
|
|
const modalHtml = `
|
|
|
|
|
|
<div class="modal fade" id="workOrderDetailsModal" tabindex="-1">
|
|
|
|
|
|
<div class="modal-dialog modal-lg">
|
|
|
|
|
|
<div class="modal-content">
|
|
|
|
|
|
<div class="modal-header">
|
|
|
|
|
|
<h5 class="modal-title">工单详情 - ${workorder.order_id || workorder.id}</h5>
|
|
|
|
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="modal-body">
|
|
|
|
|
|
<div class="row mb-3">
|
|
|
|
|
|
<div class="col-md-6">
|
|
|
|
|
|
<h6>基本信息</h6>
|
|
|
|
|
|
<table class="table table-sm">
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td><strong>工单号:</strong></td>
|
|
|
|
|
|
<td>${workorder.order_id || workorder.id}</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td><strong>标题:</strong></td>
|
|
|
|
|
|
<td>${workorder.title}</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td><strong>分类:</strong></td>
|
|
|
|
|
|
<td>${workorder.category}</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td><strong>优先级:</strong></td>
|
|
|
|
|
|
<td><span class="badge bg-${this.getPriorityColor(workorder.priority)}">${this.getPriorityText(workorder.priority)}</span></td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td><strong>状态:</strong></td>
|
|
|
|
|
|
<td><span class="badge bg-${this.getStatusColor(workorder.status)}">${this.getStatusText(workorder.status)}</span></td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td><strong>创建时间:</strong></td>
|
|
|
|
|
|
<td>${new Date(workorder.created_at).toLocaleString()}</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td><strong>更新时间:</strong></td>
|
|
|
|
|
|
<td>${new Date(workorder.updated_at).toLocaleString()}</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
</table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="col-md-6">
|
|
|
|
|
|
<h6>问题描述</h6>
|
|
|
|
|
|
<div class="border p-3 rounded">
|
|
|
|
|
|
${workorder.description}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
${workorder.resolution ? `
|
|
|
|
|
|
<h6 class="mt-3">解决方案</h6>
|
|
|
|
|
|
<div class="border p-3 rounded bg-light">
|
|
|
|
|
|
${workorder.resolution}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
` : ''}
|
|
|
|
|
|
${workorder.satisfaction_score ? `
|
|
|
|
|
|
<h6 class="mt-3">满意度评分</h6>
|
|
|
|
|
|
<div class="border p-3 rounded">
|
|
|
|
|
|
<div class="progress">
|
|
|
|
|
|
<div class="progress-bar" style="width: ${workorder.satisfaction_score * 100}%"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<small class="text-muted">${workorder.satisfaction_score}/5.0</small>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
` : ''}
|
2025-09-16 17:05:50 +01:00
|
|
|
|
<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>
|
2025-09-10 23:13:08 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
${workorder.conversations && workorder.conversations.length > 0 ? `
|
|
|
|
|
|
<h6>对话记录</h6>
|
|
|
|
|
|
<div class="conversation-history" style="max-height: 300px; overflow-y: auto;">
|
|
|
|
|
|
${workorder.conversations.map(conv => `
|
|
|
|
|
|
<div class="border-bottom pb-2 mb-2">
|
|
|
|
|
|
<div class="d-flex justify-content-between">
|
|
|
|
|
|
<small class="text-muted">${new Date(conv.timestamp).toLocaleString()}</small>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="mb-1">
|
|
|
|
|
|
<strong>用户:</strong> ${conv.user_message}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<strong>助手:</strong> ${conv.assistant_response}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`).join('')}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
` : ''}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="modal-footer">
|
|
|
|
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
|
|
|
|
|
|
<button type="button" class="btn btn-primary" onclick="dashboard.updateWorkOrder(${workorder.id})">编辑工单</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
// 移除已存在的模态框
|
|
|
|
|
|
const existingModal = document.getElementById('workOrderDetailsModal');
|
|
|
|
|
|
if (existingModal) {
|
|
|
|
|
|
existingModal.remove();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 添加新的模态框到页面
|
|
|
|
|
|
document.body.insertAdjacentHTML('beforeend', modalHtml);
|
|
|
|
|
|
|
|
|
|
|
|
// 显示模态框
|
|
|
|
|
|
const modal = new bootstrap.Modal(document.getElementById('workOrderDetailsModal'));
|
|
|
|
|
|
modal.show();
|
|
|
|
|
|
|
|
|
|
|
|
// 模态框关闭时移除DOM元素
|
|
|
|
|
|
document.getElementById('workOrderDetailsModal').addEventListener('hidden.bs.modal', function() {
|
|
|
|
|
|
this.remove();
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async updateWorkOrder(workorderId) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 获取工单详情
|
|
|
|
|
|
const response = await fetch(`/api/workorders/${workorderId}`);
|
|
|
|
|
|
const workorder = await response.json();
|
|
|
|
|
|
|
2025-09-18 19:37:14 +01:00
|
|
|
|
if (workorder.id) {
|
2025-09-10 23:13:08 +08:00
|
|
|
|
this.showEditWorkOrderModal(workorder);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
throw new Error(workorder.error || '获取工单详情失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('获取工单详情失败:', error);
|
|
|
|
|
|
this.showNotification('获取工单详情失败: ' + error.message, 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-18 19:37:14 +01:00
|
|
|
|
async deleteWorkOrder(workorderId) {
|
|
|
|
|
|
console.log('deleteWorkOrder called with ID:', workorderId);
|
|
|
|
|
|
|
|
|
|
|
|
if (!confirm('确定要删除这个工单吗?此操作不可撤销。')) {
|
|
|
|
|
|
console.log('用户取消了删除操作');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
console.log('发送删除请求到:', `/api/workorders/${workorderId}`);
|
|
|
|
|
|
const response = await fetch(`/api/workorders/${workorderId}`, {
|
|
|
|
|
|
method: 'DELETE'
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
console.log('删除响应状态:', response.status);
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
console.log('删除响应数据:', data);
|
|
|
|
|
|
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
this.showNotification('工单删除成功', 'success');
|
|
|
|
|
|
// 立即刷新工单列表和统计
|
|
|
|
|
|
await this.loadWorkOrders();
|
|
|
|
|
|
await this.loadAnalytics();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.showNotification('删除工单失败: ' + (data.error || '未知错误'), 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('删除工单失败:', error);
|
|
|
|
|
|
this.showNotification('删除工单失败: ' + error.message, 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-10 23:13:08 +08:00
|
|
|
|
showEditWorkOrderModal(workorder) {
|
|
|
|
|
|
// 创建编辑工单模态框
|
|
|
|
|
|
const modalHtml = `
|
|
|
|
|
|
<div class="modal fade" id="editWorkOrderModal" tabindex="-1" aria-labelledby="editWorkOrderModalLabel" aria-hidden="true">
|
|
|
|
|
|
<div class="modal-dialog modal-lg">
|
|
|
|
|
|
<div class="modal-content">
|
|
|
|
|
|
<div class="modal-header">
|
|
|
|
|
|
<h5 class="modal-title" id="editWorkOrderModalLabel">编辑工单 #${workorder.id}</h5>
|
|
|
|
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="modal-body">
|
|
|
|
|
|
<form id="editWorkOrderForm">
|
|
|
|
|
|
<div class="row">
|
|
|
|
|
|
<div class="col-md-8">
|
|
|
|
|
|
<div class="mb-3">
|
|
|
|
|
|
<label for="editTitle" class="form-label">标题 *</label>
|
|
|
|
|
|
<input type="text" class="form-control" id="editTitle" value="${workorder.title}" required>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="col-md-4">
|
|
|
|
|
|
<div class="mb-3">
|
|
|
|
|
|
<label for="editPriority" class="form-label">优先级</label>
|
|
|
|
|
|
<select class="form-select" id="editPriority">
|
|
|
|
|
|
<option value="low" ${workorder.priority === 'low' ? 'selected' : ''}>低</option>
|
|
|
|
|
|
<option value="medium" ${workorder.priority === 'medium' ? 'selected' : ''}>中</option>
|
|
|
|
|
|
<option value="high" ${workorder.priority === 'high' ? 'selected' : ''}>高</option>
|
|
|
|
|
|
<option value="urgent" ${workorder.priority === 'urgent' ? 'selected' : ''}>紧急</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="row">
|
|
|
|
|
|
<div class="col-md-6">
|
|
|
|
|
|
<div class="mb-3">
|
|
|
|
|
|
<label for="editCategory" class="form-label">分类</label>
|
|
|
|
|
|
<select class="form-select" id="editCategory">
|
|
|
|
|
|
<option value="技术问题" ${workorder.category === '技术问题' ? 'selected' : ''}>技术问题</option>
|
|
|
|
|
|
<option value="业务问题" ${workorder.category === '业务问题' ? 'selected' : ''}>业务问题</option>
|
|
|
|
|
|
<option value="系统故障" ${workorder.category === '系统故障' ? 'selected' : ''}>系统故障</option>
|
|
|
|
|
|
<option value="功能需求" ${workorder.category === '功能需求' ? 'selected' : ''}>功能需求</option>
|
|
|
|
|
|
<option value="其他" ${workorder.category === '其他' ? 'selected' : ''}>其他</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="col-md-6">
|
|
|
|
|
|
<div class="mb-3">
|
|
|
|
|
|
<label for="editStatus" class="form-label">状态</label>
|
|
|
|
|
|
<select class="form-select" id="editStatus">
|
|
|
|
|
|
<option value="open" ${workorder.status === 'open' ? 'selected' : ''}>待处理</option>
|
|
|
|
|
|
<option value="in_progress" ${workorder.status === 'in_progress' ? 'selected' : ''}>处理中</option>
|
|
|
|
|
|
<option value="resolved" ${workorder.status === 'resolved' ? 'selected' : ''}>已解决</option>
|
|
|
|
|
|
<option value="closed" ${workorder.status === 'closed' ? 'selected' : ''}>已关闭</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="mb-3">
|
|
|
|
|
|
<label for="editDescription" class="form-label">描述 *</label>
|
|
|
|
|
|
<textarea class="form-control" id="editDescription" rows="4" required>${workorder.description}</textarea>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="mb-3">
|
|
|
|
|
|
<label for="editResolution" class="form-label">解决方案</label>
|
|
|
|
|
|
<textarea class="form-control" id="editResolution" rows="3" placeholder="请输入解决方案...">${workorder.resolution || ''}</textarea>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="row">
|
|
|
|
|
|
<div class="col-md-6">
|
|
|
|
|
|
<div class="mb-3">
|
|
|
|
|
|
<label for="editSatisfactionScore" class="form-label">满意度评分 (1-5)</label>
|
|
|
|
|
|
<input type="number" class="form-control" id="editSatisfactionScore" min="1" max="5" value="${workorder.satisfaction_score || ''}">
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</form>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="modal-footer">
|
|
|
|
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
|
|
|
|
|
<button type="button" class="btn btn-primary" onclick="dashboard.saveWorkOrder(${workorder.id})">保存修改</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
// 移除已存在的模态框
|
|
|
|
|
|
const existingModal = document.getElementById('editWorkOrderModal');
|
|
|
|
|
|
if (existingModal) {
|
|
|
|
|
|
existingModal.remove();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 添加新模态框到页面
|
|
|
|
|
|
document.body.insertAdjacentHTML('beforeend', modalHtml);
|
|
|
|
|
|
|
|
|
|
|
|
// 显示模态框
|
|
|
|
|
|
const modal = new bootstrap.Modal(document.getElementById('editWorkOrderModal'));
|
|
|
|
|
|
modal.show();
|
|
|
|
|
|
|
|
|
|
|
|
// 模态框关闭时清理
|
|
|
|
|
|
document.getElementById('editWorkOrderModal').addEventListener('hidden.bs.modal', function() {
|
|
|
|
|
|
this.remove();
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async saveWorkOrder(workorderId) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 获取表单数据
|
|
|
|
|
|
const formData = {
|
|
|
|
|
|
title: document.getElementById('editTitle').value,
|
|
|
|
|
|
description: document.getElementById('editDescription').value,
|
|
|
|
|
|
category: document.getElementById('editCategory').value,
|
|
|
|
|
|
priority: document.getElementById('editPriority').value,
|
|
|
|
|
|
status: document.getElementById('editStatus').value,
|
|
|
|
|
|
resolution: document.getElementById('editResolution').value,
|
|
|
|
|
|
satisfaction_score: parseInt(document.getElementById('editSatisfactionScore').value) || null
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 验证必填字段
|
|
|
|
|
|
if (!formData.title.trim() || !formData.description.trim()) {
|
|
|
|
|
|
this.showNotification('标题和描述不能为空', 'error');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 发送更新请求
|
|
|
|
|
|
const response = await fetch(`/api/workorders/${workorderId}`, {
|
|
|
|
|
|
method: 'PUT',
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
|
},
|
|
|
|
|
|
body: JSON.stringify(formData)
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const result = await response.json();
|
|
|
|
|
|
|
2025-09-18 19:37:14 +01:00
|
|
|
|
if (result.success) {
|
2025-09-10 23:13:08 +08:00
|
|
|
|
this.showNotification('工单更新成功', 'success');
|
|
|
|
|
|
// 关闭模态框
|
|
|
|
|
|
const modal = bootstrap.Modal.getInstance(document.getElementById('editWorkOrderModal'));
|
|
|
|
|
|
modal.hide();
|
2025-09-18 19:37:14 +01:00
|
|
|
|
// 刷新工单列表和统计
|
|
|
|
|
|
await this.loadWorkOrders();
|
|
|
|
|
|
await this.loadAnalytics();
|
2025-09-10 23:13:08 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
throw new Error(result.error || '更新工单失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('更新工单失败:', error);
|
|
|
|
|
|
this.showNotification('更新工单失败: ' + error.message, 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 工单导入功能
|
|
|
|
|
|
showImportModal() {
|
|
|
|
|
|
// 显示导入模态框
|
|
|
|
|
|
const modal = new bootstrap.Modal(document.getElementById('importWorkOrderModal'));
|
|
|
|
|
|
modal.show();
|
|
|
|
|
|
|
|
|
|
|
|
// 重置表单
|
|
|
|
|
|
document.getElementById('excel-file-input').value = '';
|
|
|
|
|
|
document.getElementById('import-progress').classList.add('d-none');
|
|
|
|
|
|
document.getElementById('import-result').classList.add('d-none');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async downloadTemplate() {
|
|
|
|
|
|
try {
|
2025-09-16 17:05:50 +01:00
|
|
|
|
// 直接请求文件接口,避免浏览器跨源/权限限制
|
|
|
|
|
|
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');
|
2025-09-10 23:13:08 +08:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('下载模板失败:', error);
|
|
|
|
|
|
this.showNotification('下载模板失败: ' + error.message, 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async importWorkOrders() {
|
|
|
|
|
|
const fileInput = document.getElementById('excel-file-input');
|
|
|
|
|
|
const file = fileInput.files[0];
|
|
|
|
|
|
|
|
|
|
|
|
if (!file) {
|
|
|
|
|
|
this.showNotification('请选择要导入的Excel文件', 'error');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证文件类型
|
|
|
|
|
|
if (!file.name.match(/\.(xlsx|xls)$/)) {
|
|
|
|
|
|
this.showNotification('只支持Excel文件(.xlsx, .xls)', 'error');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证文件大小
|
|
|
|
|
|
if (file.size > 16 * 1024 * 1024) {
|
|
|
|
|
|
this.showNotification('文件大小不能超过16MB', 'error');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 显示进度条
|
|
|
|
|
|
document.getElementById('import-progress').classList.remove('d-none');
|
|
|
|
|
|
document.getElementById('import-result').classList.add('d-none');
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 创建FormData
|
|
|
|
|
|
const formData = new FormData();
|
|
|
|
|
|
formData.append('file', file);
|
|
|
|
|
|
|
|
|
|
|
|
// 发送导入请求
|
|
|
|
|
|
const response = await fetch('/api/workorders/import', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
body: formData
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const result = await response.json();
|
|
|
|
|
|
|
2025-09-18 19:37:14 +01:00
|
|
|
|
if (result.success) {
|
2025-09-10 23:13:08 +08:00
|
|
|
|
// 显示成功消息
|
|
|
|
|
|
document.getElementById('import-progress').classList.add('d-none');
|
|
|
|
|
|
document.getElementById('import-result').classList.remove('d-none');
|
|
|
|
|
|
document.getElementById('import-success-message').textContent =
|
|
|
|
|
|
`成功导入 ${result.imported_count} 个工单`;
|
|
|
|
|
|
|
|
|
|
|
|
this.showNotification(result.message, 'success');
|
|
|
|
|
|
|
|
|
|
|
|
// 刷新工单列表
|
|
|
|
|
|
this.loadWorkOrders();
|
|
|
|
|
|
|
|
|
|
|
|
// 3秒后关闭模态框
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
const modal = bootstrap.Modal.getInstance(document.getElementById('importWorkOrderModal'));
|
|
|
|
|
|
modal.hide();
|
|
|
|
|
|
}, 3000);
|
|
|
|
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
throw new Error(result.error || '导入工单失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('导入工单失败:', error);
|
|
|
|
|
|
document.getElementById('import-progress').classList.add('d-none');
|
|
|
|
|
|
this.showNotification('导入工单失败: ' + error.message, 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-18 19:37:14 +01:00
|
|
|
|
// 对话历史管理
|
|
|
|
|
|
async loadConversationHistory(page = 1, perPage = 10) {
|
2025-09-06 21:06:18 +08:00
|
|
|
|
try {
|
2025-09-18 19:37:14 +01:00
|
|
|
|
const response = await fetch(`/api/conversations?page=${page}&per_page=${perPage}`);
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
this.renderConversationList(data.conversations || []);
|
|
|
|
|
|
this.renderConversationPagination(data.pagination || {});
|
|
|
|
|
|
this.updateConversationStats(data.stats || {});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
throw new Error(data.error || '加载对话历史失败');
|
|
|
|
|
|
}
|
2025-09-06 21:06:18 +08:00
|
|
|
|
} catch (error) {
|
2025-09-18 19:37:14 +01:00
|
|
|
|
console.error('加载对话历史失败:', error);
|
|
|
|
|
|
this.showNotification('加载对话历史失败: ' + error.message, 'error');
|
2025-09-06 21:06:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-18 19:37:14 +01:00
|
|
|
|
renderConversationList(conversations) {
|
|
|
|
|
|
const container = document.getElementById('conversation-list');
|
|
|
|
|
|
if (!conversations || conversations.length === 0) {
|
|
|
|
|
|
container.innerHTML = `
|
|
|
|
|
|
<div class="empty-state">
|
|
|
|
|
|
<i class="fas fa-comments"></i>
|
|
|
|
|
|
<p>暂无对话记录</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const html = conversations.map(conv => `
|
|
|
|
|
|
<div class="card mb-3 conversation-item" data-conversation-id="${conv.id}">
|
|
|
|
|
|
<div class="card-body">
|
|
|
|
|
|
<div class="d-flex justify-content-between align-items-start mb-2">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<h6 class="mb-1">用户: ${conv.user_id || '匿名'}</h6>
|
|
|
|
|
|
<small class="text-muted">${new Date(conv.timestamp).toLocaleString()}</small>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="btn-group btn-group-sm">
|
|
|
|
|
|
<button class="btn btn-outline-primary" onclick="dashboard.viewConversation(${conv.id})">
|
|
|
|
|
|
<i class="fas fa-eye"></i>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button class="btn btn-outline-danger" onclick="dashboard.deleteConversation(${conv.id})">
|
|
|
|
|
|
<i class="fas fa-trash"></i>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="conversation-preview">
|
|
|
|
|
|
<p class="mb-1"><strong>用户:</strong> ${conv.user_message?.substring(0, 100)}${conv.user_message?.length > 100 ? '...' : ''}</p>
|
|
|
|
|
|
<p class="mb-0"><strong>助手:</strong> ${conv.assistant_response?.substring(0, 100)}${conv.assistant_response?.length > 100 ? '...' : ''}</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="mt-2">
|
|
|
|
|
|
<span class="badge bg-secondary">响应时间: ${conv.response_time || 0}ms</span>
|
|
|
|
|
|
${conv.work_order_id ? `<span class="badge bg-info">工单: ${conv.work_order_id}</span>` : ''}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`).join('');
|
|
|
|
|
|
|
|
|
|
|
|
container.innerHTML = html;
|
2025-09-10 23:13:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-18 19:37:14 +01:00
|
|
|
|
renderConversationPagination(pagination) {
|
|
|
|
|
|
const container = document.getElementById('conversation-pagination');
|
|
|
|
|
|
if (!pagination || !pagination.total_pages || pagination.total_pages <= 1) {
|
|
|
|
|
|
container.innerHTML = '';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-09-10 23:13:08 +08:00
|
|
|
|
|
2025-09-18 19:37:14 +01:00
|
|
|
|
const currentPage = pagination.current_page || 1;
|
|
|
|
|
|
const totalPages = pagination.total_pages;
|
|
|
|
|
|
|
|
|
|
|
|
let html = '<nav><ul class="pagination justify-content-center">';
|
|
|
|
|
|
|
|
|
|
|
|
// 上一页
|
|
|
|
|
|
if (currentPage > 1) {
|
|
|
|
|
|
html += `<li class="page-item"><a class="page-link" href="#" onclick="dashboard.loadConversationHistory(${currentPage - 1})">上一页</a></li>`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 页码
|
|
|
|
|
|
for (let i = Math.max(1, currentPage - 2); i <= Math.min(totalPages, currentPage + 2); i++) {
|
|
|
|
|
|
const activeClass = i === currentPage ? 'active' : '';
|
|
|
|
|
|
html += `<li class="page-item ${activeClass}"><a class="page-link" href="#" onclick="dashboard.loadConversationHistory(${i})">${i}</a></li>`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 下一页
|
|
|
|
|
|
if (currentPage < totalPages) {
|
|
|
|
|
|
html += `<li class="page-item"><a class="page-link" href="#" onclick="dashboard.loadConversationHistory(${currentPage + 1})">下一页</a></li>`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
html += '</ul></nav>';
|
|
|
|
|
|
container.innerHTML = html;
|
|
|
|
|
|
}
|
2025-09-10 23:13:08 +08:00
|
|
|
|
|
2025-09-18 19:37:14 +01:00
|
|
|
|
updateConversationStats(stats) {
|
|
|
|
|
|
document.getElementById('conversation-total').textContent = stats.total || 0;
|
|
|
|
|
|
document.getElementById('conversation-today').textContent = stats.today || 0;
|
|
|
|
|
|
document.getElementById('conversation-avg-response').textContent = `${stats.avg_response_time || 0}ms`;
|
|
|
|
|
|
document.getElementById('conversation-active-users').textContent = stats.active_users || 0;
|
|
|
|
|
|
}
|
2025-09-10 23:13:08 +08:00
|
|
|
|
|
2025-09-18 19:37:14 +01:00
|
|
|
|
async refreshConversationHistory() {
|
|
|
|
|
|
await this.loadConversationHistory();
|
|
|
|
|
|
this.showNotification('对话历史已刷新', 'success');
|
|
|
|
|
|
}
|
2025-09-10 23:13:08 +08:00
|
|
|
|
|
2025-09-18 19:37:14 +01:00
|
|
|
|
async clearAllConversations() {
|
|
|
|
|
|
if (!confirm('确定要清空所有对话历史吗?此操作不可恢复!')) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-09-10 23:13:08 +08:00
|
|
|
|
|
2025-09-18 19:37:14 +01:00
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/api/conversations/clear', { method: 'DELETE' });
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
this.showNotification('对话历史已清空', 'success');
|
|
|
|
|
|
await this.loadConversationHistory();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
throw new Error(data.error || '清空对话历史失败');
|
|
|
|
|
|
}
|
2025-09-10 23:13:08 +08:00
|
|
|
|
} catch (error) {
|
2025-09-18 19:37:14 +01:00
|
|
|
|
console.error('清空对话历史失败:', error);
|
|
|
|
|
|
this.showNotification('清空对话历史失败: ' + error.message, 'error');
|
2025-09-10 23:13:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-18 19:37:14 +01:00
|
|
|
|
async deleteConversation(conversationId) {
|
|
|
|
|
|
if (!confirm('确定要删除这条对话记录吗?')) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-09-10 23:13:08 +08:00
|
|
|
|
|
2025-09-18 19:37:14 +01:00
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch(`/api/conversations/${conversationId}`, { method: 'DELETE' });
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
this.showNotification('对话记录已删除', 'success');
|
|
|
|
|
|
await this.loadConversationHistory();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
throw new Error(data.error || '删除对话记录失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('删除对话记录失败:', error);
|
|
|
|
|
|
this.showNotification('删除对话记录失败: ' + error.message, 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-09-10 23:13:08 +08:00
|
|
|
|
|
2025-09-18 19:37:14 +01:00
|
|
|
|
async viewConversation(conversationId) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch(`/api/conversations/${conversationId}`);
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
this.showConversationModal(data);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
throw new Error(data.error || '获取对话详情失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('获取对话详情失败:', error);
|
|
|
|
|
|
this.showNotification('获取对话详情失败: ' + error.message, 'error');
|
2025-09-10 23:13:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-18 19:37:14 +01:00
|
|
|
|
showConversationModal(conversation) {
|
|
|
|
|
|
const modalHtml = `
|
|
|
|
|
|
<div class="modal fade" id="conversationModal" tabindex="-1">
|
|
|
|
|
|
<div class="modal-dialog modal-lg">
|
|
|
|
|
|
<div class="modal-content">
|
|
|
|
|
|
<div class="modal-header">
|
|
|
|
|
|
<h5 class="modal-title">对话详情</h5>
|
|
|
|
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="modal-body">
|
|
|
|
|
|
<div class="mb-3">
|
|
|
|
|
|
<strong>用户:</strong> ${conversation.user_id || '匿名'}<br>
|
|
|
|
|
|
<strong>时间:</strong> ${new Date(conversation.timestamp).toLocaleString()}<br>
|
|
|
|
|
|
<strong>响应时间:</strong> ${conversation.response_time || 0}ms
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="mb-3">
|
|
|
|
|
|
<h6>用户消息:</h6>
|
|
|
|
|
|
<div class="border p-3 rounded">${conversation.user_message || ''}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="mb-3">
|
|
|
|
|
|
<h6>助手回复:</h6>
|
|
|
|
|
|
<div class="border p-3 rounded">${conversation.assistant_response || ''}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="modal-footer">
|
|
|
|
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
2025-09-10 23:13:08 +08:00
|
|
|
|
|
2025-09-18 19:37:14 +01:00
|
|
|
|
// 移除已存在的模态框
|
|
|
|
|
|
const existingModal = document.getElementById('conversationModal');
|
|
|
|
|
|
if (existingModal) {
|
|
|
|
|
|
existingModal.remove();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 添加新模态框
|
|
|
|
|
|
document.body.insertAdjacentHTML('beforeend', modalHtml);
|
|
|
|
|
|
|
|
|
|
|
|
// 显示模态框
|
|
|
|
|
|
const modal = new bootstrap.Modal(document.getElementById('conversationModal'));
|
|
|
|
|
|
modal.show();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async filterConversations() {
|
|
|
|
|
|
const search = document.getElementById('conversation-search').value;
|
|
|
|
|
|
const userFilter = document.getElementById('conversation-user-filter').value;
|
|
|
|
|
|
const dateFilter = document.getElementById('conversation-date-filter').value;
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const params = new URLSearchParams();
|
|
|
|
|
|
if (search) params.append('search', search);
|
|
|
|
|
|
if (userFilter) params.append('user_id', userFilter);
|
|
|
|
|
|
if (dateFilter) params.append('date_filter', dateFilter);
|
|
|
|
|
|
|
|
|
|
|
|
const response = await fetch(`/api/conversations?${params.toString()}`);
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
this.renderConversationList(data.conversations || []);
|
|
|
|
|
|
this.renderConversationPagination(data.pagination || {});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
throw new Error(data.error || '筛选对话失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('筛选对话失败:', error);
|
|
|
|
|
|
this.showNotification('筛选对话失败: ' + error.message, 'error');
|
2025-09-10 23:13:08 +08:00
|
|
|
|
}
|
2025-09-18 19:37:14 +01:00
|
|
|
|
}
|
2025-09-10 23:13:08 +08:00
|
|
|
|
|
2025-09-18 19:37:14 +01:00
|
|
|
|
// Token监控
|
|
|
|
|
|
async loadTokenMonitor() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/api/token-monitor/stats');
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
this.updateTokenStats(data);
|
|
|
|
|
|
this.loadTokenChart();
|
|
|
|
|
|
this.loadTokenRecords();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
throw new Error(data.error || '加载Token监控数据失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('加载Token监控数据失败:', error);
|
|
|
|
|
|
this.showNotification('加载Token监控数据失败: ' + error.message, 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
updateTokenStats(stats) {
|
|
|
|
|
|
document.getElementById('token-today').textContent = stats.today_tokens || 0;
|
|
|
|
|
|
document.getElementById('token-month').textContent = stats.month_tokens || 0;
|
|
|
|
|
|
document.getElementById('token-cost').textContent = `¥${stats.total_cost || 0}`;
|
|
|
|
|
|
document.getElementById('token-budget').textContent = `¥${stats.budget_limit || 1000}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async loadTokenChart() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/api/token-monitor/chart');
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
this.renderTokenChart(data);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('加载Token图表失败:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
renderTokenChart(data) {
|
|
|
|
|
|
const ctx = document.getElementById('tokenChart').getContext('2d');
|
2025-09-10 23:13:08 +08:00
|
|
|
|
|
2025-09-18 19:37:14 +01:00
|
|
|
|
if (this.charts.tokenChart) {
|
|
|
|
|
|
this.charts.tokenChart.destroy();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this.charts.tokenChart = new Chart(ctx, {
|
|
|
|
|
|
type: 'line',
|
|
|
|
|
|
data: {
|
|
|
|
|
|
labels: data.labels || [],
|
|
|
|
|
|
datasets: [{
|
|
|
|
|
|
label: 'Token消耗',
|
|
|
|
|
|
data: data.tokens || [],
|
|
|
|
|
|
borderColor: '#007bff',
|
|
|
|
|
|
backgroundColor: 'rgba(0, 123, 255, 0.1)',
|
|
|
|
|
|
tension: 0.4
|
|
|
|
|
|
}, {
|
|
|
|
|
|
label: '成本',
|
|
|
|
|
|
data: data.costs || [],
|
|
|
|
|
|
borderColor: '#28a745',
|
|
|
|
|
|
backgroundColor: 'rgba(40, 167, 69, 0.1)',
|
|
|
|
|
|
tension: 0.4,
|
|
|
|
|
|
yAxisID: 'y1'
|
|
|
|
|
|
}]
|
|
|
|
|
|
},
|
2025-09-10 23:13:08 +08:00
|
|
|
|
options: {
|
|
|
|
|
|
responsive: true,
|
|
|
|
|
|
maintainAspectRatio: false,
|
2025-09-18 19:37:14 +01:00
|
|
|
|
scales: {
|
|
|
|
|
|
y: {
|
|
|
|
|
|
type: 'linear',
|
2025-09-10 23:13:08 +08:00
|
|
|
|
display: true,
|
2025-09-18 19:37:14 +01:00
|
|
|
|
position: 'left',
|
2025-09-10 23:13:08 +08:00
|
|
|
|
title: {
|
|
|
|
|
|
display: true,
|
2025-09-18 19:37:14 +01:00
|
|
|
|
text: 'Token数量'
|
2025-09-10 23:13:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
2025-09-18 19:37:14 +01:00
|
|
|
|
y1: {
|
|
|
|
|
|
type: 'linear',
|
2025-09-10 23:13:08 +08:00
|
|
|
|
display: true,
|
2025-09-18 19:37:14 +01:00
|
|
|
|
position: 'right',
|
2025-09-10 23:13:08 +08:00
|
|
|
|
title: {
|
|
|
|
|
|
display: true,
|
2025-09-18 19:37:14 +01:00
|
|
|
|
text: '成本 (元)'
|
|
|
|
|
|
},
|
|
|
|
|
|
grid: {
|
|
|
|
|
|
drawOnChartArea: false,
|
|
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async loadTokenRecords() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/api/token-monitor/records');
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
this.renderTokenRecords(data.records || []);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('加载Token记录失败:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
renderTokenRecords(records) {
|
|
|
|
|
|
const tbody = document.getElementById('token-records');
|
|
|
|
|
|
|
|
|
|
|
|
if (!records || records.length === 0) {
|
|
|
|
|
|
tbody.innerHTML = `
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td colspan="8" class="text-center text-muted">暂无记录</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
`;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const html = records.map(record => `
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td>${new Date(record.timestamp).toLocaleString()}</td>
|
|
|
|
|
|
<td>${record.user_id || '匿名'}</td>
|
|
|
|
|
|
<td>${record.model || 'qwen-turbo'}</td>
|
|
|
|
|
|
<td>${record.input_tokens || 0}</td>
|
|
|
|
|
|
<td>${record.output_tokens || 0}</td>
|
|
|
|
|
|
<td>${record.total_tokens || 0}</td>
|
|
|
|
|
|
<td>¥${record.cost || 0}</td>
|
|
|
|
|
|
<td>${record.response_time || 0}ms</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
`).join('');
|
|
|
|
|
|
|
|
|
|
|
|
tbody.innerHTML = html;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async saveTokenSettings() {
|
|
|
|
|
|
const dailyThreshold = document.getElementById('daily-threshold').value;
|
|
|
|
|
|
const monthlyBudget = document.getElementById('monthly-budget').value;
|
|
|
|
|
|
const enableAlerts = document.getElementById('enable-alerts').checked;
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/api/token-monitor/settings', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
|
daily_threshold: parseInt(dailyThreshold),
|
|
|
|
|
|
monthly_budget: parseFloat(monthlyBudget),
|
|
|
|
|
|
enable_alerts: enableAlerts
|
|
|
|
|
|
})
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
this.showNotification('Token设置已保存', 'success');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
throw new Error(data.error || '保存Token设置失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('保存Token设置失败:', error);
|
|
|
|
|
|
this.showNotification('保存Token设置失败: ' + error.message, 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async updateTokenChart(period) {
|
|
|
|
|
|
// 更新按钮状态
|
|
|
|
|
|
document.querySelectorAll('#tokenChart').forEach(btn => {
|
|
|
|
|
|
btn.classList.remove('active');
|
|
|
|
|
|
});
|
|
|
|
|
|
event.target.classList.add('active');
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch(`/api/token-monitor/chart?period=${period}`);
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
this.renderTokenChart(data);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('更新Token图表失败:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async exportTokenData() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/api/token-monitor/export');
|
|
|
|
|
|
const blob = await response.blob();
|
|
|
|
|
|
const url = window.URL.createObjectURL(blob);
|
|
|
|
|
|
const a = document.createElement('a');
|
|
|
|
|
|
a.href = url;
|
|
|
|
|
|
a.download = 'token_usage_data.xlsx';
|
|
|
|
|
|
document.body.appendChild(a);
|
|
|
|
|
|
a.click();
|
|
|
|
|
|
document.body.removeChild(a);
|
|
|
|
|
|
window.URL.revokeObjectURL(url);
|
|
|
|
|
|
this.showNotification('Token数据导出成功', 'success');
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('导出Token数据失败:', error);
|
|
|
|
|
|
this.showNotification('导出Token数据失败: ' + error.message, 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async refreshTokenData() {
|
|
|
|
|
|
await this.loadTokenMonitor();
|
|
|
|
|
|
this.showNotification('Token数据已刷新', 'success');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// AI监控
|
|
|
|
|
|
async loadAIMonitor() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/api/ai-monitor/stats');
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
this.updateAIStats(data);
|
|
|
|
|
|
this.loadModelComparisonChart();
|
|
|
|
|
|
this.loadErrorDistributionChart();
|
|
|
|
|
|
this.loadErrorLog();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
throw new Error(data.error || '加载AI监控数据失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('加载AI监控数据失败:', error);
|
|
|
|
|
|
this.showNotification('加载AI监控数据失败: ' + error.message, 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
updateAIStats(stats) {
|
|
|
|
|
|
document.getElementById('ai-success-rate').textContent = `${stats.success_rate || 0}%`;
|
|
|
|
|
|
document.getElementById('ai-response-time').textContent = `${stats.avg_response_time || 0}ms`;
|
|
|
|
|
|
document.getElementById('ai-error-rate').textContent = `${stats.error_rate || 0}%`;
|
|
|
|
|
|
document.getElementById('ai-total-calls').textContent = stats.total_calls || 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async loadModelComparisonChart() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/api/ai-monitor/model-comparison');
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
this.renderModelComparisonChart(data);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('加载模型对比图表失败:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
renderModelComparisonChart(data) {
|
|
|
|
|
|
const ctx = document.getElementById('modelComparisonChart').getContext('2d');
|
|
|
|
|
|
|
|
|
|
|
|
if (this.charts.modelComparisonChart) {
|
|
|
|
|
|
this.charts.modelComparisonChart.destroy();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this.charts.modelComparisonChart = new Chart(ctx, {
|
|
|
|
|
|
type: 'bar',
|
|
|
|
|
|
data: {
|
|
|
|
|
|
labels: data.models || [],
|
|
|
|
|
|
datasets: [{
|
|
|
|
|
|
label: '成功率 (%)',
|
|
|
|
|
|
data: data.success_rates || [],
|
|
|
|
|
|
backgroundColor: 'rgba(40, 167, 69, 0.8)'
|
|
|
|
|
|
}, {
|
|
|
|
|
|
label: '平均响应时间 (ms)',
|
|
|
|
|
|
data: data.response_times || [],
|
|
|
|
|
|
backgroundColor: 'rgba(255, 193, 7, 0.8)',
|
|
|
|
|
|
yAxisID: 'y1'
|
|
|
|
|
|
}]
|
|
|
|
|
|
},
|
|
|
|
|
|
options: {
|
|
|
|
|
|
responsive: true,
|
|
|
|
|
|
maintainAspectRatio: false,
|
|
|
|
|
|
scales: {
|
|
|
|
|
|
y: {
|
|
|
|
|
|
type: 'linear',
|
|
|
|
|
|
display: true,
|
|
|
|
|
|
position: 'left',
|
|
|
|
|
|
title: {
|
|
|
|
|
|
display: true,
|
|
|
|
|
|
text: '成功率 (%)'
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
y1: {
|
|
|
|
|
|
type: 'linear',
|
|
|
|
|
|
display: true,
|
|
|
|
|
|
position: 'right',
|
|
|
|
|
|
title: {
|
|
|
|
|
|
display: true,
|
|
|
|
|
|
text: '响应时间 (ms)'
|
|
|
|
|
|
},
|
|
|
|
|
|
grid: {
|
|
|
|
|
|
drawOnChartArea: false,
|
|
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async loadErrorDistributionChart() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/api/ai-monitor/error-distribution');
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
this.renderErrorDistributionChart(data);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('加载错误分布图表失败:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
renderErrorDistributionChart(data) {
|
|
|
|
|
|
const ctx = document.getElementById('errorDistributionChart').getContext('2d');
|
|
|
|
|
|
|
|
|
|
|
|
if (this.charts.errorDistributionChart) {
|
|
|
|
|
|
this.charts.errorDistributionChart.destroy();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this.charts.errorDistributionChart = new Chart(ctx, {
|
|
|
|
|
|
type: 'doughnut',
|
|
|
|
|
|
data: {
|
|
|
|
|
|
labels: data.error_types || [],
|
|
|
|
|
|
datasets: [{
|
|
|
|
|
|
data: data.counts || [],
|
|
|
|
|
|
backgroundColor: [
|
|
|
|
|
|
'#dc3545',
|
|
|
|
|
|
'#fd7e14',
|
|
|
|
|
|
'#ffc107',
|
|
|
|
|
|
'#17a2b8',
|
|
|
|
|
|
'#6c757d'
|
|
|
|
|
|
]
|
|
|
|
|
|
}]
|
|
|
|
|
|
},
|
|
|
|
|
|
options: {
|
|
|
|
|
|
responsive: true,
|
|
|
|
|
|
maintainAspectRatio: false,
|
|
|
|
|
|
plugins: {
|
|
|
|
|
|
legend: {
|
|
|
|
|
|
position: 'bottom'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async loadErrorLog() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/api/ai-monitor/error-log');
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
this.renderErrorLog(data.errors || []);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('加载错误日志失败:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
renderErrorLog(errors) {
|
|
|
|
|
|
const tbody = document.getElementById('error-log');
|
|
|
|
|
|
|
|
|
|
|
|
if (!errors || errors.length === 0) {
|
|
|
|
|
|
tbody.innerHTML = `
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td colspan="6" class="text-center text-muted">暂无错误记录</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
`;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const html = errors.map(error => `
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td>${new Date(error.timestamp).toLocaleString()}</td>
|
|
|
|
|
|
<td><span class="badge bg-danger">${error.error_type || '未知'}</span></td>
|
|
|
|
|
|
<td>${error.error_message || ''}</td>
|
|
|
|
|
|
<td>${error.model || 'qwen-turbo'}</td>
|
|
|
|
|
|
<td>${error.user_id || '匿名'}</td>
|
|
|
|
|
|
<td>
|
|
|
|
|
|
<button class="btn btn-outline-primary btn-sm" onclick="dashboard.viewErrorDetail(${error.id})">
|
|
|
|
|
|
<i class="fas fa-eye"></i>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
`).join('');
|
|
|
|
|
|
|
|
|
|
|
|
tbody.innerHTML = html;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async refreshErrorLog() {
|
|
|
|
|
|
await this.loadErrorLog();
|
|
|
|
|
|
this.showNotification('错误日志已刷新', 'success');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async clearErrorLog() {
|
|
|
|
|
|
if (!confirm('确定要清空错误日志吗?')) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/api/ai-monitor/error-log', { method: 'DELETE' });
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
this.showNotification('错误日志已清空', 'success');
|
|
|
|
|
|
await this.loadErrorLog();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
throw new Error(data.error || '清空错误日志失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('清空错误日志失败:', error);
|
|
|
|
|
|
this.showNotification('清空错误日志失败: ' + error.message, 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 系统优化
|
|
|
|
|
|
async loadSystemOptimizer() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/api/system-optimizer/status');
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
this.updateSystemStats(data);
|
|
|
|
|
|
this.loadSecuritySettings();
|
|
|
|
|
|
this.loadTrafficSettings();
|
|
|
|
|
|
this.loadCostSettings();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
throw new Error(data.error || '加载系统优化数据失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('加载系统优化数据失败:', error);
|
|
|
|
|
|
this.showNotification('加载系统优化数据失败: ' + error.message, 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
updateSystemStats(stats) {
|
|
|
|
|
|
document.getElementById('cpu-usage').textContent = `${stats.cpu_usage || 0}%`;
|
|
|
|
|
|
document.getElementById('memory-usage-percent').textContent = `${stats.memory_usage || 0}%`;
|
|
|
|
|
|
document.getElementById('disk-usage').textContent = `${stats.disk_usage || 0}%`;
|
|
|
|
|
|
document.getElementById('network-latency').textContent = `${stats.network_latency || 0}ms`;
|
|
|
|
|
|
|
|
|
|
|
|
// 更新健康指标
|
|
|
|
|
|
this.updateHealthIndicator('system-health-indicator', stats.system_health || 95);
|
|
|
|
|
|
this.updateHealthIndicator('database-health-indicator', stats.database_health || 98);
|
|
|
|
|
|
this.updateHealthIndicator('api-health-indicator', stats.api_health || 92);
|
|
|
|
|
|
this.updateHealthIndicator('cache-health-indicator', stats.cache_health || 99);
|
|
|
|
|
|
|
|
|
|
|
|
document.getElementById('system-health-score').textContent = `${stats.system_health || 95}%`;
|
|
|
|
|
|
document.getElementById('database-health-score').textContent = `${stats.database_health || 98}%`;
|
|
|
|
|
|
document.getElementById('api-health-score').textContent = `${stats.api_health || 92}%`;
|
|
|
|
|
|
document.getElementById('cache-health-score').textContent = `${stats.cache_health || 99}%`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
updateHealthIndicator(elementId, score) {
|
|
|
|
|
|
const element = document.getElementById(elementId);
|
|
|
|
|
|
if (!element) return;
|
|
|
|
|
|
|
|
|
|
|
|
element.className = 'health-dot';
|
|
|
|
|
|
if (score >= 95) element.classList.add('excellent');
|
|
|
|
|
|
else if (score >= 85) element.classList.add('good');
|
|
|
|
|
|
else if (score >= 70) element.classList.add('fair');
|
|
|
|
|
|
else if (score >= 50) element.classList.add('poor');
|
|
|
|
|
|
else element.classList.add('critical');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async optimizeCPU() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/api/system-optimizer/optimize-cpu', { method: 'POST' });
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
this.showNotification('CPU优化完成', 'success');
|
|
|
|
|
|
this.updateOptimizationProgress('cpu-optimization', data.progress || 100);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
throw new Error(data.error || 'CPU优化失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('CPU优化失败:', error);
|
|
|
|
|
|
this.showNotification('CPU优化失败: ' + error.message, 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async optimizeMemory() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/api/system-optimizer/optimize-memory', { method: 'POST' });
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
this.showNotification('内存优化完成', 'success');
|
|
|
|
|
|
this.updateOptimizationProgress('memory-optimization', data.progress || 100);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
throw new Error(data.error || '内存优化失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('内存优化失败:', error);
|
|
|
|
|
|
this.showNotification('内存优化失败: ' + error.message, 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async optimizeDisk() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/api/system-optimizer/optimize-disk', { method: 'POST' });
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
this.showNotification('磁盘优化完成', 'success');
|
|
|
|
|
|
this.updateOptimizationProgress('disk-optimization', data.progress || 100);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
throw new Error(data.error || '磁盘优化失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('磁盘优化失败:', error);
|
|
|
|
|
|
this.showNotification('磁盘优化失败: ' + error.message, 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
updateOptimizationProgress(elementId, progress) {
|
|
|
|
|
|
const element = document.getElementById(elementId);
|
|
|
|
|
|
if (element) {
|
|
|
|
|
|
element.style.width = `${progress}%`;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async saveSecuritySettings() {
|
|
|
|
|
|
const settings = {
|
|
|
|
|
|
input_validation: document.getElementById('input-validation').checked,
|
|
|
|
|
|
rate_limiting: document.getElementById('rate-limiting').checked,
|
|
|
|
|
|
sql_injection_protection: document.getElementById('sql-injection-protection').checked,
|
|
|
|
|
|
xss_protection: document.getElementById('xss-protection').checked
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/api/system-optimizer/security-settings', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
|
body: JSON.stringify(settings)
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
this.showNotification('安全设置已保存', 'success');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
throw new Error(data.error || '保存安全设置失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('保存安全设置失败:', error);
|
|
|
|
|
|
this.showNotification('保存安全设置失败: ' + error.message, 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async saveTrafficSettings() {
|
|
|
|
|
|
const settings = {
|
|
|
|
|
|
request_limit: parseInt(document.getElementById('request-limit').value),
|
|
|
|
|
|
concurrent_limit: parseInt(document.getElementById('concurrent-limit').value),
|
|
|
|
|
|
ip_whitelist: document.getElementById('ip-whitelist').value.split('\n').filter(ip => ip.trim())
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/api/system-optimizer/traffic-settings', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
|
body: JSON.stringify(settings)
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
this.showNotification('流量设置已保存', 'success');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
throw new Error(data.error || '保存流量设置失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('保存流量设置失败:', error);
|
|
|
|
|
|
this.showNotification('保存流量设置失败: ' + error.message, 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async saveCostSettings() {
|
|
|
|
|
|
const settings = {
|
|
|
|
|
|
monthly_budget_limit: parseFloat(document.getElementById('monthly-budget-limit').value),
|
|
|
|
|
|
per_call_cost_limit: parseFloat(document.getElementById('per-call-cost-limit').value),
|
|
|
|
|
|
auto_cost_control: document.getElementById('auto-cost-control').checked
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/api/system-optimizer/cost-settings', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
|
body: JSON.stringify(settings)
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
this.showNotification('成本设置已保存', 'success');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
throw new Error(data.error || '保存成本设置失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('保存成本设置失败:', error);
|
|
|
|
|
|
this.showNotification('保存成本设置失败: ' + error.message, 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async runHealthCheck() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/api/system-optimizer/health-check', { method: 'POST' });
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
this.showNotification('健康检查完成', 'success');
|
|
|
|
|
|
this.updateSystemStats(data);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
throw new Error(data.error || '健康检查失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('健康检查失败:', error);
|
|
|
|
|
|
this.showNotification('健康检查失败: ' + error.message, 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async refreshSystemStatus() {
|
|
|
|
|
|
await this.loadSystemOptimizer();
|
|
|
|
|
|
this.showNotification('系统状态已刷新', 'success');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async loadSecuritySettings() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/api/system-optimizer/security-settings');
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
document.getElementById('input-validation').checked = data.input_validation || false;
|
|
|
|
|
|
document.getElementById('rate-limiting').checked = data.rate_limiting || false;
|
|
|
|
|
|
document.getElementById('sql-injection-protection').checked = data.sql_injection_protection || false;
|
|
|
|
|
|
document.getElementById('xss-protection').checked = data.xss_protection || false;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('加载安全设置失败:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async loadTrafficSettings() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/api/system-optimizer/traffic-settings');
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
document.getElementById('request-limit').value = data.request_limit || 100;
|
|
|
|
|
|
document.getElementById('concurrent-limit').value = data.concurrent_limit || 50;
|
|
|
|
|
|
document.getElementById('ip-whitelist').value = (data.ip_whitelist || []).join('\n');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('加载流量设置失败:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async loadCostSettings() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/api/system-optimizer/cost-settings');
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
document.getElementById('monthly-budget-limit').value = data.monthly_budget_limit || 1000;
|
|
|
|
|
|
document.getElementById('per-call-cost-limit').value = data.per_call_cost_limit || 0.1;
|
|
|
|
|
|
document.getElementById('auto-cost-control').checked = data.auto_cost_control || false;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('加载成本设置失败:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 数据分析
|
|
|
|
|
|
async loadAnalytics() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/api/analytics');
|
|
|
|
|
|
const analytics = await response.json();
|
|
|
|
|
|
this.updateAnalyticsDisplay(analytics);
|
|
|
|
|
|
this.initializeCharts();
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('加载分析数据失败:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化图表
|
|
|
|
|
|
initializeCharts() {
|
|
|
|
|
|
this.charts = {};
|
|
|
|
|
|
this.updateCharts();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 更新所有图表
|
|
|
|
|
|
async updateCharts() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const timeRange = document.getElementById('timeRange').value;
|
|
|
|
|
|
const chartType = document.getElementById('chartType').value;
|
|
|
|
|
|
const dataDimension = document.getElementById('dataDimension').value;
|
|
|
|
|
|
|
|
|
|
|
|
// 获取数据
|
|
|
|
|
|
const response = await fetch(`/api/analytics?timeRange=${timeRange}&dimension=${dataDimension}`);
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
// 更新统计卡片
|
|
|
|
|
|
this.updateStatisticsCards(data);
|
|
|
|
|
|
|
|
|
|
|
|
// 更新图表
|
|
|
|
|
|
this.updateMainChart(data, chartType);
|
|
|
|
|
|
this.updateDistributionChart(data);
|
|
|
|
|
|
this.updateTrendChart(data);
|
|
|
|
|
|
this.updatePriorityChart(data);
|
|
|
|
|
|
|
|
|
|
|
|
// 更新分析报告
|
|
|
|
|
|
this.updateAnalyticsReport(data);
|
|
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('更新图表失败:', error);
|
|
|
|
|
|
this.showNotification('更新图表失败: ' + error.message, 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 更新统计卡片
|
|
|
|
|
|
updateStatisticsCards(data) {
|
|
|
|
|
|
const total = data.workorders?.total || 0;
|
|
|
|
|
|
const open = data.workorders?.open || 0;
|
|
|
|
|
|
const resolved = data.workorders?.resolved || 0;
|
|
|
|
|
|
const avgSatisfaction = data.satisfaction?.average || 0;
|
|
|
|
|
|
|
|
|
|
|
|
document.getElementById('totalWorkorders').textContent = total;
|
|
|
|
|
|
document.getElementById('openWorkorders').textContent = open;
|
|
|
|
|
|
document.getElementById('resolvedWorkorders').textContent = resolved;
|
|
|
|
|
|
document.getElementById('avgSatisfaction').textContent = avgSatisfaction.toFixed(1);
|
|
|
|
|
|
|
|
|
|
|
|
// 更新进度条
|
|
|
|
|
|
if (total > 0) {
|
|
|
|
|
|
document.getElementById('openProgress').style.width = `${(open / total) * 100}%`;
|
|
|
|
|
|
document.getElementById('resolvedProgress').style.width = `${(resolved / total) * 100}%`;
|
|
|
|
|
|
document.getElementById('satisfactionProgress').style.width = `${(avgSatisfaction / 5) * 100}%`;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 更新主图表
|
|
|
|
|
|
updateMainChart(data, chartType) {
|
|
|
|
|
|
const ctx = document.getElementById('mainChart').getContext('2d');
|
|
|
|
|
|
|
|
|
|
|
|
// 销毁现有图表
|
|
|
|
|
|
if (this.charts.mainChart) {
|
|
|
|
|
|
this.charts.mainChart.destroy();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const chartData = this.prepareChartData(data, chartType);
|
|
|
|
|
|
|
|
|
|
|
|
this.charts.mainChart = new Chart(ctx, {
|
|
|
|
|
|
type: chartType,
|
|
|
|
|
|
data: chartData,
|
|
|
|
|
|
options: {
|
|
|
|
|
|
responsive: true,
|
|
|
|
|
|
maintainAspectRatio: false,
|
|
|
|
|
|
plugins: {
|
|
|
|
|
|
title: {
|
|
|
|
|
|
display: true,
|
|
|
|
|
|
text: '数据分析趋势'
|
|
|
|
|
|
},
|
|
|
|
|
|
legend: {
|
|
|
|
|
|
display: true,
|
|
|
|
|
|
position: 'top'
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
scales: chartType === 'pie' || chartType === 'doughnut' ? {} : {
|
|
|
|
|
|
x: {
|
|
|
|
|
|
display: true,
|
|
|
|
|
|
title: {
|
|
|
|
|
|
display: true,
|
|
|
|
|
|
text: '时间'
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
y: {
|
|
|
|
|
|
display: true,
|
|
|
|
|
|
title: {
|
|
|
|
|
|
display: true,
|
|
|
|
|
|
text: '数量'
|
2025-09-10 23:13:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 更新分布图表
|
|
|
|
|
|
updateDistributionChart(data) {
|
|
|
|
|
|
const ctx = document.getElementById('distributionChart').getContext('2d');
|
|
|
|
|
|
|
|
|
|
|
|
if (this.charts.distributionChart) {
|
|
|
|
|
|
this.charts.distributionChart.destroy();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const categories = data.workorders?.by_category || {};
|
|
|
|
|
|
const labels = Object.keys(categories);
|
|
|
|
|
|
const values = Object.values(categories);
|
|
|
|
|
|
|
|
|
|
|
|
this.charts.distributionChart = new Chart(ctx, {
|
|
|
|
|
|
type: 'doughnut',
|
|
|
|
|
|
data: {
|
|
|
|
|
|
labels: labels,
|
|
|
|
|
|
datasets: [{
|
|
|
|
|
|
data: values,
|
|
|
|
|
|
backgroundColor: [
|
|
|
|
|
|
'#FF6384',
|
|
|
|
|
|
'#36A2EB',
|
|
|
|
|
|
'#FFCE56',
|
|
|
|
|
|
'#4BC0C0',
|
|
|
|
|
|
'#9966FF',
|
|
|
|
|
|
'#FF9F40'
|
|
|
|
|
|
]
|
|
|
|
|
|
}]
|
|
|
|
|
|
},
|
|
|
|
|
|
options: {
|
|
|
|
|
|
responsive: true,
|
|
|
|
|
|
maintainAspectRatio: false,
|
|
|
|
|
|
plugins: {
|
|
|
|
|
|
title: {
|
|
|
|
|
|
display: true,
|
|
|
|
|
|
text: '工单分类分布'
|
|
|
|
|
|
},
|
|
|
|
|
|
legend: {
|
|
|
|
|
|
display: true,
|
|
|
|
|
|
position: 'bottom'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 更新趋势图表
|
|
|
|
|
|
updateTrendChart(data) {
|
|
|
|
|
|
const ctx = document.getElementById('trendChart').getContext('2d');
|
|
|
|
|
|
|
|
|
|
|
|
if (this.charts.trendChart) {
|
|
|
|
|
|
this.charts.trendChart.destroy();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const trendData = data.trend || [];
|
|
|
|
|
|
const labels = trendData.map(item => item.date);
|
|
|
|
|
|
const workorders = trendData.map(item => item.workorders);
|
|
|
|
|
|
const alerts = trendData.map(item => item.alerts);
|
|
|
|
|
|
|
|
|
|
|
|
this.charts.trendChart = new Chart(ctx, {
|
|
|
|
|
|
type: 'line',
|
|
|
|
|
|
data: {
|
|
|
|
|
|
labels: labels,
|
|
|
|
|
|
datasets: [{
|
|
|
|
|
|
label: '工单数量',
|
|
|
|
|
|
data: workorders,
|
|
|
|
|
|
borderColor: '#36A2EB',
|
|
|
|
|
|
backgroundColor: 'rgba(54, 162, 235, 0.1)',
|
|
|
|
|
|
tension: 0.4
|
|
|
|
|
|
}, {
|
|
|
|
|
|
label: '预警数量',
|
|
|
|
|
|
data: alerts,
|
|
|
|
|
|
borderColor: '#FF6384',
|
|
|
|
|
|
backgroundColor: 'rgba(255, 99, 132, 0.1)',
|
|
|
|
|
|
tension: 0.4
|
|
|
|
|
|
}]
|
|
|
|
|
|
},
|
|
|
|
|
|
options: {
|
|
|
|
|
|
responsive: true,
|
|
|
|
|
|
maintainAspectRatio: false,
|
|
|
|
|
|
plugins: {
|
|
|
|
|
|
title: {
|
|
|
|
|
|
display: true,
|
|
|
|
|
|
text: '时间趋势分析'
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
scales: {
|
|
|
|
|
|
x: {
|
|
|
|
|
|
display: true,
|
|
|
|
|
|
title: {
|
|
|
|
|
|
display: true,
|
|
|
|
|
|
text: '日期'
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
y: {
|
|
|
|
|
|
display: true,
|
|
|
|
|
|
title: {
|
|
|
|
|
|
display: true,
|
|
|
|
|
|
text: '数量'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 更新优先级图表
|
|
|
|
|
|
updatePriorityChart(data) {
|
|
|
|
|
|
const ctx = document.getElementById('priorityChart').getContext('2d');
|
|
|
|
|
|
|
|
|
|
|
|
if (this.charts.priorityChart) {
|
|
|
|
|
|
this.charts.priorityChart.destroy();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const priorities = data.workorders?.by_priority || {};
|
|
|
|
|
|
const labels = Object.keys(priorities).map(p => this.getPriorityText(p));
|
|
|
|
|
|
const values = Object.values(priorities);
|
|
|
|
|
|
|
|
|
|
|
|
this.charts.priorityChart = new Chart(ctx, {
|
|
|
|
|
|
type: 'bar',
|
|
|
|
|
|
data: {
|
|
|
|
|
|
labels: labels,
|
|
|
|
|
|
datasets: [{
|
|
|
|
|
|
label: '工单数量',
|
|
|
|
|
|
data: values,
|
|
|
|
|
|
backgroundColor: [
|
|
|
|
|
|
'#28a745', // 低 - 绿色
|
|
|
|
|
|
'#ffc107', // 中 - 黄色
|
|
|
|
|
|
'#fd7e14', // 高 - 橙色
|
|
|
|
|
|
'#dc3545' // 紧急 - 红色
|
|
|
|
|
|
]
|
|
|
|
|
|
}]
|
|
|
|
|
|
},
|
|
|
|
|
|
options: {
|
|
|
|
|
|
responsive: true,
|
|
|
|
|
|
maintainAspectRatio: false,
|
|
|
|
|
|
plugins: {
|
|
|
|
|
|
title: {
|
|
|
|
|
|
display: true,
|
|
|
|
|
|
text: '优先级分布'
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
scales: {
|
|
|
|
|
|
y: {
|
|
|
|
|
|
beginAtZero: true
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 准备图表数据
|
|
|
|
|
|
prepareChartData(data, chartType) {
|
|
|
|
|
|
const trendData = data.trend || [];
|
|
|
|
|
|
const labels = trendData.map(item => item.date);
|
|
|
|
|
|
const workorders = trendData.map(item => item.workorders);
|
|
|
|
|
|
|
|
|
|
|
|
if (chartType === 'pie' || chartType === 'doughnut') {
|
|
|
|
|
|
const categories = data.workorders?.by_category || {};
|
|
|
|
|
|
return {
|
|
|
|
|
|
labels: Object.keys(categories),
|
|
|
|
|
|
datasets: [{
|
|
|
|
|
|
data: Object.values(categories),
|
|
|
|
|
|
backgroundColor: [
|
|
|
|
|
|
'#FF6384',
|
|
|
|
|
|
'#36A2EB',
|
|
|
|
|
|
'#FFCE56',
|
|
|
|
|
|
'#4BC0C0',
|
|
|
|
|
|
'#9966FF',
|
|
|
|
|
|
'#FF9F40'
|
|
|
|
|
|
]
|
|
|
|
|
|
}]
|
|
|
|
|
|
};
|
|
|
|
|
|
} else {
|
|
|
|
|
|
return {
|
|
|
|
|
|
labels: labels,
|
|
|
|
|
|
datasets: [{
|
|
|
|
|
|
label: '工单数量',
|
|
|
|
|
|
data: workorders,
|
|
|
|
|
|
borderColor: '#36A2EB',
|
|
|
|
|
|
backgroundColor: 'rgba(54, 162, 235, 0.1)',
|
|
|
|
|
|
tension: chartType === 'line' ? 0.4 : 0
|
|
|
|
|
|
}]
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 导出图表
|
|
|
|
|
|
exportChart(chartId) {
|
|
|
|
|
|
if (this.charts[chartId]) {
|
|
|
|
|
|
const link = document.createElement('a');
|
|
|
|
|
|
link.download = `${chartId}_chart.png`;
|
|
|
|
|
|
link.href = this.charts[chartId].toBase64Image();
|
|
|
|
|
|
link.click();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 全屏图表
|
|
|
|
|
|
fullscreenChart(chartId) {
|
|
|
|
|
|
// 这里可以实现全屏显示功能
|
|
|
|
|
|
this.showNotification('全屏功能开发中', 'info');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 导出报告
|
|
|
|
|
|
async exportReport() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/api/analytics/export');
|
|
|
|
|
|
const blob = await response.blob();
|
|
|
|
|
|
const url = window.URL.createObjectURL(blob);
|
|
|
|
|
|
const link = document.createElement('a');
|
|
|
|
|
|
link.href = url;
|
|
|
|
|
|
link.download = 'analytics_report.xlsx';
|
|
|
|
|
|
link.click();
|
|
|
|
|
|
window.URL.revokeObjectURL(url);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('导出报告失败:', error);
|
|
|
|
|
|
this.showNotification('导出报告失败: ' + error.message, 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 打印报告
|
|
|
|
|
|
printReport() {
|
|
|
|
|
|
window.print();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-11 00:01:12 +08:00
|
|
|
|
// Agent执行历史相关功能
|
|
|
|
|
|
async refreshAgentHistory() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/api/agent/action-history?limit=20');
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
this.updateAgentExecutionHistory(data.history);
|
|
|
|
|
|
this.showNotification(`已加载 ${data.count} 条执行历史`, 'success');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
throw new Error(data.error || '获取执行历史失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('刷新Agent历史失败:', error);
|
|
|
|
|
|
this.showNotification('刷新Agent历史失败: ' + error.message, 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async triggerSampleAction() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/api/agent/trigger-sample', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
this.showNotification(data.message, 'success');
|
|
|
|
|
|
// 刷新执行历史
|
|
|
|
|
|
await this.refreshAgentHistory();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
throw new Error(data.error || '触发示例动作失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('触发示例动作失败:', error);
|
|
|
|
|
|
this.showNotification('触发示例动作失败: ' + error.message, 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async clearAgentHistory() {
|
|
|
|
|
|
if (!confirm('确定要清空Agent执行历史吗?此操作不可恢复。')) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/api/agent/clear-history', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
this.showNotification(data.message, 'success');
|
|
|
|
|
|
// 清空显示
|
|
|
|
|
|
this.updateAgentExecutionHistory([]);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
throw new Error(data.error || '清空历史失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('清空Agent历史失败:', error);
|
|
|
|
|
|
this.showNotification('清空Agent历史失败: ' + error.message, 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
updateAgentExecutionHistory(history) {
|
|
|
|
|
|
const container = document.getElementById('agent-execution-history');
|
|
|
|
|
|
|
|
|
|
|
|
if (!history || history.length === 0) {
|
|
|
|
|
|
container.innerHTML = `
|
|
|
|
|
|
<div class="empty-state">
|
|
|
|
|
|
<i class="fas fa-history"></i>
|
|
|
|
|
|
<p>暂无执行历史</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const historyHtml = history.map(record => {
|
|
|
|
|
|
const startTime = new Date(record.start_time * 1000).toLocaleString();
|
|
|
|
|
|
const endTime = new Date(record.end_time * 1000).toLocaleString();
|
|
|
|
|
|
const duration = Math.round((record.end_time - record.start_time) * 100) / 100;
|
|
|
|
|
|
|
|
|
|
|
|
const priorityColor = {
|
|
|
|
|
|
5: 'danger',
|
|
|
|
|
|
4: 'warning',
|
|
|
|
|
|
3: 'info',
|
|
|
|
|
|
2: 'secondary',
|
|
|
|
|
|
1: 'light'
|
|
|
|
|
|
}[record.priority] || 'secondary';
|
|
|
|
|
|
|
|
|
|
|
|
const confidenceColor = record.confidence >= 0.8 ? 'success' :
|
|
|
|
|
|
record.confidence >= 0.5 ? 'warning' : 'danger';
|
|
|
|
|
|
|
|
|
|
|
|
return `
|
|
|
|
|
|
<div class="card mb-2">
|
|
|
|
|
|
<div class="card-body">
|
|
|
|
|
|
<div class="d-flex justify-content-between align-items-start">
|
|
|
|
|
|
<div class="flex-grow-1">
|
|
|
|
|
|
<h6 class="mb-1">${record.description}</h6>
|
|
|
|
|
|
<div class="d-flex gap-3 mb-2">
|
|
|
|
|
|
<span class="badge bg-${priorityColor}">优先级 ${record.priority}</span>
|
|
|
|
|
|
<span class="badge bg-${confidenceColor}">置信度 ${(record.confidence * 100).toFixed(0)}%</span>
|
|
|
|
|
|
<span class="badge bg-${record.success ? 'success' : 'danger'}">${record.success ? '成功' : '失败'}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<small class="text-muted">
|
|
|
|
|
|
<i class="fas fa-clock me-1"></i>开始: ${startTime} |
|
|
|
|
|
|
<i class="fas fa-stopwatch me-1"></i>耗时: ${duration}秒
|
|
|
|
|
|
</small>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="ms-3">
|
|
|
|
|
|
<span class="badge bg-primary">${record.action_type}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
${record.result && record.result.message ? `
|
|
|
|
|
|
<div class="mt-2">
|
|
|
|
|
|
<small class="text-muted">结果: ${record.result.message}</small>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
` : ''}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
}).join('');
|
|
|
|
|
|
|
|
|
|
|
|
container.innerHTML = historyHtml;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-10 23:19:35 +08:00
|
|
|
|
// 更新分析报告
|
|
|
|
|
|
updateAnalyticsReport(data) {
|
|
|
|
|
|
const reportContainer = document.getElementById('analytics-report');
|
|
|
|
|
|
|
|
|
|
|
|
if (!reportContainer) return;
|
|
|
|
|
|
|
|
|
|
|
|
const summary = data.summary || {};
|
|
|
|
|
|
const workorders = data.workorders || {};
|
|
|
|
|
|
const satisfaction = data.satisfaction || {};
|
|
|
|
|
|
const alerts = data.alerts || {};
|
|
|
|
|
|
const performance = data.performance || {};
|
|
|
|
|
|
|
|
|
|
|
|
const reportHtml = `
|
|
|
|
|
|
<div class="row">
|
|
|
|
|
|
<div class="col-md-6">
|
|
|
|
|
|
<h6><i class="fas fa-chart-bar me-2"></i>工单统计概览</h6>
|
|
|
|
|
|
<div class="table-responsive">
|
|
|
|
|
|
<table class="table table-sm">
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td>总工单数</td>
|
|
|
|
|
|
<td><span class="badge bg-primary">${workorders.total || 0}</span></td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td>待处理</td>
|
|
|
|
|
|
<td><span class="badge bg-warning">${workorders.open || 0}</span></td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td>处理中</td>
|
|
|
|
|
|
<td><span class="badge bg-info">${workorders.in_progress || 0}</span></td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td>已解决</td>
|
|
|
|
|
|
<td><span class="badge bg-success">${workorders.resolved || 0}</span></td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td>已关闭</td>
|
|
|
|
|
|
<td><span class="badge bg-secondary">${workorders.closed || 0}</span></td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
</table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="col-md-6">
|
|
|
|
|
|
<h6><i class="fas fa-star me-2"></i>满意度分析</h6>
|
|
|
|
|
|
<div class="table-responsive">
|
|
|
|
|
|
<table class="table table-sm">
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td>平均满意度</td>
|
|
|
|
|
|
<td><span class="badge bg-success">${satisfaction.average || 0}/5.0</span></td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td>5星评价</td>
|
|
|
|
|
|
<td>${satisfaction.distribution?.['5'] || 0} 个</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td>4星评价</td>
|
|
|
|
|
|
<td>${satisfaction.distribution?.['4'] || 0} 个</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td>3星评价</td>
|
|
|
|
|
|
<td>${satisfaction.distribution?.['3'] || 0} 个</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td>2星及以下</td>
|
|
|
|
|
|
<td>${(satisfaction.distribution?.['2'] || 0) + (satisfaction.distribution?.['1'] || 0)} 个</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
</table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="row mt-4">
|
|
|
|
|
|
<div class="col-md-6">
|
|
|
|
|
|
<h6><i class="fas fa-exclamation-triangle me-2"></i>预警统计</h6>
|
|
|
|
|
|
<div class="table-responsive">
|
|
|
|
|
|
<table class="table table-sm">
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td>总预警数</td>
|
|
|
|
|
|
<td><span class="badge bg-danger">${alerts.total || 0}</span></td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td>活跃预警</td>
|
|
|
|
|
|
<td><span class="badge bg-warning">${alerts.active || 0}</span></td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td>已解决</td>
|
|
|
|
|
|
<td><span class="badge bg-success">${alerts.resolved || 0}</span></td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
</table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="col-md-6">
|
|
|
|
|
|
<h6><i class="fas fa-tachometer-alt me-2"></i>性能指标</h6>
|
|
|
|
|
|
<div class="table-responsive">
|
|
|
|
|
|
<table class="table table-sm">
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td>响应时间</td>
|
|
|
|
|
|
<td>${performance.response_time || 0} 秒</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td>系统可用性</td>
|
|
|
|
|
|
<td>${performance.uptime || 0}%</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td>错误率</td>
|
|
|
|
|
|
<td>${performance.error_rate || 0}%</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td>吞吐量</td>
|
|
|
|
|
|
<td>${performance.throughput || 0} 请求/小时</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
</table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="row mt-4">
|
|
|
|
|
|
<div class="col-12">
|
|
|
|
|
|
<h6><i class="fas fa-chart-line me-2"></i>关键指标总结</h6>
|
|
|
|
|
|
<div class="row">
|
|
|
|
|
|
<div class="col-md-3">
|
|
|
|
|
|
<div class="card text-center">
|
|
|
|
|
|
<div class="card-body">
|
|
|
|
|
|
<h5 class="card-title text-primary">${summary.resolution_rate || 0}%</h5>
|
|
|
|
|
|
<p class="card-text">解决率</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="col-md-3">
|
|
|
|
|
|
<div class="card text-center">
|
|
|
|
|
|
<div class="card-body">
|
|
|
|
|
|
<h5 class="card-title text-success">${summary.avg_satisfaction || 0}</h5>
|
|
|
|
|
|
<p class="card-text">平均满意度</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="col-md-3">
|
|
|
|
|
|
<div class="card text-center">
|
|
|
|
|
|
<div class="card-body">
|
|
|
|
|
|
<h5 class="card-title text-warning">${summary.active_alerts || 0}</h5>
|
|
|
|
|
|
<p class="card-text">活跃预警</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="col-md-3">
|
|
|
|
|
|
<div class="card text-center">
|
|
|
|
|
|
<div class="card-body">
|
|
|
|
|
|
<h5 class="card-title text-info">${summary.total_workorders || 0}</h5>
|
|
|
|
|
|
<p class="card-text">总工单数</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
reportContainer.innerHTML = reportHtml;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-06 21:06:18 +08:00
|
|
|
|
updateAnalyticsDisplay(analytics) {
|
|
|
|
|
|
// 更新分析报告
|
|
|
|
|
|
const reportContainer = document.getElementById('analytics-report');
|
|
|
|
|
|
if (analytics.summary) {
|
|
|
|
|
|
reportContainer.innerHTML = `
|
|
|
|
|
|
<div class="row">
|
|
|
|
|
|
<div class="col-md-6">
|
|
|
|
|
|
<h6>性能指标</h6>
|
|
|
|
|
|
<ul class="list-unstyled">
|
|
|
|
|
|
<li>总工单数: ${analytics.summary.total_orders || 0}</li>
|
|
|
|
|
|
<li>解决率: ${Math.round((analytics.summary.resolution_rate || 0) * 100)}%</li>
|
|
|
|
|
|
<li>平均解决时间: ${analytics.summary.avg_resolution_time_hours || 0}小时</li>
|
|
|
|
|
|
<li>平均满意度: ${analytics.summary.avg_satisfaction || 0}</li>
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="col-md-6">
|
|
|
|
|
|
<h6>趋势分析</h6>
|
|
|
|
|
|
<ul class="list-unstyled">
|
|
|
|
|
|
<li>工单趋势: ${analytics.summary.trends?.orders_trend ? '上升' : '下降'}</li>
|
|
|
|
|
|
<li>满意度趋势: ${analytics.summary.trends?.satisfaction_trend ? '上升' : '下降'}</li>
|
|
|
|
|
|
<li>解决时间趋势: ${analytics.summary.trends?.resolution_time_trend ? '上升' : '下降'}</li>
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 更新类别分布图
|
|
|
|
|
|
if (analytics.category_distribution && this.charts.category) {
|
|
|
|
|
|
const labels = Object.keys(analytics.category_distribution);
|
|
|
|
|
|
const data = Object.values(analytics.category_distribution);
|
|
|
|
|
|
|
|
|
|
|
|
this.charts.category.data.labels = labels;
|
|
|
|
|
|
this.charts.category.data.datasets[0].data = data;
|
|
|
|
|
|
this.charts.category.update();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 系统设置
|
|
|
|
|
|
async loadSettings() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/api/settings');
|
|
|
|
|
|
const settings = await response.json();
|
|
|
|
|
|
this.updateSettingsDisplay(settings);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('加载设置失败:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
updateSettingsDisplay(settings) {
|
2025-09-16 17:05:50 +01:00
|
|
|
|
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;
|
2025-09-06 21:06:18 +08:00
|
|
|
|
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;
|
2025-09-16 17:05:50 +01:00
|
|
|
|
// 新增: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);
|
2025-09-06 21:06:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async saveSystemSettings() {
|
|
|
|
|
|
const settings = {
|
|
|
|
|
|
api_timeout: parseInt(document.getElementById('api-timeout').value),
|
|
|
|
|
|
max_history: parseInt(document.getElementById('max-history').value),
|
|
|
|
|
|
refresh_interval: parseInt(document.getElementById('refresh-interval').value),
|
|
|
|
|
|
auto_monitoring: document.getElementById('auto-monitoring').checked,
|
2025-09-16 17:05:50 +01:00
|
|
|
|
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'
|
2025-09-06 21:06:18 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/api/settings', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
|
|
},
|
|
|
|
|
|
body: JSON.stringify(settings)
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
this.showNotification('设置保存成功', 'success');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.showNotification('保存设置失败', 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('保存设置失败:', error);
|
|
|
|
|
|
this.showNotification('保存设置失败', 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-16 17:05:50 +01:00
|
|
|
|
// 更新服务状态显示
|
|
|
|
|
|
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');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-06 21:06:18 +08:00
|
|
|
|
async loadSystemInfo() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/api/system/info');
|
|
|
|
|
|
const info = await response.json();
|
|
|
|
|
|
this.updateSystemInfoDisplay(info);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('加载系统信息失败:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
updateSystemInfoDisplay(info) {
|
|
|
|
|
|
const container = document.getElementById('system-info');
|
|
|
|
|
|
container.innerHTML = `
|
|
|
|
|
|
<div class="mb-3">
|
|
|
|
|
|
<strong>系统版本:</strong> ${info.version || '1.0.0'}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="mb-3">
|
|
|
|
|
|
<strong>Python版本:</strong> ${info.python_version || '未知'}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="mb-3">
|
|
|
|
|
|
<strong>数据库:</strong> ${info.database || 'SQLite'}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="mb-3">
|
|
|
|
|
|
<strong>运行时间:</strong> ${info.uptime || '未知'}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="mb-3">
|
|
|
|
|
|
<strong>内存使用:</strong> ${info.memory_usage || '0'} MB
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 工具函数
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
getAlertColor(level) {
|
|
|
|
|
|
const colorMap = {
|
|
|
|
|
|
'critical': 'danger',
|
|
|
|
|
|
'error': 'danger',
|
|
|
|
|
|
'warning': 'warning',
|
|
|
|
|
|
'info': 'info'
|
|
|
|
|
|
};
|
|
|
|
|
|
return colorMap[level] || 'secondary';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
getPriorityText(priority) {
|
|
|
|
|
|
const priorityMap = {
|
|
|
|
|
|
'low': '低',
|
|
|
|
|
|
'medium': '中',
|
|
|
|
|
|
'high': '高',
|
|
|
|
|
|
'urgent': '紧急'
|
|
|
|
|
|
};
|
|
|
|
|
|
return priorityMap[priority] || priority;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
getPriorityColor(priority) {
|
|
|
|
|
|
const colorMap = {
|
|
|
|
|
|
'low': 'secondary',
|
|
|
|
|
|
'medium': 'primary',
|
|
|
|
|
|
'high': 'warning',
|
|
|
|
|
|
'urgent': 'danger'
|
|
|
|
|
|
};
|
|
|
|
|
|
return colorMap[priority] || 'secondary';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
getStatusText(status) {
|
|
|
|
|
|
const statusMap = {
|
|
|
|
|
|
'open': '待处理',
|
|
|
|
|
|
'in_progress': '处理中',
|
|
|
|
|
|
'resolved': '已解决',
|
|
|
|
|
|
'closed': '已关闭'
|
|
|
|
|
|
};
|
|
|
|
|
|
return statusMap[status] || status;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
getStatusColor(status) {
|
|
|
|
|
|
const colorMap = {
|
|
|
|
|
|
'open': 'warning',
|
|
|
|
|
|
'in_progress': 'info',
|
|
|
|
|
|
'resolved': 'success',
|
|
|
|
|
|
'closed': 'secondary'
|
|
|
|
|
|
};
|
|
|
|
|
|
return colorMap[status] || 'secondary';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
showNotification(message, type = 'info') {
|
|
|
|
|
|
const notification = document.createElement('div');
|
|
|
|
|
|
notification.className = `notification alert alert-${type === 'error' ? 'danger' : type} alert-dismissible fade show`;
|
|
|
|
|
|
notification.innerHTML = `
|
|
|
|
|
|
${message}
|
|
|
|
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
document.body.appendChild(notification);
|
|
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
if (notification.parentNode) {
|
|
|
|
|
|
notification.parentNode.removeChild(notification);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 3000);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
showCreateWorkOrderModal() {
|
|
|
|
|
|
const modal = new bootstrap.Modal(document.getElementById('createWorkOrderModal'));
|
|
|
|
|
|
modal.show();
|
|
|
|
|
|
}
|
2025-09-18 19:37:14 +01:00
|
|
|
|
|
|
|
|
|
|
// 新增Agent对话功能
|
|
|
|
|
|
async sendAgentMessage() {
|
|
|
|
|
|
const messageInput = document.getElementById('agent-message-input');
|
|
|
|
|
|
const message = messageInput.value.trim();
|
|
|
|
|
|
|
|
|
|
|
|
if (!message) {
|
|
|
|
|
|
this.showNotification('请输入消息', 'warning');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 显示发送状态
|
|
|
|
|
|
const sendBtn = document.getElementById('send-agent-message');
|
|
|
|
|
|
const originalText = sendBtn.innerHTML;
|
|
|
|
|
|
sendBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>发送中...';
|
|
|
|
|
|
sendBtn.disabled = true;
|
|
|
|
|
|
|
|
|
|
|
|
const response = await fetch('/api/agent/chat', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
|
|
},
|
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
|
message: message,
|
|
|
|
|
|
context: {
|
|
|
|
|
|
user_id: 'admin',
|
|
|
|
|
|
session_id: `agent_session_${Date.now()}`
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
this.showNotification('Agent响应成功', 'success');
|
|
|
|
|
|
// 清空输入框
|
|
|
|
|
|
messageInput.value = '';
|
|
|
|
|
|
// 刷新执行历史
|
|
|
|
|
|
await this.loadAgentExecutionHistory();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.showNotification('Agent响应失败: ' + (data.error || '未知错误'), 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('发送Agent消息失败:', error);
|
|
|
|
|
|
this.showNotification('发送Agent消息失败: ' + error.message, 'error');
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
// 恢复按钮状态
|
|
|
|
|
|
const sendBtn = document.getElementById('send-agent-message');
|
|
|
|
|
|
sendBtn.innerHTML = '<i class="fas fa-paper-plane me-1"></i>发送';
|
|
|
|
|
|
sendBtn.disabled = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 清空Agent对话
|
|
|
|
|
|
clearAgentChat() {
|
|
|
|
|
|
document.getElementById('agent-message-input').value = '';
|
|
|
|
|
|
this.showNotification('对话已清空', 'info');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 加载Agent执行历史
|
|
|
|
|
|
async loadAgentExecutionHistory() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/api/agent/action-history?limit=10');
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
this.updateExecutionHistory(data.history);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('加载Agent执行历史失败:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 触发示例动作
|
|
|
|
|
|
async triggerSampleAction() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/api/agent/trigger-sample', {
|
|
|
|
|
|
method: 'POST'
|
|
|
|
|
|
});
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
this.showNotification('示例动作执行成功', 'success');
|
|
|
|
|
|
await this.loadAgentExecutionHistory();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.showNotification('示例动作执行失败: ' + (data.error || '未知错误'), 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('触发示例动作失败:', error);
|
|
|
|
|
|
this.showNotification('触发示例动作失败: ' + error.message, 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 清空Agent历史
|
|
|
|
|
|
async clearAgentHistory() {
|
|
|
|
|
|
if (!confirm('确定要清空Agent执行历史吗?')) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('/api/agent/clear-history', {
|
|
|
|
|
|
method: 'POST'
|
|
|
|
|
|
});
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
this.showNotification('Agent历史已清空', 'success');
|
|
|
|
|
|
await this.loadAgentExecutionHistory();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.showNotification('清空Agent历史失败: ' + (data.error || '未知错误'), 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('清空Agent历史失败:', error);
|
|
|
|
|
|
this.showNotification('清空Agent历史失败: ' + error.message, 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-09-06 21:06:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化应用
|
|
|
|
|
|
let dashboard;
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
|
|
|
|
dashboard = new TSPDashboard();
|
|
|
|
|
|
});
|