/** * 铸渊运维守卫 · 前端逻辑 v2.0 * 编号: ZY-OPS-WEB-002 * 版权: 国作登字-2026-A-00037559 * * Phase 2 增强: * - 多轮对话(会话保持) * - 工具调用显示 * - 系统信息面板 * - Markdown 渲染增强 * - 打字指示器 */ 'use strict'; // ── API 配置 ────────────────────────────── const API_BASE = window.location.origin; const REFRESH_INTERVAL = 30000; const SYSINFO_INTERVAL = 60000; // ── 会话管理 ────────────────────────────── let currentSessionId = null; // ── DOM 引用 ────────────────────────────── const el = { connectionDot: document.getElementById('connectionDot'), connectionText: document.getElementById('connectionText'), statChecks: document.getElementById('statChecks'), statRepairs: document.getElementById('statRepairs'), statOpenTickets: document.getElementById('statOpenTickets'), statChats: document.getElementById('statChats'), chatMessages: document.getElementById('chatMessages'), chatInput: document.getElementById('chatInput'), btnSend: document.getElementById('btnSend'), btnClearChat: document.getElementById('btnClearChat'), btnNewSession: document.getElementById('btnNewSession'), sessionInfo: document.getElementById('sessionInfo'), ticketsList: document.getElementById('ticketsList'), btnQuickCheck: document.getElementById('btnQuickCheck'), btnDeepCheck: document.getElementById('btnDeepCheck'), lastCheckTime: document.getElementById('lastCheckTime'), eventsList: document.getElementById('eventsList'), sysinfoContent: document.getElementById('sysinfoContent'), btnRefreshSysInfo: document.getElementById('btnRefreshSysInfo') }; // ── SSE 实时连接 ────────────────────────── let eventSource = null; function connectSSE() { if (eventSource) { eventSource.close(); } try { eventSource = new EventSource(`${API_BASE}/ops/events`); eventSource.onopen = () => { el.connectionDot.className = 'status-dot online'; el.connectionText.textContent = '已连接'; }; eventSource.onmessage = (event) => { try { const data = JSON.parse(event.data); handleSSEEvent(data); } catch { /* ignore parse errors */ } }; eventSource.onerror = () => { el.connectionDot.className = 'status-dot offline'; el.connectionText.textContent = '连接断开'; setTimeout(connectSSE, 5000); }; } catch { el.connectionDot.className = 'status-dot offline'; el.connectionText.textContent = '连接失败'; setTimeout(connectSSE, 10000); } } function handleSSEEvent(data) { switch (data.type) { case 'new_ticket': addEventLog('🎫 新工单', data.ticket?.title || '', 'warning'); loadTickets(); break; case 'ticket_updated': loadTickets(); break; case 'check_complete': addEventLog( data.healthy ? '✅ 巡检通过' : '⚠️ 巡检发现问题', data.summary || '', data.healthy ? 'success' : 'warning' ); loadStats(); break; case 'daily_report': addEventLog('📊 每日报告', data.summary || '', 'info'); break; case 'connected': addEventLog('🔗 已连接', data.message || '', 'info'); break; } } function addEventLog(title, detail, level) { const div = document.createElement('div'); div.className = `event-item event-${level || 'info'}`; div.innerHTML = ` ${new Date().toLocaleTimeString('zh-CN')} ${escapeHtml(title)} ${escapeHtml(detail).slice(0, 120)} `; el.eventsList.prepend(div); while (el.eventsList.children.length > 30) { el.eventsList.removeChild(el.eventsList.lastChild); } } // ── API 请求 ────────────────────────────── async function api(method, path, body) { const options = { method, headers: { 'Content-Type': 'application/json' } }; if (body) options.body = JSON.stringify(body); const res = await fetch(`${API_BASE}${path}`, options); return res.json(); } // ── 统计刷新 ────────────────────────────── async function loadStats() { try { const stats = await api('GET', '/api/ops/stats'); el.statChecks.textContent = stats.totalChecks || 0; el.statRepairs.textContent = stats.totalRepairs || 0; el.statChats.textContent = stats.totalChats || 0; if (stats.lastQuickCheck) { el.lastCheckTime.textContent = `上次巡检: ${new Date(stats.lastQuickCheck).toLocaleString('zh-CN')}`; } const ticketResult = await api('GET', '/api/ops/tickets?status=open'); el.statOpenTickets.textContent = ticketResult.total || 0; } catch { // 静默失败 } } // ── 工单加载 ────────────────────────────── let currentFilter = 'open'; async function loadTickets() { try { const result = await api('GET', `/api/ops/tickets${currentFilter === 'open' ? '?status=open' : ''}`); renderTickets(result.tickets || []); } catch { el.ticketsList.innerHTML = '
正在检查并思考...
'; } else { div.innerHTML = formatMessage(text); } el.chatMessages.appendChild(div); el.chatMessages.scrollTop = el.chatMessages.scrollHeight; return id; } function removeChatMsg(id) { const msg = document.getElementById(id); if (msg) msg.remove(); } function formatMessage(text) { let html = escapeHtml(text); // **bold** html = html.replace(/\*\*(.*?)\*\*/g, '$1'); // code blocks html = html.replace(/```[\s\S]*?```/g, (match) => { const code = match.replace(/```\w*\n?/g, '').replace(/```/g, ''); return `${code}`;
});
// inline code
html = html.replace(/`([^`]+)`/g, '$1');
// headings
html = html.replace(/^### (.*?)$/gm, '${html}
`; } // ── 巡检按钮 ────────────────────────────── async function runCheck(type) { const btn = type === 'deep' ? el.btnDeepCheck : el.btnQuickCheck; const originalText = btn.textContent; btn.disabled = true; btn.textContent = '⏳ 巡检中...'; try { const result = await api('GET', `/api/ops/check/${type}`); if (result.skipped) { addEventLog('⏳ 巡检跳过', result.reason, 'info'); } else { addEventLog( result.healthy ? '✅ 巡检通过' : '⚠️ 发现问题', result.summary || '', result.healthy ? 'success' : 'warning' ); } loadStats(); loadTickets(); } catch (err) { addEventLog('❌ 巡检失败', err.message, 'error'); } finally { btn.disabled = false; btn.textContent = originalText; } } // ── 工具函数 ────────────────────────────── function escapeHtml(str) { if (!str) return ''; return String(str) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } // ── 事件绑定 ────────────────────────────── el.btnSend.addEventListener('click', sendMessage); el.chatInput.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } }); el.btnClearChat.addEventListener('click', () => { el.chatMessages.innerHTML = ''; appendChatMsg('system', '💬 对话已清空。有什么可以帮你?(我还记得之前的上下文)'); }); el.btnNewSession.addEventListener('click', () => { currentSessionId = null; el.chatMessages.innerHTML = ''; el.sessionInfo.textContent = '🧠 多轮对话'; appendChatMsg('system', '🔄 已开始新对话。之前的上下文已清除。'); }); el.btnQuickCheck.addEventListener('click', () => runCheck('quick')); el.btnDeepCheck.addEventListener('click', () => runCheck('deep')); el.btnRefreshSysInfo.addEventListener('click', loadSystemInfo); document.querySelectorAll('.btn-filter').forEach(btn => { btn.addEventListener('click', () => { document.querySelectorAll('.btn-filter').forEach(b => b.classList.remove('active')); btn.classList.add('active'); currentFilter = btn.dataset.filter; loadTickets(); }); }); // ── 初始化 ────────────────────────────── connectSSE(); loadStats(); loadTickets(); loadSystemInfo(); setInterval(loadStats, REFRESH_INTERVAL); setInterval(loadSystemInfo, SYSINFO_INTERVAL);