export const logsModule = { toggleLogsNavItem(show) { const logsNavItem = document.getElementById('logs-nav-item'); if (logsNavItem) { logsNavItem.style.display = show ? '' : 'none'; } }, async refreshLogs(incremental = false) { const logsContent = document.getElementById('logs-content'); if (!logsContent) return; try { if (incremental && !this.latestLogTimestamp) { incremental = false; } if (!incremental) { logsContent.innerHTML = '
' + i18n.t('logs.loading') + '
'; } let url = '/logs'; if (incremental && this.latestLogTimestamp) { url += `?after=${this.latestLogTimestamp}`; } const response = await this.makeRequest(url, { method: 'GET' }); if (response && response.lines) { if (response['latest-timestamp']) { this.latestLogTimestamp = response['latest-timestamp']; } if (incremental && response.lines.length > 0) { this.appendLogs(response.lines, response['line-count'] || 0); } else if (!incremental && response.lines.length > 0) { this.renderLogs(response.lines, response['line-count'] || response.lines.length, true); } else if (!incremental) { logsContent.innerHTML = '

' + i18n.t('logs.empty_title') + '

' + i18n.t('logs.empty_desc') + '

'; this.latestLogTimestamp = null; } } else if (!incremental) { logsContent.innerHTML = '

' + i18n.t('logs.empty_title') + '

' + i18n.t('logs.empty_desc') + '

'; this.latestLogTimestamp = null; } } catch (error) { console.error('加载日志失败:', error); if (!incremental) { const is404 = error.message && (error.message.includes('404') || error.message.includes('Not Found')); if (is404) { logsContent.innerHTML = '

' + i18n.t('logs.upgrade_required_title') + '

' + i18n.t('logs.upgrade_required_desc') + '

'; } else { logsContent.innerHTML = '

' + i18n.t('logs.load_error') + '

' + error.message + '

'; } } } }, renderLogs(lines, lineCount, scrollToBottom = true) { const logsContent = document.getElementById('logs-content'); if (!logsContent) return; if (!lines || lines.length === 0) { this.displayedLogLines = []; logsContent.innerHTML = '

' + i18n.t('logs.empty_title') + '

' + i18n.t('logs.empty_desc') + '

'; return; } const filteredLines = lines.filter(line => !line.includes('/v0/management/')); let displayedLines = filteredLines; if (filteredLines.length > this.maxDisplayLogLines) { const linesToRemove = filteredLines.length - this.maxDisplayLogLines; displayedLines = filteredLines.slice(linesToRemove); } this.displayedLogLines = displayedLines.slice(); const displayedLineCount = this.displayedLogLines.length; logsContent.innerHTML = `
${displayedLineCount} ${i18n.t('logs.lines')}
${this.buildLogsHtml(this.displayedLogLines)}
`; if (scrollToBottom) { const logsTextElement = logsContent.querySelector('.logs-text'); if (logsTextElement) { logsTextElement.scrollTop = logsTextElement.scrollHeight; } } }, appendLogs(newLines, totalLineCount) { const logsContent = document.getElementById('logs-content'); if (!logsContent) return; if (!newLines || newLines.length === 0) { return; } const logsTextElement = logsContent.querySelector('.logs-text'); const logsInfoElement = logsContent.querySelector('.logs-info'); const filteredNewLines = newLines.filter(line => !line.includes('/v0/management/')); if (filteredNewLines.length === 0) { return; } if (!logsTextElement) { this.renderLogs(filteredNewLines, totalLineCount || filteredNewLines.length, true); return; } const isAtBottom = logsTextElement.scrollHeight - logsTextElement.scrollTop - logsTextElement.clientHeight < 50; this.displayedLogLines = this.displayedLogLines.concat(filteredNewLines); if (this.displayedLogLines.length > this.maxDisplayLogLines) { this.displayedLogLines = this.displayedLogLines.slice(this.displayedLogLines.length - this.maxDisplayLogLines); } logsTextElement.innerHTML = this.buildLogsHtml(this.displayedLogLines); if (logsInfoElement) { const displayedLines = this.displayedLogLines.length; logsInfoElement.innerHTML = ` ${displayedLines} ${i18n.t('logs.lines')}`; } if (isAtBottom) { logsTextElement.scrollTop = logsTextElement.scrollHeight; } }, buildLogsHtml(lines) { if (!lines || lines.length === 0) { return ''; } return lines.map(line => { let processedLine = line.replace(/\[GIN\]\s+\d{4}\/\d{2}\/\d{2}\s+-\s+\d{2}:\d{2}:\d{2}\s+/g, ''); const highlights = []; const statusInfo = this.detectHttpStatus(line); if (statusInfo) { const statusPattern = new RegExp(`\\b${statusInfo.code}\\b`); const match = statusPattern.exec(processedLine); if (match) { highlights.push({ start: match.index, end: match.index + match[0].length, className: `log-status-tag log-status-${statusInfo.bucket}`, priority: 10 }); } } const timestampPattern = /\d{4}[-/]\d{2}[-/]\d{2}[T]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})?|\[\d{2}:\d{2}:\d{2}\]/g; let match; while ((match = timestampPattern.exec(processedLine)) !== null) { highlights.push({ start: match.index, end: match.index + match[0].length, className: 'log-timestamp', priority: 5 }); } const bracketTimestampPattern = /\[\d{4}[-/]\d{2}[-/]\d{2}\s+\d{2}:\d{2}:\d{2}(?:\.\d+)?\]/g; while ((match = bracketTimestampPattern.exec(processedLine)) !== null) { highlights.push({ start: match.index, end: match.index + match[0].length, className: 'log-timestamp', priority: 5 }); } const levelPattern = /\[(ERROR|ERRO|ERR|FATAL|CRITICAL|CRIT|WARN|WARNING|INFO|DEBUG|TRACE|PANIC)\]/gi; while ((match = levelPattern.exec(processedLine)) !== null) { const level = match[1].toUpperCase(); let className = 'log-level'; if (['ERROR', 'ERRO', 'ERR', 'FATAL', 'CRITICAL', 'CRIT', 'PANIC'].includes(level)) { className += ' log-level-error'; } else if (['WARN', 'WARNING'].includes(level)) { className += ' log-level-warn'; } else if (level === 'INFO') { className += ' log-level-info'; } else if (['DEBUG', 'TRACE'].includes(level)) { className += ' log-level-debug'; } highlights.push({ start: match.index, end: match.index + match[0].length, className, priority: 8 }); } const methodPattern = /\b(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS|CONNECT|TRACE)\b/g; while ((match = methodPattern.exec(processedLine)) !== null) { highlights.push({ start: match.index, end: match.index + match[0].length, className: 'log-http-method', priority: 6 }); } const urlPattern = /(https?:\/\/[^\s<>"']+)/g; while ((match = urlPattern.exec(processedLine)) !== null) { highlights.push({ start: match.index, end: match.index + match[0].length, className: 'log-path', priority: 4 }); } const ipPattern = /\b(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\b/g; while ((match = ipPattern.exec(processedLine)) !== null) { highlights.push({ start: match.index, end: match.index + match[0].length, className: 'log-ip', priority: 7 }); } const successPattern = /\b(success|successful|succeeded|completed|ok|done|passed)\b/gi; while ((match = successPattern.exec(processedLine)) !== null) { highlights.push({ start: match.index, end: match.index + match[0].length, className: 'log-keyword-success', priority: 3 }); } const errorPattern = /\b(failed|failure|error|exception|panic|fatal|critical|aborted|denied|refused|timeout|invalid)\b/gi; while ((match = errorPattern.exec(processedLine)) !== null) { highlights.push({ start: match.index, end: match.index + match[0].length, className: 'log-keyword-error', priority: 3 }); } const headersPattern = /\b(x-[a-z0-9-]+|authorization|content-type|user-agent)\b/gi; while ((match = headersPattern.exec(processedLine)) !== null) { highlights.push({ start: match.index, end: match.index + match[0].length, className: 'log-header-key', priority: 2 }); } highlights.sort((a, b) => { if (a.start === b.start) { return b.priority - a.priority; } return a.start - b.start; }); let cursor = 0; let result = ''; highlights.forEach((highlight) => { if (highlight.start < cursor) { return; } result += this.escapeHtml(processedLine.slice(cursor, highlight.start)); result += `${this.escapeHtml(processedLine.slice(highlight.start, highlight.end))}`; cursor = highlight.end; }); result += this.escapeHtml(processedLine.slice(cursor)); return `${result}`; }).join(''); }, detectHttpStatus(line) { if (!line) return null; const patterns = [ /\|\s*([1-5]\d{2})\s*\|/, /\b([1-5]\d{2})\s*-/, /\b(?:GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS|CONNECT|TRACE)\s+\S+\s+([1-5]\d{2})\b/, /\b(?:status|code|http)[:\s]+([1-5]\d{2})\b/i, /\b([1-5]\d{2})\s+(?:OK|Created|Accepted|No Content|Moved|Found|Bad Request|Unauthorized|Forbidden|Not Found|Method Not Allowed|Internal Server Error|Bad Gateway|Service Unavailable|Gateway Timeout)\b/i ]; for (const pattern of patterns) { const match = line.match(pattern); if (match) { const code = parseInt(match[1], 10); if (Number.isNaN(code)) { continue; } if (code >= 500) { return { code, bucket: '5xx', match: match[1] }; } if (code >= 400) { return { code, bucket: '4xx', match: match[1] }; } if (code >= 300) { return { code, bucket: '3xx', match: match[1] }; } if (code >= 200) { return { code, bucket: '2xx', match: match[1] }; } if (code >= 100) { return { code, bucket: '1xx', match: match[1] }; } } } return null; }, async downloadLogs() { try { const response = await this.makeRequest('/logs', { method: 'GET' }); if (response && response.lines && response.lines.length > 0) { const logsText = response.lines.join('\n'); const blob = new Blob([logsText], { type: 'text/plain' }); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `cli-proxy-api-logs-${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.log`; document.body.appendChild(a); a.click(); document.body.removeChild(a); window.URL.revokeObjectURL(url); this.showNotification(i18n.t('logs.download_success'), 'success'); } else { this.showNotification(i18n.t('logs.empty_title'), 'info'); } } catch (error) { console.error('下载日志失败:', error); this.showNotification(`${i18n.t('notification.download_failed')}: ${error.message}`, 'error'); } }, async clearLogs() { if (!confirm(i18n.t('logs.clear_confirm'))) { return; } try { const response = await this.makeRequest('/logs', { method: 'DELETE' }); if (response && response.status === 'ok') { const removedCount = response.removed || 0; const message = `${i18n.t('logs.clear_success')} (${i18n.t('logs.removed')}: ${removedCount} ${i18n.t('logs.lines')})`; this.showNotification(message, 'success'); } else { this.showNotification(i18n.t('logs.clear_success'), 'success'); } this.latestLogTimestamp = null; await this.refreshLogs(false); } catch (error) { console.error('清空日志失败:', error); this.showNotification(`${i18n.t('notification.delete_failed')}: ${error.message}`, 'error'); } }, toggleLogsAutoRefresh(enabled) { if (enabled) { if (this.logsRefreshTimer) { clearInterval(this.logsRefreshTimer); } this.logsRefreshTimer = setInterval(() => { const logsSection = document.getElementById('logs'); if (logsSection && logsSection.classList.contains('active')) { this.refreshLogs(true); } }, 5000); this.showNotification(i18n.t('logs.auto_refresh_enabled'), 'success'); } else { if (this.logsRefreshTimer) { clearInterval(this.logsRefreshTimer); this.logsRefreshTimer = null; } this.showNotification(i18n.t('logs.auto_refresh_disabled'), 'info'); } } };