diff --git a/templates/index.html b/templates/index.html index ab3bd4f..4937131 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1392,7 +1392,10 @@ refreshForumLog(); } if (currentApp === 'report') { - refreshReportLog(); + // 使用新的日志管理器刷新 + if (reportLogManager && reportLogManager.isRunning) { + reportLogManager.refresh(); + } } }, 100); } else { @@ -3536,7 +3539,7 @@ // 仅保留特殊页面的初始化逻辑 if (app === 'report') { // 【修复】切换到Report Engine时启动日志刷新 - startReportLogRefresh(); + reportLogManager.start(); // 只在报告界面未初始化时才重新加载 const reportContent = document.getElementById('reportContent'); @@ -3549,7 +3552,7 @@ }, 500); } else { // 【修复】切换离开Report Engine时停止日志刷新,节省资源 - stopReportLogRefresh(); + reportLogManager.stop(); } } @@ -3822,9 +3825,12 @@ refreshForumLog(); return; } - + if (currentApp === 'report') { - refreshReportLog(); + // 使用新的日志管理器刷新 + if (reportLogManager && reportLogManager.isRunning) { + reportLogManager.refresh(); + } return; } @@ -4246,6 +4252,325 @@ let reportLockCheckInterval = null; let lastCompletedReportTask = null; + // ====== Report Engine 日志管理器 ====== + class ReportLogManager { + constructor() { + this.intervalId = null; + this.lineCount = 0; + this.isRunning = false; + this.refreshInterval = 250; // 250ms轮询一次,更加实时 + this.lastError = null; + this.retryCount = 0; + this.maxRetries = 3; + } + + // 启动日志轮询 + start() { + if (this.isRunning) { + console.log('[ReportLogManager] 已在运行,跳过重复启动'); + return; + } + + console.log('[ReportLogManager] ===== 启动日志轮询系统 ====='); + this.isRunning = true; + this.retryCount = 0; + + // 立即执行一次 + console.log('[ReportLogManager] 执行初始刷新...'); + this.refresh(); + + // 启动定时轮询 + this.intervalId = setInterval(() => { + if (currentApp === 'report' && this.isRunning) { + console.log('[ReportLogManager] 定时器触发,执行刷新...'); + this.refresh(); + } + }, this.refreshInterval); + + console.log(`[ReportLogManager] 轮询已启动,频率: ${this.refreshInterval}ms, intervalId: ${this.intervalId}`); + } + + // 停止日志轮询 + stop() { + if (!this.isRunning) { + console.log('[ReportLogManager] 未在运行,无需停止'); + return; + } + + console.log('[ReportLogManager] ===== 停止日志轮询系统 ====='); + this.isRunning = false; + + if (this.intervalId) { + clearInterval(this.intervalId); + this.intervalId = null; + } + + console.log('[ReportLogManager] 轮询已停止'); + } + + // 重置计数器(任务开始时调用) + reset() { + console.log(`[ReportLogManager] 重置计数器,原值: ${this.lineCount}`); + this.lineCount = 0; + this.lastError = null; + this.retryCount = 0; + } + + // 刷新日志 + refresh() { + if (!this.isRunning) { + console.log('[ReportLogManager.refresh] 管理器未运行,跳过刷新'); + return; + } + + console.log('[ReportLogManager.refresh] 开始刷新日志...'); + + // 【修复】使用传统的Promise方式,避免async/await兼容性问题 + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 3000); + + fetch('/api/report/log', { + method: 'GET', + headers: { 'Cache-Control': 'no-cache' }, + signal: controller.signal + }) + .then(response => { + clearTimeout(timeoutId); + console.log(`[ReportLogManager.refresh] 收到响应,状态: ${response.status}`); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + + return response.json(); + }) + .then(data => { + console.log('[ReportLogManager.refresh] 解析JSON成功'); + + if (!data.success) { + throw new Error(data.error || '未知错误'); + } + + // 成功后重置重试计数 + this.retryCount = 0; + + // 【调试】输出获取到的日志行数 + if (data.log_lines) { + console.log(`[ReportLogManager.refresh] API返回 ${data.log_lines.length} 行日志`); + } else { + console.log('[ReportLogManager.refresh] API返回的log_lines为空'); + } + + // 处理日志数据 + this.processLogs(data.log_lines || []); + }) + .catch(error => { + clearTimeout(timeoutId); + console.error('[ReportLogManager.refresh] 错误:', error); + this.handleError(error); + }); + } + + // 处理日志数据 + processLogs(logLines) { + const totalLines = logLines.length; + + console.log(`[ReportLogManager.processLogs] 总行数: ${totalLines}, 当前计数: ${this.lineCount}`); + + // 如果有新日志 + if (totalLines > this.lineCount) { + const newLines = logLines.slice(this.lineCount); + console.log(`[ReportLogManager] 发现 ${newLines.length} 条新日志 (${this.lineCount} -> ${totalLines})`); + + // 输出前3行新日志用于调试 + if (newLines.length > 0) { + console.log('[ReportLogManager] 新日志样本:'); + newLines.slice(0, 3).forEach((line, idx) => { + console.log(` [${idx}] ${line.substring(0, 100)}...`); + }); + } + + // 逐行处理并显示 + newLines.forEach((line, index) => { + console.log(`[ReportLogManager.processLogs] 处理第 ${index + 1}/${newLines.length} 行`); + this.displayLogLine(line); + }); + + // 更新计数器 + this.lineCount = totalLines; + console.log(`[ReportLogManager] 计数器更新为: ${this.lineCount}`); + } else { + console.log(`[ReportLogManager] 没有新日志 (总数: ${totalLines}, 已读: ${this.lineCount})`); + } + } + + // 显示单行日志(带格式化) + displayLogLine(line) { + // 解析loguru格式的日志 + const logPattern = /^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3})\s*\|\s*(INFO|DEBUG|WARNING|ERROR|CRITICAL)\s*\|\s*(.+?)\s*-\s*(.*)$/; + const match = line.match(logPattern); + + if (match) { + const [, timestamp, level, location, message] = match; + + // 【调试】输出匹配到的日志级别 + if (level === 'WARNING' || level === 'ERROR' || level === 'DEBUG') { + console.log(`[ReportLogManager] 检测到 ${level} 日志: ${message.substring(0, 50)}...`); + } + + // 格式化输出 - 简化时间戳,只显示时间部分 + const timeOnly = timestamp.split(' ')[1]; + const formattedLine = `[${timeOnly}] [${level}] ${message}`; + + // 添加到控制台(带样式提示) + if (level === 'ERROR' || level === 'CRITICAL') { + appendConsoleTextLine('report', formattedLine, 'error'); + } else if (level === 'WARNING') { + appendConsoleTextLine('report', formattedLine, 'warning'); + } else if (level === 'DEBUG') { + appendConsoleTextLine('report', formattedLine, 'debug'); + } else { + appendConsoleTextLine('report', formattedLine); + } + + // 同时在浏览器控制台输出(便于调试)- 降低输出频率 + if (level === 'ERROR' || level === 'CRITICAL') { + console.error(`[ReportLog] ${formattedLine}`); + } else if (level === 'WARNING') { + console.warn(`[ReportLog] ${formattedLine}`); + } else if (level === 'DEBUG') { + console.debug(`[ReportLog] ${formattedLine}`); + } + } else { + // 非标准格式的日志,直接显示 + // 【调试】输出未匹配的行,帮助调试正则 + if (line.includes('WARNING') || line.includes('ERROR') || line.includes('DEBUG')) { + console.log(`[ReportLogManager] 未匹配的日志行: "${line}"`); + } + appendConsoleTextLine('report', line); + } + } + + // 处理错误 + handleError(error) { + // 避免重复错误日志 + const errorMsg = error.message || error.toString(); + if (errorMsg === this.lastError) { + return; // 相同错误不重复输出 + } + + this.lastError = errorMsg; + this.retryCount++; + + // 只在前几次重试时输出错误 + if (this.retryCount <= this.maxRetries) { + console.warn(`[ReportLogManager] 获取日志失败 (${this.retryCount}/${this.maxRetries}): ${errorMsg}`); + } + + // 超过最大重试次数时暂停一段时间 + if (this.retryCount > this.maxRetries) { + this.stop(); + console.error('[ReportLogManager] 多次失败,暂停轮询'); + + // 5秒后自动重试 + setTimeout(() => { + if (currentApp === 'report') { + console.log('[ReportLogManager] 尝试恢复轮询...'); + this.start(); + } + }, 5000); + } + } + + // 获取状态信息 + getStatus() { + return { + isRunning: this.isRunning, + lineCount: this.lineCount, + intervalId: this.intervalId, + lastError: this.lastError, + retryCount: this.retryCount + }; + } + } + + // 创建全局日志管理器实例 + const reportLogManager = new ReportLogManager(); + + // 【调试】测试日志管理器 + window.testReportLogManager = function() { + console.log('[测试] ===== 开始测试Report日志管理器 ====='); + + // 检查当前状态 + const status = reportLogManager.getStatus(); + console.log('[测试] 当前状态:', status); + + // 如果未运行,启动它 + if (!status.isRunning) { + console.log('[测试] 启动日志管理器...'); + reportLogManager.start(); + } + + // 手动刷新一次 + console.log('[测试] 手动触发刷新...'); + reportLogManager.refresh(); + + // 模拟添加日志 + console.log('[测试] 模拟添加WARNING日志...'); + appendConsoleTextLine('report', '[21:02:43.014] [WARNING] 测试警告消息', 'warning'); + + console.log('[测试] 模拟添加ERROR日志...'); + appendConsoleTextLine('report', '[21:02:43.018] [ERROR] 测试错误消息', 'error'); + + console.log('[测试] ===== 测试完成 ====='); + }; + + // 【调试】直接测试API + window.testReportAPI = function() { + console.log('[测试API] ===== 开始测试Report API ====='); + + fetch('/api/report/log', { + method: 'GET', + headers: { 'Cache-Control': 'no-cache' } + }) + .then(response => { + console.log('[测试API] 响应状态:', response.status); + return response.json(); + }) + .then(data => { + console.log('[测试API] 返回数据:', data); + if (data.success && data.log_lines) { + console.log('[测试API] 日志行数:', data.log_lines.length); + console.log('[测试API] 前5行日志:'); + data.log_lines.slice(0, 5).forEach((line, idx) => { + console.log(` ${idx}: ${line}`); + }); + + // 查找WARNING和ERROR日志 + const warnings = data.log_lines.filter(line => line.includes('WARNING')); + const errors = data.log_lines.filter(line => line.includes('ERROR')); + + console.log(`[测试API] 找到 ${warnings.length} 条WARNING日志`); + console.log(`[测试API] 找到 ${errors.length} 条ERROR日志`); + + if (warnings.length > 0) { + console.log('[测试API] WARNING日志示例:'); + warnings.slice(0, 3).forEach(line => console.log(' ', line)); + } + + if (errors.length > 0) { + console.log('[测试API] ERROR日志示例:'); + errors.slice(0, 3).forEach(line => console.log(' ', line)); + } + } + }) + .catch(error => { + console.error('[测试API] 错误:', error); + }); + + console.log('[测试API] ===== 测试完成 ====='); + }; + // 实时刷新论坛消息(适用于所有页面) function refreshForumMessages() { fetch('/api/forum/log') @@ -4466,119 +4791,30 @@ }); } + // 【重构】刷新Report日志(使用新的日志管理器) function refreshReportLog() { - // 【修复】添加超时控制和完整错误处理 - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 5000); // 5秒超时 - - fetch('/api/report/log', { signal: controller.signal }) - .then(response => { - clearTimeout(timeoutId); - // 【修复】检查HTTP状态 - if (!response.ok) { - throw new Error(`HTTP ${response.status}: ${response.statusText}`); - } - return response.json(); - }) - .then(data => { - // 【修复】检查返回的成功状态 - if (!data.success) { - console.error('[Report日志] 刷新失败:', data.error || '未知错误'); - return; - } - - if (data.log_lines && data.log_lines.length > reportLogLineCount) { - // 只添加新的行 - const newLines = data.log_lines.slice(reportLogLineCount); - - // 【调试日志】记录实时读取的日志数量 - if (newLines.length > 0) { - console.log(`[Report日志] 读取 ${newLines.length} 条新日志(总计 ${data.log_lines.length})`); - } - - newLines.forEach(line => { - // 直接添加,使用LogVirtualList的默认批处理机制 - appendConsoleTextLine('report', line); - }); - - reportLogLineCount = data.log_lines.length; - } - }) - .catch(error => { - clearTimeout(timeoutId); - // 【修复】区分错误类型 - if (error.name === 'AbortError') { - console.warn('[Report日志] 刷新超时(5秒)'); - } else if (error.message.includes('Failed to fetch')) { - console.error('[Report日志] 刷新失败: 网络连接错误'); - } else { - console.error('[Report日志] 刷新失败:', error.message || error); - } - }); + // 兼容旧代码:直接调用日志管理器的刷新 + if (reportLogManager && reportLogManager.isRunning) { + reportLogManager.refresh(); + } else { + console.log('[RefreshReportLog] 日志管理器未运行,跳过刷新'); + } } - // 加载Report Engine日志 + // 加载Report Engine日志(初始化时使用) function loadReportLog() { - fetch('/api/report/log') - .then(response => response.json()) - .then(data => { - // 【FIX Bug #5】检查是否仍然在report页面 - if (currentApp !== 'report') { - console.log('忽略report日志响应(已切换到其他app)'); - return; - } + // 使用新的日志管理器 + if (!reportLogManager.isRunning) { + // 清空控制台 + clearConsoleLayer('report', '[系统] Report Engine 日志监控已启动'); - if (data.success) { - if (reportLogLineCount === 0) { - clearConsoleLayer('report', '[系统] Report Engine 日志监控已启动'); - logRenderers['report'].render(); // 立即渲染 - } - - if (data.log_lines && data.log_lines.length > 0) { - const newLines = data.log_lines.slice(reportLogLineCount); - const linesToProcess = reportLogLineCount === 0 ? data.log_lines : newLines; - - linesToProcess.forEach(line => { - appendConsoleTextLine('report', line); - }); - - // 重置计数器以确保后续消息能正确显示 - reportLogLineCount = data.log_lines.length; - - // 移除"正在加载"提示(如果存在) - const renderer = logRenderers['report']; - if (renderer && renderer.lines.length > 0) { - const firstLine = renderer.lines[0]; - if (firstLine && firstLine.text.includes('正在加载')) { - renderer.lines.shift(); - renderer.lastRenderHash = null; - renderer.scheduleRender(true); - } - } - } else { - // 如果没有日志,重置计数器 - reportLogLineCount = 0; - } - } else { - // 【优化】加载失败显示错误 - const renderer = logRenderers['report']; - if (renderer && currentApp === 'report') { - renderer.clear('[错误] 加载Report日志失败'); - renderer.render(); - } - } - }) - .catch(error => { - console.error('加载Report日志失败:', error); - // 【优化】显示错误提示 - if (currentApp === 'report') { - const renderer = logRenderers['report']; - if (renderer) { - renderer.clear('[错误] 加载Report日志失败: ' + error.message); - renderer.render(); - } - } - }); + // 重置计数器并启动 + reportLogManager.reset(); + reportLogManager.start(); + } else { + // 如果已经在运行,只是刷新一次 + reportLogManager.refresh(); + } } // 解析论坛消息并添加到对话区 @@ -4739,7 +4975,7 @@ // 【修复】加载Report界面时启动日志刷新 if (currentApp === 'report') { - startReportLogRefresh(); + reportLogManager.start(); } } else { reportContent.innerHTML = ` @@ -5110,9 +5346,10 @@ } const query = document.getElementById('searchInput').value.trim() || '智能舆情分析报告'; - - // 重置日志计数器,因为后台会清空日志文件 - reportLogLineCount = 0; + + // 【修复】先停止现有的日志轮询,避免与后端清空日志的竞态条件 + reportLogManager.stop(); + reportAutoPreviewLoaded = false; safeCloseReportStream(true); @@ -5157,10 +5394,14 @@ appendReportStreamLine('任务创建成功,正在建立流式连接...', 'info', { force: true }); - // 【优化】先启动日志轮询,再建立SSE连接 + // 【修复】在API成功后重置计数器,此时后端已清空日志文件 + // 避免在API调用期间旧interval读取旧日志导致的竞态条件 + reportLogManager.reset(); + + // 【优化】启动日志轮询 // 确保从任务开始就能读取日志 - startReportLogRefresh(); - console.log('[Report日志] 任务创建成功,启动日志轮询'); + reportLogManager.start(); + console.log('[ReportLogManager] 任务创建成功,计数器已重置,启动日志轮询'); if (window.EventSource) { openReportStream(reportTaskId); @@ -5192,38 +5433,8 @@ } // 【修复】启动Report Engine日志实时刷新 - function startReportLogRefresh() { - // 防重复:如果已经在运行,直接返回 - if (reportLogRefreshInterval) { - console.log('[Report日志] 日志轮询已在运行,跳过重复启动'); - return; - } - - // 立即刷新一次,获取当前所有日志 - refreshReportLog(); - - // 启动定时器,每秒刷新一次 - reportLogRefreshInterval = setInterval(() => { - if (currentApp === 'report') { - refreshReportLog(); - } else { - // 如果切换到其他应用,自动停止轮询 - console.log('[Report日志] 检测到应用切换,停止日志轮询'); - stopReportLogRefresh(); - } - }, 1000); // 每秒刷新一次 - - console.log('[Report日志] 启动实时日志刷新,频率: 1秒'); - } - - // 【修复】停止Report Engine日志刷新 - function stopReportLogRefresh() { - if (reportLogRefreshInterval) { - clearInterval(reportLogRefreshInterval); - reportLogRefreshInterval = null; - console.log('[Report日志] 停止日志刷新'); - } - } + // 【新函数】使用新的日志管理器 + // 旧的startReportLogRefresh和stopReportLogRefresh已废弃,请使用reportLogManager // 开始进度轮询 function startProgressPolling(taskId) { @@ -5243,10 +5454,10 @@ .then(data => { if (data.success) { updateProgressDisplay(data.task); - - // 在检查进度时也刷新日志 - refreshReportLog(); - + + // 在检查进度时也刷新日志(使用新的日志管理器) + // reportLogManager会自动处理轮询 + if (data.task.status === 'completed') { clearInterval(reportPollingInterval); showMessage('报告生成完成!', 'success'); @@ -5460,8 +5671,8 @@ // 【修复】启动日志轮询,读取logger.info/debug/warning/error // SSE只推送显式事件(stage/chapter_status等),logger日志需要轮询读取 - startReportLogRefresh(); - console.log('[Report日志] SSE连接建立,同步启动日志轮询'); + reportLogManager.start(); + console.log('[ReportLogManager] SSE连接建立,同步启动日志轮询'); }; reportEventSource.onerror = () => { appendReportStreamLine('检测到网络抖动,SSE正在等待自动重连...', 'warn', { badge: 'SSE' }); @@ -5488,12 +5699,10 @@ clearTimeout(reportStreamReconnectTimer); reportStreamReconnectTimer = null; } - // 清除日志刷新定时器 - if (reportLogRefreshInterval) { - clearInterval(reportLogRefreshInterval); - reportLogRefreshInterval = null; - console.log('[Report日志] SSE连接关闭,停止日志轮询'); - } + // 清除日志刷新(使用新的日志管理器) + reportLogManager.stop(); + console.log('[ReportLogManager] SSE连接关闭,停止日志轮询'); + clearStreamHeartbeat(); if (!keepIndicator) { updateReportStreamStatus('idle'); @@ -5600,8 +5809,10 @@ case 'completed': appendReportStreamLine(payload.message || '任务完成', 'success'); - // 【修复】任务完成前最后一次刷新日志,确保所有日志都被读取 - refreshReportLog(); + // 【修复】任务完成前强制刷新最后一次日志,确保所有日志都被读取 + if (reportLogManager && reportLogManager.isRunning) { + reportLogManager.refresh(); + } // 延迟500ms后关闭SSE,确保最后一次日志刷新完成 setTimeout(() => {