From 58b3c615ef72a26bd643e2eadf49aa38effb5560 Mon Sep 17 00:00:00 2001
From: Jeason <1710884619@qq.com>
Date: Thu, 2 Apr 2026 15:10:23 +0800
Subject: [PATCH] =?UTF-8?q?refactor:=20dashboard.js=20=E6=A8=A1=E5=9D=97?=
=?UTF-8?q?=E5=8C=96=E6=8B=86=E5=88=86=20=20=E4=BB=8E7766=E8=A1=8C?=
=?UTF-8?q?=E6=8B=86=E4=B8=BA13=E4=B8=AA=E7=8B=AC=E7=AB=8B=E6=A8=A1?=
=?UTF-8?q?=E5=9D=97?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
核心 dashboard.js 缩减至266行,只保留:
- TSPDashboard class 定义(constructor、init、bindEvents、switchTab)
- 分页组件、缓存、状态管理、自动刷新、WebSocket、i18n
功能模块拆分到 modules/ 目录:
- chat.js (207行) 智能对话
- agent.js (424行) Agent管理
- alerts.js (348行) 预警管理
- knowledge.js (699行) 知识库管理
- workorders.js (约500行) 工单管理
- conversations.js (671行) 对话历史
- monitoring.js (455行) Token/AI监控
- system.js (1388行) 系统优化+设置+数据分析图表
- tenants.js (165行) 租户管理
- utils.js (573行) 工具函数
- dashboard-home.js (286行) 仪表板首页
- feishu-sync.js (698行) 飞书同步管理器
拆分方式:Object.assign(TSPDashboard.prototype, {...})
this 引用完全不会断,所有模块方法互相调用正常
---
src/web/static/js/dashboard.js | 7728 +------------------
src/web/static/js/modules/agent.js | 424 +
src/web/static/js/modules/alerts.js | 348 +
src/web/static/js/modules/chat.js | 207 +
src/web/static/js/modules/conversations.js | 671 ++
src/web/static/js/modules/dashboard-home.js | 286 +
src/web/static/js/modules/feishu-sync.js | 698 ++
src/web/static/js/modules/knowledge.js | 699 ++
src/web/static/js/modules/monitoring.js | 455 ++
src/web/static/js/modules/system.js | 1388 ++++
src/web/static/js/modules/tenants.js | 165 +
src/web/static/js/modules/utils.js | 573 ++
src/web/static/js/modules/workorders.js | 458 ++
src/web/templates/dashboard.html | 15 +-
14 files changed, 6550 insertions(+), 7565 deletions(-)
create mode 100644 src/web/static/js/modules/agent.js
create mode 100644 src/web/static/js/modules/alerts.js
create mode 100644 src/web/static/js/modules/chat.js
create mode 100644 src/web/static/js/modules/conversations.js
create mode 100644 src/web/static/js/modules/dashboard-home.js
create mode 100644 src/web/static/js/modules/feishu-sync.js
create mode 100644 src/web/static/js/modules/knowledge.js
create mode 100644 src/web/static/js/modules/monitoring.js
create mode 100644 src/web/static/js/modules/system.js
create mode 100644 src/web/static/js/modules/tenants.js
create mode 100644 src/web/static/js/modules/utils.js
create mode 100644 src/web/static/js/modules/workorders.js
diff --git a/src/web/static/js/dashboard.js b/src/web/static/js/dashboard.js
index 03f9bd0..b51068e 100644
--- a/src/web/static/js/dashboard.js
+++ b/src/web/static/js/dashboard.js
@@ -1,164 +1,70 @@
-// TSP智能助手综合管理平台前端脚本
+// TSP智能助手综合管理平台 - 核心模块
+// 模块文件在 modules/ 目录下,通过 Object.assign(TSPDashboard.prototype, {...}) 扩展
// 多语言支持
const translations = {
zh: {
- // 导航栏
- 'nav-title': 'TSP智能助手',
- 'nav-health-checking': '检查中...',
- 'nav-health-normal': '系统正常',
- 'nav-health-warning': '系统警告',
- 'nav-health-error': '系统错误',
-
- // 侧边栏
- 'sidebar-dashboard': '仪表板',
- 'sidebar-workorders': '工单管理',
- 'sidebar-conversations': '智能对话',
- 'sidebar-agent': 'Agent管理',
- 'sidebar-alerts': '预警管理',
- 'sidebar-knowledge': '知识库',
- 'sidebar-analytics': '数据分析',
- 'sidebar-feishu-sync': '飞书同步',
- 'sidebar-conversation-history': '对话历史',
- 'sidebar-token-monitor': 'Token监控',
- 'sidebar-ai-monitor': 'AI监控',
- 'sidebar-system-optimizer': '系统优化',
- 'sidebar-system': '系统设置',
-
- // 预警管理页面
- 'alerts-critical': '严重预警',
- 'alerts-warning': '警告预警',
- 'alerts-info': '信息预警',
- 'alerts-total': '总预警数',
- 'alerts-active': '活跃预警',
- 'alerts-resolve': '解决',
-
- // 设置页面
- 'settings-title': '系统设置',
- 'settings-basic': '基础设置',
- 'settings-system-info': '系统信息',
- 'settings-log-config': '日志配置',
- 'settings-current-port': '当前服务端口',
- 'settings-websocket-port': 'WebSocket端口',
- 'settings-log-level': '日志级别',
- 'settings-save': '保存设置',
- 'settings-save-success': '设置保存成功',
+ 'nav-title': 'TSP智能助手', 'nav-health-checking': '检查中...', 'nav-health-normal': '系统正常',
+ 'nav-health-warning': '系统警告', 'nav-health-error': '系统错误',
+ 'sidebar-dashboard': '仪表板', 'sidebar-workorders': '工单管理', 'sidebar-conversations': '智能对话',
+ 'sidebar-agent': 'Agent管理', 'sidebar-alerts': '预警管理', 'sidebar-knowledge': '知识库',
+ 'sidebar-analytics': '数据分析', 'sidebar-feishu-sync': '飞书同步',
+ 'sidebar-conversation-history': '对话历史', 'sidebar-token-monitor': 'Token监控',
+ 'sidebar-ai-monitor': 'AI监控', 'sidebar-system-optimizer': '系统优化', 'sidebar-system': '系统设置',
+ 'alerts-critical': '严重预警', 'alerts-warning': '警告预警', 'alerts-info': '信息预警',
+ 'alerts-total': '总预警数', 'alerts-active': '活跃预警', 'alerts-resolve': '解决',
+ 'settings-title': '系统设置', 'settings-basic': '基础设置', 'settings-system-info': '系统信息',
+ 'settings-log-config': '日志配置', 'settings-current-port': '当前服务端口',
+ 'settings-websocket-port': 'WebSocket端口', 'settings-log-level': '日志级别',
+ 'settings-save': '保存设置', 'settings-save-success': '设置保存成功',
'settings-save-failed': '保存设置失败',
'settings-port-note': '服务端口配置需要在配置文件中修改,前端页面仅显示当前状态。',
'settings-log-note': '调整系统日志的详细程度'
},
en: {
- // Navigation
- 'nav-title': 'TSP Intelligent Assistant',
- 'nav-health-checking': 'Checking...',
- 'nav-health-normal': 'System Normal',
- 'nav-health-warning': 'System Warning',
+ 'nav-title': 'TSP Intelligent Assistant', 'nav-health-checking': 'Checking...',
+ 'nav-health-normal': 'System Normal', 'nav-health-warning': 'System Warning',
'nav-health-error': 'System Error',
-
- // Sidebar
- 'sidebar-dashboard': 'Dashboard',
- 'sidebar-workorders': 'Work Orders',
- 'sidebar-conversations': 'Smart Chat',
- 'sidebar-agent': 'Agent Management',
- 'sidebar-alerts': 'Alert Management',
- 'sidebar-knowledge': 'Knowledge Base',
- 'sidebar-analytics': 'Analytics',
- 'sidebar-feishu-sync': 'Feishu Sync',
- 'sidebar-conversation-history': 'Conversation History',
- 'sidebar-token-monitor': 'Token Monitor',
- 'sidebar-ai-monitor': 'AI Monitor',
- 'sidebar-system-optimizer': 'System Optimizer',
+ 'sidebar-dashboard': 'Dashboard', 'sidebar-workorders': 'Work Orders',
+ 'sidebar-conversations': 'Smart Chat', 'sidebar-agent': 'Agent Management',
+ 'sidebar-alerts': 'Alert Management', 'sidebar-knowledge': 'Knowledge Base',
+ 'sidebar-analytics': 'Analytics', 'sidebar-feishu-sync': 'Feishu Sync',
+ 'sidebar-conversation-history': 'Conversation History', 'sidebar-token-monitor': 'Token Monitor',
+ 'sidebar-ai-monitor': 'AI Monitor', 'sidebar-system-optimizer': 'System Optimizer',
'sidebar-system': 'System Settings',
-
- // Alert Management page
- 'alerts-critical': 'Critical Alerts',
- 'alerts-warning': 'Warning Alerts',
- 'alerts-info': 'Info Alerts',
- 'alerts-total': 'Total Alerts',
- 'alerts-active': 'Active Alerts',
- 'alerts-resolve': 'Resolve',
-
- // Settings page
- 'settings-title': 'System Settings',
- 'settings-basic': 'Basic Settings',
- 'settings-system-info': 'System Information',
- 'settings-log-config': 'Log Configuration',
- 'settings-current-port': 'Current Service Port',
- 'settings-websocket-port': 'WebSocket Port',
- 'settings-log-level': 'Log Level',
- 'settings-save': 'Save Settings',
- 'settings-save-success': 'Settings saved successfully',
- 'settings-save-failed': 'Failed to save settings',
- 'settings-port-note': 'Service port configuration needs to be modified in configuration files. Frontend only displays current status.',
+ 'alerts-critical': 'Critical Alerts', 'alerts-warning': 'Warning Alerts',
+ 'alerts-info': 'Info Alerts', 'alerts-total': 'Total Alerts',
+ 'alerts-active': 'Active Alerts', 'alerts-resolve': 'Resolve',
+ 'settings-title': 'System Settings', 'settings-basic': 'Basic Settings',
+ 'settings-system-info': 'System Information', 'settings-log-config': 'Log Configuration',
+ 'settings-current-port': 'Current Service Port', 'settings-websocket-port': 'WebSocket Port',
+ 'settings-log-level': 'Log Level', 'settings-save': 'Save Settings',
+ 'settings-save-success': 'Settings saved successfully', 'settings-save-failed': 'Failed to save settings',
+ 'settings-port-note': 'Service port configuration needs to be modified in configuration files.',
'settings-log-note': 'Adjust the detail level of system logs'
}
};
-// 全局语言切换函数
function switchLanguage(lang) {
localStorage.setItem('preferred-language', lang);
document.documentElement.lang = lang;
-
- // 更新按钮状态
document.getElementById('lang-zh').classList.toggle('active', lang === 'zh');
document.getElementById('lang-en').classList.toggle('active', lang === 'en');
-
- // 更新页面文本
updatePageLanguage(lang);
}
-// 更新页面语言
function updatePageLanguage(lang) {
const t = translations[lang];
if (!t) return;
-
- // 更新所有带有 data-i18n 属性的元素
- const elements = document.querySelectorAll('[data-i18n]');
- elements.forEach(element => {
- const key = element.getAttribute('data-i18n');
+ document.querySelectorAll('[data-i18n]').forEach(el => {
+ const key = el.getAttribute('data-i18n');
if (t[key]) {
- // 检查元素是否包含图标(i标签)
- const icon = element.querySelector('i');
- if (icon) {
- // 如果包含图标,只更新文本部分
- const textNodes = Array.from(element.childNodes).filter(node =>
- node.nodeType === Node.TEXT_NODE || (node.nodeType === Node.ELEMENT_NODE && node.tagName !== 'I')
- );
- // 保留图标,更新其他文本内容
- element.innerHTML = icon.outerHTML + ' ' + t[key];
- } else {
- // 如果没有图标,直接更新文本内容
- element.textContent = t[key];
- }
+ const icon = el.querySelector('i');
+ el.innerHTML = icon ? icon.outerHTML + ' ' + t[key] : t[key];
}
});
-
- // 更新导航栏标题(特殊处理,因为包含图标)
const navBrand = document.querySelector('.navbar-brand');
- if (navBrand) {
- const icon = navBrand.querySelector('i');
- if (icon) {
- navBrand.innerHTML = `${t['nav-title']}`;
- }
- }
-
- // 更新当前标签页标题
- const currentTabTitle = document.getElementById('current-tab-title');
- if (currentTabTitle) {
- const currentTab = document.querySelector('.nav-link.active');
- if (currentTab) {
- const tabKey = currentTab.getAttribute('data-i18n');
- if (tabKey && t[tabKey]) {
- // 检查当前标签是否包含图标
- const icon = currentTab.querySelector('i');
- if (icon) {
- currentTabTitle.innerHTML = icon.outerHTML + ' ' + t[tabKey];
- } else {
- currentTabTitle.textContent = t[tabKey];
- }
- }
- }
- }
+ if (navBrand) navBrand.innerHTML = `${t['nav-title']}`;
}
class TSPDashboard {
@@ -170,211 +76,34 @@ class TSPDashboard {
this.sessionId = null;
this.isAgentMode = true;
this.currentLanguage = localStorage.getItem('preferred-language') || 'zh';
-
- // 优化:添加前端缓存
this.cache = new Map();
- this.cacheTimeout = 30000; // 30秒缓存
-
- // 智能更新机制
- this.lastUpdateTimes = {
- alerts: 0,
- workorders: 0,
- health: 0,
- analytics: 0
- };
-
- this.updateThresholds = {
- alerts: 10000, // 10秒
- workorders: 30000, // 30秒
- health: 30000, // 30秒
- analytics: 60000 // 60秒
- };
-
+ this.cacheTimeout = 30000;
+ this.lastUpdateTimes = { alerts: 0, workorders: 0, health: 0, analytics: 0 };
+ this.updateThresholds = { alerts: 10000, workorders: 30000, health: 30000, analytics: 60000 };
this.isPageVisible = true;
-
- // 知识库租户视图状态
this.knowledgeCurrentTenantId = null;
-
- // 对话历史租户视图状态
this.conversationCurrentTenantId = null;
-
- // 分页配置
this.paginationConfig = {
- defaultPageSize: 10,
- pageSizeOptions: [5, 10, 20, 50],
- maxVisiblePages: 5,
- currentKnowledgePage: 1, // 追踪知识库当前页
- currentWorkOrderPage: 1, // 追踪工单当前页
- currentConversationPage: 1 // 追踪对话历史当前页
+ defaultPageSize: 10, pageSizeOptions: [5, 10, 20, 50], maxVisiblePages: 5,
+ currentKnowledgePage: 1, currentWorkOrderPage: 1, currentConversationPage: 1
};
-
this.init();
this.restorePageState();
this.initLanguage();
this.initSmartUpdate();
-
- // 添加页面卸载时的清理逻辑
- window.addEventListener('beforeunload', () => {
- this.destroyAllCharts();
- this.cleanupConnections();
- });
+ window.addEventListener('beforeunload', () => { this.destroyAllCharts(); this.cleanupConnections(); });
}
-
+
initLanguage() {
- // 初始化语言设置
document.documentElement.lang = this.currentLanguage;
document.getElementById('lang-zh').classList.toggle('active', this.currentLanguage === 'zh');
document.getElementById('lang-en').classList.toggle('active', this.currentLanguage === 'en');
updatePageLanguage(this.currentLanguage);
}
- async generateAISuggestion(workorderId) {
- const button = document.querySelector(`button[onclick="dashboard.generateAISuggestion(${workorderId})"]`);
- const textarea = document.getElementById(`aiSuggestion_${workorderId}`);
-
- try {
- // 添加加载状态
- if (button) {
- button.classList.add('btn-loading');
- button.disabled = true;
- }
- if (textarea) {
- textarea.classList.add('ai-loading');
- textarea.value = '正在生成AI建议,请稍候...';
- }
-
- const resp = await fetch(`/api/workorders/${workorderId}/ai-suggestion`, { method: 'POST' });
- const data = await resp.json();
-
- if (data.success) {
- if (textarea) {
- textarea.value = data.suggestion || '';
- textarea.classList.remove('ai-loading');
- textarea.classList.add('success-animation');
-
- // 移除成功动画类
- setTimeout(() => {
- textarea.classList.remove('success-animation');
- }, 600);
- }
- this.showNotification('AI建议已生成', 'success');
- } else {
- throw new Error(data.error || '生成失败');
- }
- } catch (e) {
- console.error('生成AI建议失败:', e);
- if (textarea) {
- textarea.value = 'AI建议生成失败,请重试';
- textarea.classList.remove('ai-loading');
- }
- this.showNotification('生成AI建议失败: ' + e.message, 'error');
- } finally {
- // 移除加载状态
- if (button) {
- button.classList.remove('btn-loading');
- button.disabled = false;
- }
- }
- }
-
- 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.innerHTML = `语义相似度: ${percent}%`;
-
- // 使用新的CSS类
- if (percent >= 90) {
- simEl.className = 'similarity-badge high';
- } else if (percent >= 80) {
- simEl.className = 'similarity-badge medium';
- } else {
- simEl.className = 'similarity-badge low';
- }
-
- simEl.title = this.getSimilarityExplanation(percent);
- }
-
- // 更新审批状态
- if (apprEl) {
- if (data.use_human_resolution) {
- apprEl.textContent = '将使用人工描述入库';
- apprEl.className = 'status-badge human-resolution';
- } else if (data.approved) {
- apprEl.textContent = '已自动审批';
- apprEl.className = 'status-badge approved';
- } else {
- apprEl.textContent = '未审批';
- apprEl.className = 'status-badge pending';
- }
- }
-
- // 更新审批按钮状态
- if (approveBtn) {
- const canApprove = data.approved || data.use_human_resolution;
- approveBtn.disabled = !canApprove;
-
- if (data.use_human_resolution) {
- approveBtn.textContent = '使用人工描述入库';
- approveBtn.className = 'approve-btn';
- approveBtn.title = 'AI准确率低于90%,将使用人工描述入库';
- } else if (data.approved) {
- approveBtn.textContent = '已自动审批';
- approveBtn.className = 'approve-btn approved';
- approveBtn.title = 'AI建议与人工描述高度一致';
- } else {
- approveBtn.textContent = '审批入库';
- approveBtn.className = 'approve-btn';
- approveBtn.title = '手动审批入库';
- }
- }
-
- // 显示更详细的反馈信息
- const message = this.getSimilarityMessage(percent, data.approved, data.use_human_resolution);
- this.showNotification(message, data.approved ? 'success' : data.use_human_resolution ? 'warning' : 'info');
- } 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) {
- const contentType = data.used_content === 'human_resolution' ? '人工描述' : 'AI建议';
- const confidence = Math.round((data.confidence_score || 0) * 100);
- this.showNotification(`已入库为知识条目!使用${contentType},置信度: ${confidence}%`, 'success');
- } else {
- throw new Error(data.error || '入库失败');
- }
- } catch (e) {
- console.error('入库失败:', e);
- this.showNotification('入库失败: ' + e.message, 'error');
- }
- }
init() {
this.bindEvents();
this.populateTenantSelectors();
- // 优化:并行加载初始数据,提高响应速度
this.loadInitialDataAsync();
this.startAutoRefresh();
this.initCharts();
@@ -385,7375 +114,246 @@ class TSPDashboard {
const response = await fetch('/api/tenants');
const tenants = await response.json();
if (!Array.isArray(tenants)) return;
-
- // 填充对话租户选择器
const chatSelect = document.getElementById('chat-tenant-id');
if (chatSelect) {
- const currentVal = chatSelect.value;
- chatSelect.innerHTML = tenants.map(t =>
- ``
- ).join('');
+ const cv = chatSelect.value;
+ chatSelect.innerHTML = tenants.map(t => ``).join('');
}
-
- // 填充工单租户筛选器
const woFilter = document.getElementById('workorder-tenant-filter');
if (woFilter) {
- const currentVal = woFilter.value;
- woFilter.innerHTML = '' + tenants.map(t =>
- ``
- ).join('');
+ const cv = woFilter.value;
+ woFilter.innerHTML = '' + tenants.map(t => ``).join('');
}
- } catch (e) {
- console.warn('加载租户列表失败:', e);
- }
+ } catch (e) { console.warn('加载租户列表失败:', e); }
}
-
+
async loadInitialDataAsync() {
- // 并行加载多个数据源
- try {
- await Promise.all([
- this.loadDashboard(),
- this.loadWorkOrders(),
- this.loadConversationHistory(),
- this.loadKnowledgeBase()
- ]);
- } catch (error) {
- console.error('并行加载数据失败:', error);
- // 回退到串行加载
- await this.loadInitialData();
- }
+ try { await Promise.all([this.loadDashboardData(), this.loadWorkOrders()]); }
+ catch (error) { console.error('并行加载数据失败:', error); }
}
-
- // 优化:添加缓存方法
+
getCachedData(key) {
const cached = this.cache.get(key);
- if (cached && Date.now() - cached.timestamp < this.cacheTimeout) {
- return cached.data;
- }
- return null;
+ return (cached && Date.now() - cached.timestamp < this.cacheTimeout) ? cached.data : null;
}
+ setCachedData(key, data) { this.cache.set(key, { data, timestamp: Date.now() }); }
+
// 统一分页组件
createPaginationComponent(data, containerId, loadFunction, itemName = '条记录') {
- const paginationContainer = document.getElementById(containerId);
- if (!paginationContainer) return;
-
- // 调试信息
- console.log(`分页数据 (${containerId}):`, data);
-
+ const pc = document.getElementById(containerId);
+ if (!pc) return;
const { page, total_pages, total, per_page } = data;
-
- // 检查必要字段
- if (page === undefined || total_pages === undefined || total === undefined || per_page === undefined) {
- console.error(`分页数据不完整 (${containerId}):`, { page, total_pages, total, per_page });
- paginationContainer.innerHTML = '
分页数据加载中...
';
- return;
- }
-
- if (total_pages <= 1) {
- paginationContainer.innerHTML = '';
- return;
- }
-
- let paginationHtml = `
-
-
-
共 ${total} ${itemName},第 ${page} / ${total_pages} 页
-
-
-
-
-
-
`;
+ pc.innerHTML = html;
}
- // 加载指定页面
loadPage(loadFunction, page, containerId) {
- const pageSize = this.getPageSize(containerId);
- if (loadFunction === 'loadAlerts') {
- this.loadAlerts(page, true);
- } else if (loadFunction === 'loadWorkOrders') {
- this.loadWorkOrders(page, true);
- } else if (loadFunction === 'loadKnowledge') {
- this.loadKnowledge(page);
- } else if (loadFunction === 'loadKnowledgeTenantDetailPage') {
- this.loadKnowledgeTenantDetail(this.knowledgeCurrentTenantId, page);
- } else if (loadFunction === 'loadConversationHistory') {
- this.loadConversationHistory(page);
- } else if (loadFunction === 'loadConversationTenantDetailPage') {
- this.loadConversationTenantDetail(this.conversationCurrentTenantId, page);
- }
+ const map = {
+ 'loadAlerts': () => this.loadAlerts(page, true),
+ 'loadWorkOrders': () => this.loadWorkOrders(page, true),
+ 'loadKnowledge': () => this.loadKnowledge(page),
+ 'loadKnowledgeTenantDetailPage': () => this.loadKnowledgeTenantDetail(this.knowledgeCurrentTenantId, page),
+ 'loadConversationHistory': () => this.loadConversationHistory(page),
+ 'loadConversationTenantDetailPage': () => this.loadConversationTenantDetail(this.conversationCurrentTenantId, page)
+ };
+ if (map[loadFunction]) map[loadFunction]();
}
- // 改变每页显示条数
changePageSize(containerId, pageSize, loadFunction) {
- // 保存页面大小到localStorage
localStorage.setItem(`pageSize_${containerId}`, pageSize);
-
- // 重新加载第一页
- if (loadFunction === 'loadAlerts') {
- this.loadAlerts(1, true);
- } else if (loadFunction === 'loadWorkOrders') {
- this.loadWorkOrders(1, true);
- } else if (loadFunction === 'loadKnowledge') {
- this.loadKnowledge(1);
- } else if (loadFunction === 'loadKnowledgeTenantDetailPage') {
- this.loadKnowledgeTenantDetail(this.knowledgeCurrentTenantId, 1);
- } else if (loadFunction === 'loadConversationHistory') {
- this.loadConversationHistory(1);
- } else if (loadFunction === 'loadConversationTenantDetailPage') {
- this.loadConversationTenantDetail(this.conversationCurrentTenantId, 1);
- }
+ const map = {
+ 'loadAlerts': () => this.loadAlerts(1, true),
+ 'loadWorkOrders': () => this.loadWorkOrders(1, true),
+ 'loadKnowledge': () => this.loadKnowledge(1),
+ 'loadKnowledgeTenantDetailPage': () => this.loadKnowledgeTenantDetail(this.knowledgeCurrentTenantId, 1),
+ 'loadConversationHistory': () => this.loadConversationHistory(1),
+ 'loadConversationTenantDetailPage': () => this.loadConversationTenantDetail(this.conversationCurrentTenantId, 1)
+ };
+ if (map[loadFunction]) map[loadFunction]();
}
- // 获取页面大小
getPageSize(containerId) {
const saved = localStorage.getItem(`pageSize_${containerId}`);
return saved ? parseInt(saved) : this.paginationConfig.defaultPageSize;
}
-
- setCachedData(key, data) {
- this.cache.set(key, {
- data: data,
- timestamp: Date.now()
- });
- }
-
- clearCache() {
- this.cache.clear();
- }
bindEvents() {
- // 标签页切换
document.querySelectorAll('[data-tab]').forEach(tab => {
- tab.addEventListener('click', (e) => {
- e.preventDefault();
- this.switchTab(tab.dataset.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.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();
- });
+ btn.addEventListener('click', () => { document.getElementById('message-input').value = btn.dataset.message; this.sendMessage(); });
});
-
- // Agent控制
- document.getElementById('agent-mode-toggle').addEventListener('change', (e) => {
- this.toggleAgentMode(e.target.checked);
- });
-
- // Agent对话功能
+ document.getElementById('agent-mode-toggle').addEventListener('change', (e) => this.toggleAgentMode(e.target.checked));
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('agent-message-input').addEventListener('keypress', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); this.sendAgentMessage(); } });
document.getElementById('trigger-sample-action').addEventListener('click', () => this.triggerSampleAction());
document.getElementById('clear-agent-history').addEventListener('click', () => this.clearAgentHistory());
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.addEventListener('click', (e) => {
- if (e.target.classList.contains('clickable-stat')) {
- const type = e.target.dataset.type;
- const status = e.target.dataset.status || e.target.dataset.level;
- this.showStatPreview(type, status);
- }
+ if (e.target.classList.contains('clickable-stat')) this.showStatPreview(e.target.dataset.type, e.target.dataset.status || e.target.dataset.level);
});
-
- // 知识库管理
document.getElementById('search-knowledge').addEventListener('click', () => this.searchKnowledge());
- document.getElementById('knowledge-search').addEventListener('keypress', (e) => {
- if (e.key === 'Enter') 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();
- });
-
- // API测试按钮事件
+ 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) => { document.getElementById('manual-question-div').style.display = e.target.value === 'manual' ? 'block' : 'none'; });
+ document.getElementById('system-settings-form').addEventListener('submit', (e) => { e.preventDefault(); this.saveSystemSettings(); });
const testApiBtn = document.getElementById('test-api-connection');
- if (testApiBtn) {
- testApiBtn.addEventListener('click', () => this.testApiConnection());
- }
-
+ 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');
- }
- });
- }
+ if (testModelBtn) testModelBtn.addEventListener('click', () => this.testModelResponse());
}
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';
-
+ document.querySelectorAll('.nav-link').forEach(link => link.classList.remove('active'));
+ const tabLink = document.querySelector(`[data-tab="${tabName}"]`);
+ if (tabLink) tabLink.classList.add('active');
+ document.querySelectorAll('.tab-content').forEach(c => c.style.display = 'none');
+ const tabEl = document.getElementById(`${tabName}-tab`);
+ if (tabEl) tabEl.style.display = 'block';
this.currentTab = tabName;
-
- // 如果切换到分析页面,重新初始化图表
- if (tabName === 'analytics') {
- // 延迟一点时间确保DOM已更新
- setTimeout(() => {
- this.initializeCharts();
- }, 100);
- }
-
- // 保存当前页面状态
+ if (tabName === 'analytics') setTimeout(() => this.initializeCharts(), 100);
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;
- case 'conversation-history':
- this.loadConversationTenantList();
- break;
- case 'token-monitor':
- this.loadTokenMonitor();
- break;
- case 'ai-monitor':
- this.loadAIMonitor();
- break;
- case 'system-optimizer':
- this.loadSystemOptimizer();
- break;
- case 'analytics':
- this.loadAnalytics();
- break;
- case 'settings':
- this.loadSettings();
- break;
- case 'tenant-management':
- this.loadTenantList();
- break;
- }
- }
-
- savePageState() {
- const state = {
- currentTab: this.currentTab,
- timestamp: Date.now()
+ const loaders = {
+ 'dashboard': () => this.loadDashboardData(), 'chat': () => {}, 'agent': () => this.loadAgentData(),
+ 'alerts': () => this.loadAlerts(), 'knowledge': () => this.loadKnowledge(),
+ 'workorders': () => this.loadWorkOrders(), 'conversation-history': () => this.loadConversationTenantList(),
+ 'token-monitor': () => this.loadTokenMonitor(), 'ai-monitor': () => this.loadAIMonitor(),
+ 'system-optimizer': () => this.loadSystemOptimizer(), 'analytics': () => this.loadAnalytics(),
+ 'settings': () => this.loadSettings(), 'tenant-management': () => this.loadTenantList()
};
- localStorage.setItem('tsp_dashboard_state', JSON.stringify(state));
+ if (loaders[tabName]) loaders[tabName]();
}
+ savePageState() { localStorage.setItem('tsp_dashboard_state', JSON.stringify({ currentTab: this.currentTab, timestamp: Date.now() })); }
+
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);
- }
+ const s = localStorage.getItem('tsp_dashboard_state');
+ if (s) { const state = JSON.parse(s); if (Date.now() - state.timestamp < 3600000) this.switchTab(state.currentTab); }
+ } catch (e) { console.warn('恢复页面状态失败:', e); }
}
- async loadInitialData() {
- await Promise.all([
- this.loadHealth(),
- this.loadDashboardData(),
- this.loadSystemInfo()
- ]);
- }
+ async loadInitialData() { await Promise.all([this.loadHealth(), this.loadDashboardData(), this.loadSystemInfo()]); }
- // 初始化智能更新机制
initSmartUpdate() {
- // 页面可见性检测
document.addEventListener('visibilitychange', () => {
this.isPageVisible = !document.hidden;
- if (this.isPageVisible) {
- // 页面重新可见时,立即更新数据
- this.smartRefresh();
- }
+ if (this.isPageVisible) this.smartRefresh();
});
-
- // 尝试连接WebSocket获取实时更新
this.initWebSocketConnection();
-
- // 智能定时刷新
this.startSmartRefresh();
}
- // 初始化WebSocket连接
initWebSocketConnection() {
try {
- const wsUrl = `ws://localhost:8765/dashboard`;
- this.websocket = new WebSocket(wsUrl);
-
- this.websocket.onopen = () => {
- console.log('WebSocket连接已建立');
- // 发送订阅消息
- this.websocket.send(JSON.stringify({
- type: 'subscribe',
- topics: ['alerts', 'workorders', 'health']
- }));
- };
-
- this.websocket.onmessage = (event) => {
- try {
- const data = JSON.parse(event.data);
- this.handleRealtimeUpdate(data);
- } catch (error) {
- console.error('WebSocket消息解析失败:', error);
- }
- };
-
- this.websocket.onclose = () => {
- console.log('WebSocket连接已关闭');
- // 5秒后重连
- setTimeout(() => {
- if (this.isPageVisible) {
- this.initWebSocketConnection();
- }
- }, 5000);
- };
-
- this.websocket.onerror = (error) => {
- console.error('WebSocket连接错误:', error);
- };
- } catch (error) {
- console.log('WebSocket连接失败,使用轮询模式:', error);
- }
+ this.websocket = new WebSocket('ws://localhost:8765/dashboard');
+ this.websocket.onopen = () => { this.websocket.send(JSON.stringify({ type: 'subscribe', topics: ['alerts', 'workorders', 'health'] })); };
+ this.websocket.onmessage = (event) => { try { this.handleRealtimeUpdate(JSON.parse(event.data)); } catch (e) {} };
+ this.websocket.onclose = () => { setTimeout(() => { if (this.isPageVisible) this.initWebSocketConnection(); }, 5000); };
+ this.websocket.onerror = () => {};
+ } catch (e) {}
}
- // 处理实时更新
handleRealtimeUpdate(data) {
switch (data.type) {
- case 'alert_update':
- // 直接更新预警统计,无需API调用
- this.updateAlertStatistics(data.alerts);
- this.lastUpdateTimes.alerts = Date.now();
- break;
- case 'workorder_update':
- // 更新工单统计
- if (this.currentTab === 'workorders') {
- this.loadWorkOrders();
- }
- this.lastUpdateTimes.workorders = Date.now();
- break;
- case 'health_update':
- // 更新健康状态
- this.updateHealthDisplay(data.health);
- this.lastUpdateTimes.health = Date.now();
- break;
+ case 'alert_update': if (this.currentTab === 'alerts') this.loadAlerts(); this.lastUpdateTimes.alerts = Date.now(); break;
+ case 'workorder_update': if (this.currentTab === 'workorders') this.loadWorkOrders(); this.lastUpdateTimes.workorders = Date.now(); break;
+ case 'health_update': this.updateHealthDisplay(data.data); this.lastUpdateTimes.health = Date.now(); break;
}
}
- // 智能刷新机制
startSmartRefresh() {
- // 每5秒检查一次是否需要更新
- this.refreshIntervals.smart = setInterval(() => {
- if (this.isPageVisible) {
- this.smartRefresh();
- }
- }, 5000);
+ setInterval(() => { if (this.isPageVisible) this.smartRefresh(); }, 15000);
}
- // 智能刷新逻辑
smartRefresh() {
const now = Date.now();
-
- // 如果最近有用户操作,跳过自动更新
- if (now - this.lastUpdateTimes.alerts < 5000) { // 5秒内不自动更新
- return;
+ if (this.currentTab === 'alerts' && now - this.lastUpdateTimes.alerts > this.updateThresholds.alerts) {
+ this.loadAlerts(); this.lastUpdateTimes.alerts = now;
}
-
- // 检查预警数据是否需要更新
- if (now - this.lastUpdateTimes.alerts > this.updateThresholds.alerts) {
- this.refreshAlertStats();
- this.lastUpdateTimes.alerts = now;
- }
-
- // 检查健康状态是否需要更新
- if (now - this.lastUpdateTimes.health > this.updateThresholds.health) {
- this.loadHealth();
- this.lastUpdateTimes.health = now;
- }
-
- // 检查分析数据是否需要更新
- if (now - this.lastUpdateTimes.analytics > this.updateThresholds.analytics) {
- this.loadAnalytics();
- this.lastUpdateTimes.analytics = now;
- }
-
- // 根据当前标签页决定是否更新工单数据
- if (this.currentTab === 'workorders' &&
- now - this.lastUpdateTimes.workorders > this.updateThresholds.workorders) {
- this.loadWorkOrders();
- this.lastUpdateTimes.workorders = now;
+ if (this.currentTab === 'workorders' && now - this.lastUpdateTimes.workorders > this.updateThresholds.workorders) {
+ this.loadWorkOrders(); this.lastUpdateTimes.workorders = now;
}
}
startAutoRefresh() {
- // 保留原有的刷新机制作为备用
- this.refreshIntervals.health = setInterval(() => {
- if (this.isPageVisible) {
- this.loadHealth();
- }
- }, 30000);
-
- this.refreshIntervals.currentTab = setInterval(() => {
- if (this.isPageVisible) {
- this.refreshCurrentTab();
- }
- }, 30000);
+ this.refreshIntervals.health = setInterval(() => { if (this.isPageVisible) this.loadHealth(); }, 60000);
+ this.refreshIntervals.alerts = setInterval(() => { if (this.isPageVisible && this.currentTab === 'alerts') this.refreshAlertStats(); }, 30000);
}
refreshCurrentTab() {
- switch(this.currentTab) {
- case 'dashboard':
- this.loadDashboardData();
- break;
- case 'alerts':
- this.loadAlerts();
- break;
- case 'agent':
- this.loadAgentData();
- break;
- }
+ const map = { 'alerts': () => this.loadAlerts(), 'workorders': () => this.loadWorkOrders(), 'knowledge': () => this.loadKnowledge(), 'dashboard': () => this.loadDashboardData() };
+ if (map[this.currentTab]) map[this.currentTab]();
}
- // 刷新预警统计(优化版)
async refreshAlertStats() {
try {
- // 检查缓存
- const cacheKey = 'alerts_stats';
- const cachedData = this.cache.get(cacheKey);
- const now = Date.now();
-
- if (cachedData && (now - cachedData.timestamp) < this.cacheTimeout) {
- // 使用缓存数据
- this.updateAlertStatistics(cachedData.data);
- return;
- }
-
- const response = await fetch('/api/alerts?per_page=1000'); // 获取全部数据
+ const response = await fetch('/api/alerts?per_page=1000');
const data = await response.json();
-
if (data.alerts) {
- // 更新缓存
- this.cache.set(cacheKey, {
- data: data.alerts,
- timestamp: now
- });
-
- this.updateAlertStatistics(data.alerts);
+ const stats = data.alerts.reduce((acc, a) => { acc[a.level] = (acc[a.level] || 0) + 1; acc.total = (acc.total || 0) + 1; return acc; }, {});
+ const el = (id) => document.getElementById(id);
+ if (el('critical-alerts')) el('critical-alerts').textContent = stats.critical || 0;
+ if (el('warning-alerts')) el('warning-alerts').textContent = stats.warning || 0;
+ if (el('info-alerts')) el('info-alerts').textContent = stats.info || 0;
+ if (el('total-alerts-count')) el('total-alerts-count').textContent = stats.total || 0;
}
- } catch (error) {
- console.error('刷新预警统计失败:', error);
- // 静默处理错误,避免频繁的错误日志
- }
+ } catch (e) { console.error('刷新预警统计失败:', e); }
}
- // 更新健康状态显示
- updateHealthDisplay(healthData) {
- if (healthData.status) {
- const statusElement = document.getElementById('health-status');
- if (statusElement) {
- statusElement.textContent = healthData.status;
- statusElement.className = `badge ${this.getHealthBadgeClass(healthData.status)}`;
- }
- }
-
- if (healthData.details) {
- const detailsElement = document.getElementById('health-details');
- if (detailsElement) {
- detailsElement.innerHTML = healthData.details;
- }
+ updateHealthDisplay(health) {
+ if (!health) return;
+ const score = health.health_score || health.score || 0;
+ const el = document.getElementById('health-score');
+ if (el) el.textContent = `${score}%`;
+ const badge = document.getElementById('health-badge');
+ if (badge) {
+ badge.className = score >= 80 ? 'badge bg-success' : score >= 60 ? 'badge bg-warning' : 'badge bg-danger';
+ badge.textContent = score >= 80 ? '系统正常' : score >= 60 ? '系统警告' : '系统错误';
}
}
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;
- const healthStatus = health.status || 'unknown';
-
- // 更新健康指示器
- 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?per_page=1000'), // 获取全部预警数据
- 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.alerts?.length || 0;
- document.getElementById('total-workorders').textContent = workorders.workorders?.filter(w => w.status === 'open').length || 0;
- document.getElementById('knowledge-count').textContent = knowledge.total_entries || 0;
-
- // 更新预警统计数字
- if (alerts.alerts) {
- this.updateAlertStatistics(alerts.alerts);
- }
-
- // 更新知识库详细统计
- 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-bar').style.width = `${confidencePercent}%`;
- document.getElementById('knowledge-confidence-bar').setAttribute('aria-valuenow', confidencePercent);
- document.getElementById('knowledge-confidence-bar').textContent = `${confidencePercent}%`;
-
- // 更新性能图表
- await this.updatePerformanceChart(sessions, alerts, workorders);
-
- // 更新系统健康状态
- await this.updateSystemHealth();
-
- // 加载分析数据并更新统计卡片
- await this.loadAnalytics();
-
- } catch (error) {
- console.error('加载仪表板数据失败:', error);
- }
- }
-
-
-
- initCharts() {
- // 性能趋势图
- const performanceCtx = document.getElementById('performanceChart');
- if (performanceCtx) {
- this.charts.performance = new Chart(performanceCtx, {
- type: 'line',
- data: {
- labels: [],
- datasets: [{
- label: '工单数量',
- data: [],
- borderColor: '#007bff',
- backgroundColor: 'rgba(0, 123, 255, 0.1)',
- tension: 0.4
- }, {
- label: '预警数量',
- 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
- }
- });
- }
- }
-
- async updatePerformanceChart(sessions, alerts, workorders) {
- if (!this.charts.performance) return;
-
- 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();
- }
- }
-
- // 更新系统健康状态显示
- 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);
- }
- }
-
- // 对话功能
- async startChat() {
- try {
- const userId = document.getElementById('user-id').value;
- const workOrderId = document.getElementById('work-order-id').value;
- const tenantId = document.getElementById('chat-tenant-id')?.value || 'default';
-
- 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,
- tenant_id: tenantId
- })
- });
-
- 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 = '已连接';
-
- 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 = '未连接';
-
- 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 = '';
-
- // 显示占位提示:小奇正在查询中
- const typingId = this.showTypingIndicator();
-
- // 发送消息到服务器
- 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) {
- this.updateTypingIndicator(typingId, data.response, data.knowledge_used);
- } else {
- this.updateTypingIndicator(typingId, '抱歉,处理您的消息时出现了错误。', null, true);
- }
- } catch (error) {
- console.error('发送消息失败:', error);
- this.updateTypingIndicator(typingId, '网络连接错误,请稍后重试。', null, true);
- }
- }
-
- showTypingIndicator() {
- const messagesContainer = document.getElementById('chat-messages');
- const id = `typing-${Date.now()}`;
- const messageDiv = document.createElement('div');
- messageDiv.className = 'message assistant';
- messageDiv.id = id;
- const avatar = document.createElement('div');
- avatar.className = 'message-avatar';
- avatar.innerHTML = '';
- const contentDiv = document.createElement('div');
- contentDiv.className = 'message-content';
- contentDiv.innerHTML = `
- 小奇正在查询中,请稍后…
- ${new Date().toLocaleTimeString()}
- `;
- 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 = `
- ${content}
- ${new Date().toLocaleTimeString()}
- `;
- if (knowledgeUsed && knowledgeUsed.length > 0) {
- const knowledgeDiv = document.createElement('div');
- knowledgeDiv.className = 'knowledge-info';
- knowledgeDiv.innerHTML = `
-
- 使用了知识库: ${knowledgeUsed.map(k => k.question || k.source || '实时数据').join(', ')}
- `;
- contentDiv.appendChild(knowledgeDiv);
- }
- if (isError) {
- contentDiv.style.borderLeft = '4px solid #dc3545';
- } else {
- contentDiv.style.borderLeft = '';
- }
- }
- const messagesContainer = document.getElementById('chat-messages');
- messagesContainer.scrollTop = messagesContainer.scrollHeight;
- }
-
- addMessage(role, content, knowledgeUsed = null, isError = false) {
- const messagesContainer = document.getElementById('chat-messages');
-
- // 移除欢迎消息
- 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' ? '' : '';
-
- const contentDiv = document.createElement('div');
- contentDiv.className = 'message-content';
- contentDiv.innerHTML = `
- ${content}
- ${new Date().toLocaleTimeString()}
- `;
-
- if (knowledgeUsed && knowledgeUsed.length > 0) {
- const knowledgeDiv = document.createElement('div');
- knowledgeDiv.className = 'knowledge-info';
- knowledgeDiv.innerHTML = `
-
- 使用了知识库: ${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 {
- const [statusResp, toolsResp] = await Promise.all([
- fetch('/api/agent/status'),
- fetch('/api/agent/tools/stats')
- ]);
- const data = await statusResp.json();
- const toolsData = await toolsResp.json();
-
- if (data.success) {
- // 更新 ReactAgent 状态
- const react = data.react_agent || {};
- const stateEl = document.getElementById('agent-current-state');
- stateEl.textContent = react.status || (data.is_active ? 'active' : 'inactive');
- stateEl.className = `badge ${data.is_active ? 'bg-success' : 'bg-secondary'}`;
- document.getElementById('agent-available-tools').textContent = react.tool_count || 0;
- document.getElementById('agent-max-rounds').textContent = react.max_tool_rounds || 5;
- document.getElementById('agent-history-count').textContent = react.history_count || 0;
-
- // 工具列表 — 使用 ReactAgent 的工具定义
- const tools = (toolsData.success ? toolsData.tools : []) || [];
- this.updateToolsList(tools);
-
- // 执行历史
- const history = (toolsData.success ? toolsData.recent_history : []) || [];
- this.updateAgentExecutionHistory(history);
- }
- } catch (error) {
- console.error('加载Agent数据失败:', error);
- }
- }
-
- updateToolsList(tools) {
- const toolsList = document.getElementById('tools-list');
- if (!tools || tools.length === 0) {
- toolsList.innerHTML = '';
- return;
- }
-
- const toolsHtml = tools.map(tool => {
- // ReactAgent 工具定义格式: { name, description, parameters }
- const params = tool.parameters || {};
- const paramList = Object.entries(params).map(([k, v]) =>
- `${k}(${v.required ? '必填' : '可选'})`
- ).join(', ');
- return `
-
-
-
-
${tool.name}
-
${tool.description || ''}
- ${paramList ? `
参数: ${paramList}
` : ''}
-
-
-
-
- `;
- }).join('');
-
- toolsList.innerHTML = toolsHtml;
-
- // 绑定执行事件
- toolsList.querySelectorAll('button[data-tool]').forEach(btn => {
- btn.addEventListener('click', async () => {
- const tool = btn.getAttribute('data-tool');
- let params = {};
- try {
- const input = prompt('请输入执行参数(JSON):', '{}');
- if (input === null) return;
- params = JSON.parse(input);
- } catch (e) {
- this.showNotification('参数格式错误,应为JSON', 'warning');
- return;
- }
- try {
- const resp = await fetch('/api/agent/tools/execute', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ tool, parameters: params })
- });
- const res = await resp.json();
- if (res.success) {
- this.showNotification(`工具 ${tool} 执行成功`, 'success');
- await this.loadAgentData();
- } else {
- this.showNotification(res.error || `工具 ${tool} 执行失败`, 'error');
- }
- } catch (err) {
- this.showNotification('执行工具失败: ' + err.message, 'error');
- }
- });
- });
- }
-
- updateExecutionHistory(history) {
- const historyContainer = document.getElementById('agent-execution-history');
- if (history.length === 0) {
- historyContainer.innerHTML = '';
- return;
- }
-
- const historyHtml = history.slice(-5).map(item => `
-
-
- ${item.type || '未知任务'}
- ${new Date(item.timestamp).toLocaleString()}
-
-
${item.description || '无描述'}
-
-
- ${item.success ? '成功' : '失败'}
-
-
-
- `).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(page = 1, forceRefresh = false) {
- const cacheKey = `alerts_page_${page}`;
-
- if (!forceRefresh && this.cache.has(cacheKey)) {
- const cachedData = this.cache.get(cacheKey);
- this.updateAlertsDisplay(cachedData.alerts);
- this.updateAlertsPagination(cachedData);
- this.updateAlertStatistics(cachedData.alerts); // 添加统计更新
- return;
- }
-
- try {
- const pageSize = this.getPageSize('alerts-pagination');
- const response = await fetch(`/api/alerts?page=${page}&per_page=${pageSize}`);
- const data = await response.json();
-
- this.cache.set(cacheKey, data);
- this.updateAlertsDisplay(data.alerts);
- this.updateAlertsPagination(data);
- this.updateAlertStatistics(data.alerts); // 添加统计更新
- } catch (error) {
- console.error('加载预警失败:', error);
- this.showNotification('加载预警失败', 'error');
- }
- }
-
- updateAlertsDisplay(alerts) {
- const container = document.getElementById('alerts-container');
-
- if (alerts.length === 0) {
- container.innerHTML = `
-
-
-
暂无活跃预警
-
系统运行正常,没有需要处理的预警
-
- `;
- return;
- }
-
- // 添加预警列表的批量操作头部
- const headerHtml = `
-
-
-
-
-
-
-
-
-
- `;
-
- const alertsHtml = alerts.map(alert => `
-
-
-
-
-
-
- ${this.getLevelText(alert.level)}
- ${alert.rule_name || '未知规则'}
- ${this.formatTime(alert.created_at)}
-
-
${alert.message}
-
- 类型: ${this.getTypeText(alert.alert_type)} |
- 级别: ${this.getLevelText(alert.level)}
-
-
-
-
-
-
-
-
- `).join('');
-
- container.innerHTML = headerHtml + alertsHtml;
- }
-
- updateAlertsPagination(data) {
- this.createPaginationComponent(data, 'alerts-pagination', 'loadAlerts', '条预警');
- }
-
- updateAlertStatistics(alerts) {
- // 如果传入的是分页数据,需要重新获取全部数据来计算统计
- if (alerts && alerts.length > 0) {
- // 检查是否是分页数据(通常分页数据少于50条)
- const pageSize = this.getPageSize('alerts-pagination');
- if (alerts.length <= pageSize) {
- // 可能是分页数据,需要获取全部数据
- this.updateAlertStatisticsFromAPI();
- return;
- }
- }
-
- // 使用传入的数据计算统计
- 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;
- }
-
- // 从API获取全部预警数据来计算统计
- async updateAlertStatisticsFromAPI() {
- try {
- // 获取全部预警数据(不分页)
- const response = await fetch('/api/alerts?per_page=1000'); // 获取大量数据
- const data = await response.json();
-
- if (data.alerts) {
- const stats = data.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;
-
- // 更新缓存
- this.cache.set('alerts_stats', {
- data: data.alerts,
- timestamp: Date.now()
- });
- }
- } catch (error) {
- console.error('获取全部预警统计失败:', error);
- }
- }
-
- // 预警批量删除功能
- toggleSelectAllAlerts() {
- const selectAllCheckbox = document.getElementById('select-all-alerts');
- const alertCheckboxes = document.querySelectorAll('.alert-checkbox');
-
- alertCheckboxes.forEach(checkbox => {
- checkbox.checked = selectAllCheckbox.checked;
- });
-
- this.updateBatchDeleteAlertsButton();
- }
-
- updateBatchDeleteAlertsButton() {
- const selectedCheckboxes = document.querySelectorAll('.alert-checkbox:checked');
- const batchDeleteBtn = document.getElementById('batch-delete-alerts');
-
- if (batchDeleteBtn) {
- batchDeleteBtn.disabled = selectedCheckboxes.length === 0;
- batchDeleteBtn.textContent = selectedCheckboxes.length > 0
- ? `批量删除 (${selectedCheckboxes.length})`
- : '批量删除';
- }
- }
-
- async batchDeleteAlerts() {
- const selectedCheckboxes = document.querySelectorAll('.alert-checkbox:checked');
- const selectedIds = Array.from(selectedCheckboxes).map(cb => parseInt(cb.value));
-
- if (selectedIds.length === 0) {
- this.showNotification('请选择要删除的预警', 'warning');
- return;
- }
-
- if (!confirm(`确定要删除选中的 ${selectedIds.length} 个预警吗?此操作不可撤销。`)) {
- return;
- }
-
- try {
- const response = await fetch('/api/batch-delete/alerts', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({ ids: selectedIds })
- });
-
- const data = await response.json();
-
- if (data.success) {
- this.showNotification(data.message, 'success');
-
- // 清除所有相关缓存
- this.cache.delete('alerts');
- this.cache.delete('alerts_stats');
-
- // 立即更新统计数字,避免跳动
- await this.updateStatsAfterDelete(selectedIds.length);
-
- // 重新加载预警列表
- await this.loadAlerts();
-
- // 重置批量删除按钮状态
- this.updateBatchDeleteAlertsButton();
- } else {
- this.showNotification(data.error || '批量删除失败', 'error');
- }
- } catch (error) {
- console.error('批量删除预警失败:', error);
- this.showNotification('批量删除预警失败', 'error');
- }
- }
-
- // 删除后更新统计数字(平滑更新)
- async updateStatsAfterDelete(deletedCount) {
- try {
- // 直接调用API获取最新统计,不依赖页面显示数据
- await this.updateAlertStatisticsFromAPI();
-
- // 更新最后更新时间,避免智能更新机制干扰
- this.lastUpdateTimes.alerts = Date.now();
-
- } catch (error) {
- console.error('更新删除后统计失败:', error);
- // 如果计算失败,直接刷新
- await this.refreshAlertStats();
- }
- }
-
- // 获取当前显示的预警数据
- getCurrentDisplayedAlerts() {
- const alertElements = document.querySelectorAll('.alert-item');
- const alerts = [];
-
- alertElements.forEach(element => {
- const level = element.querySelector('.alert-level')?.textContent?.trim();
- if (level) {
- alerts.push({ level: level.toLowerCase() });
- }
- });
-
- return alerts;
- }
-
- // 计算预警统计
- calculateAlertStats(alerts) {
- const stats = { critical: 0, warning: 0, info: 0, total: 0 };
-
- alerts.forEach(alert => {
- const level = alert.level.toLowerCase();
- if (stats.hasOwnProperty(level)) {
- stats[level]++;
- }
- stats.total++;
- });
-
- return stats;
- }
-
- // 平滑更新预警统计数字
- smoothUpdateAlertStats(newStats) {
- // 获取当前显示的数字
- const currentCritical = parseInt(document.getElementById('critical-alerts')?.textContent || '0');
- const currentWarning = parseInt(document.getElementById('warning-alerts')?.textContent || '0');
- const currentInfo = parseInt(document.getElementById('info-alerts')?.textContent || '0');
- const currentTotal = parseInt(document.getElementById('total-alerts-count')?.textContent || '0');
-
- // 平滑过渡到新数字
- this.animateNumberChange('critical-alerts', currentCritical, newStats.critical);
- this.animateNumberChange('warning-alerts', currentWarning, newStats.warning);
- this.animateNumberChange('info-alerts', currentInfo, newStats.info);
- this.animateNumberChange('total-alerts-count', currentTotal, newStats.total);
- }
-
- // 数字变化动画
- animateNumberChange(elementId, from, to) {
- const element = document.getElementById(elementId);
- if (!element) return;
-
- const duration = 300; // 300ms动画
- const startTime = Date.now();
-
- const animate = () => {
- const elapsed = Date.now() - startTime;
- const progress = Math.min(elapsed / duration, 1);
-
- // 使用缓动函数
- const easeOut = 1 - Math.pow(1 - progress, 3);
- const currentValue = Math.round(from + (to - from) * easeOut);
-
- element.textContent = currentValue;
-
- if (progress < 1) {
- requestAnimationFrame(animate);
- }
- };
-
- requestAnimationFrame(animate);
- }
-
- // 解决预警后更新统计数字
- async updateStatsAfterResolve(alertId) {
- try {
- // 直接调用API获取最新统计,不依赖页面显示数据
- await this.updateAlertStatisticsFromAPI();
-
- // 更新最后更新时间,避免智能更新机制干扰
- this.lastUpdateTimes.alerts = Date.now();
-
- } catch (error) {
- console.error('更新解决后统计失败:', error);
- // 如果计算失败,直接刷新
- await this.refreshAlertStats();
- }
- }
-
- 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.cache.delete('alerts');
- this.cache.delete('alerts_stats');
-
- // 立即更新统计数字
- await this.updateStatsAfterResolve(alertId);
-
- // 重新加载预警列表
- this.loadAlerts();
- } else {
- this.showNotification('解决预警失败', 'error');
- }
- } catch (error) {
- console.error('解决预警失败:', error);
- this.showNotification('解决预警失败', 'error');
- }
- }
-
- // 知识库管理
- async loadKnowledge(page = 1) {
- // 委托给租户列表视图
- this.loadKnowledgeTenantList();
- }
-
- // Task 6.1: 加载租户列表视图
- async loadKnowledgeTenantList() {
- this.knowledgeCurrentTenantId = null;
- this.renderKnowledgeBreadcrumb(null);
-
- // 显示租户列表容器,隐藏详情容器
- const tenantListEl = document.getElementById('knowledge-tenant-list');
- const tenantDetailEl = document.getElementById('knowledge-tenant-detail');
- const searchBar = document.getElementById('knowledge-search-bar');
- const addBtn = document.getElementById('knowledge-add-btn');
- const uploadBtn = document.getElementById('knowledge-upload-btn');
-
- tenantListEl.style.display = '';
- tenantDetailEl.style.display = 'none';
- if (searchBar) searchBar.style.display = 'none';
- if (addBtn) addBtn.style.display = 'none';
- if (uploadBtn) uploadBtn.style.display = 'none';
-
- // 显示加载中
- tenantListEl.innerHTML = '';
-
- // 加载全局统计
- this.loadKnowledgeStats(null);
-
- try {
- const response = await fetch('/api/knowledge/tenants');
- const tenants = await response.json();
-
- if (!Array.isArray(tenants) || tenants.length === 0) {
- tenantListEl.innerHTML = '';
- return;
- }
-
- const cardsHtml = tenants.map(t => `
-
-
-
-
${t.tenant_id}
-
-
- 知识条目
-
${t.entry_count}
-
-
- 已验证
-
${t.verified_count}
-
-
-
-
${t.entry_count > 0 ? Math.round(t.verified_count / t.entry_count * 100) : 0}% 已验证
-
-
-
- `).join('');
-
- tenantListEl.innerHTML = cardsHtml;
- } catch (error) {
- console.error('加载租户列表失败:', error);
- tenantListEl.innerHTML = ' 加载失败
';
- this.showNotification('加载租户列表失败', 'error');
- }
- }
-
- // Task 6.2: 刷新按钮
- refreshKnowledge() {
- if (this.knowledgeCurrentTenantId) {
- this.loadKnowledgeTenantDetail(this.knowledgeCurrentTenantId, this.paginationConfig.currentKnowledgePage);
- } else {
- this.loadKnowledgeTenantList();
- }
- }
-
- // Task 7.1: 加载租户详情视图
- async loadKnowledgeTenantDetail(tenantId, page = 1) {
- this.knowledgeCurrentTenantId = tenantId;
- this.paginationConfig.currentKnowledgePage = page;
- this.renderKnowledgeBreadcrumb(tenantId);
-
- // 隐藏租户列表,显示详情容器
- const tenantListEl = document.getElementById('knowledge-tenant-list');
- const tenantDetailEl = document.getElementById('knowledge-tenant-detail');
- const searchBar = document.getElementById('knowledge-search-bar');
- const addBtn = document.getElementById('knowledge-add-btn');
- const uploadBtn = document.getElementById('knowledge-upload-btn');
-
- tenantListEl.style.display = 'none';
- tenantDetailEl.style.display = '';
- if (searchBar) searchBar.style.display = '';
- if (addBtn) addBtn.style.display = '';
- if (uploadBtn) uploadBtn.style.display = '';
-
- // 清空搜索框
- const searchInput = document.getElementById('knowledge-search');
- if (searchInput) searchInput.value = '';
-
- // 加载租户级统计
- this.loadKnowledgeStats(tenantId);
-
- const listEl = document.getElementById('knowledge-list');
- listEl.innerHTML = '
';
-
- try {
- const pageSize = this.getPageSize('knowledge-pagination');
- const categoryFilter = document.getElementById('knowledge-category-filter')?.value || '';
- const verifiedFilter = document.getElementById('knowledge-verified-filter')?.value || '';
-
- let url = `/api/knowledge?tenant_id=${encodeURIComponent(tenantId)}&page=${page}&per_page=${pageSize}`;
- if (categoryFilter) url += `&category=${encodeURIComponent(categoryFilter)}`;
- if (verifiedFilter) url += `&verified=${encodeURIComponent(verifiedFilter)}`;
-
- const response = await fetch(url);
- const data = await response.json();
-
- if (data.knowledge) {
- this.updateKnowledgeDisplay(data.knowledge);
- this.updateKnowledgeTenantPagination(data);
- } else {
- this.updateKnowledgeDisplay(data);
- }
- } catch (error) {
- console.error('加载租户详情失败:', error);
- listEl.innerHTML = ' 加载失败
';
- this.showNotification('加载租户详情失败', 'error');
- }
- }
-
- // Task 7.1: 填充分类筛选下拉框(从统计数据获取完整分类列表)
- populateCategoryFilter(categories) {
- const select = document.getElementById('knowledge-category-filter');
- if (!select) return;
- const currentValue = select.value;
- const sorted = categories.slice().sort();
- const existingOptions = Array.from(select.options).slice(1).map(o => o.value);
- if (JSON.stringify(sorted) !== JSON.stringify(existingOptions)) {
- select.innerHTML = '' +
- sorted.map(c => ``).join('');
- }
- }
-
- // Task 7.1: 从租户卡片进入详情(重置筛选条件)
- openTenantDetail(tenantId) {
- const categoryFilterEl = document.getElementById('knowledge-category-filter');
- const verifiedFilterEl = document.getElementById('knowledge-verified-filter');
- if (categoryFilterEl) categoryFilterEl.value = '';
- if (verifiedFilterEl) verifiedFilterEl.value = '';
- this.loadKnowledgeTenantDetail(tenantId, 1);
- }
-
- // Task 7.1: 应用筛选条件
- applyKnowledgeFilters() {
- if (this.knowledgeCurrentTenantId) {
- this.loadKnowledgeTenantDetail(this.knowledgeCurrentTenantId, 1);
- }
- }
-
- // Task 7.1: 租户详情分页
- updateKnowledgeTenantPagination(data) {
- this.createPaginationComponent(data, 'knowledge-pagination', 'loadKnowledgeTenantDetailPage', '条知识');
- }
-
- // Task 7.2: 面包屑导航
- renderKnowledgeBreadcrumb(tenantId) {
- const breadcrumbEl = document.getElementById('knowledge-breadcrumb');
- if (!breadcrumbEl) return;
-
- if (!tenantId) {
- breadcrumbEl.innerHTML = '知识库
';
- } else {
- breadcrumbEl.innerHTML = `
-
- `;
- }
- }
-
- // Task 7.3 / 8.2: 刷新当前知识库视图的辅助方法
- async refreshKnowledgeCurrentView() {
- if (this.knowledgeCurrentTenantId) {
- await this.loadKnowledgeTenantDetail(this.knowledgeCurrentTenantId, this.paginationConfig.currentKnowledgePage);
- } else {
- await this.loadKnowledgeTenantList();
- }
- }
-
- // Task 8.2: 加载知识库统计(支持租户级别)
- async loadKnowledgeStats(tenantId) {
- try {
- let url = '/api/knowledge/stats';
- if (tenantId) {
- url += `?tenant_id=${encodeURIComponent(tenantId)}`;
- }
- const response = await fetch(url);
- const knowledge = await response.json();
-
- const totalEl = document.getElementById('knowledge-total');
- const activeEl = document.getElementById('knowledge-active');
- const confidenceBarEl = document.getElementById('knowledge-confidence-bar');
-
- if (totalEl) totalEl.textContent = knowledge.total_entries || 0;
- if (activeEl) activeEl.textContent = knowledge.active_entries || 0;
-
- const confidencePercent = Math.round((knowledge.average_confidence || 0) * 100);
- if (confidenceBarEl) {
- confidenceBarEl.style.width = `${confidencePercent}%`;
- confidenceBarEl.setAttribute('aria-valuenow', confidencePercent);
- confidenceBarEl.textContent = `${confidencePercent}%`;
- }
-
- // Task 7.1: 在租户详情视图中,用统计数据填充分类筛选下拉框
- if (tenantId && knowledge.category_distribution) {
- this.populateCategoryFilter(Object.keys(knowledge.category_distribution));
- }
- } catch (error) {
- console.error('加载知识库统计失败:', error);
- }
- }
-
- updateKnowledgeDisplay(knowledge) {
- const container = document.getElementById('knowledge-list');
-
- if (knowledge.length === 0) {
- container.innerHTML = '';
- return;
- }
-
- // 添加知识库列表的批量操作头部
- const headerHtml = `
-
-
-
-
-
-
-
-
-
-
-
- `;
-
- const knowledgeHtml = knowledge.map(item => `
-
-
-
-
-
-
${item.question}
-
${item.answer}
-
- 分类: ${item.category}
- 置信度: ${Math.round(item.confidence_score * 100)}%
- 使用次数: ${item.usage_count || 0}
-
- ${item.is_verified ? '已验证' : '未验证'}
-
-
-
-
-
-
- ${item.is_verified ?
- `` :
- ``
- }
-
-
-
-
-
- `).join('');
-
- container.innerHTML = headerHtml + knowledgeHtml;
- }
-
- updateKnowledgePagination(data) {
- this.createPaginationComponent(data, 'knowledge-pagination', 'loadKnowledge', '条知识');
- }
-
- async searchKnowledge() {
- const query = document.getElementById('knowledge-search').value.trim();
- if (!query) {
- // Task 8.1: 清空搜索时恢复当前租户的分页列表
- if (this.knowledgeCurrentTenantId) {
- this.loadKnowledgeTenantDetail(this.knowledgeCurrentTenantId);
- } else {
- this.loadKnowledge();
- }
- return;
- }
-
- try {
- // Task 8.1: 搜索时附加 tenant_id 参数
- let url = `/api/knowledge/search?q=${encodeURIComponent(query)}`;
- if (this.knowledgeCurrentTenantId) {
- url += `&tenant_id=${encodeURIComponent(this.knowledgeCurrentTenantId)}`;
- }
- const response = await fetch(url);
- const results = await response.json();
- this.updateKnowledgeDisplay(results);
- // 搜索结果不显示分页
- const paginationEl = document.getElementById('knowledge-pagination');
- if (paginationEl) paginationEl.innerHTML = '';
- } 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 {
- // Task 7.3: 添加知识条目时自动设置 tenant_id
- const body = {
- question,
- answer,
- category,
- confidence_score: confidence
- };
- if (this.knowledgeCurrentTenantId) {
- body.tenant_id = this.knowledgeCurrentTenantId;
- }
- const response = await fetch('/api/knowledge', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify(body)
- });
-
- const data = await response.json();
- if (data.success) {
- this.showNotification('知识添加成功', 'success');
- bootstrap.Modal.getInstance(document.getElementById('addKnowledgeModal')).hide();
- document.getElementById('knowledge-form').reset();
- // Task 7.3: 刷新当前视图
- if (this.knowledgeCurrentTenantId) {
- this.loadKnowledgeTenantDetail(this.knowledgeCurrentTenantId, this.paginationConfig.currentKnowledgePage);
- } else {
- this.loadKnowledgeTenantList();
- }
- } 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.refreshKnowledgeCurrentView();
- } 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.refreshKnowledgeCurrentView();
- } 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.refreshKnowledgeCurrentView();
- } 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');
-
- // Task 7.3: 删除后检查是否所有条目已删除,若是则返回租户列表
- if (this.knowledgeCurrentTenantId) {
- const checkResp = await fetch(`/api/knowledge?tenant_id=${encodeURIComponent(this.knowledgeCurrentTenantId)}&page=1&per_page=1`);
- const checkData = await checkResp.json();
- if (checkData.total === 0) {
- this.showNotification('该租户下已无知识条目,返回租户列表', 'info');
- this.loadKnowledgeTenantList();
- return;
- }
- }
-
- this.refreshKnowledgeCurrentView();
- } else {
- this.showNotification('知识库删除失败', 'error');
- }
- } catch (error) {
- console.error('删除知识库失败:', error);
- this.showNotification('删除知识库失败', 'error');
- }
- }
-
- // 知识库批量删除功能
- toggleSelectAllKnowledge() {
- const selectAllCheckbox = document.getElementById('select-all-knowledge');
- const knowledgeCheckboxes = document.querySelectorAll('.knowledge-checkbox');
-
- knowledgeCheckboxes.forEach(checkbox => {
- checkbox.checked = selectAllCheckbox.checked;
- });
-
- this.updateBatchDeleteKnowledgeButton();
- }
-
- updateBatchDeleteKnowledgeButton() {
- const selectedCheckboxes = document.querySelectorAll('.knowledge-checkbox:checked');
- const batchDeleteBtn = document.getElementById('batch-delete-knowledge');
- const batchVerifyBtn = document.getElementById('batch-verify-knowledge');
- const batchUnverifyBtn = document.getElementById('batch-unverify-knowledge');
- const hasSelection = selectedCheckboxes.length > 0;
-
- if (batchDeleteBtn) {
- batchDeleteBtn.disabled = !hasSelection;
- batchDeleteBtn.innerHTML = hasSelection
- ? `批量删除 (${selectedCheckboxes.length})`
- : '批量删除';
- }
- if (batchVerifyBtn) {
- batchVerifyBtn.disabled = !hasSelection;
- }
- if (batchUnverifyBtn) {
- batchUnverifyBtn.disabled = !hasSelection;
- }
- }
-
- async batchDeleteKnowledge() {
- const selectedCheckboxes = document.querySelectorAll('.knowledge-checkbox:checked');
- const selectedIds = Array.from(selectedCheckboxes).map(cb => parseInt(cb.value));
-
- if (selectedIds.length === 0) {
- this.showNotification('请选择要删除的知识库条目', 'warning');
- return;
- }
-
- if (!confirm(`确定要删除选中的 ${selectedIds.length} 个知识库条目吗?此操作不可撤销。`)) {
- return;
- }
-
- try {
- const response = await fetch('/api/batch-delete/knowledge', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({ ids: selectedIds })
- });
-
- const data = await response.json();
-
- if (data.success) {
- this.showNotification(data.message, 'success');
-
- // 清除缓存并强制刷新
- this.cache.delete('knowledge');
-
- // Task 7.3: 删除后检查是否所有条目已删除,若是则返回租户列表
- if (this.knowledgeCurrentTenantId) {
- const checkResp = await fetch(`/api/knowledge?tenant_id=${encodeURIComponent(this.knowledgeCurrentTenantId)}&page=1&per_page=1`);
- const checkData = await checkResp.json();
- if (checkData.total === 0) {
- this.showNotification('该租户下已无知识条目,返回租户列表', 'info');
- this.loadKnowledgeTenantList();
- return;
- }
- }
-
- await this.refreshKnowledgeCurrentView();
-
- // 重置批量操作按钮状态
- this.updateBatchDeleteKnowledgeButton();
- } else {
- this.showNotification(data.error || '批量删除失败', 'error');
- }
- } catch (error) {
- console.error('批量删除知识库条目失败:', error);
- this.showNotification('批量删除知识库条目失败', 'error');
- }
- }
-
- // Task 7.3: 批量验证知识条目
- async batchVerifyKnowledge() {
- const selectedCheckboxes = document.querySelectorAll('.knowledge-checkbox:checked');
- const selectedIds = Array.from(selectedCheckboxes).map(cb => parseInt(cb.value));
-
- if (selectedIds.length === 0) {
- this.showNotification('请选择要验证的知识库条目', 'warning');
- return;
- }
-
- try {
- const response = await fetch('/api/knowledge/batch_verify', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ ids: selectedIds })
- });
-
- const data = await response.json();
- if (data.success) {
- this.showNotification(data.message || '批量验证成功', 'success');
- await this.refreshKnowledgeCurrentView();
- this.updateBatchDeleteKnowledgeButton();
- } else {
- this.showNotification(data.error || '批量验证失败', 'error');
- }
- } catch (error) {
- console.error('批量验证知识库条目失败:', error);
- this.showNotification('批量验证知识库条目失败', 'error');
- }
- }
-
- // Task 7.3: 批量取消验证知识条目
- async batchUnverifyKnowledge() {
- const selectedCheckboxes = document.querySelectorAll('.knowledge-checkbox:checked');
- const selectedIds = Array.from(selectedCheckboxes).map(cb => parseInt(cb.value));
-
- if (selectedIds.length === 0) {
- this.showNotification('请选择要取消验证的知识库条目', 'warning');
- return;
- }
-
- try {
- const response = await fetch('/api/knowledge/batch_unverify', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ ids: selectedIds })
- });
-
- const data = await response.json();
- if (data.success) {
- this.showNotification(data.message || '批量取消验证成功', 'success');
- await this.refreshKnowledgeCurrentView();
- this.updateBatchDeleteKnowledgeButton();
- } else {
- this.showNotification(data.error || '批量取消验证失败', 'error');
- }
- } catch (error) {
- console.error('批量取消验证知识库条目失败:', error);
- this.showNotification('批量取消验证知识库条目失败', 'error');
- }
- }
-
- // 工单管理
- async loadWorkOrders(page = 1, forceRefresh = false) {
- this.paginationConfig.currentWorkOrderPage = page;
- const cacheKey = `workorders_page_${page}`;
-
- if (!forceRefresh && this.cache.has(cacheKey)) {
- const cachedData = this.cache.get(cacheKey);
- this.updateWorkOrdersDisplay(cachedData.workorders);
- this.updateWorkOrdersPagination(cachedData);
- // 不在这里更新统计,因为分页数据不完整
- return;
- }
-
- try {
- const statusFilter = document.getElementById('workorder-status-filter')?.value || 'all';
- const priorityFilter = document.getElementById('workorder-priority-filter')?.value || 'all';
- const tenantFilter = document.getElementById('workorder-tenant-filter')?.value || 'all';
-
- let url = '/api/workorders';
- const params = new URLSearchParams();
- params.append('page', page);
- const pageSize = this.getPageSize('workorders-pagination');
- params.append('per_page', pageSize.toString());
- if (statusFilter !== 'all') params.append('status', statusFilter);
- if (priorityFilter !== 'all') params.append('priority', priorityFilter);
- if (tenantFilter !== 'all') params.append('tenant_id', tenantFilter);
-
- // 添加强制刷新参数
- if (forceRefresh) {
- params.append('_t', Date.now().toString());
- }
-
- if (params.toString()) url += '?' + params.toString();
-
- const response = await fetch(url, {
- cache: forceRefresh ? 'no-cache' : 'default',
- headers: forceRefresh ? {
- 'Cache-Control': 'no-cache',
- 'Pragma': 'no-cache'
- } : {}
- });
-
- if (!response.ok) {
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
- }
-
- const data = await response.json();
- this.updateWorkOrdersDisplay(data.workorders);
- this.updateWorkOrdersPagination(data);
- // 不在这里更新统计,因为分页数据不完整
-
- // 更新缓存
- this.cache.set(cacheKey, data);
-
- } catch (error) {
- console.error('加载工单失败:', error);
- this.showNotification('加载工单失败: ' + error.message, 'error');
- }
- }
-
- updateWorkOrdersDisplay(workorders) {
- const container = document.getElementById('workorders-list');
-
- if (workorders.length === 0) {
- container.innerHTML = '';
- return;
- }
-
- // 添加工单列表的批量操作头部
- const headerHtml = `
-
-
-
-
-
-
-
-
-
- `;
-
- const workordersHtml = workorders.map(workorder => `
-
-
-
-
-
-
${workorder.title}
-
${workorder.description ? workorder.description.substring(0, 100) + (workorder.description.length > 100 ? '...' : '') : '无问题描述'}
-
- ${this.getPriorityText(workorder.priority)}
- ${this.getStatusText(workorder.status)}
- 分类: ${workorder.category}
- 创建时间: ${new Date(workorder.created_at).toLocaleString()}
-
-
-
-
-
-
-
-
-
-
-
-
- `).join('');
-
- container.innerHTML = headerHtml + workordersHtml;
- }
-
- updateWorkOrdersPagination(data) {
- this.createPaginationComponent(data, 'workorders-pagination', 'loadWorkOrders', '个工单');
- }
-
- updateWorkOrderStatistics(workorders) {
- const stats = workorders.reduce((acc, wo) => {
- acc.total = (acc.total || 0) + 1;
- acc[wo.status] = (acc[wo.status] || 0) + 1;
- return acc;
- }, {});
-
- // 状态映射
- const statusMapping = {
- 'open': ['open', '待处理', '新建', 'new'],
- 'in_progress': ['in_progress', '处理中', '进行中', 'progress', 'processing'],
- 'resolved': ['resolved', '已解决', '已完成'],
- 'closed': ['closed', '已关闭', '关闭']
- };
-
- // 统计各状态的数量
- const mapped_counts = {'open': 0, 'in_progress': 0, 'resolved': 0, 'closed': 0};
-
- for (const [status, count] of Object.entries(stats)) {
- if (status === 'total') continue;
-
- const status_lower = String(status).toLowerCase();
- let mapped = false;
-
- for (const [mapped_status, possible_values] of Object.entries(statusMapping)) {
- if (possible_values.some(v => v.toLowerCase() === status_lower)) {
- mapped_counts[mapped_status] += count;
- mapped = true;
- break;
- }
- }
-
- if (!mapped) {
- console.warn(`未映射的状态: '${status}' (数量: ${count})`);
- }
- }
-
- document.getElementById('workorders-total').textContent = stats.total || 0;
- document.getElementById('workorders-open').textContent = mapped_counts['open'];
- document.getElementById('workorders-progress').textContent = mapped_counts['in_progress'];
- document.getElementById('workorders-resolved').textContent = mapped_counts['resolved'];
-
- console.log('工单统计更新:', {
- total: stats.total,
- open: mapped_counts['open'],
- in_progress: mapped_counts['in_progress'],
- resolved: mapped_counts['resolved'],
- closed: mapped_counts['closed']
- });
- }
-
- // 工单批量删除功能
- toggleSelectAllWorkorders() {
- const selectAllCheckbox = document.getElementById('select-all-workorders');
- const workorderCheckboxes = document.querySelectorAll('.workorder-checkbox');
-
- workorderCheckboxes.forEach(checkbox => {
- checkbox.checked = selectAllCheckbox.checked;
- });
-
- this.updateBatchDeleteButton();
- }
-
- updateBatchDeleteButton() {
- const selectedCheckboxes = document.querySelectorAll('.workorder-checkbox:checked');
- const batchDeleteBtn = document.getElementById('batch-delete-workorders');
-
- if (batchDeleteBtn) {
- batchDeleteBtn.disabled = selectedCheckboxes.length === 0;
- batchDeleteBtn.textContent = selectedCheckboxes.length > 0
- ? `批量删除 (${selectedCheckboxes.length})`
- : '批量删除';
- }
- }
-
- async batchDeleteWorkorders() {
- const selectedCheckboxes = document.querySelectorAll('.workorder-checkbox:checked');
- const selectedIds = Array.from(selectedCheckboxes).map(cb => parseInt(cb.value));
-
- if (selectedIds.length === 0) {
- this.showNotification('请选择要删除的工单', 'warning');
- return;
- }
-
- if (!confirm(`确定要删除选中的 ${selectedIds.length} 个工单吗?此操作不可撤销。`)) {
- return;
- }
-
- try {
- const response = await fetch('/api/batch-delete/workorders', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({ ids: selectedIds })
- });
-
- const data = await response.json();
-
- if (data.success) {
- this.showNotification(data.message, 'success');
-
- // 清除缓存并强制刷新
- this.cache.delete('workorders');
- await this.loadWorkOrders(true); // 强制刷新
- await this.loadAnalytics();
-
- // 重置批量删除按钮状态
- this.updateBatchDeleteButton();
- } else {
- this.showNotification(data.error || '批量删除失败', 'error');
- }
- } catch (error) {
- console.error('批量删除工单失败:', error);
- this.showNotification('批量删除工单失败', 'error');
- }
- }
-
- 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();
- // 立即刷新工单列表和统计
- await this.loadWorkOrders();
- await this.loadAnalytics();
- } else {
- this.showNotification('创建工单失败: ' + (data.error || '未知错误'), 'error');
- }
- } catch (error) {
- console.error('创建工单失败:', error);
- this.showNotification('创建工单失败', 'error');
- }
- }
-
- 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 = `
-
-
-
-
-
-
-
-
基本信息
-
-
- | 工单号: |
- ${workorder.order_id || workorder.id} |
-
-
- | 标题: |
- ${workorder.title} |
-
-
- | 分类: |
- ${workorder.category} |
-
-
- | 优先级: |
- ${this.getPriorityText(workorder.priority)} |
-
-
- | 状态: |
- ${this.getStatusText(workorder.status)} |
-
-
- | 创建时间: |
- ${new Date(workorder.created_at).toLocaleString()} |
-
-
- | 更新时间: |
- ${new Date(workorder.updated_at).toLocaleString()} |
-
-
-
-
-
问题描述
-
- ${workorder.description || '无问题描述'}
-
- ${workorder.resolution ? `
-
解决方案
-
- ${workorder.resolution}
-
- ` : ''}
- ${workorder.satisfaction_score ? `
-
满意度评分
-
-
-
${workorder.satisfaction_score}/5.0
-
- ` : ''}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 相似度: --
-
- 未审批
-
-
-
-
-
-
-
-
-
- ${workorder.conversations && workorder.conversations.length > 0 ? `
-
对话记录
-
- ${workorder.conversations.map(conv => `
-
-
- ${new Date(conv.timestamp).toLocaleString()}
-
-
- 用户: ${conv.user_message}
-
-
- 助手: ${conv.assistant_response}
-
-
- `).join('')}
-
- ` : ''}
-
-
-
-
-
- `;
-
- // 移除已存在的模态框
- 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();
-
- if (workorder.id) {
- this.showEditWorkOrderModal(workorder);
- } else {
- throw new Error(workorder.error || '获取工单详情失败');
- }
- } catch (error) {
- console.error('获取工单详情失败:', error);
- this.showNotification('获取工单详情失败: ' + error.message, 'error');
- }
- }
-
- 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(this.paginationConfig.currentWorkOrderPage, true);
- await this.loadAnalytics();
- } else {
- this.showNotification('删除工单失败: ' + (data.error || '未知错误'), 'error');
- }
- } catch (error) {
- console.error('删除工单失败:', error);
- this.showNotification('删除工单失败: ' + error.message, 'error');
- }
- }
-
- showEditWorkOrderModal(workorder) {
- // 创建编辑工单模态框
- const modalHtml = `
-
- `;
-
- // 移除已存在的模态框
- 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();
-
- if (result.success) {
- this.showNotification('工单更新成功', 'success');
- // 关闭模态框
- const modal = bootstrap.Modal.getInstance(document.getElementById('editWorkOrderModal'));
- modal.hide();
- // 刷新工单列表和统计
- await this.loadWorkOrders();
- await this.loadAnalytics();
- } 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 {
- // 直接请求文件接口,避免浏览器跨源/权限限制
- const resp = await fetch('/api/workorders/import/template/file');
- if (!resp.ok) throw new Error('下载接口返回错误');
- const blob = await resp.blob();
- const blobUrl = window.URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = blobUrl;
- a.download = '工单导入模板.xlsx';
- document.body.appendChild(a);
- a.click();
- document.body.removeChild(a);
- window.URL.revokeObjectURL(blobUrl);
- this.showNotification('模板下载成功', 'success');
- } catch (error) {
- console.error('下载模板失败:', error);
- this.showNotification('下载模板失败: ' + error.message, 'error');
- }
- }
-
- 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();
-
- if (result.success) {
- // 显示成功消息
- 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');
- }
- }
-
- // 对话历史租户列表视图
- async loadConversationTenantList() {
- this.conversationCurrentTenantId = null;
- this.renderConversationBreadcrumb(null);
-
- // 加载全局统计
- this.loadConversationStats(null);
-
- // 显示租户列表容器,隐藏详情容器
- const tenantListEl = document.getElementById('conversation-tenant-list');
- const tenantDetailEl = document.getElementById('conversation-tenant-detail');
-
- if (tenantListEl) tenantListEl.style.display = '';
- if (tenantDetailEl) tenantDetailEl.style.display = 'none';
-
- // 显示加载中 spinner
- if (tenantListEl) {
- tenantListEl.innerHTML = '';
- }
-
- try {
- const response = await fetch('/api/conversations/tenants');
- const tenants = await response.json();
-
- if (!Array.isArray(tenants) || tenants.length === 0) {
- if (tenantListEl) {
- tenantListEl.innerHTML = '';
- }
- return;
- }
-
- const cardsHtml = tenants.map(t => {
- const lastActive = t.last_active_time ? new Date(t.last_active_time).toLocaleString() : '无';
- return `
-
-
-
-
${t.tenant_id}
-
-
- 会话总数
-
${t.session_count}
-
-
- 消息总数
-
${t.message_count}
-
-
- 活跃会话
-
${t.active_session_count}
-
-
-
- 最近活跃: ${lastActive}
-
-
-
-
- `;
- }).join('');
-
- if (tenantListEl) {
- tenantListEl.innerHTML = cardsHtml;
- }
- } catch (error) {
- console.error('加载对话租户列表失败:', error);
- if (tenantListEl) {
- tenantListEl.innerHTML = ' 加载失败
';
- }
- this.showNotification('加载对话租户列表失败', 'error');
- }
- }
-
- // Task 7.1: 加载对话历史租户详情视图
- async loadConversationTenantDetail(tenantId, page = 1) {
- this.conversationCurrentTenantId = tenantId;
- this.paginationConfig.currentConversationPage = page;
-
- // 隐藏租户列表,显示详情容器
- const tenantListEl = document.getElementById('conversation-tenant-list');
- const tenantDetailEl = document.getElementById('conversation-tenant-detail');
-
- if (tenantListEl) tenantListEl.style.display = 'none';
- if (tenantDetailEl) tenantDetailEl.style.display = '';
-
- // 渲染面包屑(如果 renderConversationBreadcrumb 已实现)
- if (typeof this.renderConversationBreadcrumb === 'function') {
- this.renderConversationBreadcrumb(tenantId);
- }
-
- // 加载租户级统计(如果 loadConversationStats 已实现)
- if (typeof this.loadConversationStats === 'function') {
- this.loadConversationStats(tenantId);
- }
-
- // 显示加载中 spinner
- const sessionListEl = document.getElementById('conversation-session-list');
- if (sessionListEl) {
- sessionListEl.innerHTML = '';
- }
-
- try {
- const perPage = this.getPageSize('conversation-session-pagination');
- const statusFilter = document.getElementById('conversation-status-filter')?.value || '';
- const dateFilter = document.getElementById('conversation-detail-date-filter')?.value || '';
-
- let url = `/api/conversations/sessions?tenant_id=${encodeURIComponent(tenantId)}&page=${page}&per_page=${perPage}`;
- if (statusFilter) url += `&status=${encodeURIComponent(statusFilter)}`;
- if (dateFilter) url += `&date_filter=${encodeURIComponent(dateFilter)}`;
-
- const response = await fetch(url);
- const data = await response.json();
-
- if (data.sessions) {
- this.renderConversationSessionTable(data.sessions, tenantId);
- this.updateConversationTenantPagination(data);
- } else {
- if (sessionListEl) {
- sessionListEl.innerHTML = '';
- }
- }
- } catch (error) {
- console.error('加载租户会话列表失败:', error);
- if (sessionListEl) {
- sessionListEl.innerHTML = ' 加载失败
';
- }
- this.showNotification('加载租户会话列表失败', 'error');
- }
- }
-
- // Task 7.1: 渲染会话表格
- renderConversationSessionTable(sessions, tenantId) {
- const sessionListEl = document.getElementById('conversation-session-list');
- if (!sessionListEl) return;
-
- if (!sessions || sessions.length === 0) {
- sessionListEl.innerHTML = '';
- return;
- }
-
- const statusBadge = (status) => {
- if (status === 'active') return '活跃';
- if (status === 'ended') return '已结束';
- return `${status || '未知'}`;
- };
-
- const sourceBadge = (source) => {
- if (source === 'feishu') return '飞书';
- if (source === 'websocket') return 'WebSocket';
- if (source === 'api') return 'API';
- return `${source || '未知'}`;
- };
-
- const formatTime = (isoStr) => {
- if (!isoStr) return '-';
- return new Date(isoStr).toLocaleString();
- };
-
- const rowsHtml = sessions.map(s => `
-
- | ${s.title || s.session_id || '-'} |
- ${s.message_count || 0} |
- ${statusBadge(s.status)} |
- ${sourceBadge(s.source)} |
- ${formatTime(s.created_at)} |
- ${formatTime(s.updated_at)} |
-
-
- |
-
- `).join('');
-
- sessionListEl.innerHTML = `
-
-
-
-
- | 会话标题 |
- 消息数 |
- 状态 |
- 来源 |
- 创建时间 |
- 更新时间 |
- 操作 |
-
-
-
- ${rowsHtml}
-
-
-
- `;
- }
-
- // Task 7.1: 查看会话消息详情
- async viewSessionMessages(sessionId, sessionTitle) {
- try {
- const response = await fetch(`/api/conversations/sessions/${encodeURIComponent(sessionId)}`);
- const data = await response.json();
-
- if (data.success && data.messages) {
- // 更新面包屑到第三层(如果已实现)
- if (typeof this.renderConversationBreadcrumb === 'function') {
- this.renderConversationBreadcrumb(this.conversationCurrentTenantId, sessionTitle || sessionId);
- }
- this.showSessionMessagesModal(data.session, data.messages);
- } else {
- throw new Error(data.error || '获取会话消息失败');
- }
- } catch (error) {
- console.error('获取会话消息失败:', error);
- this.showNotification('获取会话消息失败: ' + error.message, 'error');
- }
- }
-
- // Task 7.1: 显示会话消息模态框
- showSessionMessagesModal(session, messages) {
- const messagesHtml = messages.map(msg => `
-
-
-
用户:
-
${msg.user_message || ''}
-
-
-
助手:
-
${msg.assistant_response || ''}
-
-
-
- ${msg.timestamp ? new Date(msg.timestamp).toLocaleString() : ''}
- ${msg.response_time ? ` | 响应: ${msg.response_time}s` : ''}
- ${msg.confidence_score ? ` | 置信度: ${Math.round(msg.confidence_score * 100)}%` : ''}
-
-
-
- `).join('');
-
- const modalHtml = `
-
- `;
-
- // 移除已存在的模态框
- const existingModal = document.getElementById('sessionMessagesModal');
- if (existingModal) existingModal.remove();
-
- document.body.insertAdjacentHTML('beforeend', modalHtml);
- const modal = new bootstrap.Modal(document.getElementById('sessionMessagesModal'));
- modal.show();
-
- // 模态框关闭后清理
- document.getElementById('sessionMessagesModal').addEventListener('hidden.bs.modal', function () {
- this.remove();
- });
- }
-
- // Task 7.1: 对话租户详情分页
- updateConversationTenantPagination(data) {
- this.createPaginationComponent(data, 'conversation-session-pagination', 'loadConversationTenantDetailPage', '条会话');
- }
-
- // Task 7.2: 面包屑导航
- renderConversationBreadcrumb(tenantId, sessionTitle) {
- const breadcrumbEl = document.getElementById('conversation-breadcrumb');
- if (!breadcrumbEl) return;
-
- if (!tenantId) {
- breadcrumbEl.innerHTML = '';
- return;
- }
-
- if (!sessionTitle) {
- // 租户详情视图: "对话历史 > {tenant_id}"
- breadcrumbEl.innerHTML = `
-
- `;
- } else {
- // 消息详情视图: "对话历史 > {tenant_id} > {session_title}"
- breadcrumbEl.innerHTML = `
-
- `;
- }
-
- this.conversationCurrentTenantId = tenantId;
- }
-
- // Task 7.1: 应用对话筛选条件
- applyConversationFilters() {
- if (this.conversationCurrentTenantId) {
- this.loadConversationTenantDetail(this.conversationCurrentTenantId, 1);
- }
- }
-
- // Task 7.3: 删除会话并处理空租户自动返回
- async deleteConversationSession(sessionId) {
- if (!confirm('确定要删除这个会话及其所有消息吗?')) return;
- try {
- const response = await fetch(`/api/conversations/sessions/${encodeURIComponent(sessionId)}`, { method: 'DELETE' });
- const data = await response.json();
- if (data.success) {
- this.showNotification('会话已删除', 'success');
- // 刷新当前租户详情视图并检查是否还有剩余会话
- if (this.conversationCurrentTenantId) {
- const tenantId = this.conversationCurrentTenantId;
- const perPage = this.getPageSize('conversation-session-pagination');
- const checkUrl = `/api/conversations/sessions?tenant_id=${encodeURIComponent(tenantId)}&page=1&per_page=${perPage}`;
- const checkResp = await fetch(checkUrl);
- const checkData = await checkResp.json();
-
- if (!checkData.sessions || checkData.sessions.length === 0 || checkData.total === 0) {
- // 该租户下已无会话,自动返回租户列表视图
- this.loadConversationTenantList();
- } else {
- // 仍有会话,刷新当前详情视图(调整页码避免越界)
- const maxPage = checkData.total_pages || 1;
- const currentPage = Math.min(this.paginationConfig.currentConversationPage, maxPage);
- await this.loadConversationTenantDetail(tenantId, currentPage);
- }
- }
- } else {
- throw new Error(data.error || '删除失败');
- }
- } catch (error) {
- console.error('删除会话失败:', error);
- this.showNotification('删除会话失败: ' + error.message, 'error');
- }
- }
-
- // 对话历史管理
- async loadConversationHistory(page = 1) {
- try {
- const pageSize = this.getPageSize('conversations-pagination');
- const response = await fetch(`/api/conversations?page=${page}&per_page=${pageSize}`);
- const data = await response.json();
-
- if (data.conversations) {
- this.renderConversationList(data.conversations || []);
- this.updateConversationPagination(data);
- this.updateConversationStats(data.stats || {});
- } else {
- throw new Error(data.error || '加载对话历史失败');
- }
- } catch (error) {
- console.error('加载对话历史失败:', error);
- this.showNotification('加载对话历史失败: ' + error.message, 'error');
- }
- }
-
- renderConversationList(conversations) {
- const container = document.getElementById('conversation-list');
- if (!conversations || conversations.length === 0) {
- container.innerHTML = `
-
- `;
- return;
- }
-
- const html = conversations.map(conv => {
- // 识别来源和类型
- let sourceBadge = '';
- const method = conv.invocation_method || '';
- const userId = conv.user_id || '';
-
- // 判断是否来自飞书 (根据调用方式或ID格式)
- if (method.includes('feishu') || userId.startsWith('ou_')) {
- sourceBadge = '飞书';
- } else {
- sourceBadge = 'Web';
- }
-
- // 判断群聊/私聊
- let typeBadge = '';
- if (method.includes('group')) {
- typeBadge = '群聊';
- } else if (method.includes('p2p')) {
- typeBadge = '私聊';
- }
-
- return `
-
-
-
-
-
- ${sourceBadge}
- ${typeBadge}
- 用户: ${userId || '匿名'}
-
- ${new Date(conv.timestamp).toLocaleString()}
-
-
-
-
-
-
-
-
用户: ${conv.user_message?.substring(0, 100)}${conv.user_message?.length > 100 ? '...' : ''}
-
助手: ${conv.assistant_response?.substring(0, 100)}${conv.assistant_response?.length > 100 ? '...' : ''}
-
-
- 响应时间: ${conv.response_time || 0}ms
- ${conv.work_order_id ? `工单: ${conv.work_order_id}` : ''}
-
-
-
- `;
- }).join('');
-
- container.innerHTML = html;
- }
-
- updateConversationPagination(data) {
- this.createPaginationComponent(data, 'conversations-pagination', 'loadConversationHistory', '条对话');
- }
-
- 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;
- }
-
- // Task 8.3: 加载对话统计(支持租户级别)
- async loadConversationStats(tenantId) {
- try {
- let url = '/api/conversations/analytics';
- if (tenantId) {
- url += `?tenant_id=${encodeURIComponent(tenantId)}`;
- }
- const response = await fetch(url);
- const data = await response.json();
-
- const analytics = data.analytics || {};
- const convStats = analytics.conversations || {};
-
- const totalEl = document.getElementById('conversation-total');
- const todayEl = document.getElementById('conversation-today');
- const avgResponseEl = document.getElementById('conversation-avg-response');
- const activeUsersEl = document.getElementById('conversation-active-users');
-
- if (totalEl) totalEl.textContent = convStats.total || 0;
- if (todayEl) todayEl.textContent = convStats.today || 0;
- if (avgResponseEl) avgResponseEl.textContent = `${Math.round(convStats.avg_response_time || 0)}ms`;
- if (activeUsersEl) activeUsersEl.textContent = convStats.active_users || 0;
- } catch (error) {
- console.error('加载对话统计失败:', error);
- }
- }
-
- async refreshConversationHistory() {
- // 先尝试触发一次合并迁移(幂等,重复调用也安全)
- try {
- await fetch('/api/conversations/migrate-merge', { method: 'POST' });
- } catch (e) { /* 忽略迁移失败 */ }
- // 根据当前视图状态刷新:租户详情视图或租户列表视图
- if (this.conversationCurrentTenantId) {
- await this.loadConversationTenantDetail(this.conversationCurrentTenantId, this.paginationConfig.currentConversationPage);
- } else {
- await this.loadConversationTenantList();
- }
- this.showNotification('对话历史已刷新', 'success');
- }
-
- async clearAllConversations() {
- if (!confirm('确定要清空所有对话历史吗?此操作不可恢复!')) {
- return;
- }
-
- 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 || '清空对话历史失败');
- }
- } catch (error) {
- console.error('清空对话历史失败:', error);
- this.showNotification('清空对话历史失败: ' + error.message, 'error');
- }
- }
-
- async deleteConversation(conversationId) {
- if (!confirm('确定要删除这条对话记录吗?')) {
- return;
- }
-
- 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');
- }
- }
-
- async viewConversation(conversationId) {
- try {
- const response = await fetch(`/api/conversations/${conversationId}`);
- const data = await response.json();
-
- if (data.success) {
- data.user_id = data.user_id || '匿名';
- this.showConversationModal(data);
- } else {
- throw new Error(data.error || '获取对话详情失败');
- }
- } catch (error) {
- console.error('获取对话详情失败:', error);
- this.showNotification('获取对话详情失败: ' + error.message, 'error');
- }
- }
-
- showConversationModal(conversation) {
- const modalHtml = `
-
-
-
-
-
-
- 用户: ${conversation.user_id || '匿名'}
- 时间: ${new Date(conversation.timestamp).toLocaleString()}
- 响应时间: ${conversation.response_time || 0}ms
-
-
-
用户消息:
-
${conversation.user_message || ''}
-
-
-
助手回复:
-
${conversation.assistant_response || ''}
-
-
-
-
-
-
- `;
-
- // 移除已存在的模态框
- 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.trim();
- const userFilter = document.getElementById('conversation-user-filter')?.value || '';
- const dateFilter = document.getElementById('conversation-date-filter')?.value || '';
-
- // Task 8.1: 在 Tenant_Detail_View 中搜索时自动附加 tenant_id 参数
- if (this.conversationCurrentTenantId) {
- if (!search) {
- // 清空搜索时恢复当前租户的完整分页列表
- this.loadConversationTenantDetail(this.conversationCurrentTenantId);
- return;
- }
- try {
- const perPage = this.getPageSize('conversation-session-pagination');
- let url = `/api/conversations/sessions?search=${encodeURIComponent(search)}&tenant_id=${encodeURIComponent(this.conversationCurrentTenantId)}&page=1&per_page=${perPage}`;
- if (dateFilter) url += `&date_filter=${encodeURIComponent(dateFilter)}`;
-
- const response = await fetch(url);
- const data = await response.json();
-
- if (data.sessions) {
- this.renderConversationSessionTable(data.sessions, this.conversationCurrentTenantId);
- this.updateConversationTenantPagination(data);
- } else {
- const sessionListEl = document.getElementById('conversation-session-list');
- if (sessionListEl) {
- sessionListEl.innerHTML = '';
- }
- const paginationEl = document.getElementById('conversation-session-pagination');
- if (paginationEl) paginationEl.innerHTML = '';
- }
- } catch (error) {
- console.error('搜索租户会话失败:', error);
- this.showNotification('搜索会话失败: ' + error.message, 'error');
- }
- return;
- }
-
- // 非租户详情视图:保持原有行为
- 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');
- }
- }
-
- // 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');
-
- 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'
- }]
- },
- options: {
- responsive: true,
- maintainAspectRatio: false,
- scales: {
- y: {
- type: 'linear',
- display: true,
- position: 'left',
- title: {
- display: true,
- text: 'Token数量'
- }
- },
- y1: {
- type: 'linear',
- display: true,
- position: 'right',
- title: {
- display: true,
- 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 = `
-
- | 暂无记录 |
-
- `;
- return;
- }
-
- const html = records.map(record => `
-
- | ${new Date(record.timestamp).toLocaleString()} |
- ${record.user_id || '匿名'} |
- ${record.model || 'qwen-turbo'} |
- ${record.input_tokens || 0} |
- ${record.output_tokens || 0} |
- ${record.total_tokens || 0} |
- ¥${record.cost || 0} |
- ${record.response_time || 0}ms |
-
- `).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 = `
-
- | 暂无错误记录 |
-
- `;
- return;
- }
-
- const html = errors.map(error => `
-
- | ${new Date(error.timestamp).toLocaleString()} |
- ${error.error_type || '未知'} |
- ${error.error_message || ''} |
- ${error.model || 'qwen-turbo'} |
- ${error.user_id || '匿名'} |
-
-
- |
-
- `).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 viewErrorDetail(convId) {
- const modal = new bootstrap.Modal(document.getElementById('errorDetailModal'));
- const body = document.getElementById('errorDetailBody');
- body.innerHTML = '加载中...
';
- modal.show();
-
- try {
- const response = await fetch(`/api/ai-monitor/error-log/${convId}`);
- const data = await response.json();
-
- if (data.success) {
- const d = data.detail;
- body.innerHTML = `
-
- | 记录ID | ${d.id} |
- | 时间 | ${d.timestamp ? new Date(d.timestamp).toLocaleString() : '-'} |
- | 分类 | ${d.category || '-'} |
- | 来源 | ${d.source || '-'} |
- | 置信度 | ${d.confidence_score != null ? d.confidence_score : '-'} |
- | 响应时间 | ${d.response_time != null ? d.response_time + ' ms' : '-'} |
- | 用户消息 | ${this.escapeHtml(d.user_message || '-')} |
- | 助手回复 | ${this.escapeHtml(d.assistant_response || '-')} |
-
- `;
- } else {
- body.innerHTML = `${data.error || '加载失败'}
`;
- }
- } catch (error) {
- body.innerHTML = `请求失败: ${error.message}
`;
- }
- }
-
- escapeHtml(text) {
- const div = document.createElement('div');
- div.textContent = text;
- return div.innerHTML;
- }
-
- // 系统优化
- 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(data.message || 'CPU优化完成', 'success');
- this.updateOptimizationProgress('cpu-optimization', data.progress || 100);
- // 刷新状态并回落进度条
- await this.loadSystemOptimizer();
- setTimeout(() => this.updateOptimizationProgress('cpu-optimization', 0), 1500);
- } else {
- throw new Error(data.error || 'CPU优化失败');
- }
- } 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(data.message || '内存优化完成', 'success');
- this.updateOptimizationProgress('memory-optimization', data.progress || 100);
- await this.loadSystemOptimizer();
- setTimeout(() => this.updateOptimizationProgress('memory-optimization', 0), 1500);
- } else {
- throw new Error(data.error || '内存优化失败');
- }
- } 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(data.message || '磁盘优化完成', 'success');
- this.updateOptimizationProgress('disk-optimization', data.progress || 100);
- await this.loadSystemOptimizer();
- setTimeout(() => this.updateOptimizationProgress('disk-optimization', 0), 1500);
- } else {
- throw new Error(data.error || '磁盘优化失败');
- }
- } 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 clearCache() {
- try {
- const response = await fetch('/api/system-optimizer/clear-cache', { method: 'POST' });
- const data = await response.json();
- if (data.success) {
- this.showNotification(data.message || '缓存已清理', 'success');
- await this.loadSystemOptimizer();
- } else {
- throw new Error(data.error || '清理缓存失败');
- }
- } catch (error) {
- console.error('清理缓存失败:', error);
- this.showNotification('清理缓存失败: ' + error.message, 'error');
- }
- }
-
- async optimizeAll() {
- try {
- const response = await fetch('/api/system-optimizer/optimize-all', { method: 'POST' });
- const data = await response.json();
- if (data.success) {
- this.showNotification(data.message || '一键优化完成', 'success');
- await this.loadSystemOptimizer();
- ['cpu-optimization','memory-optimization','disk-optimization'].forEach(id => this.updateOptimizationProgress(id, 100));
- setTimeout(() => ['cpu-optimization','memory-optimization','disk-optimization'].forEach(id => this.updateOptimizationProgress(id, 0)), 1500);
- } else {
- throw new Error(data.error || '一键优化失败');
- }
- } catch (error) {
- console.error('一键优化失败:', error);
- this.showNotification('一键优化失败: ' + error.message, 'error');
- }
- }
-
- async loadSecuritySettings() {
- try {
- const response = await fetch('/api/system-optimizer/security-settings');
- 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.updateStatisticsCards(analytics); // 添加统计卡片更新
- this.initializeCharts();
- } catch (error) {
- console.error('加载分析数据失败:', error);
- }
- }
-
- // 初始化图表
- initializeCharts() {
- if (!this.charts) {
- this.charts = {};
- }
- this.updateCharts();
- }
-
- // 清理连接
- cleanupConnections() {
- // 关闭WebSocket连接
- if (this.websocket) {
- this.websocket.close();
- this.websocket = null;
- }
-
- // 清理所有定时器
- Object.values(this.refreshIntervals).forEach(interval => {
- clearInterval(interval);
- });
- this.refreshIntervals = {};
- }
-
- // 销毁所有图表
- destroyAllCharts() {
- if (!this.charts) return;
-
- Object.keys(this.charts).forEach(chartId => {
- if (this.charts[chartId]) {
- try {
- this.charts[chartId].destroy();
- } catch (e) {
- console.warn(`Error destroying chart ${chartId}:`, e);
- }
- this.charts[chartId] = null;
- }
- });
-
- // 清理charts对象
- this.charts = {};
- }
-
- // 安全的图表创建方法
- createChart(canvasId, chartConfig) {
- const canvas = document.getElementById(canvasId);
- if (!canvas) {
- console.error(`Canvas element '${canvasId}' not found`);
- return null;
- }
-
- // 确保charts对象存在
- if (!this.charts) {
- this.charts = {};
- }
-
- // 销毁现有图表
- if (this.charts[canvasId]) {
- try {
- this.charts[canvasId].destroy();
- } catch (e) {
- console.warn(`Error destroying chart ${canvasId}:`, e);
- }
- this.charts[canvasId] = null;
- }
-
- try {
- const ctx = canvas.getContext('2d');
- this.charts[canvasId] = new Chart(ctx, chartConfig);
- return this.charts[canvasId];
- } catch (e) {
- console.error(`Error creating chart ${canvasId}:`, e);
- return null;
- }
- }
-
- // 更新所有图表
- 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 inProgress = data.workorders?.in_progress || 0;
- const resolved = data.workorders?.resolved || 0;
- const avgSatisfaction = data.satisfaction?.average || 0;
-
- // 更新工单统计数字(使用正确的元素ID)
- if (document.getElementById('workorders-total')) {
- document.getElementById('workorders-total').textContent = total;
- }
- if (document.getElementById('workorders-open')) {
- document.getElementById('workorders-open').textContent = open;
- }
- if (document.getElementById('workorders-progress')) {
- document.getElementById('workorders-progress').textContent = inProgress;
- }
- if (document.getElementById('workorders-resolved')) {
- document.getElementById('workorders-resolved').textContent = resolved;
- }
-
- // 同时更新其他可能的元素ID
- if (document.getElementById('totalWorkorders')) {
- document.getElementById('totalWorkorders').textContent = total;
- }
- if (document.getElementById('openWorkorders')) {
- document.getElementById('openWorkorders').textContent = open;
- }
- if (document.getElementById('resolvedWorkorders')) {
- document.getElementById('resolvedWorkorders').textContent = resolved;
- }
- document.getElementById('avgSatisfaction').textContent = avgSatisfaction.toFixed(1);
-
- // 更新预警统计
- const alertTotal = data.alerts?.total || 0;
- const alertActive = data.alerts?.active || 0;
- const alertCritical = data.alerts?.by_level?.critical || 0;
- const alertWarning = data.alerts?.by_level?.warning || 0;
- const alertError = data.alerts?.by_level?.error || 0;
-
- // 更新预警统计显示
- if (document.getElementById('critical-alerts')) {
- document.getElementById('critical-alerts').textContent = alertCritical;
- }
- if (document.getElementById('warning-alerts')) {
- document.getElementById('warning-alerts').textContent = alertWarning;
- }
- if (document.getElementById('error-alerts')) {
- document.getElementById('error-alerts').textContent = alertError;
- }
- if (document.getElementById('total-alerts-count')) {
- document.getElementById('total-alerts-count').textContent = alertTotal;
- }
-
- // 更新性能统计
- const performanceScore = data.performance?.score || 0;
- const performanceTrend = data.performance?.trend || 'stable';
-
- if (document.getElementById('performance-score')) {
- document.getElementById('performance-score').textContent = performanceScore.toFixed(1);
- }
- if (document.getElementById('performance-trend')) {
- document.getElementById('performance-trend').textContent = this.getPerformanceTrendText(performanceTrend);
- }
-
- // 更新满意度统计
- const satisfactionAvg = data.satisfaction?.average || 0;
- const satisfactionCount = data.satisfaction?.count || 0;
-
- if (document.getElementById('satisfaction-avg')) {
- document.getElementById('satisfaction-avg').textContent = satisfactionAvg.toFixed(1);
- }
- if (document.getElementById('satisfaction-count')) {
- document.getElementById('satisfaction-count').textContent = satisfactionCount;
- }
-
- // 更新进度条
- 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 chartData = this.prepareChartData(data, chartType);
-
- const chartConfig = {
- 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: '数量'
- }
- }
- }
- }
- };
-
- this.createChart('mainChart', chartConfig);
- }
-
- // 更新分布图表
- updateDistributionChart(data) {
- const currentDimension = document.getElementById('dataDimension')?.value || 'workorders';
-
- let labels, values, title, backgroundColor;
-
- if (currentDimension === 'alerts') {
- // 预警级别分布
- const alertLevels = data.alerts?.by_level || {};
- labels = Object.keys(alertLevels);
- values = Object.values(alertLevels);
- title = '预警级别分布';
- backgroundColor = [
- '#FF6384', // critical - 红色
- '#FFCE56', // warning - 黄色
- '#36A2EB', // error - 蓝色
- '#4BC0C0', // info - 青色
- '#9966FF' // 其他
- ];
- } else if (currentDimension === 'performance') {
- // 性能指标分布
- const performanceMetrics = data.performance?.by_level || {};
- labels = Object.keys(performanceMetrics);
- values = Object.values(performanceMetrics);
- title = '性能指标分布';
- backgroundColor = [
- '#28a745', // 优秀 - 绿色
- '#ffc107', // 良好 - 黄色
- '#fd7e14', // 一般 - 橙色
- '#dc3545' // 差 - 红色
- ];
- } else if (currentDimension === 'satisfaction') {
- // 满意度分布
- const satisfactionLevels = data.satisfaction?.by_level || {};
- labels = Object.keys(satisfactionLevels);
- values = Object.values(satisfactionLevels);
- title = '满意度分布';
- backgroundColor = [
- '#28a745', // 非常满意 - 绿色
- '#ffc107', // 满意 - 黄色
- '#fd7e14', // 一般 - 橙色
- '#dc3545' // 不满意 - 红色
- ];
- } else {
- // 工单分类分布
- const categories = data.workorders?.by_category || {};
- labels = Object.keys(categories);
- values = Object.values(categories);
- title = '工单分类分布';
- backgroundColor = [
- '#FF6384',
- '#36A2EB',
- '#FFCE56',
- '#4BC0C0',
- '#9966FF',
- '#FF9F40'
- ];
- }
-
- const chartConfig = {
- type: 'doughnut',
- data: {
- labels: labels,
- datasets: [{
- data: values,
- backgroundColor: backgroundColor
- }]
- },
- options: {
- responsive: true,
- maintainAspectRatio: false,
- plugins: {
- title: {
- display: true,
- text: title
- },
- legend: {
- display: true,
- position: 'bottom'
- }
- }
- }
- };
-
- this.createChart('distributionChart', chartConfig);
- }
-
- // 更新趋势图表
- 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 currentDimension = document.getElementById('dataDimension')?.value || 'workorders';
-
- let labels, values, title, backgroundColor, label;
-
- if (currentDimension === 'alerts') {
- // 预警严重程度分布
- const alertSeverities = data.alerts?.by_severity || {};
- labels = Object.keys(alertSeverities).map(s => this.getSeverityText(s));
- values = Object.values(alertSeverities);
- title = '预警严重程度分布';
- label = '预警数量';
- backgroundColor = [
- '#28a745', // low - 绿色
- '#ffc107', // medium - 黄色
- '#fd7e14', // high - 橙色
- '#dc3545' // critical - 红色
- ];
- } else if (currentDimension === 'performance') {
- // 性能指标分布
- const performanceMetrics = data.performance?.by_metric || {};
- labels = Object.keys(performanceMetrics);
- values = Object.values(performanceMetrics);
- title = '性能指标分布';
- label = '性能值';
- backgroundColor = [
- '#28a745', // 优秀 - 绿色
- '#ffc107', // 良好 - 黄色
- '#fd7e14', // 一般 - 橙色
- '#dc3545' // 差 - 红色
- ];
- } else if (currentDimension === 'satisfaction') {
- // 满意度分布
- const satisfactionLevels = data.satisfaction?.by_level || {};
- labels = Object.keys(satisfactionLevels).map(s => this.getSatisfactionText(s));
- values = Object.values(satisfactionLevels);
- title = '满意度分布';
- label = '满意度数量';
- backgroundColor = [
- '#28a745', // 非常满意 - 绿色
- '#ffc107', // 满意 - 黄色
- '#fd7e14', // 一般 - 橙色
- '#dc3545' // 不满意 - 红色
- ];
- } else {
- // 工单优先级分布
- const priorities = data.workorders?.by_priority || {};
- labels = Object.keys(priorities).map(p => this.getPriorityText(p));
- values = Object.values(priorities);
- title = '工单优先级分布';
- label = '工单数量';
- backgroundColor = [
- '#28a745', // 低 - 绿色
- '#ffc107', // 中 - 黄色
- '#fd7e14', // 高 - 橙色
- '#dc3545' // 紧急 - 红色
- ];
- }
-
- this.charts.priorityChart = new Chart(ctx, {
- type: 'bar',
- data: {
- labels: labels,
- datasets: [{
- label: label,
- data: values,
- backgroundColor: backgroundColor
- }]
- },
- options: {
- responsive: true,
- maintainAspectRatio: false,
- plugins: {
- title: {
- display: true,
- text: title
- }
- },
- 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);
- const alerts = trendData.map(item => item.alerts);
- const performance = trendData.map(item => item.performance || 0);
- const satisfaction = trendData.map(item => item.satisfaction || 0);
-
- if (chartType === 'pie' || chartType === 'doughnut') {
- // 根据数据维度选择显示内容
- const currentDimension = document.getElementById('dataDimension')?.value || 'workorders';
-
- if (currentDimension === 'alerts') {
- const alertLevels = data.alerts?.by_level || {};
- return {
- labels: Object.keys(alertLevels),
- datasets: [{
- data: Object.values(alertLevels),
- backgroundColor: [
- '#FF6384', // critical - 红色
- '#FFCE56', // warning - 黄色
- '#36A2EB', // error - 蓝色
- '#4BC0C0', // info - 青色
- '#9966FF' // 其他
- ]
- }]
- };
- } else if (currentDimension === 'performance') {
- // 性能指标分布
- const performanceMetrics = data.performance || {};
- return {
- labels: Object.keys(performanceMetrics),
- datasets: [{
- data: Object.values(performanceMetrics),
- backgroundColor: [
- '#28a745', // 优秀 - 绿色
- '#ffc107', // 良好 - 黄色
- '#fd7e14', // 一般 - 橙色
- '#dc3545' // 差 - 红色
- ]
- }]
- };
- } else if (currentDimension === 'satisfaction') {
- // 满意度分布
- const satisfactionLevels = data.satisfaction?.by_level || {};
- return {
- labels: Object.keys(satisfactionLevels),
- datasets: [{
- data: Object.values(satisfactionLevels),
- backgroundColor: [
- '#28a745', // 非常满意 - 绿色
- '#ffc107', // 满意 - 黄色
- '#fd7e14', // 一般 - 橙色
- '#dc3545' // 不满意 - 红色
- ]
- }]
- };
- } else {
- const categories = data.workorders?.by_category || {};
- return {
- labels: Object.keys(categories),
- datasets: [{
- data: Object.values(categories),
- backgroundColor: [
- '#FF6384',
- '#36A2EB',
- '#FFCE56',
- '#4BC0C0',
- '#9966FF',
- '#FF9F40'
- ]
- }]
- };
- }
- } else {
- // 线图和柱状图根据数据维度显示不同内容
- const currentDimension = document.getElementById('dataDimension')?.value || 'workorders';
- const datasets = [];
-
- if (currentDimension === 'performance') {
- // 性能指标图表
- datasets.push({
- label: '性能指标',
- data: performance,
- borderColor: '#28a745',
- backgroundColor: 'rgba(40, 167, 69, 0.1)',
- tension: chartType === 'line' ? 0.4 : 0
- });
- } else if (currentDimension === 'satisfaction') {
- // 满意度图表
- datasets.push({
- label: '满意度评分',
- data: satisfaction,
- borderColor: '#ffc107',
- backgroundColor: 'rgba(255, 193, 7, 0.1)',
- tension: chartType === 'line' ? 0.4 : 0
- });
- } else {
- // 默认显示工单和预警数据
- datasets.push({
- label: '工单数量',
- data: workorders,
- borderColor: '#36A2EB',
- backgroundColor: 'rgba(54, 162, 235, 0.1)',
- tension: chartType === 'line' ? 0.4 : 0
- });
-
- // 如果有预警数据,添加预警数据集
- if (alerts.some(alert => alert > 0)) {
- datasets.push({
- label: '预警数量',
- data: alerts,
- borderColor: '#FF6384',
- backgroundColor: 'rgba(255, 99, 132, 0.1)',
- tension: chartType === 'line' ? 0.4 : 0
- });
- }
- }
-
- return {
- labels: labels,
- datasets: datasets
- };
- }
- }
-
- // 导出图表
- 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();
- }
-
- // 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 = `
-
- `;
- 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 `
-
-
-
-
-
${record.description}
-
- 优先级 ${record.priority}
- 置信度 ${(record.confidence * 100).toFixed(0)}%
- ${record.success ? '成功' : '失败'}
-
-
- 开始: ${startTime} |
- 耗时: ${duration}秒
-
-
-
- ${record.action_type}
-
-
- ${record.result && record.result.message ? `
-
- 结果: ${record.result.message}
-
- ` : ''}
-
-
- `;
- }).join('');
-
- container.innerHTML = historyHtml;
- }
-
- // 更新分析报告
- 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 = `
-
-
-
工单统计概览
-
-
-
- | 总工单数 |
- ${workorders.total || 0} |
-
-
- | 待处理 |
- ${workorders.open || 0} |
-
-
- | 处理中 |
- ${workorders.in_progress || 0} |
-
-
- | 已解决 |
- ${workorders.resolved || 0} |
-
-
- | 已关闭 |
- ${workorders.closed || 0} |
-
-
-
-
-
-
满意度分析
-
-
-
- | 平均满意度 |
- ${satisfaction.average || 0}/5.0 |
-
-
- | 5星评价 |
- ${satisfaction.distribution?.['5'] || 0} 个 |
-
-
- | 4星评价 |
- ${satisfaction.distribution?.['4'] || 0} 个 |
-
-
- | 3星评价 |
- ${satisfaction.distribution?.['3'] || 0} 个 |
-
-
- | 2星及以下 |
- ${(satisfaction.distribution?.['2'] || 0) + (satisfaction.distribution?.['1'] || 0)} 个 |
-
-
-
-
-
-
-
-
-
预警统计
-
-
-
- | 总预警数 |
- ${alerts.total || 0} |
-
-
- | 活跃预警 |
- ${alerts.active || 0} |
-
-
- | 已解决 |
- ${alerts.resolved || 0} |
-
-
-
-
-
-
性能指标
-
-
-
- | 响应时间 |
- ${performance.response_time || 0} 秒 |
-
-
- | 系统可用性 |
- ${performance.uptime || 0}% |
-
-
- | 错误率 |
- ${performance.error_rate || 0}% |
-
-
- | 吞吐量 |
- ${performance.throughput || 0} 请求/小时 |
-
-
-
-
-
-
-
-
-
关键指标总结
-
-
-
-
-
${summary.resolution_rate || 0}%
-
解决率
-
-
-
-
-
-
-
${summary.avg_satisfaction || 0}
-
平均满意度
-
-
-
-
-
-
-
${summary.active_alerts || 0}
-
活跃预警
-
-
-
-
-
-
-
${summary.total_workorders || 0}
-
总工单数
-
-
-
-
-
-
- `;
-
- reportContainer.innerHTML = reportHtml;
- }
-
- updateAnalyticsDisplay(analytics) {
- // 更新分析报告
- const reportContainer = document.getElementById('analytics-report');
- if (analytics.summary) {
- reportContainer.innerHTML = `
-
-
-
性能指标
-
- - 总工单数: ${analytics.summary.total_orders || 0}
- - 解决率: ${Math.round((analytics.summary.resolution_rate || 0) * 100)}%
- - 平均解决时间: ${analytics.summary.avg_resolution_time_hours || 0}小时
- - 平均满意度: ${analytics.summary.avg_satisfaction || 0}
-
-
-
-
趋势分析
-
- - 工单趋势: ${analytics.summary.trends?.orders_trend ? '上升' : '下降'}
- - 满意度趋势: ${analytics.summary.trends?.satisfaction_trend ? '上升' : '下降'}
- - 解决时间趋势: ${analytics.summary.trends?.resolution_time_trend ? '上升' : '下降'}
-
-
-
- `;
- }
-
- // 更新类别分布图
- 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 [settingsResp, configResp] = await Promise.all([
- fetch('/api/settings'),
- fetch('/api/runtime-config'),
- ]);
- const settings = await settingsResp.json();
- this.updateSettingsDisplay(settings);
-
- // 加载运行时配置(租户、模型等)
- const config = await configResp.json();
- if (config.success) {
- const set = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val || '-'; };
- set('setting-tenant-id', config.tenant_id);
- set('setting-llm-provider', config.llm?.provider);
- set('setting-llm-model', config.llm?.model);
- set('setting-llm-base-url', config.llm?.base_url);
- set('setting-embedding-status', config.embedding?.enabled ? `启用 (${config.embedding.model})` : '禁用');
- set('setting-redis-status', config.redis?.enabled ? `启用 (${config.redis.host})` : '禁用');
- }
- } catch (error) {
- console.error('加载设置失败:', error);
- }
- }
-
- updateSettingsDisplay(settings) {
- if (settings.api_timeout !== undefined) document.getElementById('api-timeout').value = settings.api_timeout;
- if (settings.max_history !== undefined) document.getElementById('max-history').value = settings.max_history;
- if (settings.refresh_interval !== undefined) document.getElementById('refresh-interval').value = settings.refresh_interval;
- if (settings.auto_monitoring !== undefined) document.getElementById('auto-monitoring').checked = settings.auto_monitoring;
- if (settings.agent_mode !== undefined) document.getElementById('agent-mode').checked = settings.agent_mode;
- // 新增:API与模型、端口、日志级别(如页面存在对应输入框则填充)
- const map = [
- ['api-provider','api_provider'],
- ['api-base-url','api_base_url'],
- ['api-key','api_key'],
- ['model-name','model_name'],
- ['model-temperature','model_temperature'],
- ['model-max-tokens','model_max_tokens'],
- ['server-port','server_port'],
- ['websocket-port','websocket_port'],
- ['log-level','log_level']
- ];
- map.forEach(([id, key]) => {
- const el = document.getElementById(id);
- if (el && settings[key] !== undefined) el.value = settings[key];
- });
-
- // 更新温度滑块显示值
- const tempSlider = document.getElementById('model-temperature');
- const tempValue = document.getElementById('temperature-value');
- if (tempSlider && tempValue) {
- tempSlider.addEventListener('input', function() {
- tempValue.textContent = this.value;
- });
- tempValue.textContent = tempSlider.value;
- }
-
- // 更新服务状态显示
- this.updateServiceStatus(settings);
- }
-
- async saveSystemSettings() {
- 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,
- agent_mode: document.getElementById('agent-mode').checked,
- api_provider: document.getElementById('api-provider')?.value || '',
- api_base_url: document.getElementById('api-base-url')?.value || '',
- api_key: document.getElementById('api-key')?.value || '',
- model_name: document.getElementById('model-name')?.value || '',
- model_temperature: parseFloat(document.getElementById('model-temperature')?.value || 0.7),
- model_max_tokens: parseInt(document.getElementById('model-max-tokens')?.value || 1000),
- server_port: parseInt(document.getElementById('server-port')?.value || 5000),
- websocket_port: parseInt(document.getElementById('websocket-port')?.value || 8765),
- log_level: document.getElementById('log-level')?.value || 'INFO'
- };
-
- try {
- 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');
- }
- }
-
- // 更新服务状态显示
- updateServiceStatus(settings) {
- // 更新仪表板服务状态卡片
- if (settings.current_server_port !== undefined) {
- const webPortEl = document.getElementById('web-port-status');
- if (webPortEl) webPortEl.textContent = settings.current_server_port;
- }
- if (settings.current_websocket_port !== undefined) {
- const wsPortEl = document.getElementById('ws-port-status');
- if (wsPortEl) wsPortEl.textContent = settings.current_websocket_port;
- }
- if (settings.log_level !== undefined) {
- const logLevelEl = document.getElementById('log-level-status');
- if (logLevelEl) logLevelEl.textContent = settings.log_level;
- }
- if (settings.uptime_seconds !== undefined) {
- const uptimeEl = document.getElementById('uptime-status');
- if (uptimeEl) {
- const hours = Math.floor(settings.uptime_seconds / 3600);
- const minutes = Math.floor((settings.uptime_seconds % 3600) / 60);
- uptimeEl.textContent = `${hours}小时${minutes}分钟`;
- }
- }
-
- // 更新系统设置页面的当前端口显示
- const currentPortEl = document.getElementById('current-server-port');
- if (currentPortEl && settings.current_server_port !== undefined) {
- currentPortEl.textContent = settings.current_server_port;
- }
- }
-
- // 刷新服务状态
- async refreshServiceStatus() {
- try {
- const response = await fetch('/api/settings');
- const settings = await response.json();
- this.updateServiceStatus(settings);
- this.showNotification('服务状态已刷新', 'success');
- } catch (error) {
- console.error('刷新服务状态失败:', error);
- this.showNotification('刷新服务状态失败', 'error');
- }
- }
-
- // 测试API连接
- async testApiConnection() {
- try {
- const apiProvider = document.getElementById('api-provider').value;
- const apiBaseUrl = document.getElementById('api-base-url').value;
- const apiKey = document.getElementById('api-key').value;
- const modelName = document.getElementById('model-name').value;
-
- const response = await fetch('/api/test/connection', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({
- api_provider: apiProvider,
- api_base_url: apiBaseUrl,
- api_key: apiKey,
- model_name: modelName
- })
- });
-
- const result = await response.json();
- if (result.success) {
- this.showNotification(`API连接测试成功: ${result.message}`, 'success');
- } else {
- this.showNotification(`API连接测试失败: ${result.error}`, 'error');
- }
- } catch (error) {
- console.error('API连接测试失败:', error);
- this.showNotification('API连接测试失败', 'error');
- }
- }
-
- // 测试模型回答
- async testModelResponse() {
- try {
- const testMessage = prompt('请输入测试消息:', '你好,请简单介绍一下你自己');
- if (!testMessage) return;
-
- const response = await fetch('/api/test/model', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({
- test_message: testMessage
- })
- });
-
- const result = await response.json();
- if (result.success) {
- const message = `模型回答测试成功:\n\n问题: ${result.test_message}\n\n回答: ${result.response}\n\n响应时间: ${result.response_time}`;
- alert(message);
- this.showNotification('模型回答测试成功', 'success');
- } else {
- this.showNotification(`模型回答测试失败: ${result.error}`, 'error');
- }
- } catch (error) {
- console.error('模型回答测试失败:', error);
- this.showNotification('模型回答测试失败', 'error');
- }
- }
-
- async loadSystemInfo() {
- try {
- const response = await fetch('/api/system/info');
- const info = await response.json();
- this.updateSystemInfoDisplay(info);
- } catch (error) {
- console.error('加载系统信息失败:', error);
- }
- }
-
- updateSystemInfoDisplay(info) {
- const container = document.getElementById('system-info');
- container.innerHTML = `
-
- 系统版本: ${info.version || '1.0.0'}
-
-
- Python版本: ${info.python_version || '未知'}
-
-
- 数据库: ${info.database || 'SQLite'}
-
-
- 运行时间: ${info.uptime || '未知'}
-
-
- 内存使用: ${info.memory_usage || '0'} MB
-
- `;
- }
-
- // 工具函数
- 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;
- }
-
- getSeverityText(severity) {
- const severityMap = {
- 'low': '低',
- 'medium': '中',
- 'high': '高',
- 'critical': '严重'
- };
- return severityMap[severity] || severity;
- }
-
- getSatisfactionText(level) {
- const satisfactionMap = {
- 'very_satisfied': '非常满意',
- 'satisfied': '满意',
- 'neutral': '一般',
- 'dissatisfied': '不满意'
- };
- return satisfactionMap[level] || level;
- }
-
- getPerformanceTrendText(trend) {
- const trendMap = {
- 'up': '上升',
- 'down': '下降',
- 'stable': '稳定'
- };
- return trendMap[trend] || trend;
- }
-
- 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;
- }
-
- // 统计数字预览功能
- async showStatPreview(type, status) {
- try {
- let title = '';
- let data = [];
- let apiUrl = '';
-
- switch (type) {
- case 'workorder':
- title = this.getWorkorderPreviewTitle(status);
- apiUrl = status === 'all' ? '/api/workorders' : `/api/workorders/by-status/${status}`;
- break;
- case 'alert':
- title = this.getAlertPreviewTitle(status);
- apiUrl = `/api/alerts/by-level/${status}`;
- break;
- case 'knowledge':
- title = this.getKnowledgePreviewTitle(status);
- apiUrl = `/api/knowledge/by-status/${status}`;
- break;
- default:
- return;
- }
-
- // 显示加载状态
- this.showLoadingModal(title);
-
- const response = await fetch(apiUrl);
- const result = await response.json();
-
- // 处理不同的API响应结构
- if (result.success !== false) {
- if (result.success === true) {
- // 新API结构: {success: true, data: {workorders: [...]}}
- data = result.data[type + 's'] || result.data.knowledge || [];
- } else if (result.workorders) {
- // 旧API结构: {workorders: [...], page: 1, ...}
- data = result.workorders || [];
- } else if (result.alerts) {
- // 预警API结构
- data = result.alerts || [];
- } else if (result.knowledge) {
- // 知识库API结构
- data = result.knowledge || [];
- } else {
- data = [];
- }
- this.showPreviewModal(title, type, data);
- } else {
- const errorMsg = result.error || result.message || '未知错误';
- this.showNotification('获取数据失败: ' + errorMsg, 'error');
- }
- } catch (error) {
- console.error('预览失败:', error);
- this.showNotification('预览失败: ' + error.message, 'error');
- }
- }
-
- getWorkorderPreviewTitle(status) {
- const titles = {
- 'all': '所有工单',
- 'open': '待处理工单',
- 'in_progress': '处理中工单',
- 'resolved': '已解决工单',
- 'closed': '已关闭工单'
- };
- return titles[status] || '工单列表';
- }
-
- getAlertPreviewTitle(level) {
- const titles = {
- 'critical': '严重预警',
- 'warning': '警告预警',
- 'info': '信息预警'
- };
- return titles[level] || '预警列表';
- }
-
- getKnowledgePreviewTitle(status) {
- const titles = {
- 'verified': '已验证知识',
- 'unverified': '未验证知识'
- };
- return titles[status] || '知识库条目';
- }
-
- showLoadingModal(title) {
- const modalHtml = `
-
-
-
-
-
-
- 加载中...
-
-
正在加载数据...
-
-
-
-
- `;
-
- // 移除已存在的模态框和遮罩
- this.removeExistingModal();
-
- document.body.insertAdjacentHTML('beforeend', modalHtml);
- const modalElement = document.getElementById('statPreviewModal');
- const modal = new bootstrap.Modal(modalElement, {
- backdrop: true,
- keyboard: true
- });
-
- // 添加事件监听器确保正确清理
- modalElement.addEventListener('hidden.bs.modal', () => {
- this.cleanupModal();
- });
-
- modal.show();
- }
-
- showPreviewModal(title, type, data) {
- let contentHtml = '';
-
- if (data.length === 0) {
- contentHtml = `
-
-
-
暂无数据
-
当前条件下没有找到相关记录
-
- `;
- } else {
- switch (type) {
- case 'workorder':
- contentHtml = this.generateWorkorderPreviewHtml(data);
- break;
- case 'alert':
- contentHtml = this.generateAlertPreviewHtml(data);
- break;
- case 'knowledge':
- contentHtml = this.generateKnowledgePreviewHtml(data);
- break;
- }
- }
-
- const modalHtml = `
-
-
-
-
-
- ${contentHtml}
-
-
-
-
-
- `;
-
- // 移除已存在的模态框和遮罩
- this.removeExistingModal();
-
- document.body.insertAdjacentHTML('beforeend', modalHtml);
- const modalElement = document.getElementById('statPreviewModal');
- const modal = new bootstrap.Modal(modalElement, {
- backdrop: true,
- keyboard: true
- });
-
- // 添加事件监听器确保正确清理
- modalElement.addEventListener('hidden.bs.modal', () => {
- this.cleanupModal();
- });
-
- modal.show();
- }
-
- generateWorkorderPreviewHtml(workorders) {
- return `
-
-
-
-
- | 工单ID |
- 标题 |
- 状态 |
- 优先级 |
- 创建时间 |
- 操作 |
-
-
-
- ${workorders.map(wo => `
-
- | ${wo.order_id || wo.id} |
-
-
- ${wo.title}
-
- |
-
-
- ${this.getStatusText(wo.status)}
-
- |
-
-
- ${this.getPriorityText(wo.priority)}
-
- |
- ${new Date(wo.created_at).toLocaleString()} |
-
-
- |
-
- `).join('')}
-
-
-
- `;
- }
-
- generateAlertPreviewHtml(alerts) {
- return `
-
-
-
-
- | 预警ID |
- 消息 |
- 级别 |
- 类型 |
- 创建时间 |
- 操作 |
-
-
-
- ${alerts.map(alert => `
-
- | ${alert.id} |
-
-
- ${alert.message}
-
- |
-
-
- ${this.getLevelText(alert.level)}
-
- |
- ${this.getTypeText(alert.alert_type)} |
- ${new Date(alert.created_at).toLocaleString()} |
-
-
- |
-
- `).join('')}
-
-
-
- `;
- }
-
- generateKnowledgePreviewHtml(knowledge) {
- return `
-
-
-
-
- | ID |
- 标题 |
- 分类 |
- 验证状态 |
- 创建时间 |
- 操作 |
-
-
-
- ${knowledge.map(item => `
-
- | ${item.id} |
-
-
- ${item.title}
-
- |
- ${item.category || '未分类'} |
-
-
- ${item.is_verified ? '已验证' : '未验证'}
-
- |
- ${new Date(item.created_at).toLocaleString()} |
-
-
- ${item.is_verified ?
- `` :
- ``
- }
-
-
- |
-
- `).join('')}
-
-
-
- `;
- }
-
- getAlertLevelColor(level) {
- const colorMap = {
- 'critical': 'danger',
- 'warning': 'warning',
- 'info': 'info'
- };
- return colorMap[level] || 'secondary';
- }
-
- goToFullView(type, status) {
- // 关闭预览模态框
- const modal = bootstrap.Modal.getInstance(document.getElementById('statPreviewModal'));
- if (modal) {
- modal.hide();
- }
-
- // 切换到对应的标签页
- switch (type) {
- case 'workorder':
- this.switchTab('workorders');
- // 设置筛选器
- if (status !== 'all') {
- setTimeout(() => {
- const filter = document.getElementById('workorder-status-filter');
- if (filter) {
- filter.value = status;
- this.loadWorkOrders();
- }
- }, 100);
- }
- break;
- case 'alert':
- this.switchTab('alerts');
- // 设置筛选器
- setTimeout(() => {
- const filter = document.getElementById('alert-filter');
- if (filter) {
- filter.value = status;
- this.updateAlertsDisplay();
- }
- }, 100);
- break;
- case 'knowledge':
- this.switchTab('knowledge');
- break;
- }
- }
-
- // 模态框清理方法
- removeExistingModal() {
- const existingModal = document.getElementById('statPreviewModal');
- if (existingModal) {
- // 获取模态框实例并销毁
- const modalInstance = bootstrap.Modal.getInstance(existingModal);
- if (modalInstance) {
- modalInstance.dispose();
- }
- existingModal.remove();
- }
-
- // 清理可能残留的遮罩
- const backdrops = document.querySelectorAll('.modal-backdrop');
- backdrops.forEach(backdrop => backdrop.remove());
-
- // 恢复body的滚动
- document.body.classList.remove('modal-open');
- document.body.style.overflow = '';
- document.body.style.paddingRight = '';
- }
-
- cleanupModal() {
- // 延迟清理,确保动画完成
- setTimeout(() => {
- this.removeExistingModal();
- }, 300);
- }
-
- 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}
-
- `;
-
- document.body.appendChild(notification);
-
- setTimeout(() => {
- if (notification.parentNode) {
- notification.parentNode.removeChild(notification);
- }
- }, 3000);
- }
-
- getSimilarityExplanation(percent) {
- if (percent >= 95) {
- return "语义高度相似,AI建议与人工描述基本一致,建议自动审批";
- } else if (percent >= 90) {
- return "语义较为相似,AI建议与人工描述大体一致,建议人工审核";
- } else if (percent >= 80) {
- return "语义部分相似,AI建议与人工描述有一定差异,需要人工判断";
- } else if (percent >= 60) {
- return "语义相似度较低,AI建议与人工描述差异较大,建议使用人工描述";
- } else {
- return "语义差异很大,AI建议与人工描述差异很大,优先使用人工描述";
- }
- }
-
- getSimilarityMessage(percent, approved, useHumanResolution = false) {
- if (useHumanResolution) {
- return `人工描述已保存!语义相似度: ${percent}%,AI准确率低于90%,将使用人工描述入库`;
- } else if (approved) {
- return `人工描述已保存!语义相似度: ${percent}%,已自动审批入库`;
- } else if (percent >= 90) {
- return `人工描述已保存!语义相似度: ${percent}%,建议人工审核后审批`;
- } else if (percent >= 80) {
- return `人工描述已保存!语义相似度: ${percent}%,需要人工判断是否审批`;
- } else {
- return `人工描述已保存!语义相似度: ${percent}%,建议使用人工描述入库`;
- }
- }
-
- showCreateWorkOrderModal() {
- const modal = new bootstrap.Modal(document.getElementById('createWorkOrderModal'));
- modal.show();
- }
-
- // 新增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 = '发送中...';
- 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 = '发送';
- 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');
- }
- }
-
- // ==================== 租户管理 ====================
-
- async loadTenantList() {
- const container = document.getElementById('tenant-list');
- if (!container) return;
- container.innerHTML = '
';
-
- try {
- const response = await fetch('/api/tenants');
- const tenants = await response.json();
-
- if (!Array.isArray(tenants) || tenants.length === 0) {
- container.innerHTML = '暂无租户,请点击"新建租户"创建
';
- return;
- }
-
- container.innerHTML = tenants.map(t => {
- const feishuCfg = t.config?.feishu || {};
- const groupCount = (feishuCfg.chat_groups || []).length;
- const hasFeishu = feishuCfg.app_id || groupCount > 0;
- return `
-
-
-
- ${t.name}
- (${t.tenant_id})
- ${t.description ? `
${t.description}` : ''}
- ${!t.is_active ? '已禁用' : ''}
- ${hasFeishu ? `飞书${groupCount > 0 ? ` (${groupCount}群)` : ''}` : ''}
-
-
-
- ${t.tenant_id !== 'default' ? `
- ` : ''}
-
-
-
- `}).join('');
- } catch (error) {
- console.error('加载租户列表失败:', error);
- container.innerHTML = '加载失败
';
- }
- }
-
- showCreateTenantModal() {
- document.getElementById('tenantModalTitle').textContent = '新建租户';
- document.getElementById('tenant-edit-id').value = '';
- document.getElementById('tenant-id-input').value = '';
- document.getElementById('tenant-id-input').disabled = false;
- document.getElementById('tenant-id-group').style.display = '';
- document.getElementById('tenant-name-input').value = '';
- document.getElementById('tenant-desc-input').value = '';
- document.getElementById('tenant-feishu-appid').value = '';
- document.getElementById('tenant-feishu-appsecret').value = '';
- document.getElementById('tenant-feishu-chatgroups').value = '';
- new bootstrap.Modal(document.getElementById('tenantModal')).show();
- }
-
- async showEditTenantModal(tenantId, name, description) {
- document.getElementById('tenantModalTitle').textContent = '编辑租户';
- document.getElementById('tenant-edit-id').value = tenantId;
- document.getElementById('tenant-id-input').value = tenantId;
- document.getElementById('tenant-id-input').disabled = true;
- document.getElementById('tenant-name-input').value = name;
- document.getElementById('tenant-desc-input').value = description;
-
- // 加载租户的飞书配置
- try {
- const resp = await fetch('/api/tenants');
- const tenants = await resp.json();
- const tenant = tenants.find(t => t.tenant_id === tenantId);
- const feishuCfg = tenant?.config?.feishu || {};
- document.getElementById('tenant-feishu-appid').value = feishuCfg.app_id || '';
- document.getElementById('tenant-feishu-appsecret').value = feishuCfg.app_secret || '';
- document.getElementById('tenant-feishu-chatgroups').value = (feishuCfg.chat_groups || []).join('\n');
- } catch (e) {
- document.getElementById('tenant-feishu-appid').value = '';
- document.getElementById('tenant-feishu-appsecret').value = '';
- document.getElementById('tenant-feishu-chatgroups').value = '';
- }
-
- new bootstrap.Modal(document.getElementById('tenantModal')).show();
- }
-
- async saveTenant() {
- const editId = document.getElementById('tenant-edit-id').value;
- const tenantId = document.getElementById('tenant-id-input').value.trim();
- const name = document.getElementById('tenant-name-input').value.trim();
- const description = document.getElementById('tenant-desc-input').value.trim();
-
- // 飞书配置
- const feishuAppId = document.getElementById('tenant-feishu-appid').value.trim();
- const feishuAppSecret = document.getElementById('tenant-feishu-appsecret').value.trim();
- const chatGroupsText = document.getElementById('tenant-feishu-chatgroups').value.trim();
- const chatGroups = chatGroupsText ? chatGroupsText.split('\n').map(s => s.trim()).filter(Boolean) : [];
-
- const config = {};
- if (feishuAppId || feishuAppSecret || chatGroups.length > 0) {
- config.feishu = {};
- if (feishuAppId) config.feishu.app_id = feishuAppId;
- if (feishuAppSecret) config.feishu.app_secret = feishuAppSecret;
- if (chatGroups.length > 0) config.feishu.chat_groups = chatGroups;
- }
-
- if (!name) {
- this.showNotification('租户名称不能为空', 'error');
- return;
- }
-
- try {
- let response;
- if (editId) {
- response = await fetch(`/api/tenants/${encodeURIComponent(editId)}`, {
- method: 'PUT',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ name, description, config })
- });
- } else {
- if (!tenantId) {
- this.showNotification('租户标识不能为空', 'error');
- return;
- }
- response = await fetch('/api/tenants', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ tenant_id: tenantId, name, description, config })
- });
- }
-
- const data = await response.json();
- if (data.success) {
- bootstrap.Modal.getInstance(document.getElementById('tenantModal')).hide();
- this.showNotification(editId ? '租户已更新' : '租户已创建', 'success');
- this.loadTenantList();
- this.populateTenantSelectors();
- } else {
- this.showNotification(data.error || '操作失败', 'error');
- }
- } catch (error) {
- console.error('保存租户失败:', error);
- this.showNotification('保存租户失败: ' + error.message, 'error');
- }
- }
-
- async deleteTenant(tenantId) {
- if (!confirm(`确定要删除租户 "${tenantId}" 吗?该操作不会删除关联数据。`)) return;
- try {
- const response = await fetch(`/api/tenants/${encodeURIComponent(tenantId)}`, { method: 'DELETE' });
- const data = await response.json();
- if (data.success) {
- this.showNotification('租户已删除', 'success');
- this.loadTenantList();
- } else {
- this.showNotification(data.error || '删除失败', 'error');
- }
- } catch (error) {
- console.error('删除租户失败:', error);
- this.showNotification('删除租户失败: ' + error.message, 'error');
- }
- }
-}
-
-// 飞书同步管理器
-class FeishuSyncManager {
- constructor() {
- this.loadConfig();
- this.refreshStatus();
- }
-
- async loadConfig() {
- try {
- const response = await fetch('/api/feishu-sync/config');
- const data = await response.json();
-
- if (data.success) {
- const config = data.config;
- document.getElementById('appId').value = config.feishu.app_id || '';
- document.getElementById('appSecret').value = '';
- document.getElementById('appToken').value = config.feishu.app_token || '';
- document.getElementById('tableId').value = config.feishu.table_id || '';
-
- // 显示配置状态
- const statusBadge = config.feishu.status === 'active' ?
- '已配置' :
- '未配置';
-
- // 可以在这里添加状态显示
- }
- } catch (error) {
- console.error('加载配置失败:', error);
- }
- }
-
- async saveConfig() {
- const config = {
- app_id: document.getElementById('appId').value,
- app_secret: document.getElementById('appSecret').value,
- app_token: document.getElementById('appToken').value,
- table_id: document.getElementById('tableId').value
- };
-
- if (!config.app_id || !config.app_secret || !config.app_token || !config.table_id) {
- this.showNotification('请填写完整的配置信息', 'error');
- return;
- }
-
- try {
- const response = await fetch('/api/feishu-sync/config', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify(config)
- });
-
- const data = await response.json();
-
- if (data.success) {
- this.showNotification('配置保存成功', 'success');
- } else {
- this.showNotification('配置保存失败: ' + data.error, 'error');
- }
- } catch (error) {
- this.showNotification('配置保存失败: ' + error.message, 'error');
- }
- }
-
- async testConnection() {
- try {
- this.showNotification('正在测试连接...', 'info');
-
- const response = await fetch('/api/feishu-sync/test-connection');
- const data = await response.json();
-
- if (data.success) {
- this.showNotification('飞书连接正常', 'success');
- } else {
- this.showNotification('连接失败: ' + data.error, 'error');
- }
- } catch (error) {
- this.showNotification('连接测试失败: ' + error.message, 'error');
- }
- }
-
- async syncFromFeishu() {
- try {
- const limit = document.getElementById('syncLimit').value;
- this.showNotification('开始从飞书同步数据...', 'info');
- this.showProgress(true);
-
- const response = await fetch('/api/feishu-sync/sync-from-feishu', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({
- generate_ai_suggestions: false,
- limit: parseInt(limit)
- })
- });
-
- const data = await response.json();
-
- if (data.success) {
- this.showNotification(data.message, 'success');
- this.addSyncLog(data.message);
- this.refreshStatus();
- } else {
- this.showNotification('同步失败: ' + data.error, 'error');
- this.addSyncLog('同步失败: ' + data.error);
- }
- } catch (error) {
- this.showNotification('同步失败: ' + error.message, 'error');
- this.addSyncLog('同步失败: ' + error.message);
- } finally {
- this.showProgress(false);
- }
- }
-
- async syncWithAI() {
- try {
- const limit = document.getElementById('syncLimit').value;
- this.showNotification('开始同步数据并生成AI建议...', 'info');
- this.showProgress(true);
-
- const response = await fetch('/api/feishu-sync/sync-from-feishu', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({
- generate_ai_suggestions: true,
- limit: parseInt(limit)
- })
- });
-
- const data = await response.json();
-
- if (data.success) {
- this.showNotification(data.message, 'success');
- this.addSyncLog(data.message);
- this.refreshStatus();
- } else {
- this.showNotification('同步失败: ' + data.error, 'error');
- this.addSyncLog('同步失败: ' + data.error);
- }
- } catch (error) {
- this.showNotification('同步失败: ' + error.message, 'error');
- this.addSyncLog('同步失败: ' + error.message);
- } finally {
- this.showProgress(false);
- }
- }
-
- // 打开字段映射管理页面
- openFieldMapping() {
- const section = document.getElementById('fieldMappingSection');
- if (section.style.display === 'none') {
- section.style.display = 'block';
- // 自动加载映射状态
- this.loadMappingStatus();
- } else {
- section.style.display = 'none';
- }
- }
-
- async previewFeishuData() {
- try {
- this.showNotification('正在获取飞书数据预览...', 'info');
-
- const response = await fetch('/api/feishu-sync/preview-feishu-data');
- const data = await response.json();
-
- if (data.success) {
- this.displayPreviewData(data.preview_data);
- this.showNotification(`获取到 ${data.total_count} 条预览数据`, 'success');
- } else {
- this.showNotification('获取预览数据失败: ' + data.error, 'error');
- }
- } catch (error) {
- this.showNotification('获取预览数据失败: ' + error.message, 'error');
- }
- }
-
- displayPreviewData(data) {
- const tbody = document.querySelector('#previewTable tbody');
- tbody.innerHTML = '';
-
- data.forEach(item => {
- const row = document.createElement('tr');
- row.innerHTML = `
- ${item.record_id} |
- ${item.fields['TR Number'] || '-'} |
- ${item.fields['TR Description'] || '-'} |
- ${item.fields['Type of problem'] || '-'} |
- ${item.fields['Source'] || '-'} |
- ${item.fields['TR (Priority/Status)'] || '-'} |
-
-
- |
- `;
- tbody.appendChild(row);
- });
-
- document.getElementById('previewSection').style.display = 'block';
- }
-
- async refreshStatus() {
- try {
- const response = await fetch('/api/feishu-sync/status');
- const data = await response.json();
-
- if (data.success) {
- const status = data.status;
- document.getElementById('totalLocalWorkorders').textContent = status.total_local_workorders || 0;
- document.getElementById('syncedWorkorders').textContent = status.synced_workorders || 0;
- document.getElementById('unsyncedWorkorders').textContent = status.unsynced_workorders || 0;
- }
- } catch (error) {
- console.error('刷新状态失败:', error);
- }
- }
-
- showProgress(show) {
- const progress = document.getElementById('syncProgress');
- if (show) {
- progress.style.display = 'block';
- const bar = progress.querySelector('.progress-bar');
- bar.style.width = '100%';
- } else {
- setTimeout(() => {
- progress.style.display = 'none';
- const bar = progress.querySelector('.progress-bar');
- bar.style.width = '0%';
- }, 1000);
- }
- }
-
- addSyncLog(message) {
- const log = document.getElementById('syncLog');
- const timestamp = new Date().toLocaleString();
- const logEntry = document.createElement('div');
- logEntry.innerHTML = `[${timestamp}] ${message}`;
-
- if (log.querySelector('.text-muted')) {
- log.innerHTML = '';
- }
-
- log.appendChild(logEntry);
- log.scrollTop = log.scrollHeight;
- }
-
- async exportConfig() {
- try {
- const response = await fetch('/api/feishu-sync/config/export');
- const data = await response.json();
-
- if (data.success) {
- // 创建下载链接
- const blob = new Blob([data.config], { type: 'application/json' });
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url;
- a.download = `feishu_config_${new Date().toISOString().split('T')[0]}.json`;
- document.body.appendChild(a);
- a.click();
- document.body.removeChild(a);
- URL.revokeObjectURL(url);
-
- this.showNotification('配置导出成功', 'success');
- } else {
- this.showNotification('配置导出失败: ' + data.error, 'error');
- }
- } catch (error) {
- this.showNotification('配置导出失败: ' + error.message, 'error');
- }
- }
-
- showImportModal() {
- const modal = new bootstrap.Modal(document.getElementById('importConfigModal'));
- modal.show();
- }
-
- async importConfig() {
- try {
- const configJson = document.getElementById('configJson').value.trim();
-
- if (!configJson) {
- this.showNotification('请输入配置JSON数据', 'warning');
- return;
- }
-
- const response = await fetch('/api/feishu-sync/config/import', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({ config: configJson })
- });
-
- const data = await response.json();
-
- if (data.success) {
- this.showNotification('配置导入成功', 'success');
- this.loadConfig();
- this.refreshStatus();
-
- // 关闭模态框
- const modal = bootstrap.Modal.getInstance(document.getElementById('importConfigModal'));
- modal.hide();
- document.getElementById('configJson').value = '';
- } else {
- this.showNotification('配置导入失败: ' + data.error, 'error');
- }
- } catch (error) {
- this.showNotification('配置导入失败: ' + error.message, 'error');
- }
- }
-
- async resetConfig() {
- if (confirm('确定要重置所有配置吗?此操作不可撤销!')) {
- try {
- const response = await fetch('/api/feishu-sync/config/reset', {
- method: 'POST'
- });
-
- const data = await response.json();
-
- if (data.success) {
- this.showNotification('配置重置成功', 'success');
- this.loadConfig();
- this.refreshStatus();
- } else {
- this.showNotification('配置重置失败: ' + data.error, 'error');
- }
- } catch (error) {
- this.showNotification('配置重置失败: ' + error.message, 'error');
- }
- }
- }
-
- async createWorkorder(recordId) {
- if (confirm(`确定要从飞书记录 ${recordId} 创建工单吗?`)) {
- try {
- this.showNotification('正在创建工单...', 'info');
-
- const response = await fetch('/api/feishu-sync/create-workorder', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({
- record_id: recordId
- })
- });
-
- const data = await response.json();
-
- if (data.success) {
- this.showNotification(data.message, 'success');
- // 刷新工单列表(如果用户在工单页面)
- if (typeof window.refreshWorkOrders === 'function') {
- window.refreshWorkOrders();
- }
- } else {
- this.showNotification('创建工单失败: ' + data.message, 'error');
- }
- } catch (error) {
- this.showNotification('创建工单失败: ' + error.message, 'error');
- }
- }
- }
-
- // 字段映射管理方法
- async discoverFields() {
- try {
- this.showNotification('正在发现字段...', 'info');
-
- const response = await fetch('/api/feishu-sync/field-mapping/discover', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({ limit: 5 })
- });
-
- const data = await response.json();
-
- if (data.success) {
- this.displayDiscoveryResults(data.discovery_report);
- this.showNotification('字段发现完成', 'success');
- } else {
- this.showNotification('字段发现失败: ' + data.error, 'error');
- }
- } catch (error) {
- this.showNotification('字段发现失败: ' + error.message, 'error');
- }
- }
-
- displayDiscoveryResults(report) {
- const container = document.getElementById('fieldMappingContent');
- let html = '';
-
- // 已映射字段
- if (report.mapped_fields && Object.keys(report.mapped_fields).length > 0) {
- html += ' 已映射字段
';
- for (const [feishuField, localField] of Object.entries(report.mapped_fields)) {
- html += `
- ${feishuField} → ${localField}
-
`;
- }
- html += '
';
- }
-
- // 未映射字段和建议
- if (report.unmapped_fields && report.unmapped_fields.length > 0) {
- html += ' 未映射字段
';
- for (const field of report.unmapped_fields) {
- html += `
-
${field}`;
-
- const suggestions = report.suggested_mappings[field] || [];
- if (suggestions.length > 0) {
- html += '
建议映射:';
- suggestions.slice(0, 2).forEach(suggestion => {
- html += `
- ${suggestion.local_field}
- (${suggestion.reason})
-
-
`;
- });
- html += '
';
- }
-
- html += '
';
- }
- html += '
';
- }
-
- container.innerHTML = html;
- }
-
- async applySuggestion(feishuField, localField) {
- if (confirm(`确定要将 "${feishuField}" 映射到 "${localField}" 吗?`)) {
- try {
- const response = await fetch('/api/feishu-sync/field-mapping/add', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({
- feishu_field: feishuField,
- local_field: localField,
- priority: 3
- })
- });
-
- const data = await response.json();
-
- if (data.success) {
- this.showNotification('映射添加成功!', 'success');
- this.discoverFields(); // 重新发现字段
- } else {
- this.showNotification('添加映射失败: ' + data.error, 'error');
- }
- } catch (error) {
- this.showNotification('请求失败: ' + error.message, 'error');
- }
- }
- }
-
- async loadMappingStatus() {
- try {
- const response = await fetch('/api/feishu-sync/field-mapping/status');
- const data = await response.json();
-
- if (data.success) {
- this.displayMappingStatus(data.status);
- } else {
- this.showNotification('获取映射状态失败: ' + data.error, 'error');
- }
- } catch (error) {
- this.showNotification('请求失败: ' + error.message, 'error');
- }
- }
-
- displayMappingStatus(status) {
- const container = document.getElementById('fieldMappingContent');
- let html = '';
-
- html += `
-
-
-
-
${status.total_mappings}
-
直接映射
-
-
-
-
-
-
-
${status.total_aliases}
-
别名映射
-
-
-
-
-
-
-
${status.total_patterns}
-
模式匹配
-
-
-
-
-
-
-
- ${status.auto_mapping_enabled ? '启用' : '禁用'}
-
-
自动映射
-
-
-
-
`;
-
- // 显示当前映射
- if (status.field_mapping && Object.keys(status.field_mapping).length > 0) {
- html += '当前字段映射:
';
- for (const [feishuField, localField] of Object.entries(status.field_mapping)) {
- html += `
-
- ${feishuField} → ${localField}
-
-
-
`;
- }
- html += '
';
- }
-
- container.innerHTML = html;
- }
-
- async removeMapping(feishuField) {
- if (confirm(`确定要删除映射 "${feishuField}" 吗?`)) {
- try {
- const response = await fetch('/api/feishu-sync/field-mapping/remove', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({
- feishu_field: feishuField
- })
- });
-
- const data = await response.json();
-
- if (data.success) {
- this.showNotification('映射删除成功!', 'success');
- this.loadMappingStatus(); // 刷新状态
- } else {
- this.showNotification('删除映射失败: ' + data.error, 'error');
- }
- } catch (error) {
- this.showNotification('请求失败: ' + error.message, 'error');
- }
- }
- }
-
- showAddMappingModal() {
- // 简单的添加映射功能
- const feishuField = prompt('请输入飞书字段名:');
- if (!feishuField) return;
-
- const localField = prompt('请输入本地字段名 (如: order_id, description, category):');
- if (!localField) return;
-
- this.addFieldMapping(feishuField, localField);
- }
-
- async addFieldMapping(feishuField, localField) {
- try {
- const response = await fetch('/api/feishu-sync/field-mapping/add', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({
- feishu_field: feishuField,
- local_field: localField,
- priority: 3
- })
- });
-
- const data = await response.json();
-
- if (data.success) {
- this.showNotification('映射添加成功!', 'success');
- this.loadMappingStatus(); // 刷新状态
- } else {
- this.showNotification('添加映射失败: ' + data.error, 'error');
- }
- } catch (error) {
- this.showNotification('请求失败: ' + error.message, 'error');
- }
- }
-
- async checkPermissions() {
- try {
- this.showNotification('正在检查飞书权限...', 'info');
-
- const response = await fetch('/api/feishu-sync/check-permissions');
- const data = await response.json();
-
- if (data.success) {
- this.displayPermissionCheck(data.permission_check, data.summary);
- this.showNotification('权限检查完成', 'success');
- } else {
- this.showNotification('权限检查失败: ' + data.error, 'error');
- }
- } catch (error) {
- this.showNotification('权限检查失败: ' + error.message, 'error');
- }
- }
-
- displayPermissionCheck(permissionCheck, summary) {
- const container = document.getElementById('fieldMappingContent');
-
- let html = '';
-
- // 整体状态
- const statusClass = permissionCheck.success ? 'success' : 'danger';
- const statusIcon = permissionCheck.success ? 'check-circle' : 'exclamation-triangle';
- html += `
-
- 整体状态: ${permissionCheck.success ? '正常' : '异常'}
-
`;
-
- // 检查项目
- html += '
检查项目:
';
- for (const [checkName, checkResult] of Object.entries(permissionCheck.checks)) {
- const statusClass = checkResult.status === 'success' ? 'success' :
- checkResult.status === 'warning' ? 'warning' : 'danger';
- const statusIcon = checkResult.status === 'success' ? 'check-circle' :
- checkResult.status === 'warning' ? 'exclamation-triangle' : 'times-circle';
-
- html += `
-
- ${checkName}: ${checkResult.message}
-
`;
- }
-
- // 修复建议
- if (permissionCheck.recommendations && permissionCheck.recommendations.length > 0) {
- html += '
修复建议:
';
- permissionCheck.recommendations.forEach(rec => {
- html += `- ${rec}
`;
- });
- html += '
';
- }
-
- // 错误信息
- if (permissionCheck.errors && permissionCheck.errors.length > 0) {
- html += '
错误信息:
';
- permissionCheck.errors.forEach(error => {
- html += `- ${error}
`;
- });
- html += '
';
- }
-
- html += '
';
-
- container.innerHTML = html;
-
- // 显示字段映射管理区域
- const section = document.getElementById('fieldMappingSection');
- section.style.display = 'block';
- }
-
- showNotification(message, type = 'info') {
- const container = document.getElementById('notificationContainer');
- const alert = document.createElement('div');
- alert.className = `alert alert-${type === 'error' ? 'danger' : type} alert-dismissible fade show`;
- alert.innerHTML = `
- ${message}
-
- `;
-
- container.appendChild(alert);
-
- setTimeout(() => {
- if (alert.parentNode) {
- alert.parentNode.removeChild(alert);
- }
- }, 5000);
+ const health = await response.json();
+ this.updateHealthDisplay(health);
+ } catch (e) { console.error('加载健康状态失败:', e); }
}
}
diff --git a/src/web/static/js/modules/agent.js b/src/web/static/js/modules/agent.js
new file mode 100644
index 0000000..4f1d924
--- /dev/null
+++ b/src/web/static/js/modules/agent.js
@@ -0,0 +1,424 @@
+// Agent管理模块
+Object.assign(TSPDashboard.prototype, {
+
+ 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 {
+ const [statusResp, toolsResp] = await Promise.all([
+ fetch('/api/agent/status'),
+ fetch('/api/agent/tools/stats')
+ ]);
+ const data = await statusResp.json();
+ const toolsData = await toolsResp.json();
+
+ if (data.success) {
+ // 更新 ReactAgent 状态
+ const react = data.react_agent || {};
+ const stateEl = document.getElementById('agent-current-state');
+ stateEl.textContent = react.status || (data.is_active ? 'active' : 'inactive');
+ stateEl.className = `badge ${data.is_active ? 'bg-success' : 'bg-secondary'}`;
+ document.getElementById('agent-available-tools').textContent = react.tool_count || 0;
+ document.getElementById('agent-max-rounds').textContent = react.max_tool_rounds || 5;
+ document.getElementById('agent-history-count').textContent = react.history_count || 0;
+
+ // 工具列表 — 使用 ReactAgent 的工具定义
+ const tools = (toolsData.success ? toolsData.tools : []) || [];
+ this.updateToolsList(tools);
+
+ // 执行历史
+ const history = (toolsData.success ? toolsData.recent_history : []) || [];
+ this.updateAgentExecutionHistory(history);
+ }
+ } catch (error) {
+ console.error('加载Agent数据失败:', error);
+ }
+ },
+
+ updateToolsList(tools) {
+ const toolsList = document.getElementById('tools-list');
+ if (!tools || tools.length === 0) {
+ toolsList.innerHTML = '';
+ return;
+ }
+
+ const toolsHtml = tools.map(tool => {
+ // ReactAgent 工具定义格式: { name, description, parameters }
+ const params = tool.parameters || {};
+ const paramList = Object.entries(params).map(([k, v]) =>
+ `${k}(${v.required ? '必填' : '可选'})`
+ ).join(', ');
+ return `
+
+
+
+
${tool.name}
+
${tool.description || ''}
+ ${paramList ? `
参数: ${paramList}
` : ''}
+
+
+
+
+ `;
+ }).join('');
+
+ toolsList.innerHTML = toolsHtml;
+
+ // 绑定执行事件
+ toolsList.querySelectorAll('button[data-tool]').forEach(btn => {
+ btn.addEventListener('click', async () => {
+ const tool = btn.getAttribute('data-tool');
+ let params = {};
+ try {
+ const input = prompt('请输入执行参数(JSON):', '{}');
+ if (input === null) return;
+ params = JSON.parse(input);
+ } catch (e) {
+ this.showNotification('参数格式错误,应为JSON', 'warning');
+ return;
+ }
+ try {
+ const resp = await fetch('/api/agent/tools/execute', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ tool, parameters: params })
+ });
+ const res = await resp.json();
+ if (res.success) {
+ this.showNotification(`工具 ${tool} 执行成功`, 'success');
+ await this.loadAgentData();
+ } else {
+ this.showNotification(res.error || `工具 ${tool} 执行失败`, 'error');
+ }
+ } catch (err) {
+ this.showNotification('执行工具失败: ' + err.message, 'error');
+ }
+ });
+ });
+ },
+
+ updateExecutionHistory(history) {
+ const historyContainer = document.getElementById('agent-execution-history');
+ if (history.length === 0) {
+ historyContainer.innerHTML = '';
+ return;
+ }
+
+ const historyHtml = history.slice(-5).map(item => `
+
+
+ ${item.type || '未知任务'}
+ ${new Date(item.timestamp).toLocaleString()}
+
+
${item.description || '无描述'}
+
+
+ ${item.success ? '成功' : '失败'}
+
+
+
+ `).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 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 = '发送中...';
+ 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 = '发送';
+ 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);
+ }
+ },
+
+ // 触发示例动作 (from lines 6856+)
+ 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历史 (from lines 6871+)
+ 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');
+ }
+ },
+
+ updateAgentExecutionHistory(history) {
+ const container = document.getElementById('agent-execution-history');
+
+ if (!history || history.length === 0) {
+ container.innerHTML = `
+
+ `;
+ 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 `
+
+
+
+
+
${record.description}
+
+ 优先级 ${record.priority}
+ 置信度 ${(record.confidence * 100).toFixed(0)}%
+ ${record.success ? '成功' : '失败'}
+
+
+ 开始: ${startTime} |
+ 耗时: ${duration}秒
+
+
+
+ ${record.action_type}
+
+
+ ${record.result && record.result.message ? `
+
+ 结果: ${record.result.message}
+
+ ` : ''}
+
+
+ `;
+ }).join('');
+
+ container.innerHTML = historyHtml;
+ },
+
+ 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');
+ }
+ }
+
+});
diff --git a/src/web/static/js/modules/alerts.js b/src/web/static/js/modules/alerts.js
new file mode 100644
index 0000000..760ed7c
--- /dev/null
+++ b/src/web/static/js/modules/alerts.js
@@ -0,0 +1,348 @@
+// 预警管理模块
+Object.assign(TSPDashboard.prototype, {
+ async loadAlerts(page = 1, forceRefresh = false) {
+ const cacheKey = `alerts_page_${page}`;
+
+ if (!forceRefresh && this.cache.has(cacheKey)) {
+ const cachedData = this.cache.get(cacheKey);
+ this.updateAlertsDisplay(cachedData.alerts);
+ this.updateAlertsPagination(cachedData);
+ this.updateAlertStatistics(cachedData.alerts); // 添加统计更新
+ return;
+ }
+
+ try {
+ const pageSize = this.getPageSize('alerts-pagination');
+ const response = await fetch(`/api/alerts?page=${page}&per_page=${pageSize}`);
+ const data = await response.json();
+
+ this.cache.set(cacheKey, data);
+ this.updateAlertsDisplay(data.alerts);
+ this.updateAlertsPagination(data);
+ this.updateAlertStatistics(data.alerts); // 添加统计更新
+ } catch (error) {
+ console.error('加载预警失败:', error);
+ this.showNotification('加载预警失败', 'error');
+ }
+ },
+
+ updateAlertsDisplay(alerts) {
+ const container = document.getElementById('alerts-container');
+
+ if (alerts.length === 0) {
+ container.innerHTML = `
+
+
+
暂无活跃预警
+
系统运行正常,没有需要处理的预警
+
+ `;
+ return;
+ }
+
+ // 添加预警列表的批量操作头部
+ const headerHtml = `
+
+
+
+
+
+
+
+
+
+ `;
+
+ const alertsHtml = alerts.map(alert => `
+
+
+
+
+
+
+ ${this.getLevelText(alert.level)}
+ ${alert.rule_name || '未知规则'}
+ ${this.formatTime(alert.created_at)}
+
+
${alert.message}
+
+ 类型: ${this.getTypeText(alert.alert_type)} |
+ 级别: ${this.getLevelText(alert.level)}
+
+
+
+
+
+
+
+
+ `).join('');
+
+ container.innerHTML = headerHtml + alertsHtml;
+ },
+
+ updateAlertsPagination(data) {
+ this.createPaginationComponent(data, 'alerts-pagination', 'loadAlerts', '条预警');
+ },
+
+ updateAlertStatistics(alerts) {
+ // 如果传入的是分页数据,需要重新获取全部数据来计算统计
+ if (alerts && alerts.length > 0) {
+ // 检查是否是分页数据(通常分页数据少于50条)
+ const pageSize = this.getPageSize('alerts-pagination');
+ if (alerts.length <= pageSize) {
+ // 可能是分页数据,需要获取全部数据
+ this.updateAlertStatisticsFromAPI();
+ return;
+ }
+ }
+
+ // 使用传入的数据计算统计
+ 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;
+ },
+
+ // 从API获取全部预警数据来计算统计
+ async updateAlertStatisticsFromAPI() {
+ try {
+ // 获取全部预警数据(不分页)
+ const response = await fetch('/api/alerts?per_page=1000'); // 获取大量数据
+ const data = await response.json();
+
+ if (data.alerts) {
+ const stats = data.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;
+
+ // 更新缓存
+ this.cache.set('alerts_stats', {
+ data: data.alerts,
+ timestamp: Date.now()
+ });
+ }
+ } catch (error) {
+ console.error('获取全部预警统计失败:', error);
+ }
+ },
+
+ // 预警批量删除功能
+ toggleSelectAllAlerts() {
+ const selectAllCheckbox = document.getElementById('select-all-alerts');
+ const alertCheckboxes = document.querySelectorAll('.alert-checkbox');
+
+ alertCheckboxes.forEach(checkbox => {
+ checkbox.checked = selectAllCheckbox.checked;
+ });
+
+ this.updateBatchDeleteAlertsButton();
+ },
+
+ updateBatchDeleteAlertsButton() {
+ const selectedCheckboxes = document.querySelectorAll('.alert-checkbox:checked');
+ const batchDeleteBtn = document.getElementById('batch-delete-alerts');
+
+ if (batchDeleteBtn) {
+ batchDeleteBtn.disabled = selectedCheckboxes.length === 0;
+ batchDeleteBtn.textContent = selectedCheckboxes.length > 0
+ ? `批量删除 (${selectedCheckboxes.length})`
+ : '批量删除';
+ }
+ },
+
+ async batchDeleteAlerts() {
+ const selectedCheckboxes = document.querySelectorAll('.alert-checkbox:checked');
+ const selectedIds = Array.from(selectedCheckboxes).map(cb => parseInt(cb.value));
+
+ if (selectedIds.length === 0) {
+ this.showNotification('请选择要删除的预警', 'warning');
+ return;
+ }
+
+ if (!confirm(`确定要删除选中的 ${selectedIds.length} 个预警吗?此操作不可撤销。`)) {
+ return;
+ }
+
+ try {
+ const response = await fetch('/api/batch-delete/alerts', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({ ids: selectedIds })
+ });
+
+ const data = await response.json();
+
+ if (data.success) {
+ this.showNotification(data.message, 'success');
+
+ // 清除所有相关缓存
+ this.cache.delete('alerts');
+ this.cache.delete('alerts_stats');
+
+ // 立即更新统计数字,避免跳动
+ await this.updateStatsAfterDelete(selectedIds.length);
+
+ // 重新加载预警列表
+ await this.loadAlerts();
+
+ // 重置批量删除按钮状态
+ this.updateBatchDeleteAlertsButton();
+ } else {
+ this.showNotification(data.error || '批量删除失败', 'error');
+ }
+ } catch (error) {
+ console.error('批量删除预警失败:', error);
+ this.showNotification('批量删除预警失败', 'error');
+ }
+ },
+
+ // 删除后更新统计数字(平滑更新)
+ async updateStatsAfterDelete(deletedCount) {
+ try {
+ // 直接调用API获取最新统计,不依赖页面显示数据
+ await this.updateAlertStatisticsFromAPI();
+
+ // 更新最后更新时间,避免智能更新机制干扰
+ this.lastUpdateTimes.alerts = Date.now();
+
+ } catch (error) {
+ console.error('更新删除后统计失败:', error);
+ // 如果计算失败,直接刷新
+ await this.refreshAlertStats();
+ }
+ },
+
+ // 获取当前显示的预警数据
+ getCurrentDisplayedAlerts() {
+ const alertElements = document.querySelectorAll('.alert-item');
+ const alerts = [];
+
+ alertElements.forEach(element => {
+ const level = element.querySelector('.alert-level')?.textContent?.trim();
+ if (level) {
+ alerts.push({ level: level.toLowerCase() });
+ }
+ });
+
+ return alerts;
+ },
+
+ // 计算预警统计
+ calculateAlertStats(alerts) {
+ const stats = { critical: 0, warning: 0, info: 0, total: 0 };
+
+ alerts.forEach(alert => {
+ const level = alert.level.toLowerCase();
+ if (stats.hasOwnProperty(level)) {
+ stats[level]++;
+ }
+ stats.total++;
+ });
+
+ return stats;
+ },
+
+ // 平滑更新预警统计数字
+ smoothUpdateAlertStats(newStats) {
+ // 获取当前显示的数字
+ const currentCritical = parseInt(document.getElementById('critical-alerts')?.textContent || '0');
+ const currentWarning = parseInt(document.getElementById('warning-alerts')?.textContent || '0');
+ const currentInfo = parseInt(document.getElementById('info-alerts')?.textContent || '0');
+ const currentTotal = parseInt(document.getElementById('total-alerts-count')?.textContent || '0');
+
+ // 平滑过渡到新数字
+ this.animateNumberChange('critical-alerts', currentCritical, newStats.critical);
+ this.animateNumberChange('warning-alerts', currentWarning, newStats.warning);
+ this.animateNumberChange('info-alerts', currentInfo, newStats.info);
+ this.animateNumberChange('total-alerts-count', currentTotal, newStats.total);
+ },
+
+ // 数字变化动画
+ animateNumberChange(elementId, from, to) {
+ const element = document.getElementById(elementId);
+ if (!element) return;
+
+ const duration = 300; // 300ms动画
+ const startTime = Date.now();
+
+ const animate = () => {
+ const elapsed = Date.now() - startTime;
+ const progress = Math.min(elapsed / duration, 1);
+
+ // 使用缓动函数
+ const easeOut = 1 - Math.pow(1 - progress, 3);
+ const currentValue = Math.round(from + (to - from) * easeOut);
+
+ element.textContent = currentValue;
+
+ if (progress < 1) {
+ requestAnimationFrame(animate);
+ }
+ };
+
+ requestAnimationFrame(animate);
+ },
+
+ // 解决预警后更新统计数字
+ async updateStatsAfterResolve(alertId) {
+ try {
+ // 直接调用API获取最新统计,不依赖页面显示数据
+ await this.updateAlertStatisticsFromAPI();
+
+ // 更新最后更新时间,避免智能更新机制干扰
+ this.lastUpdateTimes.alerts = Date.now();
+
+ } catch (error) {
+ console.error('更新解决后统计失败:', error);
+ // 如果计算失败,直接刷新
+ await this.refreshAlertStats();
+ }
+ },
+
+ 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.cache.delete('alerts');
+ this.cache.delete('alerts_stats');
+
+ // 立即更新统计数字
+ await this.updateStatsAfterResolve(alertId);
+
+ // 重新加载预警列表
+ this.loadAlerts();
+ } else {
+ this.showNotification('解决预警失败', 'error');
+ }
+ } catch (error) {
+ console.error('解决预警失败:', error);
+ this.showNotification('解决预警失败', 'error');
+ }
+ }
+});
diff --git a/src/web/static/js/modules/chat.js b/src/web/static/js/modules/chat.js
new file mode 100644
index 0000000..7a9e3c9
--- /dev/null
+++ b/src/web/static/js/modules/chat.js
@@ -0,0 +1,207 @@
+// 智能对话模块
+Object.assign(TSPDashboard.prototype, {
+ async startChat() {
+ try {
+ const userId = document.getElementById('user-id').value;
+ const workOrderId = document.getElementById('work-order-id').value;
+ const tenantId = document.getElementById('chat-tenant-id')?.value || 'default';
+
+ 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,
+ tenant_id: tenantId
+ })
+ });
+
+ 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 = '已连接';
+
+ 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 = '未连接';
+
+ 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 = '';
+
+ // 显示占位提示:小奇正在查询中
+ const typingId = this.showTypingIndicator();
+
+ // 发送消息到服务器
+ 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) {
+ this.updateTypingIndicator(typingId, data.response, data.knowledge_used);
+ } else {
+ this.updateTypingIndicator(typingId, '抱歉,处理您的消息时出现了错误。', null, true);
+ }
+ } catch (error) {
+ console.error('发送消息失败:', error);
+ this.updateTypingIndicator(typingId, '网络连接错误,请稍后重试。', null, true);
+ }
+ },
+
+ showTypingIndicator() {
+ const messagesContainer = document.getElementById('chat-messages');
+ const id = `typing-${Date.now()}`;
+ const messageDiv = document.createElement('div');
+ messageDiv.className = 'message assistant';
+ messageDiv.id = id;
+ const avatar = document.createElement('div');
+ avatar.className = 'message-avatar';
+ avatar.innerHTML = '';
+ const contentDiv = document.createElement('div');
+ contentDiv.className = 'message-content';
+ contentDiv.innerHTML = `
+ 小奇正在查询中,请稍后…
+ ${new Date().toLocaleTimeString()}
+ `;
+ 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 = `
+ ${content}
+ ${new Date().toLocaleTimeString()}
+ `;
+ if (knowledgeUsed && knowledgeUsed.length > 0) {
+ const knowledgeDiv = document.createElement('div');
+ knowledgeDiv.className = 'knowledge-info';
+ knowledgeDiv.innerHTML = `
+
+ 使用了知识库: ${knowledgeUsed.map(k => k.question || k.source || '实时数据').join(', ')}
+ `;
+ contentDiv.appendChild(knowledgeDiv);
+ }
+ if (isError) {
+ contentDiv.style.borderLeft = '4px solid #dc3545';
+ } else {
+ contentDiv.style.borderLeft = '';
+ }
+ }
+ const messagesContainer = document.getElementById('chat-messages');
+ messagesContainer.scrollTop = messagesContainer.scrollHeight;
+ },
+
+ addMessage(role, content, knowledgeUsed = null, isError = false) {
+ const messagesContainer = document.getElementById('chat-messages');
+
+ // 移除欢迎消息
+ 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' ? '' : '';
+
+ const contentDiv = document.createElement('div');
+ contentDiv.className = 'message-content';
+ contentDiv.innerHTML = `
+ ${content}
+ ${new Date().toLocaleTimeString()}
+ `;
+
+ if (knowledgeUsed && knowledgeUsed.length > 0) {
+ const knowledgeDiv = document.createElement('div');
+ knowledgeDiv.className = 'knowledge-info';
+ knowledgeDiv.innerHTML = `
+
+ 使用了知识库: ${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;
+ }
+});
diff --git a/src/web/static/js/modules/conversations.js b/src/web/static/js/modules/conversations.js
new file mode 100644
index 0000000..9d30049
--- /dev/null
+++ b/src/web/static/js/modules/conversations.js
@@ -0,0 +1,671 @@
+// 对话历史模块
+Object.assign(TSPDashboard.prototype, {
+ async loadConversationTenantList() {
+ this.conversationCurrentTenantId = null;
+ this.renderConversationBreadcrumb(null);
+
+ // 加载全局统计
+ this.loadConversationStats(null);
+
+ // 显示租户列表容器,隐藏详情容器
+ const tenantListEl = document.getElementById('conversation-tenant-list');
+ const tenantDetailEl = document.getElementById('conversation-tenant-detail');
+
+ if (tenantListEl) tenantListEl.style.display = '';
+ if (tenantDetailEl) tenantDetailEl.style.display = 'none';
+
+ // 显示加载中 spinner
+ if (tenantListEl) {
+ tenantListEl.innerHTML = '';
+ }
+
+ try {
+ const response = await fetch('/api/conversations/tenants');
+ const tenants = await response.json();
+
+ if (!Array.isArray(tenants) || tenants.length === 0) {
+ if (tenantListEl) {
+ tenantListEl.innerHTML = '';
+ }
+ return;
+ }
+
+ const cardsHtml = tenants.map(t => {
+ const lastActive = t.last_active_time ? new Date(t.last_active_time).toLocaleString() : '无';
+ return `
+
+
+
+
${t.tenant_id}
+
+
+ 会话总数
+
${t.session_count}
+
+
+ 消息总数
+
${t.message_count}
+
+
+ 活跃会话
+
${t.active_session_count}
+
+
+
+ 最近活跃: ${lastActive}
+
+
+
+
+ `;
+ }).join('');
+
+ if (tenantListEl) {
+ tenantListEl.innerHTML = cardsHtml;
+ }
+ } catch (error) {
+ console.error('加载对话租户列表失败:', error);
+ if (tenantListEl) {
+ tenantListEl.innerHTML = ' 加载失败
';
+ }
+ this.showNotification('加载对话租户列表失败', 'error');
+ }
+ },
+
+ // Task 7.1: 加载对话历史租户详情视图
+ async loadConversationTenantDetail(tenantId, page = 1) {
+ this.conversationCurrentTenantId = tenantId;
+ this.paginationConfig.currentConversationPage = page;
+
+ // 隐藏租户列表,显示详情容器
+ const tenantListEl = document.getElementById('conversation-tenant-list');
+ const tenantDetailEl = document.getElementById('conversation-tenant-detail');
+
+ if (tenantListEl) tenantListEl.style.display = 'none';
+ if (tenantDetailEl) tenantDetailEl.style.display = '';
+
+ // 渲染面包屑(如果 renderConversationBreadcrumb 已实现)
+ if (typeof this.renderConversationBreadcrumb === 'function') {
+ this.renderConversationBreadcrumb(tenantId);
+ }
+
+ // 加载租户级统计(如果 loadConversationStats 已实现)
+ if (typeof this.loadConversationStats === 'function') {
+ this.loadConversationStats(tenantId);
+ }
+
+ // 显示加载中 spinner
+ const sessionListEl = document.getElementById('conversation-session-list');
+ if (sessionListEl) {
+ sessionListEl.innerHTML = '';
+ }
+
+ try {
+ const perPage = this.getPageSize('conversation-session-pagination');
+ const statusFilter = document.getElementById('conversation-status-filter')?.value || '';
+ const dateFilter = document.getElementById('conversation-detail-date-filter')?.value || '';
+
+ let url = `/api/conversations/sessions?tenant_id=${encodeURIComponent(tenantId)}&page=${page}&per_page=${perPage}`;
+ if (statusFilter) url += `&status=${encodeURIComponent(statusFilter)}`;
+ if (dateFilter) url += `&date_filter=${encodeURIComponent(dateFilter)}`;
+
+ const response = await fetch(url);
+ const data = await response.json();
+
+ if (data.sessions) {
+ this.renderConversationSessionTable(data.sessions, tenantId);
+ this.updateConversationTenantPagination(data);
+ } else {
+ if (sessionListEl) {
+ sessionListEl.innerHTML = '';
+ }
+ }
+ } catch (error) {
+ console.error('加载租户会话列表失败:', error);
+ if (sessionListEl) {
+ sessionListEl.innerHTML = ' 加载失败
';
+ }
+ this.showNotification('加载租户会话列表失败', 'error');
+ }
+ },
+
+ // Task 7.1: 渲染会话表格
+ renderConversationSessionTable(sessions, tenantId) {
+ const sessionListEl = document.getElementById('conversation-session-list');
+ if (!sessionListEl) return;
+
+ if (!sessions || sessions.length === 0) {
+ sessionListEl.innerHTML = '';
+ return;
+ }
+
+ const statusBadge = (status) => {
+ if (status === 'active') return '活跃';
+ if (status === 'ended') return '已结束';
+ return `${status || '未知'}`;
+ };
+
+ const sourceBadge = (source) => {
+ if (source === 'feishu') return '飞书';
+ if (source === 'websocket') return 'WebSocket';
+ if (source === 'api') return 'API';
+ return `${source || '未知'}`;
+ };
+
+ const formatTime = (isoStr) => {
+ if (!isoStr) return '-';
+ return new Date(isoStr).toLocaleString();
+ };
+
+ const rowsHtml = sessions.map(s => `
+
+ | ${s.title || s.session_id || '-'} |
+ ${s.message_count || 0} |
+ ${statusBadge(s.status)} |
+ ${sourceBadge(s.source)} |
+ ${formatTime(s.created_at)} |
+ ${formatTime(s.updated_at)} |
+
+
+ |
+
+ `).join('');
+
+ sessionListEl.innerHTML = `
+
+
+
+
+ | 会话标题 |
+ 消息数 |
+ 状态 |
+ 来源 |
+ 创建时间 |
+ 更新时间 |
+ 操作 |
+
+
+
+ ${rowsHtml}
+
+
+
+ `;
+ },
+
+ // Task 7.1: 查看会话消息详情
+ async viewSessionMessages(sessionId, sessionTitle) {
+ try {
+ const response = await fetch(`/api/conversations/sessions/${encodeURIComponent(sessionId)}`);
+ const data = await response.json();
+
+ if (data.success && data.messages) {
+ // 更新面包屑到第三层(如果已实现)
+ if (typeof this.renderConversationBreadcrumb === 'function') {
+ this.renderConversationBreadcrumb(this.conversationCurrentTenantId, sessionTitle || sessionId);
+ }
+ this.showSessionMessagesModal(data.session, data.messages);
+ } else {
+ throw new Error(data.error || '获取会话消息失败');
+ }
+ } catch (error) {
+ console.error('获取会话消息失败:', error);
+ this.showNotification('获取会话消息失败: ' + error.message, 'error');
+ }
+ },
+
+ // Task 7.1: 显示会话消息模态框
+ showSessionMessagesModal(session, messages) {
+ const messagesHtml = messages.map(msg => `
+
+
+
用户:
+
${msg.user_message || ''}
+
+
+
助手:
+
${msg.assistant_response || ''}
+
+
+
+ ${msg.timestamp ? new Date(msg.timestamp).toLocaleString() : ''}
+ ${msg.response_time ? ` | 响应: ${msg.response_time}s` : ''}
+ ${msg.confidence_score ? ` | 置信度: ${Math.round(msg.confidence_score * 100)}%` : ''}
+
+
+
+ `).join('');
+
+ const modalHtml = `
+
+ `;
+
+ // 移除已存在的模态框
+ const existingModal = document.getElementById('sessionMessagesModal');
+ if (existingModal) existingModal.remove();
+
+ document.body.insertAdjacentHTML('beforeend', modalHtml);
+ const modal = new bootstrap.Modal(document.getElementById('sessionMessagesModal'));
+ modal.show();
+
+ // 模态框关闭后清理
+ document.getElementById('sessionMessagesModal').addEventListener('hidden.bs.modal', function () {
+ this.remove();
+ });
+ },
+
+ // Task 7.1: 对话租户详情分页
+ updateConversationTenantPagination(data) {
+ this.createPaginationComponent(data, 'conversation-session-pagination', 'loadConversationTenantDetailPage', '条会话');
+ },
+
+ // Task 7.2: 面包屑导航
+ renderConversationBreadcrumb(tenantId, sessionTitle) {
+ const breadcrumbEl = document.getElementById('conversation-breadcrumb');
+ if (!breadcrumbEl) return;
+
+ if (!tenantId) {
+ breadcrumbEl.innerHTML = '';
+ return;
+ }
+
+ if (!sessionTitle) {
+ // 租户详情视图: "对话历史 > {tenant_id}"
+ breadcrumbEl.innerHTML = `
+
+ `;
+ } else {
+ // 消息详情视图: "对话历史 > {tenant_id} > {session_title}"
+ breadcrumbEl.innerHTML = `
+
+ `;
+ }
+
+ this.conversationCurrentTenantId = tenantId;
+ },
+
+ // Task 7.1: 应用对话筛选条件
+ applyConversationFilters() {
+ if (this.conversationCurrentTenantId) {
+ this.loadConversationTenantDetail(this.conversationCurrentTenantId, 1);
+ }
+ },
+
+ // Task 7.3: 删除会话并处理空租户自动返回
+ async deleteConversationSession(sessionId) {
+ if (!confirm('确定要删除这个会话及其所有消息吗?')) return;
+ try {
+ const response = await fetch(`/api/conversations/sessions/${encodeURIComponent(sessionId)}`, { method: 'DELETE' });
+ const data = await response.json();
+ if (data.success) {
+ this.showNotification('会话已删除', 'success');
+ // 刷新当前租户详情视图并检查是否还有剩余会话
+ if (this.conversationCurrentTenantId) {
+ const tenantId = this.conversationCurrentTenantId;
+ const perPage = this.getPageSize('conversation-session-pagination');
+ const checkUrl = `/api/conversations/sessions?tenant_id=${encodeURIComponent(tenantId)}&page=1&per_page=${perPage}`;
+ const checkResp = await fetch(checkUrl);
+ const checkData = await checkResp.json();
+
+ if (!checkData.sessions || checkData.sessions.length === 0 || checkData.total === 0) {
+ // 该租户下已无会话,自动返回租户列表视图
+ this.loadConversationTenantList();
+ } else {
+ // 仍有会话,刷新当前详情视图(调整页码避免越界)
+ const maxPage = checkData.total_pages || 1;
+ const currentPage = Math.min(this.paginationConfig.currentConversationPage, maxPage);
+ await this.loadConversationTenantDetail(tenantId, currentPage);
+ }
+ }
+ } else {
+ throw new Error(data.error || '删除失败');
+ }
+ } catch (error) {
+ console.error('删除会话失败:', error);
+ this.showNotification('删除会话失败: ' + error.message, 'error');
+ }
+ },
+
+ // 对话历史管理
+ async loadConversationHistory(page = 1) {
+ try {
+ const pageSize = this.getPageSize('conversations-pagination');
+ const response = await fetch(`/api/conversations?page=${page}&per_page=${pageSize}`);
+ const data = await response.json();
+
+ if (data.conversations) {
+ this.renderConversationList(data.conversations || []);
+ this.updateConversationPagination(data);
+ this.updateConversationStats(data.stats || {});
+ } else {
+ throw new Error(data.error || '加载对话历史失败');
+ }
+ } catch (error) {
+ console.error('加载对话历史失败:', error);
+ this.showNotification('加载对话历史失败: ' + error.message, 'error');
+ }
+ },
+
+ renderConversationList(conversations) {
+ const container = document.getElementById('conversation-list');
+ if (!conversations || conversations.length === 0) {
+ container.innerHTML = `
+
+ `;
+ return;
+ }
+
+ const html = conversations.map(conv => {
+ // 识别来源和类型
+ let sourceBadge = '';
+ const method = conv.invocation_method || '';
+ const userId = conv.user_id || '';
+
+ // 判断是否来自飞书 (根据调用方式或ID格式)
+ if (method.includes('feishu') || userId.startsWith('ou_')) {
+ sourceBadge = '飞书';
+ } else {
+ sourceBadge = 'Web';
+ }
+
+ // 判断群聊/私聊
+ let typeBadge = '';
+ if (method.includes('group')) {
+ typeBadge = '群聊';
+ } else if (method.includes('p2p')) {
+ typeBadge = '私聊';
+ }
+
+ return `
+
+
+
+
+
+ ${sourceBadge}
+ ${typeBadge}
+ 用户: ${userId || '匿名'}
+
+ ${new Date(conv.timestamp).toLocaleString()}
+
+
+
+
+
+
+
+
用户: ${conv.user_message?.substring(0, 100)}${conv.user_message?.length > 100 ? '...' : ''}
+
助手: ${conv.assistant_response?.substring(0, 100)}${conv.assistant_response?.length > 100 ? '...' : ''}
+
+
+ 响应时间: ${conv.response_time || 0}ms
+ ${conv.work_order_id ? `工单: ${conv.work_order_id}` : ''}
+
+
+
+ `;
+ }).join('');
+
+ container.innerHTML = html;
+ },
+
+ updateConversationPagination(data) {
+ this.createPaginationComponent(data, 'conversations-pagination', 'loadConversationHistory', '条对话');
+ },
+
+ 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;
+ },
+
+ // Task 8.3: 加载对话统计(支持租户级别)
+ async loadConversationStats(tenantId) {
+ try {
+ let url = '/api/conversations/analytics';
+ if (tenantId) {
+ url += `?tenant_id=${encodeURIComponent(tenantId)}`;
+ }
+ const response = await fetch(url);
+ const data = await response.json();
+
+ const analytics = data.analytics || {};
+ const convStats = analytics.conversations || {};
+
+ const totalEl = document.getElementById('conversation-total');
+ const todayEl = document.getElementById('conversation-today');
+ const avgResponseEl = document.getElementById('conversation-avg-response');
+ const activeUsersEl = document.getElementById('conversation-active-users');
+
+ if (totalEl) totalEl.textContent = convStats.total || 0;
+ if (todayEl) todayEl.textContent = convStats.today || 0;
+ if (avgResponseEl) avgResponseEl.textContent = `${Math.round(convStats.avg_response_time || 0)}ms`;
+ if (activeUsersEl) activeUsersEl.textContent = convStats.active_users || 0;
+ } catch (error) {
+ console.error('加载对话统计失败:', error);
+ }
+ },
+
+ async refreshConversationHistory() {
+ // 先尝试触发一次合并迁移(幂等,重复调用也安全)
+ try {
+ await fetch('/api/conversations/migrate-merge', { method: 'POST' });
+ } catch (e) { /* 忽略迁移失败 */ }
+ // 根据当前视图状态刷新:租户详情视图或租户列表视图
+ if (this.conversationCurrentTenantId) {
+ await this.loadConversationTenantDetail(this.conversationCurrentTenantId, this.paginationConfig.currentConversationPage);
+ } else {
+ await this.loadConversationTenantList();
+ }
+ this.showNotification('对话历史已刷新', 'success');
+ },
+
+ async clearAllConversations() {
+ if (!confirm('确定要清空所有对话历史吗?此操作不可恢复!')) {
+ return;
+ }
+
+ 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 || '清空对话历史失败');
+ }
+ } catch (error) {
+ console.error('清空对话历史失败:', error);
+ this.showNotification('清空对话历史失败: ' + error.message, 'error');
+ }
+ },
+
+ async deleteConversation(conversationId) {
+ if (!confirm('确定要删除这条对话记录吗?')) {
+ return;
+ }
+
+ 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');
+ }
+ },
+
+ async viewConversation(conversationId) {
+ try {
+ const response = await fetch(`/api/conversations/${conversationId}`);
+ const data = await response.json();
+
+ if (data.success) {
+ data.user_id = data.user_id || '匿名';
+ this.showConversationModal(data);
+ } else {
+ throw new Error(data.error || '获取对话详情失败');
+ }
+ } catch (error) {
+ console.error('获取对话详情失败:', error);
+ this.showNotification('获取对话详情失败: ' + error.message, 'error');
+ }
+ },
+
+ showConversationModal(conversation) {
+ const modalHtml = `
+
+
+
+
+
+
+ 用户: ${conversation.user_id || '匿名'}
+ 时间: ${new Date(conversation.timestamp).toLocaleString()}
+ 响应时间: ${conversation.response_time || 0}ms
+
+
+
用户消息:
+
${conversation.user_message || ''}
+
+
+
助手回复:
+
${conversation.assistant_response || ''}
+
+
+
+
+
+
+ `;
+
+ // 移除已存在的模态框
+ 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.trim();
+ const userFilter = document.getElementById('conversation-user-filter')?.value || '';
+ const dateFilter = document.getElementById('conversation-date-filter')?.value || '';
+
+ // Task 8.1: 在 Tenant_Detail_View 中搜索时自动附加 tenant_id 参数
+ if (this.conversationCurrentTenantId) {
+ if (!search) {
+ // 清空搜索时恢复当前租户的完整分页列表
+ this.loadConversationTenantDetail(this.conversationCurrentTenantId);
+ return;
+ }
+ try {
+ const perPage = this.getPageSize('conversation-session-pagination');
+ let url = `/api/conversations/sessions?search=${encodeURIComponent(search)}&tenant_id=${encodeURIComponent(this.conversationCurrentTenantId)}&page=1&per_page=${perPage}`;
+ if (dateFilter) url += `&date_filter=${encodeURIComponent(dateFilter)}`;
+
+ const response = await fetch(url);
+ const data = await response.json();
+
+ if (data.sessions) {
+ this.renderConversationSessionTable(data.sessions, this.conversationCurrentTenantId);
+ this.updateConversationTenantPagination(data);
+ } else {
+ const sessionListEl = document.getElementById('conversation-session-list');
+ if (sessionListEl) {
+ sessionListEl.innerHTML = '';
+ }
+ const paginationEl = document.getElementById('conversation-session-pagination');
+ if (paginationEl) paginationEl.innerHTML = '';
+ }
+ } catch (error) {
+ console.error('搜索租户会话失败:', error);
+ this.showNotification('搜索会话失败: ' + error.message, 'error');
+ }
+ return;
+ }
+
+ // 非租户详情视图:保持原有行为
+ 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');
+ }
+ }
+});
diff --git a/src/web/static/js/modules/dashboard-home.js b/src/web/static/js/modules/dashboard-home.js
new file mode 100644
index 0000000..10bd9c5
--- /dev/null
+++ b/src/web/static/js/modules/dashboard-home.js
@@ -0,0 +1,286 @@
+// Dashboard Home Module - 仪表板首页相关方法
+Object.assign(TSPDashboard.prototype, {
+
+ async loadDashboardData() {
+ try {
+ const [sessionsResponse, alertsResponse, workordersResponse, knowledgeResponse] = await Promise.all([
+ fetch('/api/chat/sessions'),
+ fetch('/api/alerts?per_page=1000'),
+ 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.alerts?.length || 0;
+ document.getElementById('total-workorders').textContent = workorders.workorders?.filter(w => w.status === 'open').length || 0;
+ document.getElementById('knowledge-count').textContent = knowledge.total_entries || 0;
+
+ if (alerts.alerts) {
+ this.updateAlertStatistics(alerts.alerts);
+ }
+
+ 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-bar').style.width = `${confidencePercent}%`;
+ document.getElementById('knowledge-confidence-bar').setAttribute('aria-valuenow', confidencePercent);
+ document.getElementById('knowledge-confidence-bar').textContent = `${confidencePercent}%`;
+
+ await this.updatePerformanceChart(sessions, alerts, workorders);
+ await this.updateSystemHealth();
+ await this.loadAnalytics();
+
+ } catch (error) {
+ console.error('加载仪表板数据失败:', error);
+ }
+ },
+
+ initCharts() {
+ const performanceCtx = document.getElementById('performanceChart');
+ if (performanceCtx) {
+ this.charts.performance = new Chart(performanceCtx, {
+ type: 'line',
+ data: {
+ labels: [],
+ datasets: [{
+ label: '工单数量',
+ data: [],
+ borderColor: '#007bff',
+ backgroundColor: 'rgba(0, 123, 255, 0.1)',
+ tension: 0.4
+ }, {
+ label: '预警数量',
+ 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
+ }
+ });
+ }
+ },
+
+ async updatePerformanceChart(sessions, alerts, workorders) {
+ if (!this.charts.performance) return;
+
+ 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();
+ }
+ },
+
+ 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);
+ }
+
+ 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);
+ }
+ },
+
+ 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;
+ const healthStatus = health.status || 'unknown';
+
+ 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)}`;
+ }
+
+ 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';
+ }
+
+});
diff --git a/src/web/static/js/modules/feishu-sync.js b/src/web/static/js/modules/feishu-sync.js
new file mode 100644
index 0000000..f95f4ef
--- /dev/null
+++ b/src/web/static/js/modules/feishu-sync.js
@@ -0,0 +1,698 @@
+class FeishuSyncManager {
+ constructor() {
+ this.loadConfig();
+ this.refreshStatus();
+ }
+
+ async loadConfig() {
+ try {
+ const response = await fetch('/api/feishu-sync/config');
+ const data = await response.json();
+
+ if (data.success) {
+ const config = data.config;
+ document.getElementById('appId').value = config.feishu.app_id || '';
+ document.getElementById('appSecret').value = '';
+ document.getElementById('appToken').value = config.feishu.app_token || '';
+ document.getElementById('tableId').value = config.feishu.table_id || '';
+
+ // 显示配置状态
+ const statusBadge = config.feishu.status === 'active' ?
+ '已配置' :
+ '未配置';
+
+ // 可以在这里添加状态显示
+ }
+ } catch (error) {
+ console.error('加载配置失败:', error);
+ }
+ }
+
+ async saveConfig() {
+ const config = {
+ app_id: document.getElementById('appId').value,
+ app_secret: document.getElementById('appSecret').value,
+ app_token: document.getElementById('appToken').value,
+ table_id: document.getElementById('tableId').value
+ };
+
+ if (!config.app_id || !config.app_secret || !config.app_token || !config.table_id) {
+ this.showNotification('请填写完整的配置信息', 'error');
+ return;
+ }
+
+ try {
+ const response = await fetch('/api/feishu-sync/config', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify(config)
+ });
+
+ const data = await response.json();
+
+ if (data.success) {
+ this.showNotification('配置保存成功', 'success');
+ } else {
+ this.showNotification('配置保存失败: ' + data.error, 'error');
+ }
+ } catch (error) {
+ this.showNotification('配置保存失败: ' + error.message, 'error');
+ }
+ }
+
+ async testConnection() {
+ try {
+ this.showNotification('正在测试连接...', 'info');
+
+ const response = await fetch('/api/feishu-sync/test-connection');
+ const data = await response.json();
+
+ if (data.success) {
+ this.showNotification('飞书连接正常', 'success');
+ } else {
+ this.showNotification('连接失败: ' + data.error, 'error');
+ }
+ } catch (error) {
+ this.showNotification('连接测试失败: ' + error.message, 'error');
+ }
+ }
+
+ async syncFromFeishu() {
+ try {
+ const limit = document.getElementById('syncLimit').value;
+ this.showNotification('开始从飞书同步数据...', 'info');
+ this.showProgress(true);
+
+ const response = await fetch('/api/feishu-sync/sync-from-feishu', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ generate_ai_suggestions: false,
+ limit: parseInt(limit)
+ })
+ });
+
+ const data = await response.json();
+
+ if (data.success) {
+ this.showNotification(data.message, 'success');
+ this.addSyncLog(data.message);
+ this.refreshStatus();
+ } else {
+ this.showNotification('同步失败: ' + data.error, 'error');
+ this.addSyncLog('同步失败: ' + data.error);
+ }
+ } catch (error) {
+ this.showNotification('同步失败: ' + error.message, 'error');
+ this.addSyncLog('同步失败: ' + error.message);
+ } finally {
+ this.showProgress(false);
+ }
+ }
+
+ async syncWithAI() {
+ try {
+ const limit = document.getElementById('syncLimit').value;
+ this.showNotification('开始同步数据并生成AI建议...', 'info');
+ this.showProgress(true);
+
+ const response = await fetch('/api/feishu-sync/sync-from-feishu', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ generate_ai_suggestions: true,
+ limit: parseInt(limit)
+ })
+ });
+
+ const data = await response.json();
+
+ if (data.success) {
+ this.showNotification(data.message, 'success');
+ this.addSyncLog(data.message);
+ this.refreshStatus();
+ } else {
+ this.showNotification('同步失败: ' + data.error, 'error');
+ this.addSyncLog('同步失败: ' + data.error);
+ }
+ } catch (error) {
+ this.showNotification('同步失败: ' + error.message, 'error');
+ this.addSyncLog('同步失败: ' + error.message);
+ } finally {
+ this.showProgress(false);
+ }
+ }
+
+ // 打开字段映射管理页面
+ openFieldMapping() {
+ const section = document.getElementById('fieldMappingSection');
+ if (section.style.display === 'none') {
+ section.style.display = 'block';
+ // 自动加载映射状态
+ this.loadMappingStatus();
+ } else {
+ section.style.display = 'none';
+ }
+ }
+
+ async previewFeishuData() {
+ try {
+ this.showNotification('正在获取飞书数据预览...', 'info');
+
+ const response = await fetch('/api/feishu-sync/preview-feishu-data');
+ const data = await response.json();
+
+ if (data.success) {
+ this.displayPreviewData(data.preview_data);
+ this.showNotification(`获取到 ${data.total_count} 条预览数据`, 'success');
+ } else {
+ this.showNotification('获取预览数据失败: ' + data.error, 'error');
+ }
+ } catch (error) {
+ this.showNotification('获取预览数据失败: ' + error.message, 'error');
+ }
+ }
+
+ displayPreviewData(data) {
+ const tbody = document.querySelector('#previewTable tbody');
+ tbody.innerHTML = '';
+
+ data.forEach(item => {
+ const row = document.createElement('tr');
+ row.innerHTML = `
+ ${item.record_id} |
+ ${item.fields['TR Number'] || '-'} |
+ ${item.fields['TR Description'] || '-'} |
+ ${item.fields['Type of problem'] || '-'} |
+ ${item.fields['Source'] || '-'} |
+ ${item.fields['TR (Priority/Status)'] || '-'} |
+
+
+ |
+ `;
+ tbody.appendChild(row);
+ });
+
+ document.getElementById('previewSection').style.display = 'block';
+ }
+
+ async refreshStatus() {
+ try {
+ const response = await fetch('/api/feishu-sync/status');
+ const data = await response.json();
+
+ if (data.success) {
+ const status = data.status;
+ document.getElementById('totalLocalWorkorders').textContent = status.total_local_workorders || 0;
+ document.getElementById('syncedWorkorders').textContent = status.synced_workorders || 0;
+ document.getElementById('unsyncedWorkorders').textContent = status.unsynced_workorders || 0;
+ }
+ } catch (error) {
+ console.error('刷新状态失败:', error);
+ }
+ }
+
+ showProgress(show) {
+ const progress = document.getElementById('syncProgress');
+ if (show) {
+ progress.style.display = 'block';
+ const bar = progress.querySelector('.progress-bar');
+ bar.style.width = '100%';
+ } else {
+ setTimeout(() => {
+ progress.style.display = 'none';
+ const bar = progress.querySelector('.progress-bar');
+ bar.style.width = '0%';
+ }, 1000);
+ }
+ }
+
+ addSyncLog(message) {
+ const log = document.getElementById('syncLog');
+ const timestamp = new Date().toLocaleString();
+ const logEntry = document.createElement('div');
+ logEntry.innerHTML = `[${timestamp}] ${message}`;
+
+ if (log.querySelector('.text-muted')) {
+ log.innerHTML = '';
+ }
+
+ log.appendChild(logEntry);
+ log.scrollTop = log.scrollHeight;
+ }
+
+ async exportConfig() {
+ try {
+ const response = await fetch('/api/feishu-sync/config/export');
+ const data = await response.json();
+
+ if (data.success) {
+ // 创建下载链接
+ const blob = new Blob([data.config], { type: 'application/json' });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = `feishu_config_${new Date().toISOString().split('T')[0]}.json`;
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+
+ this.showNotification('配置导出成功', 'success');
+ } else {
+ this.showNotification('配置导出失败: ' + data.error, 'error');
+ }
+ } catch (error) {
+ this.showNotification('配置导出失败: ' + error.message, 'error');
+ }
+ }
+
+ showImportModal() {
+ const modal = new bootstrap.Modal(document.getElementById('importConfigModal'));
+ modal.show();
+ }
+
+ async importConfig() {
+ try {
+ const configJson = document.getElementById('configJson').value.trim();
+
+ if (!configJson) {
+ this.showNotification('请输入配置JSON数据', 'warning');
+ return;
+ }
+
+ const response = await fetch('/api/feishu-sync/config/import', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({ config: configJson })
+ });
+
+ const data = await response.json();
+
+ if (data.success) {
+ this.showNotification('配置导入成功', 'success');
+ this.loadConfig();
+ this.refreshStatus();
+
+ // 关闭模态框
+ const modal = bootstrap.Modal.getInstance(document.getElementById('importConfigModal'));
+ modal.hide();
+ document.getElementById('configJson').value = '';
+ } else {
+ this.showNotification('配置导入失败: ' + data.error, 'error');
+ }
+ } catch (error) {
+ this.showNotification('配置导入失败: ' + error.message, 'error');
+ }
+ }
+
+ async resetConfig() {
+ if (confirm('确定要重置所有配置吗?此操作不可撤销!')) {
+ try {
+ const response = await fetch('/api/feishu-sync/config/reset', {
+ method: 'POST'
+ });
+
+ const data = await response.json();
+
+ if (data.success) {
+ this.showNotification('配置重置成功', 'success');
+ this.loadConfig();
+ this.refreshStatus();
+ } else {
+ this.showNotification('配置重置失败: ' + data.error, 'error');
+ }
+ } catch (error) {
+ this.showNotification('配置重置失败: ' + error.message, 'error');
+ }
+ }
+ }
+
+ async createWorkorder(recordId) {
+ if (confirm(`确定要从飞书记录 ${recordId} 创建工单吗?`)) {
+ try {
+ this.showNotification('正在创建工单...', 'info');
+
+ const response = await fetch('/api/feishu-sync/create-workorder', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ record_id: recordId
+ })
+ });
+
+ const data = await response.json();
+
+ if (data.success) {
+ this.showNotification(data.message, 'success');
+ // 刷新工单列表(如果用户在工单页面)
+ if (typeof window.refreshWorkOrders === 'function') {
+ window.refreshWorkOrders();
+ }
+ } else {
+ this.showNotification('创建工单失败: ' + data.message, 'error');
+ }
+ } catch (error) {
+ this.showNotification('创建工单失败: ' + error.message, 'error');
+ }
+ }
+ }
+
+ // 字段映射管理方法
+ async discoverFields() {
+ try {
+ this.showNotification('正在发现字段...', 'info');
+
+ const response = await fetch('/api/feishu-sync/field-mapping/discover', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({ limit: 5 })
+ });
+
+ const data = await response.json();
+
+ if (data.success) {
+ this.displayDiscoveryResults(data.discovery_report);
+ this.showNotification('字段发现完成', 'success');
+ } else {
+ this.showNotification('字段发现失败: ' + data.error, 'error');
+ }
+ } catch (error) {
+ this.showNotification('字段发现失败: ' + error.message, 'error');
+ }
+ }
+
+ displayDiscoveryResults(report) {
+ const container = document.getElementById('fieldMappingContent');
+ let html = '';
+
+ // 已映射字段
+ if (report.mapped_fields && Object.keys(report.mapped_fields).length > 0) {
+ html += ' 已映射字段
';
+ for (const [feishuField, localField] of Object.entries(report.mapped_fields)) {
+ html += `
+ ${feishuField} → ${localField}
+
`;
+ }
+ html += '
';
+ }
+
+ // 未映射字段和建议
+ if (report.unmapped_fields && report.unmapped_fields.length > 0) {
+ html += ' 未映射字段
';
+ for (const field of report.unmapped_fields) {
+ html += `
+
${field}`;
+
+ const suggestions = report.suggested_mappings[field] || [];
+ if (suggestions.length > 0) {
+ html += '
建议映射:';
+ suggestions.slice(0, 2).forEach(suggestion => {
+ html += `
+ ${suggestion.local_field}
+ (${suggestion.reason})
+
+
`;
+ });
+ html += '
';
+ }
+
+ html += '
';
+ }
+ html += '
';
+ }
+
+ container.innerHTML = html;
+ }
+
+ async applySuggestion(feishuField, localField) {
+ if (confirm(`确定要将 "${feishuField}" 映射到 "${localField}" 吗?`)) {
+ try {
+ const response = await fetch('/api/feishu-sync/field-mapping/add', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ feishu_field: feishuField,
+ local_field: localField,
+ priority: 3
+ })
+ });
+
+ const data = await response.json();
+
+ if (data.success) {
+ this.showNotification('映射添加成功!', 'success');
+ this.discoverFields(); // 重新发现字段
+ } else {
+ this.showNotification('添加映射失败: ' + data.error, 'error');
+ }
+ } catch (error) {
+ this.showNotification('请求失败: ' + error.message, 'error');
+ }
+ }
+ }
+
+ async loadMappingStatus() {
+ try {
+ const response = await fetch('/api/feishu-sync/field-mapping/status');
+ const data = await response.json();
+
+ if (data.success) {
+ this.displayMappingStatus(data.status);
+ } else {
+ this.showNotification('获取映射状态失败: ' + data.error, 'error');
+ }
+ } catch (error) {
+ this.showNotification('请求失败: ' + error.message, 'error');
+ }
+ }
+
+ displayMappingStatus(status) {
+ const container = document.getElementById('fieldMappingContent');
+ let html = '';
+
+ html += `
+
+
+
+
${status.total_mappings}
+
直接映射
+
+
+
+
+
+
+
${status.total_aliases}
+
别名映射
+
+
+
+
+
+
+
${status.total_patterns}
+
模式匹配
+
+
+
+
+
+
+
+ ${status.auto_mapping_enabled ? '启用' : '禁用'}
+
+
自动映射
+
+
+
+
`;
+
+ // 显示当前映射
+ if (status.field_mapping && Object.keys(status.field_mapping).length > 0) {
+ html += '当前字段映射:
';
+ for (const [feishuField, localField] of Object.entries(status.field_mapping)) {
+ html += `
+
+ ${feishuField} → ${localField}
+
+
+
`;
+ }
+ html += '
';
+ }
+
+ container.innerHTML = html;
+ }
+
+ async removeMapping(feishuField) {
+ if (confirm(`确定要删除映射 "${feishuField}" 吗?`)) {
+ try {
+ const response = await fetch('/api/feishu-sync/field-mapping/remove', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ feishu_field: feishuField
+ })
+ });
+
+ const data = await response.json();
+
+ if (data.success) {
+ this.showNotification('映射删除成功!', 'success');
+ this.loadMappingStatus(); // 刷新状态
+ } else {
+ this.showNotification('删除映射失败: ' + data.error, 'error');
+ }
+ } catch (error) {
+ this.showNotification('请求失败: ' + error.message, 'error');
+ }
+ }
+ }
+
+ showAddMappingModal() {
+ // 简单的添加映射功能
+ const feishuField = prompt('请输入飞书字段名:');
+ if (!feishuField) return;
+
+ const localField = prompt('请输入本地字段名 (如: order_id, description, category):');
+ if (!localField) return;
+
+ this.addFieldMapping(feishuField, localField);
+ }
+
+ async addFieldMapping(feishuField, localField) {
+ try {
+ const response = await fetch('/api/feishu-sync/field-mapping/add', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ feishu_field: feishuField,
+ local_field: localField,
+ priority: 3
+ })
+ });
+
+ const data = await response.json();
+
+ if (data.success) {
+ this.showNotification('映射添加成功!', 'success');
+ this.loadMappingStatus(); // 刷新状态
+ } else {
+ this.showNotification('添加映射失败: ' + data.error, 'error');
+ }
+ } catch (error) {
+ this.showNotification('请求失败: ' + error.message, 'error');
+ }
+ }
+
+ async checkPermissions() {
+ try {
+ this.showNotification('正在检查飞书权限...', 'info');
+
+ const response = await fetch('/api/feishu-sync/check-permissions');
+ const data = await response.json();
+
+ if (data.success) {
+ this.displayPermissionCheck(data.permission_check, data.summary);
+ this.showNotification('权限检查完成', 'success');
+ } else {
+ this.showNotification('权限检查失败: ' + data.error, 'error');
+ }
+ } catch (error) {
+ this.showNotification('权限检查失败: ' + error.message, 'error');
+ }
+ }
+
+ displayPermissionCheck(permissionCheck, summary) {
+ const container = document.getElementById('fieldMappingContent');
+
+ let html = '';
+
+ // 整体状态
+ const statusClass = permissionCheck.success ? 'success' : 'danger';
+ const statusIcon = permissionCheck.success ? 'check-circle' : 'exclamation-triangle';
+ html += `
+
+ 整体状态: ${permissionCheck.success ? '正常' : '异常'}
+
`;
+
+ // 检查项目
+ html += '
检查项目:
';
+ for (const [checkName, checkResult] of Object.entries(permissionCheck.checks)) {
+ const statusClass = checkResult.status === 'success' ? 'success' :
+ checkResult.status === 'warning' ? 'warning' : 'danger';
+ const statusIcon = checkResult.status === 'success' ? 'check-circle' :
+ checkResult.status === 'warning' ? 'exclamation-triangle' : 'times-circle';
+
+ html += `
+
+ ${checkName}: ${checkResult.message}
+
`;
+ }
+
+ // 修复建议
+ if (permissionCheck.recommendations && permissionCheck.recommendations.length > 0) {
+ html += '
修复建议:
';
+ permissionCheck.recommendations.forEach(rec => {
+ html += `- ${rec}
`;
+ });
+ html += '
';
+ }
+
+ // 错误信息
+ if (permissionCheck.errors && permissionCheck.errors.length > 0) {
+ html += '
错误信息:
';
+ permissionCheck.errors.forEach(error => {
+ html += `- ${error}
`;
+ });
+ html += '
';
+ }
+
+ html += '
';
+
+ container.innerHTML = html;
+
+ // 显示字段映射管理区域
+ const section = document.getElementById('fieldMappingSection');
+ section.style.display = 'block';
+ }
+
+ showNotification(message, type = 'info') {
+ const container = document.getElementById('notificationContainer');
+ const alert = document.createElement('div');
+ alert.className = `alert alert-${type === 'error' ? 'danger' : type} alert-dismissible fade show`;
+ alert.innerHTML = `
+ ${message}
+
+ `;
+
+ container.appendChild(alert);
+
+ setTimeout(() => {
+ if (alert.parentNode) {
+ alert.parentNode.removeChild(alert);
+ }
+ }, 5000);
+ }
+}
diff --git a/src/web/static/js/modules/knowledge.js b/src/web/static/js/modules/knowledge.js
new file mode 100644
index 0000000..e0ee71d
--- /dev/null
+++ b/src/web/static/js/modules/knowledge.js
@@ -0,0 +1,699 @@
+// 知识库管理模块
+Object.assign(TSPDashboard.prototype, {
+ async loadKnowledge(page = 1) {
+ // 委托给租户列表视图
+ this.loadKnowledgeTenantList();
+ },
+
+ // Task 6.1: 加载租户列表视图
+ async loadKnowledgeTenantList() {
+ this.knowledgeCurrentTenantId = null;
+ this.renderKnowledgeBreadcrumb(null);
+
+ // 显示租户列表容器,隐藏详情容器
+ const tenantListEl = document.getElementById('knowledge-tenant-list');
+ const tenantDetailEl = document.getElementById('knowledge-tenant-detail');
+ const searchBar = document.getElementById('knowledge-search-bar');
+ const addBtn = document.getElementById('knowledge-add-btn');
+ const uploadBtn = document.getElementById('knowledge-upload-btn');
+
+ tenantListEl.style.display = '';
+ tenantDetailEl.style.display = 'none';
+ if (searchBar) searchBar.style.display = 'none';
+ if (addBtn) addBtn.style.display = 'none';
+ if (uploadBtn) uploadBtn.style.display = 'none';
+
+ // 显示加载中
+ tenantListEl.innerHTML = '';
+
+ // 加载全局统计
+ this.loadKnowledgeStats(null);
+
+ try {
+ const response = await fetch('/api/knowledge/tenants');
+ const tenants = await response.json();
+
+ if (!Array.isArray(tenants) || tenants.length === 0) {
+ tenantListEl.innerHTML = '';
+ return;
+ }
+
+ const cardsHtml = tenants.map(t => `
+
+
+
+
${t.tenant_id}
+
+
+ 知识条目
+
${t.entry_count}
+
+
+ 已验证
+
${t.verified_count}
+
+
+
+
${t.entry_count > 0 ? Math.round(t.verified_count / t.entry_count * 100) : 0}% 已验证
+
+
+
+ `).join('');
+
+ tenantListEl.innerHTML = cardsHtml;
+ } catch (error) {
+ console.error('加载租户列表失败:', error);
+ tenantListEl.innerHTML = ' 加载失败
';
+ this.showNotification('加载租户列表失败', 'error');
+ }
+ },
+
+ // Task 6.2: 刷新按钮
+ refreshKnowledge() {
+ if (this.knowledgeCurrentTenantId) {
+ this.loadKnowledgeTenantDetail(this.knowledgeCurrentTenantId, this.paginationConfig.currentKnowledgePage);
+ } else {
+ this.loadKnowledgeTenantList();
+ }
+ },
+
+ // Task 7.1: 加载租户详情视图
+ async loadKnowledgeTenantDetail(tenantId, page = 1) {
+ this.knowledgeCurrentTenantId = tenantId;
+ this.paginationConfig.currentKnowledgePage = page;
+ this.renderKnowledgeBreadcrumb(tenantId);
+
+ // 隐藏租户列表,显示详情容器
+ const tenantListEl = document.getElementById('knowledge-tenant-list');
+ const tenantDetailEl = document.getElementById('knowledge-tenant-detail');
+ const searchBar = document.getElementById('knowledge-search-bar');
+ const addBtn = document.getElementById('knowledge-add-btn');
+ const uploadBtn = document.getElementById('knowledge-upload-btn');
+
+ tenantListEl.style.display = 'none';
+ tenantDetailEl.style.display = '';
+ if (searchBar) searchBar.style.display = '';
+ if (addBtn) addBtn.style.display = '';
+ if (uploadBtn) uploadBtn.style.display = '';
+
+ // 清空搜索框
+ const searchInput = document.getElementById('knowledge-search');
+ if (searchInput) searchInput.value = '';
+
+ // 加载租户级统计
+ this.loadKnowledgeStats(tenantId);
+
+ const listEl = document.getElementById('knowledge-list');
+ listEl.innerHTML = '
';
+
+ try {
+ const pageSize = this.getPageSize('knowledge-pagination');
+ const categoryFilter = document.getElementById('knowledge-category-filter')?.value || '';
+ const verifiedFilter = document.getElementById('knowledge-verified-filter')?.value || '';
+
+ let url = `/api/knowledge?tenant_id=${encodeURIComponent(tenantId)}&page=${page}&per_page=${pageSize}`;
+ if (categoryFilter) url += `&category=${encodeURIComponent(categoryFilter)}`;
+ if (verifiedFilter) url += `&verified=${encodeURIComponent(verifiedFilter)}`;
+
+ const response = await fetch(url);
+ const data = await response.json();
+
+ if (data.knowledge) {
+ this.updateKnowledgeDisplay(data.knowledge);
+ this.updateKnowledgeTenantPagination(data);
+ } else {
+ this.updateKnowledgeDisplay(data);
+ }
+ } catch (error) {
+ console.error('加载租户详情失败:', error);
+ listEl.innerHTML = ' 加载失败
';
+ this.showNotification('加载租户详情失败', 'error');
+ }
+ },
+
+ // Task 7.1: 填充分类筛选下拉框(从统计数据获取完整分类列表)
+ populateCategoryFilter(categories) {
+ const select = document.getElementById('knowledge-category-filter');
+ if (!select) return;
+ const currentValue = select.value;
+ const sorted = categories.slice().sort();
+ const existingOptions = Array.from(select.options).slice(1).map(o => o.value);
+ if (JSON.stringify(sorted) !== JSON.stringify(existingOptions)) {
+ select.innerHTML = '' +
+ sorted.map(c => ``).join('');
+ }
+ },
+
+ // Task 7.1: 从租户卡片进入详情(重置筛选条件)
+ openTenantDetail(tenantId) {
+ const categoryFilterEl = document.getElementById('knowledge-category-filter');
+ const verifiedFilterEl = document.getElementById('knowledge-verified-filter');
+ if (categoryFilterEl) categoryFilterEl.value = '';
+ if (verifiedFilterEl) verifiedFilterEl.value = '';
+ this.loadKnowledgeTenantDetail(tenantId, 1);
+ },
+
+ // Task 7.1: 应用筛选条件
+ applyKnowledgeFilters() {
+ if (this.knowledgeCurrentTenantId) {
+ this.loadKnowledgeTenantDetail(this.knowledgeCurrentTenantId, 1);
+ }
+ },
+
+ // Task 7.1: 租户详情分页
+ updateKnowledgeTenantPagination(data) {
+ this.createPaginationComponent(data, 'knowledge-pagination', 'loadKnowledgeTenantDetailPage', '条知识');
+ },
+
+ // Task 7.2: 面包屑导航
+ renderKnowledgeBreadcrumb(tenantId) {
+ const breadcrumbEl = document.getElementById('knowledge-breadcrumb');
+ if (!breadcrumbEl) return;
+
+ if (!tenantId) {
+ breadcrumbEl.innerHTML = '知识库
';
+ } else {
+ breadcrumbEl.innerHTML = `
+
+ `;
+ }
+ },
+
+ // Task 7.3 / 8.2: 刷新当前知识库视图的辅助方法
+ async refreshKnowledgeCurrentView() {
+ if (this.knowledgeCurrentTenantId) {
+ await this.loadKnowledgeTenantDetail(this.knowledgeCurrentTenantId, this.paginationConfig.currentKnowledgePage);
+ } else {
+ await this.loadKnowledgeTenantList();
+ }
+ },
+
+ // Task 8.2: 加载知识库统计(支持租户级别)
+ async loadKnowledgeStats(tenantId) {
+ try {
+ let url = '/api/knowledge/stats';
+ if (tenantId) {
+ url += `?tenant_id=${encodeURIComponent(tenantId)}`;
+ }
+ const response = await fetch(url);
+ const knowledge = await response.json();
+
+ const totalEl = document.getElementById('knowledge-total');
+ const activeEl = document.getElementById('knowledge-active');
+ const confidenceBarEl = document.getElementById('knowledge-confidence-bar');
+
+ if (totalEl) totalEl.textContent = knowledge.total_entries || 0;
+ if (activeEl) activeEl.textContent = knowledge.active_entries || 0;
+
+ const confidencePercent = Math.round((knowledge.average_confidence || 0) * 100);
+ if (confidenceBarEl) {
+ confidenceBarEl.style.width = `${confidencePercent}%`;
+ confidenceBarEl.setAttribute('aria-valuenow', confidencePercent);
+ confidenceBarEl.textContent = `${confidencePercent}%`;
+ }
+
+ // Task 7.1: 在租户详情视图中,用统计数据填充分类筛选下拉框
+ if (tenantId && knowledge.category_distribution) {
+ this.populateCategoryFilter(Object.keys(knowledge.category_distribution));
+ }
+ } catch (error) {
+ console.error('加载知识库统计失败:', error);
+ }
+ },
+
+ updateKnowledgeDisplay(knowledge) {
+ const container = document.getElementById('knowledge-list');
+
+ if (knowledge.length === 0) {
+ container.innerHTML = '';
+ return;
+ }
+
+ // 添加知识库列表的批量操作头部
+ const headerHtml = `
+
+
+
+
+
+
+
+
+
+
+
+ `;
+
+ const knowledgeHtml = knowledge.map(item => `
+
+
+
+
+
+
${item.question}
+
${item.answer}
+
+ 分类: ${item.category}
+ 置信度: ${Math.round(item.confidence_score * 100)}%
+ 使用次数: ${item.usage_count || 0}
+
+ ${item.is_verified ? '已验证' : '未验证'}
+
+
+
+
+
+
+ ${item.is_verified ?
+ `` :
+ ``
+ }
+
+
+
+
+
+ `).join('');
+
+ container.innerHTML = headerHtml + knowledgeHtml;
+ },
+
+ updateKnowledgePagination(data) {
+ this.createPaginationComponent(data, 'knowledge-pagination', 'loadKnowledge', '条知识');
+ },
+
+ async searchKnowledge() {
+ const query = document.getElementById('knowledge-search').value.trim();
+ if (!query) {
+ // Task 8.1: 清空搜索时恢复当前租户的分页列表
+ if (this.knowledgeCurrentTenantId) {
+ this.loadKnowledgeTenantDetail(this.knowledgeCurrentTenantId);
+ } else {
+ this.loadKnowledge();
+ }
+ return;
+ }
+
+ try {
+ // Task 8.1: 搜索时附加 tenant_id 参数
+ let url = `/api/knowledge/search?q=${encodeURIComponent(query)}`;
+ if (this.knowledgeCurrentTenantId) {
+ url += `&tenant_id=${encodeURIComponent(this.knowledgeCurrentTenantId)}`;
+ }
+ const response = await fetch(url);
+ const results = await response.json();
+ this.updateKnowledgeDisplay(results);
+ // 搜索结果不显示分页
+ const paginationEl = document.getElementById('knowledge-pagination');
+ if (paginationEl) paginationEl.innerHTML = '';
+ } 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 {
+ // Task 7.3: 添加知识条目时自动设置 tenant_id
+ const body = {
+ question,
+ answer,
+ category,
+ confidence_score: confidence
+ };
+ if (this.knowledgeCurrentTenantId) {
+ body.tenant_id = this.knowledgeCurrentTenantId;
+ }
+ const response = await fetch('/api/knowledge', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify(body)
+ });
+
+ const data = await response.json();
+ if (data.success) {
+ this.showNotification('知识添加成功', 'success');
+ bootstrap.Modal.getInstance(document.getElementById('addKnowledgeModal')).hide();
+ document.getElementById('knowledge-form').reset();
+ // Task 7.3: 刷新当前视图
+ if (this.knowledgeCurrentTenantId) {
+ this.loadKnowledgeTenantDetail(this.knowledgeCurrentTenantId, this.paginationConfig.currentKnowledgePage);
+ } else {
+ this.loadKnowledgeTenantList();
+ }
+ } 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.refreshKnowledgeCurrentView();
+ } 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.refreshKnowledgeCurrentView();
+ } 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.refreshKnowledgeCurrentView();
+ } 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');
+
+ // Task 7.3: 删除后检查是否所有条目已删除,若是则返回租户列表
+ if (this.knowledgeCurrentTenantId) {
+ const checkResp = await fetch(`/api/knowledge?tenant_id=${encodeURIComponent(this.knowledgeCurrentTenantId)}&page=1&per_page=1`);
+ const checkData = await checkResp.json();
+ if (checkData.total === 0) {
+ this.showNotification('该租户下已无知识条目,返回租户列表', 'info');
+ this.loadKnowledgeTenantList();
+ return;
+ }
+ }
+
+ this.refreshKnowledgeCurrentView();
+ } else {
+ this.showNotification('知识库删除失败', 'error');
+ }
+ } catch (error) {
+ console.error('删除知识库失败:', error);
+ this.showNotification('删除知识库失败', 'error');
+ }
+ },
+
+ // 知识库批量删除功能
+ toggleSelectAllKnowledge() {
+ const selectAllCheckbox = document.getElementById('select-all-knowledge');
+ const knowledgeCheckboxes = document.querySelectorAll('.knowledge-checkbox');
+
+ knowledgeCheckboxes.forEach(checkbox => {
+ checkbox.checked = selectAllCheckbox.checked;
+ });
+
+ this.updateBatchDeleteKnowledgeButton();
+ },
+
+ updateBatchDeleteKnowledgeButton() {
+ const selectedCheckboxes = document.querySelectorAll('.knowledge-checkbox:checked');
+ const batchDeleteBtn = document.getElementById('batch-delete-knowledge');
+ const batchVerifyBtn = document.getElementById('batch-verify-knowledge');
+ const batchUnverifyBtn = document.getElementById('batch-unverify-knowledge');
+ const hasSelection = selectedCheckboxes.length > 0;
+
+ if (batchDeleteBtn) {
+ batchDeleteBtn.disabled = !hasSelection;
+ batchDeleteBtn.innerHTML = hasSelection
+ ? `批量删除 (${selectedCheckboxes.length})`
+ : '批量删除';
+ }
+ if (batchVerifyBtn) {
+ batchVerifyBtn.disabled = !hasSelection;
+ }
+ if (batchUnverifyBtn) {
+ batchUnverifyBtn.disabled = !hasSelection;
+ }
+ },
+
+ async batchDeleteKnowledge() {
+ const selectedCheckboxes = document.querySelectorAll('.knowledge-checkbox:checked');
+ const selectedIds = Array.from(selectedCheckboxes).map(cb => parseInt(cb.value));
+
+ if (selectedIds.length === 0) {
+ this.showNotification('请选择要删除的知识库条目', 'warning');
+ return;
+ }
+
+ if (!confirm(`确定要删除选中的 ${selectedIds.length} 个知识库条目吗?此操作不可撤销。`)) {
+ return;
+ }
+
+ try {
+ const response = await fetch('/api/batch-delete/knowledge', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({ ids: selectedIds })
+ });
+
+ const data = await response.json();
+
+ if (data.success) {
+ this.showNotification(data.message, 'success');
+
+ // 清除缓存并强制刷新
+ this.cache.delete('knowledge');
+
+ // Task 7.3: 删除后检查是否所有条目已删除,若是则返回租户列表
+ if (this.knowledgeCurrentTenantId) {
+ const checkResp = await fetch(`/api/knowledge?tenant_id=${encodeURIComponent(this.knowledgeCurrentTenantId)}&page=1&per_page=1`);
+ const checkData = await checkResp.json();
+ if (checkData.total === 0) {
+ this.showNotification('该租户下已无知识条目,返回租户列表', 'info');
+ this.loadKnowledgeTenantList();
+ return;
+ }
+ }
+
+ await this.refreshKnowledgeCurrentView();
+
+ // 重置批量操作按钮状态
+ this.updateBatchDeleteKnowledgeButton();
+ } else {
+ this.showNotification(data.error || '批量删除失败', 'error');
+ }
+ } catch (error) {
+ console.error('批量删除知识库条目失败:', error);
+ this.showNotification('批量删除知识库条目失败', 'error');
+ }
+ },
+
+ // Task 7.3: 批量验证知识条目
+ async batchVerifyKnowledge() {
+ const selectedCheckboxes = document.querySelectorAll('.knowledge-checkbox:checked');
+ const selectedIds = Array.from(selectedCheckboxes).map(cb => parseInt(cb.value));
+
+ if (selectedIds.length === 0) {
+ this.showNotification('请选择要验证的知识库条目', 'warning');
+ return;
+ }
+
+ try {
+ const response = await fetch('/api/knowledge/batch_verify', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ ids: selectedIds })
+ });
+
+ const data = await response.json();
+ if (data.success) {
+ this.showNotification(data.message || '批量验证成功', 'success');
+ await this.refreshKnowledgeCurrentView();
+ this.updateBatchDeleteKnowledgeButton();
+ } else {
+ this.showNotification(data.error || '批量验证失败', 'error');
+ }
+ } catch (error) {
+ console.error('批量验证知识库条目失败:', error);
+ this.showNotification('批量验证知识库条目失败', 'error');
+ }
+ },
+
+ // Task 7.3: 批量取消验证知识条目
+ async batchUnverifyKnowledge() {
+ const selectedCheckboxes = document.querySelectorAll('.knowledge-checkbox:checked');
+ const selectedIds = Array.from(selectedCheckboxes).map(cb => parseInt(cb.value));
+
+ if (selectedIds.length === 0) {
+ this.showNotification('请选择要取消验证的知识库条目', 'warning');
+ return;
+ }
+
+ try {
+ const response = await fetch('/api/knowledge/batch_unverify', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ ids: selectedIds })
+ });
+
+ const data = await response.json();
+ if (data.success) {
+ this.showNotification(data.message || '批量取消验证成功', 'success');
+ await this.refreshKnowledgeCurrentView();
+ this.updateBatchDeleteKnowledgeButton();
+ } else {
+ this.showNotification(data.error || '批量取消验证失败', 'error');
+ }
+ } catch (error) {
+ console.error('批量取消验证知识库条目失败:', error);
+ this.showNotification('批量取消验证知识库条目失败', 'error');
+ }
+ }
+});
diff --git a/src/web/static/js/modules/monitoring.js b/src/web/static/js/modules/monitoring.js
new file mode 100644
index 0000000..41ebc4a
--- /dev/null
+++ b/src/web/static/js/modules/monitoring.js
@@ -0,0 +1,455 @@
+// 监控模块(Token监控 + AI监控)
+Object.assign(TSPDashboard.prototype, {
+ 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');
+
+ 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'
+ }]
+ },
+ options: {
+ responsive: true,
+ maintainAspectRatio: false,
+ scales: {
+ y: {
+ type: 'linear',
+ display: true,
+ position: 'left',
+ title: {
+ display: true,
+ text: 'Token数量'
+ }
+ },
+ y1: {
+ type: 'linear',
+ display: true,
+ position: 'right',
+ title: {
+ display: true,
+ 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 = `
+
+ | 暂无记录 |
+
+ `;
+ return;
+ }
+
+ const html = records.map(record => `
+
+ | ${new Date(record.timestamp).toLocaleString()} |
+ ${record.user_id || '匿名'} |
+ ${record.model || 'qwen-turbo'} |
+ ${record.input_tokens || 0} |
+ ${record.output_tokens || 0} |
+ ${record.total_tokens || 0} |
+ ¥${record.cost || 0} |
+ ${record.response_time || 0}ms |
+
+ `).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 = `
+
+ | 暂无错误记录 |
+
+ `;
+ return;
+ }
+
+ const html = errors.map(error => `
+
+ | ${new Date(error.timestamp).toLocaleString()} |
+ ${error.error_type || '未知'} |
+ ${error.error_message || ''} |
+ ${error.model || 'qwen-turbo'} |
+ ${error.user_id || '匿名'} |
+
+
+ |
+
+ `).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 viewErrorDetail(convId) {
+ const modal = new bootstrap.Modal(document.getElementById('errorDetailModal'));
+ const body = document.getElementById('errorDetailBody');
+ body.innerHTML = '加载中...
';
+ modal.show();
+
+ try {
+ const response = await fetch(`/api/ai-monitor/error-log/${convId}`);
+ const data = await response.json();
+
+ if (data.success) {
+ const d = data.detail;
+ body.innerHTML = `
+
+ | 记录ID | ${d.id} |
+ | 时间 | ${d.timestamp ? new Date(d.timestamp).toLocaleString() : '-'} |
+ | 分类 | ${d.category || '-'} |
+ | 来源 | ${d.source || '-'} |
+ | 置信度 | ${d.confidence_score != null ? d.confidence_score : '-'} |
+ | 响应时间 | ${d.response_time != null ? d.response_time + ' ms' : '-'} |
+ | 用户消息 | ${this.escapeHtml(d.user_message || '-')} |
+ | 助手回复 | ${this.escapeHtml(d.assistant_response || '-')} |
+
+ `;
+ } else {
+ body.innerHTML = `${data.error || '加载失败'}
`;
+ }
+ } catch (error) {
+ body.innerHTML = `请求失败: ${error.message}
`;
+ }
+ },
+
+ escapeHtml(text) {
+ const div = document.createElement('div');
+ div.textContent = text;
+ return div.innerHTML;
+ }
+});
diff --git a/src/web/static/js/modules/system.js b/src/web/static/js/modules/system.js
new file mode 100644
index 0000000..771d556
--- /dev/null
+++ b/src/web/static/js/modules/system.js
@@ -0,0 +1,1388 @@
+// 系统管理模块(系统优化 + 设置 + 数据分析)
+Object.assign(TSPDashboard.prototype, {
+ 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(data.message || 'CPU优化完成', 'success');
+ this.updateOptimizationProgress('cpu-optimization', data.progress || 100);
+ // 刷新状态并回落进度条
+ await this.loadSystemOptimizer();
+ setTimeout(() => this.updateOptimizationProgress('cpu-optimization', 0), 1500);
+ } else {
+ throw new Error(data.error || 'CPU优化失败');
+ }
+ } 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(data.message || '内存优化完成', 'success');
+ this.updateOptimizationProgress('memory-optimization', data.progress || 100);
+ await this.loadSystemOptimizer();
+ setTimeout(() => this.updateOptimizationProgress('memory-optimization', 0), 1500);
+ } else {
+ throw new Error(data.error || '内存优化失败');
+ }
+ } 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(data.message || '磁盘优化完成', 'success');
+ this.updateOptimizationProgress('disk-optimization', data.progress || 100);
+ await this.loadSystemOptimizer();
+ setTimeout(() => this.updateOptimizationProgress('disk-optimization', 0), 1500);
+ } else {
+ throw new Error(data.error || '磁盘优化失败');
+ }
+ } 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 clearCache() {
+ try {
+ const response = await fetch('/api/system-optimizer/clear-cache', { method: 'POST' });
+ const data = await response.json();
+ if (data.success) {
+ this.showNotification(data.message || '缓存已清理', 'success');
+ await this.loadSystemOptimizer();
+ } else {
+ throw new Error(data.error || '清理缓存失败');
+ }
+ } catch (error) {
+ console.error('清理缓存失败:', error);
+ this.showNotification('清理缓存失败: ' + error.message, 'error');
+ }
+ },
+
+ async optimizeAll() {
+ try {
+ const response = await fetch('/api/system-optimizer/optimize-all', { method: 'POST' });
+ const data = await response.json();
+ if (data.success) {
+ this.showNotification(data.message || '一键优化完成', 'success');
+ await this.loadSystemOptimizer();
+ ['cpu-optimization','memory-optimization','disk-optimization'].forEach(id => this.updateOptimizationProgress(id, 100));
+ setTimeout(() => ['cpu-optimization','memory-optimization','disk-optimization'].forEach(id => this.updateOptimizationProgress(id, 0)), 1500);
+ } else {
+ throw new Error(data.error || '一键优化失败');
+ }
+ } catch (error) {
+ console.error('一键优化失败:', error);
+ this.showNotification('一键优化失败: ' + error.message, 'error');
+ }
+ },
+
+ async loadSecuritySettings() {
+ try {
+ const response = await fetch('/api/system-optimizer/security-settings');
+ 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.updateStatisticsCards(analytics); // 添加统计卡片更新
+ this.initializeCharts();
+ } catch (error) {
+ console.error('加载分析数据失败:', error);
+ }
+ },
+
+ // 初始化图表
+ initializeCharts() {
+ if (!this.charts) {
+ this.charts = {};
+ }
+ this.updateCharts();
+ },
+
+ // 清理连接
+ cleanupConnections() {
+ // 关闭WebSocket连接
+ if (this.websocket) {
+ this.websocket.close();
+ this.websocket = null;
+ }
+
+ // 清理所有定时器
+ Object.values(this.refreshIntervals).forEach(interval => {
+ clearInterval(interval);
+ });
+ this.refreshIntervals = {};
+ },
+
+ // 销毁所有图表
+ destroyAllCharts() {
+ if (!this.charts) return;
+
+ Object.keys(this.charts).forEach(chartId => {
+ if (this.charts[chartId]) {
+ try {
+ this.charts[chartId].destroy();
+ } catch (e) {
+ console.warn(`Error destroying chart ${chartId}:`, e);
+ }
+ this.charts[chartId] = null;
+ }
+ });
+
+ // 清理charts对象
+ this.charts = {};
+ },
+
+ // 安全的图表创建方法
+ createChart(canvasId, chartConfig) {
+ const canvas = document.getElementById(canvasId);
+ if (!canvas) {
+ console.error(`Canvas element '${canvasId}' not found`);
+ return null;
+ }
+
+ // 确保charts对象存在
+ if (!this.charts) {
+ this.charts = {};
+ }
+
+ // 销毁现有图表
+ if (this.charts[canvasId]) {
+ try {
+ this.charts[canvasId].destroy();
+ } catch (e) {
+ console.warn(`Error destroying chart ${canvasId}:`, e);
+ }
+ this.charts[canvasId] = null;
+ }
+
+ try {
+ const ctx = canvas.getContext('2d');
+ this.charts[canvasId] = new Chart(ctx, chartConfig);
+ return this.charts[canvasId];
+ } catch (e) {
+ console.error(`Error creating chart ${canvasId}:`, e);
+ return null;
+ }
+ },
+
+ // 更新所有图表
+ 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 inProgress = data.workorders?.in_progress || 0;
+ const resolved = data.workorders?.resolved || 0;
+ const avgSatisfaction = data.satisfaction?.average || 0;
+
+ // 更新工单统计数字(使用正确的元素ID)
+ if (document.getElementById('workorders-total')) {
+ document.getElementById('workorders-total').textContent = total;
+ }
+ if (document.getElementById('workorders-open')) {
+ document.getElementById('workorders-open').textContent = open;
+ }
+ if (document.getElementById('workorders-progress')) {
+ document.getElementById('workorders-progress').textContent = inProgress;
+ }
+ if (document.getElementById('workorders-resolved')) {
+ document.getElementById('workorders-resolved').textContent = resolved;
+ }
+
+ // 同时更新其他可能的元素ID
+ if (document.getElementById('totalWorkorders')) {
+ document.getElementById('totalWorkorders').textContent = total;
+ }
+ if (document.getElementById('openWorkorders')) {
+ document.getElementById('openWorkorders').textContent = open;
+ }
+ if (document.getElementById('resolvedWorkorders')) {
+ document.getElementById('resolvedWorkorders').textContent = resolved;
+ }
+ document.getElementById('avgSatisfaction').textContent = avgSatisfaction.toFixed(1);
+
+ // 更新预警统计
+ const alertTotal = data.alerts?.total || 0;
+ const alertActive = data.alerts?.active || 0;
+ const alertCritical = data.alerts?.by_level?.critical || 0;
+ const alertWarning = data.alerts?.by_level?.warning || 0;
+ const alertError = data.alerts?.by_level?.error || 0;
+
+ // 更新预警统计显示
+ if (document.getElementById('critical-alerts')) {
+ document.getElementById('critical-alerts').textContent = alertCritical;
+ }
+ if (document.getElementById('warning-alerts')) {
+ document.getElementById('warning-alerts').textContent = alertWarning;
+ }
+ if (document.getElementById('error-alerts')) {
+ document.getElementById('error-alerts').textContent = alertError;
+ }
+ if (document.getElementById('total-alerts-count')) {
+ document.getElementById('total-alerts-count').textContent = alertTotal;
+ }
+
+ // 更新性能统计
+ const performanceScore = data.performance?.score || 0;
+ const performanceTrend = data.performance?.trend || 'stable';
+
+ if (document.getElementById('performance-score')) {
+ document.getElementById('performance-score').textContent = performanceScore.toFixed(1);
+ }
+ if (document.getElementById('performance-trend')) {
+ document.getElementById('performance-trend').textContent = this.getPerformanceTrendText(performanceTrend);
+ }
+
+ // 更新满意度统计
+ const satisfactionAvg = data.satisfaction?.average || 0;
+ const satisfactionCount = data.satisfaction?.count || 0;
+
+ if (document.getElementById('satisfaction-avg')) {
+ document.getElementById('satisfaction-avg').textContent = satisfactionAvg.toFixed(1);
+ }
+ if (document.getElementById('satisfaction-count')) {
+ document.getElementById('satisfaction-count').textContent = satisfactionCount;
+ }
+
+ // 更新进度条
+ 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 chartData = this.prepareChartData(data, chartType);
+
+ const chartConfig = {
+ 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: '数量'
+ }
+ }
+ }
+ }
+ };
+
+ this.createChart('mainChart', chartConfig);
+ },
+
+ // 更新分布图表
+ updateDistributionChart(data) {
+ const currentDimension = document.getElementById('dataDimension')?.value || 'workorders';
+
+ let labels, values, title, backgroundColor;
+
+ if (currentDimension === 'alerts') {
+ // 预警级别分布
+ const alertLevels = data.alerts?.by_level || {};
+ labels = Object.keys(alertLevels);
+ values = Object.values(alertLevels);
+ title = '预警级别分布';
+ backgroundColor = [
+ '#FF6384', // critical - 红色
+ '#FFCE56', // warning - 黄色
+ '#36A2EB', // error - 蓝色
+ '#4BC0C0', // info - 青色
+ '#9966FF' // 其他
+ ];
+ } else if (currentDimension === 'performance') {
+ // 性能指标分布
+ const performanceMetrics = data.performance?.by_level || {};
+ labels = Object.keys(performanceMetrics);
+ values = Object.values(performanceMetrics);
+ title = '性能指标分布';
+ backgroundColor = [
+ '#28a745', // 优秀 - 绿色
+ '#ffc107', // 良好 - 黄色
+ '#fd7e14', // 一般 - 橙色
+ '#dc3545' // 差 - 红色
+ ];
+ } else if (currentDimension === 'satisfaction') {
+ // 满意度分布
+ const satisfactionLevels = data.satisfaction?.by_level || {};
+ labels = Object.keys(satisfactionLevels);
+ values = Object.values(satisfactionLevels);
+ title = '满意度分布';
+ backgroundColor = [
+ '#28a745', // 非常满意 - 绿色
+ '#ffc107', // 满意 - 黄色
+ '#fd7e14', // 一般 - 橙色
+ '#dc3545' // 不满意 - 红色
+ ];
+ } else {
+ // 工单分类分布
+ const categories = data.workorders?.by_category || {};
+ labels = Object.keys(categories);
+ values = Object.values(categories);
+ title = '工单分类分布';
+ backgroundColor = [
+ '#FF6384',
+ '#36A2EB',
+ '#FFCE56',
+ '#4BC0C0',
+ '#9966FF',
+ '#FF9F40'
+ ];
+ }
+
+ const chartConfig = {
+ type: 'doughnut',
+ data: {
+ labels: labels,
+ datasets: [{
+ data: values,
+ backgroundColor: backgroundColor
+ }]
+ },
+ options: {
+ responsive: true,
+ maintainAspectRatio: false,
+ plugins: {
+ title: {
+ display: true,
+ text: title
+ },
+ legend: {
+ display: true,
+ position: 'bottom'
+ }
+ }
+ }
+ };
+
+ this.createChart('distributionChart', chartConfig);
+ },
+
+ // 更新趋势图表
+ 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 currentDimension = document.getElementById('dataDimension')?.value || 'workorders';
+
+ let labels, values, title, backgroundColor, label;
+
+ if (currentDimension === 'alerts') {
+ // 预警严重程度分布
+ const alertSeverities = data.alerts?.by_severity || {};
+ labels = Object.keys(alertSeverities).map(s => this.getSeverityText(s));
+ values = Object.values(alertSeverities);
+ title = '预警严重程度分布';
+ label = '预警数量';
+ backgroundColor = [
+ '#28a745', // low - 绿色
+ '#ffc107', // medium - 黄色
+ '#fd7e14', // high - 橙色
+ '#dc3545' // critical - 红色
+ ];
+ } else if (currentDimension === 'performance') {
+ // 性能指标分布
+ const performanceMetrics = data.performance?.by_metric || {};
+ labels = Object.keys(performanceMetrics);
+ values = Object.values(performanceMetrics);
+ title = '性能指标分布';
+ label = '性能值';
+ backgroundColor = [
+ '#28a745', // 优秀 - 绿色
+ '#ffc107', // 良好 - 黄色
+ '#fd7e14', // 一般 - 橙色
+ '#dc3545' // 差 - 红色
+ ];
+ } else if (currentDimension === 'satisfaction') {
+ // 满意度分布
+ const satisfactionLevels = data.satisfaction?.by_level || {};
+ labels = Object.keys(satisfactionLevels).map(s => this.getSatisfactionText(s));
+ values = Object.values(satisfactionLevels);
+ title = '满意度分布';
+ label = '满意度数量';
+ backgroundColor = [
+ '#28a745', // 非常满意 - 绿色
+ '#ffc107', // 满意 - 黄色
+ '#fd7e14', // 一般 - 橙色
+ '#dc3545' // 不满意 - 红色
+ ];
+ } else {
+ // 工单优先级分布
+ const priorities = data.workorders?.by_priority || {};
+ labels = Object.keys(priorities).map(p => this.getPriorityText(p));
+ values = Object.values(priorities);
+ title = '工单优先级分布';
+ label = '工单数量';
+ backgroundColor = [
+ '#28a745', // 低 - 绿色
+ '#ffc107', // 中 - 黄色
+ '#fd7e14', // 高 - 橙色
+ '#dc3545' // 紧急 - 红色
+ ];
+ }
+
+ this.charts.priorityChart = new Chart(ctx, {
+ type: 'bar',
+ data: {
+ labels: labels,
+ datasets: [{
+ label: label,
+ data: values,
+ backgroundColor: backgroundColor
+ }]
+ },
+ options: {
+ responsive: true,
+ maintainAspectRatio: false,
+ plugins: {
+ title: {
+ display: true,
+ text: title
+ }
+ },
+ 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);
+ const alerts = trendData.map(item => item.alerts);
+ const performance = trendData.map(item => item.performance || 0);
+ const satisfaction = trendData.map(item => item.satisfaction || 0);
+
+ if (chartType === 'pie' || chartType === 'doughnut') {
+ // 根据数据维度选择显示内容
+ const currentDimension = document.getElementById('dataDimension')?.value || 'workorders';
+
+ if (currentDimension === 'alerts') {
+ const alertLevels = data.alerts?.by_level || {};
+ return {
+ labels: Object.keys(alertLevels),
+ datasets: [{
+ data: Object.values(alertLevels),
+ backgroundColor: [
+ '#FF6384', // critical - 红色
+ '#FFCE56', // warning - 黄色
+ '#36A2EB', // error - 蓝色
+ '#4BC0C0', // info - 青色
+ '#9966FF' // 其他
+ ]
+ }]
+ };
+ } else if (currentDimension === 'performance') {
+ // 性能指标分布
+ const performanceMetrics = data.performance || {};
+ return {
+ labels: Object.keys(performanceMetrics),
+ datasets: [{
+ data: Object.values(performanceMetrics),
+ backgroundColor: [
+ '#28a745', // 优秀 - 绿色
+ '#ffc107', // 良好 - 黄色
+ '#fd7e14', // 一般 - 橙色
+ '#dc3545' // 差 - 红色
+ ]
+ }]
+ };
+ } else if (currentDimension === 'satisfaction') {
+ // 满意度分布
+ const satisfactionLevels = data.satisfaction?.by_level || {};
+ return {
+ labels: Object.keys(satisfactionLevels),
+ datasets: [{
+ data: Object.values(satisfactionLevels),
+ backgroundColor: [
+ '#28a745', // 非常满意 - 绿色
+ '#ffc107', // 满意 - 黄色
+ '#fd7e14', // 一般 - 橙色
+ '#dc3545' // 不满意 - 红色
+ ]
+ }]
+ };
+ } else {
+ const categories = data.workorders?.by_category || {};
+ return {
+ labels: Object.keys(categories),
+ datasets: [{
+ data: Object.values(categories),
+ backgroundColor: [
+ '#FF6384',
+ '#36A2EB',
+ '#FFCE56',
+ '#4BC0C0',
+ '#9966FF',
+ '#FF9F40'
+ ]
+ }]
+ };
+ }
+ } else {
+ // 线图和柱状图根据数据维度显示不同内容
+ const currentDimension = document.getElementById('dataDimension')?.value || 'workorders';
+ const datasets = [];
+
+ if (currentDimension === 'performance') {
+ // 性能指标图表
+ datasets.push({
+ label: '性能指标',
+ data: performance,
+ borderColor: '#28a745',
+ backgroundColor: 'rgba(40, 167, 69, 0.1)',
+ tension: chartType === 'line' ? 0.4 : 0
+ });
+ } else if (currentDimension === 'satisfaction') {
+ // 满意度图表
+ datasets.push({
+ label: '满意度评分',
+ data: satisfaction,
+ borderColor: '#ffc107',
+ backgroundColor: 'rgba(255, 193, 7, 0.1)',
+ tension: chartType === 'line' ? 0.4 : 0
+ });
+ } else {
+ // 默认显示工单和预警数据
+ datasets.push({
+ label: '工单数量',
+ data: workorders,
+ borderColor: '#36A2EB',
+ backgroundColor: 'rgba(54, 162, 235, 0.1)',
+ tension: chartType === 'line' ? 0.4 : 0
+ });
+
+ // 如果有预警数据,添加预警数据集
+ if (alerts.some(alert => alert > 0)) {
+ datasets.push({
+ label: '预警数量',
+ data: alerts,
+ borderColor: '#FF6384',
+ backgroundColor: 'rgba(255, 99, 132, 0.1)',
+ tension: chartType === 'line' ? 0.4 : 0
+ });
+ }
+ }
+
+ return {
+ labels: labels,
+ datasets: datasets
+ };
+ }
+ },
+
+ // 导出图表
+ 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();
+ },
+
+ // 更新分析报告
+ 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 = `
+
+
+
工单统计概览
+
+
+
+ | 总工单数 |
+ ${workorders.total || 0} |
+
+
+ | 待处理 |
+ ${workorders.open || 0} |
+
+
+ | 处理中 |
+ ${workorders.in_progress || 0} |
+
+
+ | 已解决 |
+ ${workorders.resolved || 0} |
+
+
+ | 已关闭 |
+ ${workorders.closed || 0} |
+
+
+
+
+
+
满意度分析
+
+
+
+ | 平均满意度 |
+ ${satisfaction.average || 0}/5.0 |
+
+
+ | 5星评价 |
+ ${satisfaction.distribution?.['5'] || 0} 个 |
+
+
+ | 4星评价 |
+ ${satisfaction.distribution?.['4'] || 0} 个 |
+
+
+ | 3星评价 |
+ ${satisfaction.distribution?.['3'] || 0} 个 |
+
+
+ | 2星及以下 |
+ ${(satisfaction.distribution?.['2'] || 0) + (satisfaction.distribution?.['1'] || 0)} 个 |
+
+
+
+
+
+
+
+
+
预警统计
+
+
+
+ | 总预警数 |
+ ${alerts.total || 0} |
+
+
+ | 活跃预警 |
+ ${alerts.active || 0} |
+
+
+ | 已解决 |
+ ${alerts.resolved || 0} |
+
+
+
+
+
+
性能指标
+
+
+
+ | 响应时间 |
+ ${performance.response_time || 0} 秒 |
+
+
+ | 系统可用性 |
+ ${performance.uptime || 0}% |
+
+
+ | 错误率 |
+ ${performance.error_rate || 0}% |
+
+
+ | 吞吐量 |
+ ${performance.throughput || 0} 请求/小时 |
+
+
+
+
+
+
+
+
+
关键指标总结
+
+
+
+
+
${summary.resolution_rate || 0}%
+
解决率
+
+
+
+
+
+
+
${summary.avg_satisfaction || 0}
+
平均满意度
+
+
+
+
+
+
+
${summary.active_alerts || 0}
+
活跃预警
+
+
+
+
+
+
+
${summary.total_workorders || 0}
+
总工单数
+
+
+
+
+
+
+ `;
+
+ reportContainer.innerHTML = reportHtml;
+ },
+
+ updateAnalyticsDisplay(analytics) {
+ // 更新分析报告
+ const reportContainer = document.getElementById('analytics-report');
+ if (analytics.summary) {
+ reportContainer.innerHTML = `
+
+
+
性能指标
+
+ - 总工单数: ${analytics.summary.total_orders || 0}
+ - 解决率: ${Math.round((analytics.summary.resolution_rate || 0) * 100)}%
+ - 平均解决时间: ${analytics.summary.avg_resolution_time_hours || 0}小时
+ - 平均满意度: ${analytics.summary.avg_satisfaction || 0}
+
+
+
+
趋势分析
+
+ - 工单趋势: ${analytics.summary.trends?.orders_trend ? '上升' : '下降'}
+ - 满意度趋势: ${analytics.summary.trends?.satisfaction_trend ? '上升' : '下降'}
+ - 解决时间趋势: ${analytics.summary.trends?.resolution_time_trend ? '上升' : '下降'}
+
+
+
+ `;
+ }
+
+ // 更新类别分布图
+ 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 [settingsResp, configResp] = await Promise.all([
+ fetch('/api/settings'),
+ fetch('/api/runtime-config'),
+ ]);
+ const settings = await settingsResp.json();
+ this.updateSettingsDisplay(settings);
+
+ // 加载运行时配置(租户、模型等)
+ const config = await configResp.json();
+ if (config.success) {
+ const set = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = val || '-'; };
+ set('setting-tenant-id', config.tenant_id);
+ set('setting-llm-provider', config.llm?.provider);
+ set('setting-llm-model', config.llm?.model);
+ set('setting-llm-base-url', config.llm?.base_url);
+ set('setting-embedding-status', config.embedding?.enabled ? `启用 (${config.embedding.model})` : '禁用');
+ set('setting-redis-status', config.redis?.enabled ? `启用 (${config.redis.host})` : '禁用');
+ }
+ } catch (error) {
+ console.error('加载设置失败:', error);
+ }
+ },
+
+ updateSettingsDisplay(settings) {
+ if (settings.api_timeout !== undefined) document.getElementById('api-timeout').value = settings.api_timeout;
+ if (settings.max_history !== undefined) document.getElementById('max-history').value = settings.max_history;
+ if (settings.refresh_interval !== undefined) document.getElementById('refresh-interval').value = settings.refresh_interval;
+ if (settings.auto_monitoring !== undefined) document.getElementById('auto-monitoring').checked = settings.auto_monitoring;
+ if (settings.agent_mode !== undefined) document.getElementById('agent-mode').checked = settings.agent_mode;
+ // 新增:API与模型、端口、日志级别(如页面存在对应输入框则填充)
+ const map = [
+ ['api-provider','api_provider'],
+ ['api-base-url','api_base_url'],
+ ['api-key','api_key'],
+ ['model-name','model_name'],
+ ['model-temperature','model_temperature'],
+ ['model-max-tokens','model_max_tokens'],
+ ['server-port','server_port'],
+ ['websocket-port','websocket_port'],
+ ['log-level','log_level']
+ ];
+ map.forEach(([id, key]) => {
+ const el = document.getElementById(id);
+ if (el && settings[key] !== undefined) el.value = settings[key];
+ });
+
+ // 更新温度滑块显示值
+ const tempSlider = document.getElementById('model-temperature');
+ const tempValue = document.getElementById('temperature-value');
+ if (tempSlider && tempValue) {
+ tempSlider.addEventListener('input', function() {
+ tempValue.textContent = this.value;
+ });
+ tempValue.textContent = tempSlider.value;
+ }
+
+ // 更新服务状态显示
+ this.updateServiceStatus(settings);
+ },
+
+ async saveSystemSettings() {
+ 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,
+ agent_mode: document.getElementById('agent-mode').checked,
+ api_provider: document.getElementById('api-provider')?.value || '',
+ api_base_url: document.getElementById('api-base-url')?.value || '',
+ api_key: document.getElementById('api-key')?.value || '',
+ model_name: document.getElementById('model-name')?.value || '',
+ model_temperature: parseFloat(document.getElementById('model-temperature')?.value || 0.7),
+ model_max_tokens: parseInt(document.getElementById('model-max-tokens')?.value || 1000),
+ server_port: parseInt(document.getElementById('server-port')?.value || 5000),
+ websocket_port: parseInt(document.getElementById('websocket-port')?.value || 8765),
+ log_level: document.getElementById('log-level')?.value || 'INFO'
+ };
+
+ try {
+ 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');
+ }
+ },
+
+ // 更新服务状态显示
+ updateServiceStatus(settings) {
+ // 更新仪表板服务状态卡片
+ if (settings.current_server_port !== undefined) {
+ const webPortEl = document.getElementById('web-port-status');
+ if (webPortEl) webPortEl.textContent = settings.current_server_port;
+ }
+ if (settings.current_websocket_port !== undefined) {
+ const wsPortEl = document.getElementById('ws-port-status');
+ if (wsPortEl) wsPortEl.textContent = settings.current_websocket_port;
+ }
+ if (settings.log_level !== undefined) {
+ const logLevelEl = document.getElementById('log-level-status');
+ if (logLevelEl) logLevelEl.textContent = settings.log_level;
+ }
+ if (settings.uptime_seconds !== undefined) {
+ const uptimeEl = document.getElementById('uptime-status');
+ if (uptimeEl) {
+ const hours = Math.floor(settings.uptime_seconds / 3600);
+ const minutes = Math.floor((settings.uptime_seconds % 3600) / 60);
+ uptimeEl.textContent = `${hours}小时${minutes}分钟`;
+ }
+ }
+
+ // 更新系统设置页面的当前端口显示
+ const currentPortEl = document.getElementById('current-server-port');
+ if (currentPortEl && settings.current_server_port !== undefined) {
+ currentPortEl.textContent = settings.current_server_port;
+ }
+ },
+
+ // 刷新服务状态
+ async refreshServiceStatus() {
+ try {
+ const response = await fetch('/api/settings');
+ const settings = await response.json();
+ this.updateServiceStatus(settings);
+ this.showNotification('服务状态已刷新', 'success');
+ } catch (error) {
+ console.error('刷新服务状态失败:', error);
+ this.showNotification('刷新服务状态失败', 'error');
+ }
+ },
+
+ // 测试API连接
+ async testApiConnection() {
+ try {
+ const apiProvider = document.getElementById('api-provider').value;
+ const apiBaseUrl = document.getElementById('api-base-url').value;
+ const apiKey = document.getElementById('api-key').value;
+ const modelName = document.getElementById('model-name').value;
+
+ const response = await fetch('/api/test/connection', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ api_provider: apiProvider,
+ api_base_url: apiBaseUrl,
+ api_key: apiKey,
+ model_name: modelName
+ })
+ });
+
+ const result = await response.json();
+ if (result.success) {
+ this.showNotification(`API连接测试成功: ${result.message}`, 'success');
+ } else {
+ this.showNotification(`API连接测试失败: ${result.error}`, 'error');
+ }
+ } catch (error) {
+ console.error('API连接测试失败:', error);
+ this.showNotification('API连接测试失败', 'error');
+ }
+ },
+
+ // 测试模型回答
+ async testModelResponse() {
+ try {
+ const testMessage = prompt('请输入测试消息:', '你好,请简单介绍一下你自己');
+ if (!testMessage) return;
+
+ const response = await fetch('/api/test/model', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ test_message: testMessage
+ })
+ });
+
+ const result = await response.json();
+ if (result.success) {
+ const message = `模型回答测试成功:\n\n问题: ${result.test_message}\n\n回答: ${result.response}\n\n响应时间: ${result.response_time}`;
+ alert(message);
+ this.showNotification('模型回答测试成功', 'success');
+ } else {
+ this.showNotification(`模型回答测试失败: ${result.error}`, 'error');
+ }
+ } catch (error) {
+ console.error('模型回答测试失败:', error);
+ this.showNotification('模型回答测试失败', 'error');
+ }
+ },
+
+ async loadSystemInfo() {
+ try {
+ const response = await fetch('/api/system/info');
+ const info = await response.json();
+ this.updateSystemInfoDisplay(info);
+ } catch (error) {
+ console.error('加载系统信息失败:', error);
+ }
+ },
+
+ updateSystemInfoDisplay(info) {
+ const container = document.getElementById('system-info');
+ container.innerHTML = `
+
+ 系统版本: ${info.version || '1.0.0'}
+
+
+ Python版本: ${info.python_version || '未知'}
+
+
+ 数据库: ${info.database || 'SQLite'}
+
+
+ 运行时间: ${info.uptime || '未知'}
+
+
+ 内存使用: ${info.memory_usage || '0'} MB
+
+ `;
+ }
+});
diff --git a/src/web/static/js/modules/tenants.js b/src/web/static/js/modules/tenants.js
new file mode 100644
index 0000000..1b32f8f
--- /dev/null
+++ b/src/web/static/js/modules/tenants.js
@@ -0,0 +1,165 @@
+// 租户管理模块
+Object.assign(TSPDashboard.prototype, {
+ async loadTenantList() {
+ const container = document.getElementById('tenant-list');
+ if (!container) return;
+ container.innerHTML = '
';
+
+ try {
+ const response = await fetch('/api/tenants');
+ const tenants = await response.json();
+
+ if (!Array.isArray(tenants) || tenants.length === 0) {
+ container.innerHTML = '暂无租户,请点击"新建租户"创建
';
+ return;
+ }
+
+ container.innerHTML = tenants.map(t => {
+ const feishuCfg = t.config?.feishu || {};
+ const groupCount = (feishuCfg.chat_groups || []).length;
+ const hasFeishu = feishuCfg.app_id || groupCount > 0;
+ return `
+
+
+
+ ${t.name}
+ (${t.tenant_id})
+ ${t.description ? `
${t.description}` : ''}
+ ${!t.is_active ? '已禁用' : ''}
+ ${hasFeishu ? `飞书${groupCount > 0 ? ` (${groupCount}群)` : ''}` : ''}
+
+
+
+ ${t.tenant_id !== 'default' ? `
+ ` : ''}
+
+
+
+ `}).join('');
+ } catch (error) {
+ console.error('加载租户列表失败:', error);
+ container.innerHTML = '加载失败
';
+ }
+ },
+
+ showCreateTenantModal() {
+ document.getElementById('tenantModalTitle').textContent = '新建租户';
+ document.getElementById('tenant-edit-id').value = '';
+ document.getElementById('tenant-id-input').value = '';
+ document.getElementById('tenant-id-input').disabled = false;
+ document.getElementById('tenant-id-group').style.display = '';
+ document.getElementById('tenant-name-input').value = '';
+ document.getElementById('tenant-desc-input').value = '';
+ document.getElementById('tenant-feishu-appid').value = '';
+ document.getElementById('tenant-feishu-appsecret').value = '';
+ document.getElementById('tenant-feishu-chatgroups').value = '';
+ new bootstrap.Modal(document.getElementById('tenantModal')).show();
+ },
+
+ async showEditTenantModal(tenantId, name, description) {
+ document.getElementById('tenantModalTitle').textContent = '编辑租户';
+ document.getElementById('tenant-edit-id').value = tenantId;
+ document.getElementById('tenant-id-input').value = tenantId;
+ document.getElementById('tenant-id-input').disabled = true;
+ document.getElementById('tenant-name-input').value = name;
+ document.getElementById('tenant-desc-input').value = description;
+
+ // 加载租户的飞书配置
+ try {
+ const resp = await fetch('/api/tenants');
+ const tenants = await resp.json();
+ const tenant = tenants.find(t => t.tenant_id === tenantId);
+ const feishuCfg = tenant?.config?.feishu || {};
+ document.getElementById('tenant-feishu-appid').value = feishuCfg.app_id || '';
+ document.getElementById('tenant-feishu-appsecret').value = feishuCfg.app_secret || '';
+ document.getElementById('tenant-feishu-chatgroups').value = (feishuCfg.chat_groups || []).join('\n');
+ } catch (e) {
+ document.getElementById('tenant-feishu-appid').value = '';
+ document.getElementById('tenant-feishu-appsecret').value = '';
+ document.getElementById('tenant-feishu-chatgroups').value = '';
+ }
+
+ new bootstrap.Modal(document.getElementById('tenantModal')).show();
+ },
+
+ async saveTenant() {
+ const editId = document.getElementById('tenant-edit-id').value;
+ const tenantId = document.getElementById('tenant-id-input').value.trim();
+ const name = document.getElementById('tenant-name-input').value.trim();
+ const description = document.getElementById('tenant-desc-input').value.trim();
+
+ // 飞书配置
+ const feishuAppId = document.getElementById('tenant-feishu-appid').value.trim();
+ const feishuAppSecret = document.getElementById('tenant-feishu-appsecret').value.trim();
+ const chatGroupsText = document.getElementById('tenant-feishu-chatgroups').value.trim();
+ const chatGroups = chatGroupsText ? chatGroupsText.split('\n').map(s => s.trim()).filter(Boolean) : [];
+
+ const config = {};
+ if (feishuAppId || feishuAppSecret || chatGroups.length > 0) {
+ config.feishu = {};
+ if (feishuAppId) config.feishu.app_id = feishuAppId;
+ if (feishuAppSecret) config.feishu.app_secret = feishuAppSecret;
+ if (chatGroups.length > 0) config.feishu.chat_groups = chatGroups;
+ }
+
+ if (!name) {
+ this.showNotification('租户名称不能为空', 'error');
+ return;
+ }
+
+ try {
+ let response;
+ if (editId) {
+ response = await fetch(`/api/tenants/${encodeURIComponent(editId)}`, {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ name, description, config })
+ });
+ } else {
+ if (!tenantId) {
+ this.showNotification('租户标识不能为空', 'error');
+ return;
+ }
+ response = await fetch('/api/tenants', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ tenant_id: tenantId, name, description, config })
+ });
+ }
+
+ const data = await response.json();
+ if (data.success) {
+ bootstrap.Modal.getInstance(document.getElementById('tenantModal')).hide();
+ this.showNotification(editId ? '租户已更新' : '租户已创建', 'success');
+ this.loadTenantList();
+ this.populateTenantSelectors();
+ } else {
+ this.showNotification(data.error || '操作失败', 'error');
+ }
+ } catch (error) {
+ console.error('保存租户失败:', error);
+ this.showNotification('保存租户失败: ' + error.message, 'error');
+ }
+ },
+
+ async deleteTenant(tenantId) {
+ if (!confirm(`确定要删除租户 "${tenantId}" 吗?该操作不会删除关联数据。`)) return;
+ try {
+ const response = await fetch(`/api/tenants/${encodeURIComponent(tenantId)}`, { method: 'DELETE' });
+ const data = await response.json();
+ if (data.success) {
+ this.showNotification('租户已删除', 'success');
+ this.loadTenantList();
+ } else {
+ this.showNotification(data.error || '删除失败', 'error');
+ }
+ } catch (error) {
+ console.error('删除租户失败:', error);
+ this.showNotification('删除租户失败: ' + error.message, 'error');
+ }
+ }
+});
diff --git a/src/web/static/js/modules/utils.js b/src/web/static/js/modules/utils.js
new file mode 100644
index 0000000..58f462c
--- /dev/null
+++ b/src/web/static/js/modules/utils.js
@@ -0,0 +1,573 @@
+// 工具函数模块
+Object.assign(TSPDashboard.prototype, {
+ 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;
+ },
+
+ getSeverityText(severity) {
+ const severityMap = {
+ 'low': '低',
+ 'medium': '中',
+ 'high': '高',
+ 'critical': '严重'
+ };
+ return severityMap[severity] || severity;
+ },
+
+ getSatisfactionText(level) {
+ const satisfactionMap = {
+ 'very_satisfied': '非常满意',
+ 'satisfied': '满意',
+ 'neutral': '一般',
+ 'dissatisfied': '不满意'
+ };
+ return satisfactionMap[level] || level;
+ },
+
+ getPerformanceTrendText(trend) {
+ const trendMap = {
+ 'up': '上升',
+ 'down': '下降',
+ 'stable': '稳定'
+ };
+ return trendMap[trend] || trend;
+ },
+
+ 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;
+ },
+
+ // 统计数字预览功能
+ async showStatPreview(type, status) {
+ try {
+ let title = '';
+ let data = [];
+ let apiUrl = '';
+
+ switch (type) {
+ case 'workorder':
+ title = this.getWorkorderPreviewTitle(status);
+ apiUrl = status === 'all' ? '/api/workorders' : `/api/workorders/by-status/${status}`;
+ break;
+ case 'alert':
+ title = this.getAlertPreviewTitle(status);
+ apiUrl = `/api/alerts/by-level/${status}`;
+ break;
+ case 'knowledge':
+ title = this.getKnowledgePreviewTitle(status);
+ apiUrl = `/api/knowledge/by-status/${status}`;
+ break;
+ default:
+ return;
+ }
+
+ // 显示加载状态
+ this.showLoadingModal(title);
+
+ const response = await fetch(apiUrl);
+ const result = await response.json();
+
+ // 处理不同的API响应结构
+ if (result.success !== false) {
+ if (result.success === true) {
+ // 新API结构: {success: true, data: {workorders: [...]}}
+ data = result.data[type + 's'] || result.data.knowledge || [];
+ } else if (result.workorders) {
+ // 旧API结构: {workorders: [...], page: 1, ...}
+ data = result.workorders || [];
+ } else if (result.alerts) {
+ // 预警API结构
+ data = result.alerts || [];
+ } else if (result.knowledge) {
+ // 知识库API结构
+ data = result.knowledge || [];
+ } else {
+ data = [];
+ }
+ this.showPreviewModal(title, type, data);
+ } else {
+ const errorMsg = result.error || result.message || '未知错误';
+ this.showNotification('获取数据失败: ' + errorMsg, 'error');
+ }
+ } catch (error) {
+ console.error('预览失败:', error);
+ this.showNotification('预览失败: ' + error.message, 'error');
+ }
+ },
+
+ getWorkorderPreviewTitle(status) {
+ const titles = {
+ 'all': '所有工单',
+ 'open': '待处理工单',
+ 'in_progress': '处理中工单',
+ 'resolved': '已解决工单',
+ 'closed': '已关闭工单'
+ };
+ return titles[status] || '工单列表';
+ },
+
+ getAlertPreviewTitle(level) {
+ const titles = {
+ 'critical': '严重预警',
+ 'warning': '警告预警',
+ 'info': '信息预警'
+ };
+ return titles[level] || '预警列表';
+ },
+
+ getKnowledgePreviewTitle(status) {
+ const titles = {
+ 'verified': '已验证知识',
+ 'unverified': '未验证知识'
+ };
+ return titles[status] || '知识库条目';
+ },
+
+ showLoadingModal(title) {
+ const modalHtml = `
+
+
+
+
+
+
+ 加载中...
+
+
正在加载数据...
+
+
+
+
+ `;
+
+ // 移除已存在的模态框和遮罩
+ this.removeExistingModal();
+
+ document.body.insertAdjacentHTML('beforeend', modalHtml);
+ const modalElement = document.getElementById('statPreviewModal');
+ const modal = new bootstrap.Modal(modalElement, {
+ backdrop: true,
+ keyboard: true
+ });
+
+ // 添加事件监听器确保正确清理
+ modalElement.addEventListener('hidden.bs.modal', () => {
+ this.cleanupModal();
+ });
+
+ modal.show();
+ },
+
+ showPreviewModal(title, type, data) {
+ let contentHtml = '';
+
+ if (data.length === 0) {
+ contentHtml = `
+
+
+
暂无数据
+
当前条件下没有找到相关记录
+
+ `;
+ } else {
+ switch (type) {
+ case 'workorder':
+ contentHtml = this.generateWorkorderPreviewHtml(data);
+ break;
+ case 'alert':
+ contentHtml = this.generateAlertPreviewHtml(data);
+ break;
+ case 'knowledge':
+ contentHtml = this.generateKnowledgePreviewHtml(data);
+ break;
+ }
+ }
+
+ const modalHtml = `
+
+
+
+
+
+ ${contentHtml}
+
+
+
+
+
+ `;
+
+ // 移除已存在的模态框和遮罩
+ this.removeExistingModal();
+
+ document.body.insertAdjacentHTML('beforeend', modalHtml);
+ const modalElement = document.getElementById('statPreviewModal');
+ const modal = new bootstrap.Modal(modalElement, {
+ backdrop: true,
+ keyboard: true
+ });
+
+ // 添加事件监听器确保正确清理
+ modalElement.addEventListener('hidden.bs.modal', () => {
+ this.cleanupModal();
+ });
+
+ modal.show();
+ },
+
+ generateWorkorderPreviewHtml(workorders) {
+ return `
+
+
+
+
+ | 工单ID |
+ 标题 |
+ 状态 |
+ 优先级 |
+ 创建时间 |
+ 操作 |
+
+
+
+ ${workorders.map(wo => `
+
+ | ${wo.order_id || wo.id} |
+
+
+ ${wo.title}
+
+ |
+
+
+ ${this.getStatusText(wo.status)}
+
+ |
+
+
+ ${this.getPriorityText(wo.priority)}
+
+ |
+ ${new Date(wo.created_at).toLocaleString()} |
+
+
+ |
+
+ `).join('')}
+
+
+
+ `;
+ },
+
+ generateAlertPreviewHtml(alerts) {
+ return `
+
+
+
+
+ | 预警ID |
+ 消息 |
+ 级别 |
+ 类型 |
+ 创建时间 |
+ 操作 |
+
+
+
+ ${alerts.map(alert => `
+
+ | ${alert.id} |
+
+
+ ${alert.message}
+
+ |
+
+
+ ${this.getLevelText(alert.level)}
+
+ |
+ ${this.getTypeText(alert.alert_type)} |
+ ${new Date(alert.created_at).toLocaleString()} |
+
+
+ |
+
+ `).join('')}
+
+
+
+ `;
+ },
+
+ generateKnowledgePreviewHtml(knowledge) {
+ return `
+
+
+
+
+ | ID |
+ 标题 |
+ 分类 |
+ 验证状态 |
+ 创建时间 |
+ 操作 |
+
+
+
+ ${knowledge.map(item => `
+
+ | ${item.id} |
+
+
+ ${item.title}
+
+ |
+ ${item.category || '未分类'} |
+
+
+ ${item.is_verified ? '已验证' : '未验证'}
+
+ |
+ ${new Date(item.created_at).toLocaleString()} |
+
+
+ ${item.is_verified ?
+ `` :
+ ``
+ }
+
+
+ |
+
+ `).join('')}
+
+
+
+ `;
+ },
+
+ getAlertLevelColor(level) {
+ const colorMap = {
+ 'critical': 'danger',
+ 'warning': 'warning',
+ 'info': 'info'
+ };
+ return colorMap[level] || 'secondary';
+ },
+
+ goToFullView(type, status) {
+ // 关闭预览模态框
+ const modal = bootstrap.Modal.getInstance(document.getElementById('statPreviewModal'));
+ if (modal) {
+ modal.hide();
+ }
+
+ // 切换到对应的标签页
+ switch (type) {
+ case 'workorder':
+ this.switchTab('workorders');
+ // 设置筛选器
+ if (status !== 'all') {
+ setTimeout(() => {
+ const filter = document.getElementById('workorder-status-filter');
+ if (filter) {
+ filter.value = status;
+ this.loadWorkOrders();
+ }
+ }, 100);
+ }
+ break;
+ case 'alert':
+ this.switchTab('alerts');
+ // 设置筛选器
+ setTimeout(() => {
+ const filter = document.getElementById('alert-filter');
+ if (filter) {
+ filter.value = status;
+ this.updateAlertsDisplay();
+ }
+ }, 100);
+ break;
+ case 'knowledge':
+ this.switchTab('knowledge');
+ break;
+ }
+ },
+
+ // 模态框清理方法
+ removeExistingModal() {
+ const existingModal = document.getElementById('statPreviewModal');
+ if (existingModal) {
+ // 获取模态框实例并销毁
+ const modalInstance = bootstrap.Modal.getInstance(existingModal);
+ if (modalInstance) {
+ modalInstance.dispose();
+ }
+ existingModal.remove();
+ }
+
+ // 清理可能残留的遮罩
+ const backdrops = document.querySelectorAll('.modal-backdrop');
+ backdrops.forEach(backdrop => backdrop.remove());
+
+ // 恢复body的滚动
+ document.body.classList.remove('modal-open');
+ document.body.style.overflow = '';
+ document.body.style.paddingRight = '';
+ },
+
+ cleanupModal() {
+ // 延迟清理,确保动画完成
+ setTimeout(() => {
+ this.removeExistingModal();
+ }, 300);
+ },
+
+ 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}
+
+ `;
+
+ document.body.appendChild(notification);
+
+ setTimeout(() => {
+ if (notification.parentNode) {
+ notification.parentNode.removeChild(notification);
+ }
+ }, 3000);
+ },
+
+ getSimilarityExplanation(percent) {
+ if (percent >= 95) {
+ return "语义高度相似,AI建议与人工描述基本一致,建议自动审批";
+ } else if (percent >= 90) {
+ return "语义较为相似,AI建议与人工描述大体一致,建议人工审核";
+ } else if (percent >= 80) {
+ return "语义部分相似,AI建议与人工描述有一定差异,需要人工判断";
+ } else if (percent >= 60) {
+ return "语义相似度较低,AI建议与人工描述差异较大,建议使用人工描述";
+ } else {
+ return "语义差异很大,AI建议与人工描述差异很大,优先使用人工描述";
+ }
+ },
+
+ getSimilarityMessage(percent, approved, useHumanResolution = false) {
+ if (useHumanResolution) {
+ return `人工描述已保存!语义相似度: ${percent}%,AI准确率低于90%,将使用人工描述入库`;
+ } else if (approved) {
+ return `人工描述已保存!语义相似度: ${percent}%,已自动审批入库`;
+ } else if (percent >= 90) {
+ return `人工描述已保存!语义相似度: ${percent}%,建议人工审核后审批`;
+ } else if (percent >= 80) {
+ return `人工描述已保存!语义相似度: ${percent}%,需要人工判断是否审批`;
+ } else {
+ return `人工描述已保存!语义相似度: ${percent}%,建议使用人工描述入库`;
+ }
+ }
+});
diff --git a/src/web/static/js/modules/workorders.js b/src/web/static/js/modules/workorders.js
new file mode 100644
index 0000000..aa50d0b
--- /dev/null
+++ b/src/web/static/js/modules/workorders.js
@@ -0,0 +1,458 @@
+// 工单管理模块
+Object.assign(TSPDashboard.prototype, {
+ async generateAISuggestion(workorderId) {
+ const button = document.querySelector(`button[onclick="dashboard.generateAISuggestion(${workorderId})"]`);
+ const textarea = document.getElementById(`aiSuggestion_${workorderId}`);
+ try {
+ if (button) { button.classList.add('btn-loading'); button.disabled = true; }
+ if (textarea) { textarea.classList.add('ai-loading'); textarea.value = '正在生成AI建议,请稍候...'; }
+ const resp = await fetch(`/api/workorders/${workorderId}/ai-suggestion`, { method: 'POST' });
+ const data = await resp.json();
+ if (data.success) {
+ if (textarea) {
+ textarea.value = data.suggestion || '';
+ textarea.classList.remove('ai-loading');
+ textarea.classList.add('success-animation');
+ setTimeout(() => textarea.classList.remove('success-animation'), 600);
+ }
+ this.showNotification('AI建议已生成', 'success');
+ } else { throw new Error(data.error || '生成失败'); }
+ } catch (e) {
+ console.error('生成AI建议失败:', e);
+ if (textarea) { textarea.value = 'AI建议生成失败,请重试'; textarea.classList.remove('ai-loading'); }
+ this.showNotification('生成AI建议失败: ' + e.message, 'error');
+ } finally {
+ if (button) { button.classList.remove('btn-loading'); button.disabled = false; }
+ }
+ },
+
+ 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.innerHTML = `语义相似度: ${percent}%`;
+ simEl.className = percent >= 90 ? 'similarity-badge high' : percent >= 80 ? 'similarity-badge medium' : 'similarity-badge low';
+ simEl.title = this.getSimilarityExplanation(percent);
+ }
+ if (apprEl) {
+ if (data.use_human_resolution) { apprEl.textContent = '将使用人工描述入库'; apprEl.className = 'status-badge human-resolution'; }
+ else if (data.approved) { apprEl.textContent = '已自动审批'; apprEl.className = 'status-badge approved'; }
+ else { apprEl.textContent = '未审批'; apprEl.className = 'status-badge pending'; }
+ }
+ if (approveBtn) {
+ const canApprove = data.approved || data.use_human_resolution;
+ approveBtn.disabled = !canApprove;
+ if (data.use_human_resolution) { approveBtn.textContent = '使用人工描述入库'; approveBtn.className = 'approve-btn'; }
+ else if (data.approved) { approveBtn.textContent = '已自动审批'; approveBtn.className = 'approve-btn approved'; }
+ else { approveBtn.textContent = '审批入库'; approveBtn.className = 'approve-btn'; }
+ }
+ const message = this.getSimilarityMessage(percent, data.approved, data.use_human_resolution);
+ this.showNotification(message, data.approved ? 'success' : data.use_human_resolution ? 'warning' : 'info');
+ } 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) {
+ const contentType = data.used_content === 'human_resolution' ? '人工描述' : 'AI建议';
+ const confidence = Math.round((data.confidence_score || 0) * 100);
+ this.showNotification(`已入库为知识条目!使用${contentType},置信度: ${confidence}%`, 'success');
+ } else { throw new Error(data.error || '入库失败'); }
+ } catch (e) {
+ console.error('入库失败:', e);
+ this.showNotification('入库失败: ' + e.message, 'error');
+ }
+ },
+
+ async loadWorkOrders(page = 1, forceRefresh = false) {
+ this.paginationConfig.currentWorkOrderPage = page;
+ const cacheKey = `workorders_page_${page}`;
+ if (!forceRefresh && this.cache.has(cacheKey)) {
+ const cachedData = this.cache.get(cacheKey);
+ this.updateWorkOrdersDisplay(cachedData.workorders);
+ this.updateWorkOrdersPagination(cachedData);
+ return;
+ }
+ try {
+ const statusFilter = document.getElementById('workorder-status-filter')?.value || 'all';
+ const priorityFilter = document.getElementById('workorder-priority-filter')?.value || 'all';
+ const tenantFilter = document.getElementById('workorder-tenant-filter')?.value || 'all';
+ let url = '/api/workorders';
+ const params = new URLSearchParams();
+ params.append('page', page);
+ params.append('per_page', this.getPageSize('workorders-pagination').toString());
+ if (statusFilter !== 'all') params.append('status', statusFilter);
+ if (priorityFilter !== 'all') params.append('priority', priorityFilter);
+ if (tenantFilter !== 'all') params.append('tenant_id', tenantFilter);
+ if (forceRefresh) params.append('_t', Date.now().toString());
+ if (params.toString()) url += '?' + params.toString();
+ const response = await fetch(url, {
+ cache: forceRefresh ? 'no-cache' : 'default',
+ headers: forceRefresh ? { 'Cache-Control': 'no-cache', 'Pragma': 'no-cache' } : {}
+ });
+ if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`);
+ const data = await response.json();
+ this.updateWorkOrdersDisplay(data.workorders);
+ this.updateWorkOrdersPagination(data);
+ this.cache.set(cacheKey, data);
+ } catch (error) {
+ console.error('加载工单失败:', error);
+ this.showNotification('加载工单失败: ' + error.message, 'error');
+ }
+ },
+
+ updateWorkOrdersDisplay(workorders) {
+ const container = document.getElementById('workorders-list');
+ if (workorders.length === 0) {
+ container.innerHTML = '';
+ return;
+ }
+ const headerHtml = `
+
+
+
+
+
+
+
+
`;
+ const workordersHtml = workorders.map(wo => `
+
+
+
+
+
+
${wo.title}
+
${wo.description ? wo.description.substring(0, 100) + (wo.description.length > 100 ? '...' : '') : '无问题描述'}
+
+ ${this.getPriorityText(wo.priority)}
+ ${this.getStatusText(wo.status)}
+ 分类: ${wo.category}
+ 创建时间: ${new Date(wo.created_at).toLocaleString()}
+
+
+
+
+
+
+ `).join('');
+ container.innerHTML = headerHtml + workordersHtml;
+ },
+
+ updateWorkOrdersPagination(data) {
+ this.createPaginationComponent(data, 'workorders-pagination', 'loadWorkOrders', '个工单');
+ },
+
+ updateWorkOrderStatistics(workorders) {
+ const stats = workorders.reduce((acc, wo) => { acc.total = (acc.total || 0) + 1; acc[wo.status] = (acc[wo.status] || 0) + 1; return acc; }, {});
+ const statusMapping = {
+ 'open': ['open', '待处理', '新建', 'new'],
+ 'in_progress': ['in_progress', '处理中', '进行中', 'progress', 'processing'],
+ 'resolved': ['resolved', '已解决', '已完成'],
+ 'closed': ['closed', '已关闭', '关闭']
+ };
+ const mapped_counts = {'open': 0, 'in_progress': 0, 'resolved': 0, 'closed': 0};
+ for (const [status, count] of Object.entries(stats)) {
+ if (status === 'total') continue;
+ const status_lower = String(status).toLowerCase();
+ for (const [mapped_status, possible_values] of Object.entries(statusMapping)) {
+ if (possible_values.some(v => v.toLowerCase() === status_lower)) { mapped_counts[mapped_status] += count; break; }
+ }
+ }
+ document.getElementById('workorders-total').textContent = stats.total || 0;
+ document.getElementById('workorders-open').textContent = mapped_counts['open'];
+ document.getElementById('workorders-progress').textContent = mapped_counts['in_progress'];
+ document.getElementById('workorders-resolved').textContent = mapped_counts['resolved'];
+ },
+
+ toggleSelectAllWorkorders() {
+ const selectAllCheckbox = document.getElementById('select-all-workorders');
+ document.querySelectorAll('.workorder-checkbox').forEach(cb => cb.checked = selectAllCheckbox.checked);
+ this.updateBatchDeleteButton();
+ },
+
+ updateBatchDeleteButton() {
+ const selected = document.querySelectorAll('.workorder-checkbox:checked');
+ const btn = document.getElementById('batch-delete-workorders');
+ if (btn) { btn.disabled = selected.length === 0; btn.textContent = selected.length > 0 ? `批量删除 (${selected.length})` : '批量删除'; }
+ },
+
+ async batchDeleteWorkorders() {
+ const selectedIds = Array.from(document.querySelectorAll('.workorder-checkbox:checked')).map(cb => parseInt(cb.value));
+ if (selectedIds.length === 0) { this.showNotification('请选择要删除的工单', 'warning'); return; }
+ if (!confirm(`确定要删除选中的 ${selectedIds.length} 个工单吗?此操作不可撤销。`)) return;
+ try {
+ const response = await fetch('/api/batch-delete/workorders', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ids: selectedIds }) });
+ const data = await response.json();
+ if (data.success) {
+ this.showNotification(data.message, 'success');
+ this.cache.delete('workorders');
+ await this.loadWorkOrders(true);
+ await this.loadAnalytics();
+ this.updateBatchDeleteButton();
+ } else { this.showNotification(data.error || '批量删除失败', 'error'); }
+ } catch (error) { console.error('批量删除工单失败:', error); this.showNotification('批量删除工单失败', 'error'); }
+ },
+
+ 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();
+ await this.loadWorkOrders();
+ await this.loadAnalytics();
+ } else { this.showNotification('创建工单失败: ' + (data.error || '未知错误'), 'error'); }
+ } catch (error) { console.error('创建工单失败:', error); this.showNotification('创建工单失败', 'error'); }
+ },
+
+ showCreateWorkOrderModal() {
+ const modal = new bootstrap.Modal(document.getElementById('createWorkOrderModal'));
+ modal.show();
+ },
+
+ 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) {
+ const modalHtml = `
+
+
+
+
+
+
+
基本信息
+
+ | 工单号: | ${workorder.order_id || workorder.id} |
+ | 标题: | ${workorder.title} |
+ | 分类: | ${workorder.category} |
+ | 优先级: | ${this.getPriorityText(workorder.priority)} |
+ | 状态: | ${this.getStatusText(workorder.status)} |
+ | 创建时间: | ${new Date(workorder.created_at).toLocaleString()} |
+ | 更新时间: | ${new Date(workorder.updated_at).toLocaleString()} |
+
+
+
+
问题描述
+
${workorder.description || '无问题描述'}
+ ${workorder.resolution ? `
解决方案
${workorder.resolution}
` : ''}
+ ${workorder.satisfaction_score ? `
满意度评分
${workorder.satisfaction_score}/5.0 ` : ''}
+
+
+
+
+
+
+
+
+
+
+
+
+ 相似度: --
+ 未审批
+
+
+
+
+
+
+
+ ${workorder.conversations && workorder.conversations.length > 0 ? `
+
对话记录
+
+ ${workorder.conversations.map(conv => `
${new Date(conv.timestamp).toLocaleString()}
用户: ${conv.user_message}
助手: ${conv.assistant_response}
`).join('')}
+
` : ''}
+
+
+
+
`;
+ const existingModal = document.getElementById('workOrderDetailsModal');
+ if (existingModal) existingModal.remove();
+ document.body.insertAdjacentHTML('beforeend', modalHtml);
+ const modal = new bootstrap.Modal(document.getElementById('workOrderDetailsModal'));
+ modal.show();
+ 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();
+ if (workorder.id) { this.showEditWorkOrderModal(workorder); }
+ else { throw new Error(workorder.error || '获取工单详情失败'); }
+ } catch (error) { console.error('获取工单详情失败:', error); this.showNotification('获取工单详情失败: ' + error.message, 'error'); }
+ },
+
+ async deleteWorkOrder(workorderId) {
+ if (!confirm('确定要删除这个工单吗?此操作不可撤销。')) return;
+ try {
+ const response = await fetch(`/api/workorders/${workorderId}`, { method: 'DELETE' });
+ const data = await response.json();
+ if (data.success) {
+ this.showNotification('工单删除成功', 'success');
+ await this.loadWorkOrders(this.paginationConfig.currentWorkOrderPage, true);
+ await this.loadAnalytics();
+ } else { this.showNotification('删除工单失败: ' + (data.error || '未知错误'), 'error'); }
+ } catch (error) { console.error('删除工单失败:', error); this.showNotification('删除工单失败: ' + error.message, 'error'); }
+ },
+
+ showEditWorkOrderModal(workorder) {
+ const modalHtml = `
+
+
+
+
+
`;
+ const existingModal = document.getElementById('editWorkOrderModal');
+ if (existingModal) existingModal.remove();
+ document.body.insertAdjacentHTML('beforeend', modalHtml);
+ new bootstrap.Modal(document.getElementById('editWorkOrderModal')).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();
+ if (result.success) {
+ this.showNotification('工单更新成功', 'success');
+ bootstrap.Modal.getInstance(document.getElementById('editWorkOrderModal')).hide();
+ await this.loadWorkOrders();
+ await this.loadAnalytics();
+ } 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 {
+ const resp = await fetch('/api/workorders/import/template/file');
+ if (!resp.ok) throw new Error('下载接口返回错误');
+ const blob = await resp.blob();
+ const blobUrl = window.URL.createObjectURL(blob);
+ const a = document.createElement('a'); a.href = blobUrl; a.download = '工单导入模板.xlsx';
+ document.body.appendChild(a); a.click(); document.body.removeChild(a);
+ window.URL.revokeObjectURL(blobUrl);
+ this.showNotification('模板下载成功', 'success');
+ } catch (error) { console.error('下载模板失败:', error); this.showNotification('下载模板失败: ' + error.message, 'error'); }
+ },
+
+ 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 {
+ const formData = new FormData(); formData.append('file', file);
+ const response = await fetch('/api/workorders/import', { method: 'POST', body: formData });
+ const result = await response.json();
+ if (result.success) {
+ 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();
+ setTimeout(() => { bootstrap.Modal.getInstance(document.getElementById('importWorkOrderModal')).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');
+ }
+ }
+});
diff --git a/src/web/templates/dashboard.html b/src/web/templates/dashboard.html
index 0e3b038..817b351 100644
--- a/src/web/templates/dashboard.html
+++ b/src/web/templates/dashboard.html
@@ -2629,6 +2629,19 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+