first commit

This commit is contained in:
zhaojie
2025-09-06 21:06:18 +08:00
commit 8083f136c9
94 changed files with 20559 additions and 0 deletions

View File

@@ -0,0 +1,431 @@
/* TSP助手预警管理系统样式 */
body {
background-color: #f8f9fa;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.navbar-brand {
font-weight: bold;
font-size: 1.5rem;
}
/* 健康状态圆圈 */
.health-score {
text-align: center;
}
.score-circle {
width: 100px;
height: 100px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 10px;
font-size: 1.5rem;
font-weight: bold;
color: white;
position: relative;
}
.score-circle.excellent {
background: linear-gradient(135deg, #28a745, #20c997);
}
.score-circle.good {
background: linear-gradient(135deg, #17a2b8, #6f42c1);
}
.score-circle.fair {
background: linear-gradient(135deg, #ffc107, #fd7e14);
}
.score-circle.poor {
background: linear-gradient(135deg, #dc3545, #e83e8c);
}
.score-circle.critical {
background: linear-gradient(135deg, #6c757d, #343a40);
}
.health-status {
font-size: 0.9rem;
color: #6c757d;
text-transform: capitalize;
}
/* 预警卡片 */
.alert-card {
border-left: 4px solid;
margin-bottom: 1rem;
transition: all 0.3s ease;
}
.alert-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.alert-card.critical {
border-left-color: #dc3545;
}
.alert-card.error {
border-left-color: #fd7e14;
}
.alert-card.warning {
border-left-color: #ffc107;
}
.alert-card.info {
border-left-color: #17a2b8;
}
.alert-level {
font-size: 0.8rem;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
font-weight: bold;
text-transform: uppercase;
}
.alert-level.critical {
background-color: #dc3545;
color: white;
}
.alert-level.error {
background-color: #fd7e14;
color: white;
}
.alert-level.warning {
background-color: #ffc107;
color: #212529;
}
.alert-level.info {
background-color: #17a2b8;
color: white;
}
/* 规则表格 */
.table th {
background-color: #f8f9fa;
border-top: none;
font-weight: 600;
}
.rule-status {
font-size: 0.8rem;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
}
.rule-status.enabled {
background-color: #d4edda;
color: #155724;
}
.rule-status.disabled {
background-color: #f8d7da;
color: #721c24;
}
/* 统计卡片动画 */
.card {
transition: all 0.3s ease;
}
.card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
/* 按钮样式 */
.btn {
border-radius: 0.375rem;
font-weight: 500;
transition: all 0.3s ease;
}
.btn:hover {
transform: translateY(-1px);
}
/* 加载动画 */
.loading {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid #f3f3f3;
border-top: 3px solid #007bff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 响应式设计 */
@media (max-width: 768px) {
.container-fluid {
padding: 0 15px;
}
.score-circle {
width: 80px;
height: 80px;
font-size: 1.2rem;
}
.card-body {
padding: 1rem;
}
}
/* 自定义滚动条 */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
/* 状态指示器 */
.status-indicator {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 8px;
}
.status-indicator.running {
background-color: #28a745;
animation: pulse 2s infinite;
}
.status-indicator.stopped {
background-color: #dc3545;
}
.status-indicator.unknown {
background-color: #6c757d;
}
@keyframes pulse {
0% {
box-shadow: 0 0 0 0 rgba(40, 167, 69, 0.7);
}
70% {
box-shadow: 0 0 0 10px rgba(40, 167, 69, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(40, 167, 69, 0);
}
}
/* 模态框样式 */
.modal-content {
border-radius: 0.5rem;
border: none;
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
}
.modal-header {
border-bottom: 1px solid #e9ecef;
background-color: #f8f9fa;
}
.modal-footer {
border-top: 1px solid #e9ecef;
background-color: #f8f9fa;
}
/* 表格样式 */
.table-hover tbody tr:hover {
background-color: rgba(0,123,255,0.1);
}
/* 空状态样式 */
.empty-state {
text-align: center;
padding: 3rem 1rem;
color: #6c757d;
}
.empty-state i {
font-size: 3rem;
margin-bottom: 1rem;
opacity: 0.5;
}
/* 工具提示样式 */
.tooltip {
font-size: 0.875rem;
}
.tooltip-inner {
background-color: #212529;
border-radius: 0.375rem;
}
/* 进度条样式 */
.progress {
height: 8px;
border-radius: 4px;
}
.progress-bar {
border-radius: 4px;
}
/* 徽章样式 */
.badge {
font-size: 0.75rem;
padding: 0.375rem 0.5rem;
}
/* 卡片标题样式 */
.card-header h5 {
margin: 0;
font-weight: 600;
}
.card-header h5 i {
color: #007bff;
}
/* 统计数字样式 */
.stat-number {
font-size: 2rem;
font-weight: bold;
line-height: 1;
}
.stat-label {
font-size: 0.875rem;
opacity: 0.8;
margin-top: 0.25rem;
}
/* 预警数据展示优化 */
.alert-data {
background-color: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 4px;
padding: 8px;
font-family: 'Courier New', monospace;
font-size: 12px;
max-height: 100px;
overflow-y: auto;
white-space: pre-wrap;
word-break: break-all;
}
.alert-message {
font-weight: 500;
margin-bottom: 8px;
line-height: 1.4;
}
.alert-meta {
font-size: 12px;
color: #6c757d;
margin-bottom: 8px;
}
/* 过滤和排序控件 */
.alert-controls {
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
}
.alert-controls .form-select {
min-width: 120px;
}
/* 预警卡片内容优化 */
.alert-card .card-body {
padding: 1rem;
}
.alert-card .d-flex {
align-items: flex-start;
}
.alert-card .flex-grow-1 {
min-width: 0;
}
/* 规则表格操作按钮 */
.table .btn-sm {
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
margin-right: 0.25rem;
}
/* 响应式设计优化 */
@media (max-width: 768px) {
.alert-controls {
flex-direction: column;
align-items: stretch;
}
.alert-controls .form-select {
min-width: auto;
}
.alert-card .d-flex {
flex-direction: column;
}
.alert-card .ms-3 {
margin-left: 0 !important;
margin-top: 10px;
}
.alert-data {
font-size: 10px;
max-height: 80px;
}
}
/* 预警级别颜色优化 */
.alert-card.critical {
background-color: #f8d7da;
border-color: #dc3545;
}
.alert-card.error {
background-color: #fff3cd;
border-color: #fd7e14;
}
.alert-card.warning {
background-color: #fff3cd;
border-color: #ffc107;
}
.alert-card.info {
background-color: #d1ecf1;
border-color: #17a2b8;
}

556
src/web/static/js/app.js Normal file
View File

@@ -0,0 +1,556 @@
// TSP助手预警管理系统前端脚本
class AlertManager {
constructor() {
this.alerts = [];
this.rules = [];
this.health = {};
this.monitorStatus = 'unknown';
this.refreshInterval = null;
this.init();
}
init() {
this.bindEvents();
this.loadInitialData();
this.startAutoRefresh();
}
bindEvents() {
// 监控控制按钮
document.getElementById('start-monitor').addEventListener('click', () => this.startMonitoring());
document.getElementById('stop-monitor').addEventListener('click', () => this.stopMonitoring());
document.getElementById('check-alerts').addEventListener('click', () => this.checkAlerts());
document.getElementById('refresh-alerts').addEventListener('click', () => this.loadAlerts());
// 规则管理
document.getElementById('save-rule').addEventListener('click', () => this.saveRule());
document.getElementById('update-rule').addEventListener('click', () => this.updateRule());
// 预警过滤和排序
document.getElementById('alert-filter').addEventListener('change', () => this.updateAlertsDisplay());
document.getElementById('alert-sort').addEventListener('change', () => this.updateAlertsDisplay());
// 自动刷新
setInterval(() => {
this.loadHealth();
this.loadMonitorStatus();
}, 5000);
}
async loadInitialData() {
await Promise.all([
this.loadHealth(),
this.loadAlerts(),
this.loadRules(),
this.loadMonitorStatus()
]);
}
startAutoRefresh() {
this.refreshInterval = setInterval(() => {
this.loadAlerts();
}, 10000); // 每10秒刷新一次预警
}
async loadHealth() {
try {
const response = await fetch('/api/health');
const data = await response.json();
this.health = data;
this.updateHealthDisplay();
} catch (error) {
console.error('加载健康状态失败:', error);
}
}
async loadAlerts() {
try {
const response = await fetch('/api/alerts');
const data = await response.json();
this.alerts = data;
this.updateAlertsDisplay();
this.updateAlertStatistics();
} catch (error) {
console.error('加载预警失败:', error);
}
}
async loadRules() {
try {
const response = await fetch('/api/rules');
const data = await response.json();
this.rules = data;
this.updateRulesDisplay();
} catch (error) {
console.error('加载规则失败:', error);
}
}
async loadMonitorStatus() {
try {
const response = await fetch('/api/monitor/status');
const data = await response.json();
this.monitorStatus = data.monitor_status;
this.updateMonitorStatusDisplay();
} catch (error) {
console.error('加载监控状态失败:', error);
}
}
updateHealthDisplay() {
const healthScore = this.health.health_score || 0;
const healthStatus = this.health.health_status || 'unknown';
const scoreElement = document.getElementById('health-score-text');
const circleElement = document.getElementById('health-score-circle');
const statusElement = document.getElementById('health-status');
if (scoreElement) scoreElement.textContent = Math.round(healthScore);
if (statusElement) statusElement.textContent = this.getHealthStatusText(healthStatus);
if (circleElement) {
circleElement.className = `score-circle ${healthStatus}`;
}
}
updateAlertsDisplay() {
const container = document.getElementById('alerts-container');
if (this.alerts.length === 0) {
container.innerHTML = `
<div class="empty-state">
<i class="fas fa-check-circle"></i>
<h5>暂无活跃预警</h5>
<p>系统运行正常,没有需要处理的预警</p>
</div>
`;
return;
}
// 应用过滤和排序
let filteredAlerts = this.filterAndSortAlerts(this.alerts);
const alertsHtml = filteredAlerts.map(alert => {
const dataStr = alert.data ? JSON.stringify(alert.data, null, 2) : '无数据';
return `
<div class="alert-card ${alert.level}">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start">
<div class="flex-grow-1">
<div class="d-flex align-items-center mb-2">
<span class="alert-level ${alert.level}">${this.getLevelText(alert.level)}</span>
<span class="ms-2 text-muted fw-bold">${alert.rule_name || '未知规则'}</span>
<span class="ms-auto text-muted small">${this.formatTime(alert.created_at)}</span>
</div>
<div class="alert-message">${alert.message}</div>
<div class="alert-meta">
类型: ${this.getTypeText(alert.alert_type)} |
级别: ${this.getLevelText(alert.level)}
</div>
<div class="alert-data">${dataStr}</div>
</div>
<div class="ms-3">
<button class="btn btn-sm btn-outline-success" onclick="alertManager.resolveAlert(${alert.id})">
<i class="fas fa-check me-1"></i>解决
</button>
</div>
</div>
</div>
</div>
`;
}).join('');
container.innerHTML = alertsHtml;
}
updateRulesDisplay() {
const tbody = document.getElementById('rules-table');
if (this.rules.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="6" class="text-center text-muted">暂无规则</td>
</tr>
`;
return;
}
const rulesHtml = this.rules.map(rule => `
<tr>
<td>${rule.name}</td>
<td>${this.getTypeText(rule.alert_type)}</td>
<td><span class="alert-level ${rule.level}">${this.getLevelText(rule.level)}</span></td>
<td>${rule.threshold}</td>
<td><span class="rule-status ${rule.enabled ? 'enabled' : 'disabled'}">${rule.enabled ? '启用' : '禁用'}</span></td>
<td>
<button class="btn btn-sm btn-outline-primary me-1" onclick="alertManager.editRule('${rule.name}')">
<i class="fas fa-edit"></i>
</button>
<button class="btn btn-sm btn-outline-danger" onclick="alertManager.deleteRule('${rule.name}')">
<i class="fas fa-trash"></i>
</button>
</td>
</tr>
`).join('');
tbody.innerHTML = rulesHtml;
}
updateAlertStatistics() {
const stats = this.alerts.reduce((acc, alert) => {
acc[alert.level] = (acc[alert.level] || 0) + 1;
acc.total = (acc.total || 0) + 1;
return acc;
}, {});
document.getElementById('critical-alerts').textContent = stats.critical || 0;
document.getElementById('warning-alerts').textContent = stats.warning || 0;
document.getElementById('info-alerts').textContent = stats.info || 0;
document.getElementById('total-alerts').textContent = stats.total || 0;
}
updateMonitorStatusDisplay() {
const statusElement = document.getElementById('monitor-status');
const icon = statusElement.querySelector('i');
const text = statusElement.querySelector('span') || statusElement;
let statusText = '';
let statusClass = '';
switch (this.monitorStatus) {
case 'running':
statusText = '监控运行中';
statusClass = 'text-success';
icon.className = 'fas fa-circle text-success';
break;
case 'stopped':
statusText = '监控已停止';
statusClass = 'text-danger';
icon.className = 'fas fa-circle text-danger';
break;
default:
statusText = '监控状态未知';
statusClass = 'text-warning';
icon.className = 'fas fa-circle text-warning';
}
if (text.textContent) {
text.textContent = statusText;
} else {
statusElement.innerHTML = `<i class="fas fa-circle ${statusClass}"></i> ${statusText}`;
}
}
async startMonitoring() {
try {
const response = await fetch('/api/monitor/start', { method: 'POST' });
const data = await response.json();
if (data.success) {
this.showNotification('监控服务已启动', 'success');
this.loadMonitorStatus();
} else {
this.showNotification(data.message || '启动监控失败', 'error');
}
} catch (error) {
console.error('启动监控失败:', error);
this.showNotification('启动监控失败', 'error');
}
}
async stopMonitoring() {
try {
const response = await fetch('/api/monitor/stop', { method: 'POST' });
const data = await response.json();
if (data.success) {
this.showNotification('监控服务已停止', 'success');
this.loadMonitorStatus();
} else {
this.showNotification(data.message || '停止监控失败', 'error');
}
} catch (error) {
console.error('停止监控失败:', error);
this.showNotification('停止监控失败', 'error');
}
}
async checkAlerts() {
try {
const response = await fetch('/api/check-alerts', { method: 'POST' });
const data = await response.json();
if (data.success) {
this.showNotification(`检查完成,发现 ${data.count} 个预警`, 'info');
this.loadAlerts();
} else {
this.showNotification('检查预警失败', 'error');
}
} catch (error) {
console.error('检查预警失败:', error);
this.showNotification('检查预警失败', 'error');
}
}
async resolveAlert(alertId) {
try {
const response = await fetch(`/api/alerts/${alertId}/resolve`, { method: 'POST' });
const data = await response.json();
if (data.success) {
this.showNotification('预警已解决', 'success');
this.loadAlerts();
} else {
this.showNotification(data.message || '解决预警失败', 'error');
}
} catch (error) {
console.error('解决预警失败:', error);
this.showNotification('解决预警失败', 'error');
}
}
async saveRule() {
const formData = {
name: document.getElementById('rule-name').value,
description: document.getElementById('rule-description').value,
alert_type: document.getElementById('rule-type').value,
level: document.getElementById('rule-level').value,
threshold: parseFloat(document.getElementById('rule-threshold').value),
condition: document.getElementById('rule-condition').value,
enabled: document.getElementById('rule-enabled').checked,
check_interval: parseInt(document.getElementById('rule-interval').value),
cooldown: parseInt(document.getElementById('rule-cooldown').value)
};
try {
const response = await fetch('/api/rules', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
});
const data = await response.json();
if (data.success) {
this.showNotification('规则创建成功', 'success');
this.hideModal('ruleModal');
this.loadRules();
this.resetRuleForm();
} else {
this.showNotification(data.message || '创建规则失败', 'error');
}
} catch (error) {
console.error('创建规则失败:', error);
this.showNotification('创建规则失败', 'error');
}
}
async deleteRule(ruleName) {
if (!confirm(`确定要删除规则 "${ruleName}" 吗?`)) {
return;
}
try {
const response = await fetch(`/api/rules/${ruleName}`, { method: 'DELETE' });
const data = await response.json();
if (data.success) {
this.showNotification('规则删除成功', 'success');
this.loadRules();
} else {
this.showNotification(data.message || '删除规则失败', 'error');
}
} catch (error) {
console.error('删除规则失败:', error);
this.showNotification('删除规则失败', 'error');
}
}
filterAndSortAlerts(alerts) {
// 应用过滤
const filter = document.getElementById('alert-filter').value;
let filtered = alerts;
if (filter !== 'all') {
filtered = alerts.filter(alert => alert.level === filter);
}
// 应用排序
const sort = document.getElementById('alert-sort').value;
filtered.sort((a, b) => {
switch (sort) {
case 'time-desc':
return new Date(b.created_at) - new Date(a.created_at);
case 'time-asc':
return new Date(a.created_at) - new Date(b.created_at);
case 'level-desc':
const levelOrder = { 'critical': 4, 'error': 3, 'warning': 2, 'info': 1 };
return (levelOrder[b.level] || 0) - (levelOrder[a.level] || 0);
case 'level-asc':
const levelOrderAsc = { 'critical': 4, 'error': 3, 'warning': 2, 'info': 1 };
return (levelOrderAsc[a.level] || 0) - (levelOrderAsc[b.level] || 0);
default:
return 0;
}
});
return filtered;
}
editRule(ruleName) {
// 查找规则数据
const rule = this.rules.find(r => r.name === ruleName);
if (!rule) {
this.showNotification('规则不存在', 'error');
return;
}
// 填充编辑表单
document.getElementById('edit-rule-name-original').value = rule.name;
document.getElementById('edit-rule-name').value = rule.name;
document.getElementById('edit-rule-type').value = rule.alert_type;
document.getElementById('edit-rule-level').value = rule.level;
document.getElementById('edit-rule-threshold').value = rule.threshold;
document.getElementById('edit-rule-description').value = rule.description || '';
document.getElementById('edit-rule-condition').value = rule.condition;
document.getElementById('edit-rule-interval').value = rule.check_interval;
document.getElementById('edit-rule-cooldown').value = rule.cooldown;
document.getElementById('edit-rule-enabled').checked = rule.enabled;
// 显示编辑模态框
const modal = new bootstrap.Modal(document.getElementById('editRuleModal'));
modal.show();
}
async updateRule() {
const originalName = document.getElementById('edit-rule-name-original').value;
const formData = {
name: document.getElementById('edit-rule-name').value,
description: document.getElementById('edit-rule-description').value,
alert_type: document.getElementById('edit-rule-type').value,
level: document.getElementById('edit-rule-level').value,
threshold: parseFloat(document.getElementById('edit-rule-threshold').value),
condition: document.getElementById('edit-rule-condition').value,
enabled: document.getElementById('edit-rule-enabled').checked,
check_interval: parseInt(document.getElementById('edit-rule-interval').value),
cooldown: parseInt(document.getElementById('edit-rule-cooldown').value)
};
try {
const response = await fetch(`/api/rules/${originalName}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
});
const data = await response.json();
if (data.success) {
this.showNotification('规则更新成功', 'success');
this.hideModal('editRuleModal');
this.loadRules();
} else {
this.showNotification(data.message || '更新规则失败', 'error');
}
} catch (error) {
console.error('更新规则失败:', error);
this.showNotification('更新规则失败', 'error');
}
}
resetRuleForm() {
document.getElementById('rule-form').reset();
document.getElementById('rule-interval').value = '300';
document.getElementById('rule-cooldown').value = '3600';
document.getElementById('rule-enabled').checked = true;
}
hideModal(modalId) {
const modal = document.getElementById(modalId);
const bsModal = bootstrap.Modal.getInstance(modal);
if (bsModal) {
bsModal.hide();
}
}
showNotification(message, type = 'info') {
// 创建通知元素
const notification = document.createElement('div');
notification.className = `alert alert-${type === 'error' ? 'danger' : type} alert-dismissible fade show position-fixed`;
notification.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 300px;';
notification.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.body.appendChild(notification);
// 3秒后自动移除
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
}, 3000);
}
getLevelText(level) {
const levelMap = {
'critical': '严重',
'error': '错误',
'warning': '警告',
'info': '信息'
};
return levelMap[level] || level;
}
getTypeText(type) {
const typeMap = {
'performance': '性能',
'quality': '质量',
'volume': '量级',
'system': '系统',
'business': '业务'
};
return typeMap[type] || type;
}
getHealthStatusText(status) {
const statusMap = {
'excellent': '优秀',
'good': '良好',
'fair': '一般',
'poor': '较差',
'critical': '严重',
'unknown': '未知'
};
return statusMap[status] || status;
}
formatTime(timestamp) {
const date = new Date(timestamp);
const now = new Date();
const diff = now - date;
if (diff < 60000) { // 1分钟内
return '刚刚';
} else if (diff < 3600000) { // 1小时内
return `${Math.floor(diff / 60000)}分钟前`;
} else if (diff < 86400000) { // 1天内
return `${Math.floor(diff / 3600000)}小时前`;
} else {
return date.toLocaleDateString();
}
}
}
// 初始化应用
let alertManager;
document.addEventListener('DOMContentLoaded', () => {
alertManager = new AlertManager();
});

410
src/web/static/js/chat.js Normal file
View File

@@ -0,0 +1,410 @@
// 实时对话前端脚本
class ChatClient {
constructor() {
this.websocket = null;
this.sessionId = null;
this.isConnected = false;
this.messageCount = 0;
this.init();
}
init() {
this.bindEvents();
this.updateConnectionStatus(false);
}
bindEvents() {
// 开始对话
document.getElementById('start-chat').addEventListener('click', () => this.startChat());
// 结束对话
document.getElementById('end-chat').addEventListener('click', () => this.endChat());
// 发送消息
document.getElementById('send-button').addEventListener('click', () => this.sendMessage());
// 回车发送
document.getElementById('message-input').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
this.sendMessage();
}
});
// 创建工单
document.getElementById('create-work-order').addEventListener('click', () => this.showWorkOrderModal());
document.getElementById('create-work-order-btn').addEventListener('click', () => this.createWorkOrder());
// 快速操作按钮
document.querySelectorAll('.quick-action-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const message = e.target.getAttribute('data-message');
document.getElementById('message-input').value = message;
this.sendMessage();
});
});
}
async startChat() {
try {
// 连接WebSocket
await this.connectWebSocket();
// 创建会话
const userId = document.getElementById('user-id').value || 'anonymous';
const workOrderId = document.getElementById('work-order-id').value || null;
const response = await this.sendWebSocketMessage({
type: 'create_session',
user_id: userId,
work_order_id: workOrderId ? parseInt(workOrderId) : null
});
if (response.type === 'session_created') {
this.sessionId = response.session_id;
this.updateSessionInfo();
this.enableChat();
this.addSystemMessage('对话已开始,请描述您的问题。');
} else {
this.showError('创建会话失败');
}
} catch (error) {
console.error('启动对话失败:', error);
this.showError('启动对话失败: ' + error.message);
}
}
async endChat() {
try {
if (this.sessionId) {
await this.sendWebSocketMessage({
type: 'end_session',
session_id: this.sessionId
});
}
this.sessionId = null;
this.disableChat();
this.addSystemMessage('对话已结束。');
} catch (error) {
console.error('结束对话失败:', error);
}
}
async sendMessage() {
const input = document.getElementById('message-input');
const message = input.value.trim();
if (!message || !this.sessionId) {
return;
}
// 清空输入框
input.value = '';
// 添加用户消息
this.addMessage('user', message);
// 显示打字指示器
this.showTypingIndicator();
try {
const response = await this.sendWebSocketMessage({
type: 'send_message',
session_id: this.sessionId,
message: message
});
this.hideTypingIndicator();
if (response.type === 'message_response' && response.result.success) {
const result = response.result;
// 添加助手回复
this.addMessage('assistant', result.content, {
knowledge_used: result.knowledge_used,
confidence_score: result.confidence_score,
work_order_id: result.work_order_id
});
// 更新工单ID
if (result.work_order_id) {
document.getElementById('work-order-id').value = result.work_order_id;
}
} else {
this.addMessage('assistant', '抱歉,我暂时无法处理您的问题。请稍后再试。');
}
} catch (error) {
this.hideTypingIndicator();
console.error('发送消息失败:', error);
this.addMessage('assistant', '发送消息失败,请检查网络连接。');
}
}
async createWorkOrder() {
const title = document.getElementById('wo-title').value;
const description = document.getElementById('wo-description').value;
const category = document.getElementById('wo-category').value;
const priority = document.getElementById('wo-priority').value;
if (!title || !description) {
this.showError('请填写工单标题和描述');
return;
}
try {
const response = await this.sendWebSocketMessage({
type: 'create_work_order',
session_id: this.sessionId,
title: title,
description: description,
category: category,
priority: priority
});
if (response.type === 'work_order_created' && response.result.success) {
const workOrderId = response.result.work_order_id;
document.getElementById('work-order-id').value = workOrderId;
this.addSystemMessage(`工单创建成功!工单号: ${response.result.order_id}`);
// 关闭模态框
const modal = bootstrap.Modal.getInstance(document.getElementById('workOrderModal'));
modal.hide();
// 清空表单
document.getElementById('work-order-form').reset();
} else {
this.showError('创建工单失败: ' + (response.result.error || '未知错误'));
}
} catch (error) {
console.error('创建工单失败:', error);
this.showError('创建工单失败: ' + error.message);
}
}
connectWebSocket() {
return new Promise((resolve, reject) => {
try {
this.websocket = new WebSocket('ws://localhost:8765');
// 设置连接超时
const timeout = setTimeout(() => {
if (this.websocket.readyState !== WebSocket.OPEN) {
this.websocket.close();
reject(new Error('WebSocket连接超时请检查服务器是否启动'));
}
}, 5000); // 5秒超时
this.websocket.onopen = () => {
clearTimeout(timeout);
this.isConnected = true;
this.updateConnectionStatus(true);
resolve();
};
this.websocket.onclose = () => {
clearTimeout(timeout);
this.isConnected = false;
this.updateConnectionStatus(false);
};
this.websocket.onerror = (error) => {
clearTimeout(timeout);
console.error('WebSocket错误:', error);
reject(new Error('WebSocket连接失败请检查服务器是否启动'));
};
this.websocket.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
this.handleWebSocketMessage(data);
} catch (error) {
console.error('解析WebSocket消息失败:', error);
}
};
} catch (error) {
reject(error);
}
});
}
sendWebSocketMessage(message) {
return new Promise((resolve, reject) => {
if (!this.websocket || this.websocket.readyState !== WebSocket.OPEN) {
reject(new Error('WebSocket未连接'));
return;
}
const messageId = 'msg_' + Date.now();
message.messageId = messageId;
// 设置超时
const timeout = setTimeout(() => {
reject(new Error('请求超时'));
}, 10000);
// 监听响应
const handleResponse = (event) => {
try {
const data = JSON.parse(event.data);
if (data.messageId === messageId) {
clearTimeout(timeout);
this.websocket.removeEventListener('message', handleResponse);
resolve(data);
}
} catch (error) {
// 忽略解析错误
}
};
this.websocket.addEventListener('message', handleResponse);
this.websocket.send(JSON.stringify(message));
});
}
handleWebSocketMessage(data) {
// 处理WebSocket消息
console.log('收到WebSocket消息:', data);
}
addMessage(role, content, metadata = {}) {
const messagesContainer = document.getElementById('chat-messages');
// 如果是第一条消息,清空欢迎信息
if (this.messageCount === 0) {
messagesContainer.innerHTML = '';
}
const messageDiv = document.createElement('div');
messageDiv.className = `message ${role}`;
const avatar = document.createElement('div');
avatar.className = 'message-avatar';
avatar.textContent = role === 'user' ? 'U' : 'A';
const contentDiv = document.createElement('div');
contentDiv.className = 'message-content';
contentDiv.innerHTML = content;
// 添加时间戳
const timeDiv = document.createElement('div');
timeDiv.className = 'message-time';
timeDiv.textContent = new Date().toLocaleTimeString();
contentDiv.appendChild(timeDiv);
// 添加元数据
if (metadata.knowledge_used && metadata.knowledge_used.length > 0) {
const knowledgeDiv = document.createElement('div');
knowledgeDiv.className = 'knowledge-info';
knowledgeDiv.innerHTML = `<i class="fas fa-lightbulb me-1"></i>基于 ${metadata.knowledge_used.length} 条知识库信息生成`;
contentDiv.appendChild(knowledgeDiv);
}
if (metadata.confidence_score) {
const confidenceDiv = document.createElement('div');
confidenceDiv.className = 'confidence-score';
confidenceDiv.textContent = `置信度: ${(metadata.confidence_score * 100).toFixed(1)}%`;
contentDiv.appendChild(confidenceDiv);
}
if (metadata.work_order_id) {
const workOrderDiv = document.createElement('div');
workOrderDiv.className = 'work-order-info';
workOrderDiv.innerHTML = `<i class="fas fa-ticket-alt me-1"></i>关联工单: ${metadata.work_order_id}`;
contentDiv.appendChild(workOrderDiv);
}
if (role === 'user') {
messageDiv.appendChild(contentDiv);
messageDiv.appendChild(avatar);
} else {
messageDiv.appendChild(avatar);
messageDiv.appendChild(contentDiv);
}
messagesContainer.appendChild(messageDiv);
messagesContainer.scrollTop = messagesContainer.scrollHeight;
this.messageCount++;
}
addSystemMessage(content) {
const messagesContainer = document.getElementById('chat-messages');
const messageDiv = document.createElement('div');
messageDiv.className = 'text-center text-muted py-2';
messageDiv.innerHTML = `<small><i class="fas fa-info-circle me-1"></i>${content}</small>`;
messagesContainer.appendChild(messageDiv);
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}
showTypingIndicator() {
document.getElementById('typing-indicator').classList.add('show');
}
hideTypingIndicator() {
document.getElementById('typing-indicator').classList.remove('show');
}
updateConnectionStatus(connected) {
const statusElement = document.getElementById('connection-status');
if (connected) {
statusElement.className = 'connection-status connected';
statusElement.innerHTML = '<i class="fas fa-circle me-1"></i>已连接';
} else {
statusElement.className = 'connection-status disconnected';
statusElement.innerHTML = '<i class="fas fa-circle me-1"></i>未连接';
}
}
updateSessionInfo() {
const sessionInfo = document.getElementById('session-info');
sessionInfo.innerHTML = `
<div><strong>会话ID:</strong> ${this.sessionId}</div>
<div><strong>消息数:</strong> ${this.messageCount}</div>
<div><strong>状态:</strong> 活跃</div>
`;
}
enableChat() {
document.getElementById('start-chat').disabled = true;
document.getElementById('end-chat').disabled = false;
document.getElementById('message-input').disabled = false;
document.getElementById('send-button').disabled = false;
}
disableChat() {
document.getElementById('start-chat').disabled = false;
document.getElementById('end-chat').disabled = true;
document.getElementById('message-input').disabled = true;
document.getElementById('send-button').disabled = true;
}
showWorkOrderModal() {
if (!this.sessionId) {
this.showError('请先开始对话');
return;
}
const modal = new bootstrap.Modal(document.getElementById('workOrderModal'));
modal.show();
}
showError(message) {
this.addSystemMessage(`<span class="text-danger">错误: ${message}</span>`);
}
}
// 初始化聊天客户端
document.addEventListener('DOMContentLoaded', () => {
window.chatClient = new ChatClient();
});

View File

@@ -0,0 +1,334 @@
// HTTP版本实时对话前端脚本
class ChatHttpClient {
constructor() {
this.sessionId = null;
this.messageCount = 0;
this.apiBase = '/api/chat';
this.init();
}
init() {
this.bindEvents();
this.updateConnectionStatus(true);
}
bindEvents() {
// 开始对话
document.getElementById('start-chat').addEventListener('click', () => this.startChat());
// 结束对话
document.getElementById('end-chat').addEventListener('click', () => this.endChat());
// 发送消息
document.getElementById('send-button').addEventListener('click', () => this.sendMessage());
// 回车发送
document.getElementById('message-input').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
this.sendMessage();
}
});
// 创建工单
document.getElementById('create-work-order').addEventListener('click', () => this.showWorkOrderModal());
document.getElementById('create-work-order-btn').addEventListener('click', () => this.createWorkOrder());
// 快速操作按钮
document.querySelectorAll('.quick-action-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const message = e.target.getAttribute('data-message');
document.getElementById('message-input').value = message;
this.sendMessage();
});
});
}
async startChat() {
try {
// 创建会话
const userId = document.getElementById('user-id').value || 'anonymous';
const workOrderId = document.getElementById('work-order-id').value || null;
const response = await this.sendRequest('POST', '/session', {
user_id: userId,
work_order_id: workOrderId ? parseInt(workOrderId) : null
});
if (response.success) {
this.sessionId = response.session_id;
this.updateSessionInfo();
this.enableChat();
this.addSystemMessage('对话已开始,请描述您的问题。');
} else {
this.showError('创建会话失败');
}
} catch (error) {
console.error('启动对话失败:', error);
this.showError('启动对话失败: ' + error.message);
}
}
async endChat() {
try {
if (this.sessionId) {
await this.sendRequest('DELETE', `/session/${this.sessionId}`);
}
this.sessionId = null;
this.disableChat();
this.addSystemMessage('对话已结束。');
} catch (error) {
console.error('结束对话失败:', error);
}
}
async sendMessage() {
const input = document.getElementById('message-input');
const message = input.value.trim();
if (!message || !this.sessionId) {
return;
}
// 清空输入框
input.value = '';
// 添加用户消息
this.addMessage('user', message);
// 显示打字指示器
this.showTypingIndicator();
try {
const response = await this.sendRequest('POST', '/message', {
session_id: this.sessionId,
message: message
});
this.hideTypingIndicator();
if (response.success) {
// 添加助手回复
this.addMessage('assistant', response.content, {
knowledge_used: response.knowledge_used,
confidence_score: response.confidence_score,
work_order_id: response.work_order_id
});
// 更新工单ID
if (response.work_order_id) {
document.getElementById('work-order-id').value = response.work_order_id;
}
} else {
this.addMessage('assistant', '抱歉,我暂时无法处理您的问题。请稍后再试。');
}
} catch (error) {
this.hideTypingIndicator();
console.error('发送消息失败:', error);
this.addMessage('assistant', '发送消息失败,请检查网络连接。');
}
}
async createWorkOrder() {
const title = document.getElementById('wo-title').value;
const description = document.getElementById('wo-description').value;
const category = document.getElementById('wo-category').value;
const priority = document.getElementById('wo-priority').value;
if (!title || !description) {
this.showError('请填写工单标题和描述');
return;
}
try {
const response = await this.sendRequest('POST', '/work-order', {
session_id: this.sessionId,
title: title,
description: description,
category: category,
priority: priority
});
if (response.success) {
const workOrderId = response.work_order_id;
document.getElementById('work-order-id').value = workOrderId;
this.addSystemMessage(`工单创建成功!工单号: ${response.order_id}`);
// 关闭模态框
const modal = bootstrap.Modal.getInstance(document.getElementById('workOrderModal'));
modal.hide();
// 清空表单
document.getElementById('work-order-form').reset();
} else {
this.showError('创建工单失败: ' + (response.error || '未知错误'));
}
} catch (error) {
console.error('创建工单失败:', error);
this.showError('创建工单失败: ' + error.message);
}
}
async sendRequest(method, endpoint, data = null) {
const url = this.apiBase + endpoint;
const options = {
method: method,
headers: {
'Content-Type': 'application/json',
}
};
if (data) {
options.body = JSON.stringify(data);
}
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP错误: ${response.status}`);
}
return await response.json();
}
addMessage(role, content, metadata = {}) {
const messagesContainer = document.getElementById('chat-messages');
// 如果是第一条消息,清空欢迎信息
if (this.messageCount === 0) {
messagesContainer.innerHTML = '';
}
const messageDiv = document.createElement('div');
messageDiv.className = `message ${role}`;
const avatar = document.createElement('div');
avatar.className = 'message-avatar';
avatar.textContent = role === 'user' ? 'U' : 'A';
const contentDiv = document.createElement('div');
contentDiv.className = 'message-content';
contentDiv.innerHTML = content;
// 添加时间戳
const timeDiv = document.createElement('div');
timeDiv.className = 'message-time';
timeDiv.textContent = new Date().toLocaleTimeString();
contentDiv.appendChild(timeDiv);
// 添加元数据
if (metadata.knowledge_used && metadata.knowledge_used.length > 0) {
const knowledgeDiv = document.createElement('div');
knowledgeDiv.className = 'knowledge-info';
knowledgeDiv.innerHTML = `<i class="fas fa-lightbulb me-1"></i>基于 ${metadata.knowledge_used.length} 条知识库信息生成`;
contentDiv.appendChild(knowledgeDiv);
}
if (metadata.confidence_score) {
const confidenceDiv = document.createElement('div');
confidenceDiv.className = 'confidence-score';
confidenceDiv.textContent = `置信度: ${(metadata.confidence_score * 100).toFixed(1)}%`;
contentDiv.appendChild(confidenceDiv);
}
if (metadata.work_order_id) {
const workOrderDiv = document.createElement('div');
workOrderDiv.className = 'work-order-info';
workOrderDiv.innerHTML = `<i class="fas fa-ticket-alt me-1"></i>关联工单: ${metadata.work_order_id}`;
contentDiv.appendChild(workOrderDiv);
}
if (role === 'user') {
messageDiv.appendChild(contentDiv);
messageDiv.appendChild(avatar);
} else {
messageDiv.appendChild(avatar);
messageDiv.appendChild(contentDiv);
}
messagesContainer.appendChild(messageDiv);
messagesContainer.scrollTop = messagesContainer.scrollHeight;
this.messageCount++;
}
addSystemMessage(content) {
const messagesContainer = document.getElementById('chat-messages');
const messageDiv = document.createElement('div');
messageDiv.className = 'text-center text-muted py-2';
messageDiv.innerHTML = `<small><i class="fas fa-info-circle me-1"></i>${content}</small>`;
messagesContainer.appendChild(messageDiv);
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}
showTypingIndicator() {
document.getElementById('typing-indicator').classList.add('show');
}
hideTypingIndicator() {
document.getElementById('typing-indicator').classList.remove('show');
}
updateConnectionStatus(connected) {
const statusElement = document.getElementById('connection-status');
if (connected) {
statusElement.className = 'connection-status connected';
statusElement.innerHTML = '<i class="fas fa-circle me-1"></i>HTTP连接';
} else {
statusElement.className = 'connection-status disconnected';
statusElement.innerHTML = '<i class="fas fa-circle me-1"></i>连接断开';
}
}
updateSessionInfo() {
const sessionInfo = document.getElementById('session-info');
sessionInfo.innerHTML = `
<div><strong>会话ID:</strong> ${this.sessionId}</div>
<div><strong>消息数:</strong> ${this.messageCount}</div>
<div><strong>状态:</strong> 活跃</div>
`;
}
enableChat() {
document.getElementById('start-chat').disabled = true;
document.getElementById('end-chat').disabled = false;
document.getElementById('message-input').disabled = false;
document.getElementById('send-button').disabled = false;
}
disableChat() {
document.getElementById('start-chat').disabled = false;
document.getElementById('end-chat').disabled = true;
document.getElementById('message-input').disabled = true;
document.getElementById('send-button').disabled = true;
}
showWorkOrderModal() {
if (!this.sessionId) {
this.showError('请先开始对话');
return;
}
const modal = new bootstrap.Modal(document.getElementById('workOrderModal'));
modal.show();
}
showError(message) {
this.addSystemMessage(`<span class="text-danger">错误: ${message}</span>`);
}
}
// 初始化聊天客户端
document.addEventListener('DOMContentLoaded', () => {
window.chatClient = new ChatHttpClient();
});

File diff suppressed because it is too large Load Diff