马一丁

Fixed the Front-End Progress Bar Display Logic

@@ -1212,6 +1212,13 @@ @@ -1212,6 +1212,13 @@
1212 forum: 'stopped', // 前端启动后再标记为 running 1212 forum: 'stopped', // 前端启动后再标记为 running
1213 report: 'stopped' // Report Engine 1213 report: 'stopped' // Report Engine
1214 }; 1214 };
  1215 + // 为每个Engine存储进度条状态
  1216 + let engineProgress = {
  1217 + insight: null,
  1218 + media: null,
  1219 + query: null,
  1220 + report: null
  1221 + };
1215 let customTemplate = ''; // 存储用户上传的自定义模板内容 1222 let customTemplate = ''; // 存储用户上传的自定义模板内容
1216 let configValues = {}; 1223 let configValues = {};
1217 let configDirty = false; 1224 let configDirty = false;
@@ -1228,6 +1235,122 @@ @@ -1228,6 +1235,122 @@
1228 let activeConsoleLayer = currentApp; 1235 let activeConsoleLayer = currentApp;
1229 const logRenderers = {}; 1236 const logRenderers = {};
1230 1237
  1238 + // 页面可见性状态管理
  1239 + let isPageVisible = !document.hidden;
  1240 + let allTimers = {
  1241 + updateTime: null,
  1242 + checkStatus: null,
  1243 + refreshConsole: null,
  1244 + refreshForum: null,
  1245 + reportLockCheck: null,
  1246 + connectionProbe: null,
  1247 + updateEngineProgress: null // 新增:更新所有Engine进度的定时器
  1248 + };
  1249 +
  1250 + // 页面可见性变化处理
  1251 + function handleVisibilityChange() {
  1252 + isPageVisible = !document.hidden;
  1253 +
  1254 + if (isPageVisible) {
  1255 + console.log('页面可见,恢复定时器');
  1256 + startAllTimers();
  1257 + } else {
  1258 + console.log('页面隐藏,暂停定时器以节省资源');
  1259 + pauseAllTimers();
  1260 + }
  1261 + }
  1262 +
  1263 + // 启动所有定时器
  1264 + function startAllTimers() {
  1265 + // 清理旧定时器
  1266 + stopAllTimers();
  1267 +
  1268 + // 时间更新定时器 - 只在页面可见时更新
  1269 + if (isPageVisible) {
  1270 + allTimers.updateTime = setInterval(updateTime, 1000);
  1271 + }
  1272 +
  1273 + // 状态检查定时器 - 从5秒增加到10秒
  1274 + allTimers.checkStatus = setInterval(checkStatus, 10000);
  1275 +
  1276 + // 控制台刷新定时器 - 从2秒增加到3秒,只在有运行中应用时执行
  1277 + allTimers.refreshConsole = setInterval(() => {
  1278 + if (appStatus[currentApp] === 'running' || appStatus[currentApp] === 'starting') {
  1279 + refreshConsoleOutput();
  1280 + }
  1281 + }, 3000);
  1282 +
  1283 + // 论坛刷新定时器 - 从3秒增加到5秒
  1284 + allTimers.refreshForum = setInterval(() => {
  1285 + if (currentApp === 'forum' || appStatus.forum === 'running') {
  1286 + refreshForumMessages();
  1287 + }
  1288 + }, 5000);
  1289 +
  1290 + // 报告锁定检查定时器 - 从10秒增加到15秒
  1291 + allTimers.reportLockCheck = setInterval(checkReportLockStatus, 15000);
  1292 +
  1293 + // 更新所有Engine进度的定时器 - 每5秒更新一次
  1294 + allTimers.updateEngineProgress = setInterval(updateAllEngineProgress, 5000);
  1295 + }
  1296 +
  1297 + // 暂停所有定时器
  1298 + function pauseAllTimers() {
  1299 + // 只保留关键的连接检查定时器,其他全部暂停
  1300 + Object.keys(allTimers).forEach(key => {
  1301 + if (key !== 'connectionProbe' && allTimers[key]) {
  1302 + clearInterval(allTimers[key]);
  1303 + allTimers[key] = null;
  1304 + }
  1305 + });
  1306 + }
  1307 +
  1308 + // 停止所有定时器
  1309 + function stopAllTimers() {
  1310 + Object.keys(allTimers).forEach(key => {
  1311 + if (allTimers[key]) {
  1312 + clearInterval(allTimers[key]);
  1313 + allTimers[key] = null;
  1314 + }
  1315 + });
  1316 + }
  1317 +
  1318 + // 页面卸载时清理资源
  1319 + function cleanupOnUnload() {
  1320 + console.log('页面卸载,清理所有资源');
  1321 +
  1322 + // 停止所有定时器
  1323 + stopAllTimers();
  1324 +
  1325 + // 清理所有日志渲染器
  1326 + Object.values(logRenderers).forEach(renderer => {
  1327 + if (renderer && typeof renderer.dispose === 'function') {
  1328 + renderer.dispose();
  1329 + }
  1330 + });
  1331 +
  1332 + // 卸载所有iframe
  1333 + Object.keys(preloadedIframes).forEach(app => {
  1334 + unloadIframe(app);
  1335 + });
  1336 +
  1337 + // 关闭Socket连接
  1338 + if (socket) {
  1339 + socket.close();
  1340 + }
  1341 +
  1342 + // 关闭SSE连接
  1343 + safeCloseReportStream();
  1344 +
  1345 + // 清理全局变量
  1346 + Object.keys(consoleLayers).forEach(key => {
  1347 + delete consoleLayers[key];
  1348 + });
  1349 + Object.keys(logRenderers).forEach(key => {
  1350 + delete logRenderers[key];
  1351 + });
  1352 + }
  1353 +
1231 // 轻量日志虚拟渲染器:可视窗口渲染 + 节流 + 包级别截断,降低内存占用 1354 // 轻量日志虚拟渲染器:可视窗口渲染 + 节流 + 包级别截断,降低内存占用
1232 class LogVirtualList { 1355 class LogVirtualList {
1233 constructor(container) { 1356 constructor(container) {
@@ -1238,8 +1361,9 @@ @@ -1238,8 +1361,9 @@
1238 this.pool = []; 1361 this.pool = [];
1239 this.lineHeight = 18; 1362 this.lineHeight = 18;
1240 this.maxVisible = 120; 1363 this.maxVisible = 120;
1241 - this.maxLines = 2000; // 硬性保留的最大行数,超出时裁剪老旧数据  
1242 - this.trimTarget = 1500; // 裁剪后保留的目标行数,避免频繁裁剪 1364 + this.maxLines = 500; // 减少到500行,降低75%内存占用
  1365 + this.trimTarget = 300; // 裁剪后保留300行
  1366 + this.maxPoolSize = 200; // 限制DOM节点池大小
1243 this.rafId = null; 1367 this.rafId = null;
1244 this.autoScrollEnabled = true; 1368 this.autoScrollEnabled = true;
1245 this.resumeDelay = 3000; // 手动滚动后重新自动滚动的延迟(降低到3秒) 1369 this.resumeDelay = 3000; // 手动滚动后重新自动滚动的延迟(降低到3秒)
@@ -1249,19 +1373,57 @@ @@ -1249,19 +1373,57 @@
1249 this.needsScroll = false; // 标记是否需要滚动 1373 this.needsScroll = false; // 标记是否需要滚动
1250 this.lastScrollTime = 0; // 上次滚动时间,用于节流 1374 this.lastScrollTime = 0; // 上次滚动时间,用于节流
1251 this.scrollThrottle = 100; // 滚动节流时间(毫秒) 1375 this.scrollThrottle = 100; // 滚动节流时间(毫秒)
  1376 + this.scrollHandler = null; // 存储滚动处理器引用
1252 this.attachScroll(); 1377 this.attachScroll();
1253 } 1378 }
1254 1379
1255 attachScroll() { 1380 attachScroll() {
1256 if (!this.scrollElement) return; 1381 if (!this.scrollElement) return;
  1382 + if (this.scrollHandler) return; // 防止重复绑定
  1383 +
1257 let scrollTimer = null; 1384 let scrollTimer = null;
1258 - this.scrollElement.addEventListener('scroll', () => { 1385 + this.scrollHandler = () => {
1259 // 防抖处理,避免频繁触发 1386 // 防抖处理,避免频繁触发
1260 if (scrollTimer) clearTimeout(scrollTimer); 1387 if (scrollTimer) clearTimeout(scrollTimer);
1261 scrollTimer = setTimeout(() => { 1388 scrollTimer = setTimeout(() => {
1262 this.handleUserScroll(); 1389 this.handleUserScroll();
1263 - }, 100);  
1264 - }, { passive: true }); 1390 + }, 150); // 增加防抖时间到150ms
  1391 + };
  1392 +
  1393 + this.scrollElement.addEventListener('scroll', this.scrollHandler, { passive: true });
  1394 + }
  1395 +
  1396 + // 添加清理方法
  1397 + dispose() {
  1398 + // 清理定时器
  1399 + if (this.rafId) {
  1400 + cancelAnimationFrame(this.rafId);
  1401 + this.rafId = null;
  1402 + }
  1403 + this.clearResumeTimer();
  1404 +
  1405 + // 移除事件监听器
  1406 + if (this.scrollElement && this.scrollHandler) {
  1407 + this.scrollElement.removeEventListener('scroll', this.scrollHandler);
  1408 + this.scrollHandler = null;
  1409 + }
  1410 +
  1411 + // 清空数据结构
  1412 + this.lines = [];
  1413 + this.pending = [];
  1414 +
  1415 + // 清空并释放DOM节点池
  1416 + this.pool.forEach(node => {
  1417 + if (node && node.parentNode) {
  1418 + node.parentNode.removeChild(node);
  1419 + }
  1420 + });
  1421 + this.pool = [];
  1422 +
  1423 + // 清空容器
  1424 + if (this.container) {
  1425 + this.container.innerHTML = '';
  1426 + }
1265 } 1427 }
1266 1428
1267 handleUserScroll() { 1429 handleUserScroll() {
@@ -1352,8 +1514,8 @@ @@ -1352,8 +1514,8 @@
1352 } 1514 }
1353 1515
1354 this.pending.push({ text, className }); 1516 this.pending.push({ text, className });
1355 - // 降低批处理阈值到 50,更快响应  
1356 - if (this.pending.length > 50) { 1517 + // 增加批处理阈值到 100,减少渲染频率
  1518 + if (this.pending.length > 100) {
1357 this.flush(); 1519 this.flush();
1358 } 1520 }
1359 this.maybeTrim(); 1521 this.maybeTrim();
@@ -1409,6 +1571,8 @@ @@ -1409,6 +1571,8 @@
1409 if (!total) { 1571 if (!total) {
1410 if (this.container.innerHTML !== '') { 1572 if (this.container.innerHTML !== '') {
1411 this.container.innerHTML = ''; 1573 this.container.innerHTML = '';
  1574 + // 清空时也清理pool
  1575 + this.pool = [];
1412 } 1576 }
1413 return; 1577 return;
1414 } 1578 }
@@ -1438,19 +1602,29 @@ @@ -1438,19 +1602,29 @@
1438 1602
1439 const needed = Math.max(0, end - start); 1603 const needed = Math.max(0, end - start);
1440 1604
  1605 + // 限制DOM节点池大小,防止内存泄漏
  1606 + if (this.pool.length > this.maxPoolSize) {
  1607 + const excess = this.pool.length - this.maxPoolSize;
  1608 + // 移除多余的节点
  1609 + this.pool.splice(this.maxPoolSize, excess).forEach(node => {
  1610 + if (node && node.parentNode) {
  1611 + node.parentNode.removeChild(node);
  1612 + }
  1613 + });
  1614 + }
  1615 +
1441 // 复用现有的 DOM 节点池 1616 // 复用现有的 DOM 节点池
1442 - while (this.pool.length < needed) { 1617 + while (this.pool.length < needed && this.pool.length < this.maxPoolSize) {
1443 const node = document.createElement('div'); 1618 const node = document.createElement('div');
1444 node.className = 'console-line'; 1619 node.className = 'console-line';
1445 this.pool.push(node); 1620 this.pool.push(node);
1446 } 1621 }
1447 1622
1448 - // 不要完全清空容器,而是更新现有节点  
1449 - const existingChildren = Array.from(this.container.children); 1623 + // 使用DocumentFragment来减少DOM重绘
1450 const fragment = document.createDocumentFragment(); 1624 const fragment = document.createDocumentFragment();
1451 1625
1452 // 更新或创建前置占位符 1626 // 更新或创建前置占位符
1453 - let beforeSpacer = existingChildren.find(el => el.dataset.spacer === 'before'); 1627 + let beforeSpacer = this.container.querySelector('[data-spacer="before"]');
1454 if (!beforeSpacer) { 1628 if (!beforeSpacer) {
1455 beforeSpacer = document.createElement('div'); 1629 beforeSpacer = document.createElement('div');
1456 beforeSpacer.dataset.spacer = 'before'; 1630 beforeSpacer.dataset.spacer = 'before';
@@ -1458,7 +1632,7 @@ @@ -1458,7 +1632,7 @@
1458 beforeSpacer.style.height = `${beforeHeight}px`; 1632 beforeSpacer.style.height = `${beforeHeight}px`;
1459 1633
1460 // 更新或创建后置占位符 1634 // 更新或创建后置占位符
1461 - let afterSpacer = existingChildren.find(el => el.dataset.spacer === 'after'); 1635 + let afterSpacer = this.container.querySelector('[data-spacer="after"]');
1462 if (!afterSpacer) { 1636 if (!afterSpacer) {
1463 afterSpacer = document.createElement('div'); 1637 afterSpacer = document.createElement('div');
1464 afterSpacer.dataset.spacer = 'after'; 1638 afterSpacer.dataset.spacer = 'after';
@@ -1468,7 +1642,8 @@ @@ -1468,7 +1642,8 @@
1468 // 只更新可见区域的节点 1642 // 只更新可见区域的节点
1469 for (let idx = start; idx < end; idx++) { 1643 for (let idx = start; idx < end; idx++) {
1470 const line = this.lines[idx]; 1644 const line = this.lines[idx];
1471 - const node = this.pool[idx - start]; 1645 + const poolIdx = idx - start;
  1646 + const node = this.pool[poolIdx];
1472 if (!node) continue; 1647 if (!node) continue;
1473 1648
1474 // 只在内容或类名变化时才更新节点 1649 // 只在内容或类名变化时才更新节点
@@ -1603,40 +1778,40 @@ @@ -1603,40 +1778,40 @@
1603 initializeEventListeners(); 1778 initializeEventListeners();
1604 ensureSystemReadyOnLoad(); 1779 ensureSystemReadyOnLoad();
1605 loadConsoleOutput(currentApp); 1780 loadConsoleOutput(currentApp);
1606 - updateTime();  
1607 - setInterval(updateTime, 1000);  
1608 - checkStatus();  
1609 - setInterval(checkStatus, 5000);  
1610 - startConnectionProbe();  
1611 - 1781 +
  1782 + // 使用新的定时器管理系统
  1783 + updateTime(); // 立即更新一次
  1784 + checkStatus(); // 立即检查一次
  1785 + checkReportLockStatus(); // 立即检查一次
  1786 +
  1787 + // 启动所有定时器
  1788 + startAllTimers();
  1789 +
  1790 + // 立即更新一次所有Engine的进度,恢复刷新前的状态
  1791 + updateAllEngineProgress();
  1792 +
  1793 + // 监听页面可见性变化
  1794 + document.addEventListener('visibilitychange', handleVisibilityChange);
  1795 +
  1796 + // 监听页面卸载事件
  1797 + window.addEventListener('beforeunload', cleanupOnUnload);
  1798 + window.addEventListener('unload', cleanupOnUnload);
  1799 +
1612 // 初始化密码切换功能(事件委托,只需调用一次) 1800 // 初始化密码切换功能(事件委托,只需调用一次)
1613 attachConfigPasswordToggles(); 1801 attachConfigPasswordToggles();
1614 -  
1615 - // 初始化Report Engine锁定状态检查  
1616 - checkReportLockStatus();  
1617 - reportLockCheckInterval = setInterval(checkReportLockStatus, 10000); // 每10秒检查一次  
1618 -  
1619 - // 优化控制台刷新频率:从 1 秒改为 2 秒,减少不必要的 API 调用  
1620 - setInterval(() => {  
1621 - if (appStatus[currentApp] === 'running' || appStatus[currentApp] === 'starting') {  
1622 - refreshConsoleOutput();  
1623 - }  
1624 - }, 2000);  
1625 1802
1626 - // 优化论坛对话刷新频率:从 2 秒改为 3 秒  
1627 - setInterval(() => {  
1628 - if (currentApp === 'forum' || appStatus.forum === 'running') {  
1629 - refreshForumMessages();  
1630 - }  
1631 - }, 3000);  
1632 -  
1633 // 初始化论坛相关功能 1803 // 初始化论坛相关功能
1634 initializeForum(); 1804 initializeForum();
1635 -  
1636 - // 延迟预加载iframe以确保应用启动完成 1805 +
  1806 + // 延迟预加载iframe以确保应用启动完成,并且只在页面可见时加载
1637 setTimeout(() => { 1807 setTimeout(() => {
1638 - preloadIframes();  
1639 - }, 3000); 1808 + if (isPageVisible) {
  1809 + preloadIframes();
  1810 + }
  1811 + }, 5000); // 延迟时间从3秒增加到5秒,减少初始加载压力
  1812 +
  1813 + // 连接探测定时器(保持运行)
  1814 + startConnectionProbe();
1640 }); 1815 });
1641 1816
1642 // Socket.IO连接 1817 // Socket.IO连接
@@ -2228,45 +2403,48 @@ @@ -2228,45 +2403,48 @@
2228 if (reportPreview) { 2403 if (reportPreview) {
2229 reportPreview.innerHTML = '<div class="report-loading">等待新的搜索结果生成报告...</div>'; 2404 reportPreview.innerHTML = '<div class="report-loading">等待新的搜索结果生成报告...</div>';
2230 } 2405 }
2231 - 2406 +
2232 // 清除任务进度显示 2407 // 清除任务进度显示
2233 const taskProgressArea = document.getElementById('taskProgressArea'); 2408 const taskProgressArea = document.getElementById('taskProgressArea');
2234 if (taskProgressArea) { 2409 if (taskProgressArea) {
2235 taskProgressArea.innerHTML = ''; 2410 taskProgressArea.innerHTML = '';
2236 } 2411 }
2237 - 2412 +
2238 // 重置自动生成相关标志 2413 // 重置自动生成相关标志
2239 autoGenerateTriggered = false; 2414 autoGenerateTriggered = false;
2240 reportTaskId = null; 2415 reportTaskId = null;
2241 - 2416 +
2242 // 停止可能正在进行的轮询 2417 // 停止可能正在进行的轮询
2243 if (reportPollingInterval) { 2418 if (reportPollingInterval) {
2244 clearInterval(reportPollingInterval); 2419 clearInterval(reportPollingInterval);
2245 reportPollingInterval = null; 2420 reportPollingInterval = null;
2246 } 2421 }
2247 2422
2248 - // 确保所有iframe已初始化  
2249 - if (!iframesInitialized) {  
2250 - preloadIframes();  
2251 - }  
2252 -  
2253 // 向所有运行中的应用发送搜索请求(通过刷新iframe传递参数) 2423 // 向所有运行中的应用发送搜索请求(通过刷新iframe传递参数)
2254 let totalRunning = 0; 2424 let totalRunning = 0;
2255 const ports = { insight: 8501, media: 8502, query: 8503 }; 2425 const ports = { insight: 8501, media: 8502, query: 8503 };
2256 - 2426 +
2257 Object.keys(appStatus).forEach(app => { 2427 Object.keys(appStatus).forEach(app => {
2258 - if (appStatus[app] === 'running' && preloadedIframes[app]) { 2428 + if (appStatus[app] === 'running' && ports[app]) {
2259 totalRunning++; 2429 totalRunning++;
2260 -  
2261 - // 构建搜索URL  
2262 - const searchUrl = `http://${window.location.hostname}:${ports[app]}?query=${encodeURIComponent(query)}&auto_search=true`;  
2263 - console.log(`向 ${app} 发送搜索请求: ${searchUrl}`);  
2264 -  
2265 - // 直接更新主iframe的src来传递搜索参数  
2266 - preloadedIframes[app].src = searchUrl; 2430 +
  2431 + // 懒加载iframe(如果还没有加载)
  2432 + let iframe = preloadedIframes[app];
  2433 + if (!iframe) {
  2434 + iframe = lazyLoadIframe(app);
  2435 + }
  2436 +
  2437 + if (iframe) {
  2438 + // 构建搜索URL
  2439 + const searchUrl = `http://${window.location.hostname}:${ports[app]}?query=${encodeURIComponent(query)}&auto_search=true`;
  2440 + console.log(`向 ${app} 发送搜索请求: ${searchUrl}`);
  2441 +
  2442 + // 直接更新iframe的src来传递搜索参数
  2443 + iframe.src = searchUrl;
  2444 + }
2267 } 2445 }
2268 }); 2446 });
2269 - 2447 +
2270 if (totalRunning === 0) { 2448 if (totalRunning === 0) {
2271 button.disabled = false; 2449 button.disabled = false;
2272 button.innerHTML = '搜索'; 2450 button.innerHTML = '搜索';
@@ -2291,6 +2469,12 @@ @@ -2291,6 +2469,12 @@
2291 } 2469 }
2292 } 2470 }
2293 2471
  2472 + // 隐藏当前Engine的进度条
  2473 + const engines = ['insight', 'media', 'query'];
  2474 + if (engines.includes(currentApp)) {
  2475 + hideEngineProgress(currentApp);
  2476 + }
  2477 +
2294 // 更新按钮状态 2478 // 更新按钮状态
2295 document.querySelectorAll('.app-button').forEach(btn => { 2479 document.querySelectorAll('.app-button').forEach(btn => {
2296 btn.classList.remove('active'); 2480 btn.classList.remove('active');
@@ -2304,49 +2488,56 @@ @@ -2304,49 +2488,56 @@
2304 if (app === 'forum') { 2488 if (app === 'forum') {
2305 // 切换到论坛模式 2489 // 切换到论坛模式
2306 document.getElementById('embeddedHeader').textContent = 'Forum Engine - 多智能体交流'; 2490 document.getElementById('embeddedHeader').textContent = 'Forum Engine - 多智能体交流';
2307 - 2491 +
2308 // 显示论坛容器,隐藏其他内容 2492 // 显示论坛容器,隐藏其他内容
2309 document.getElementById('forumContainer').classList.add('active'); 2493 document.getElementById('forumContainer').classList.add('active');
2310 document.getElementById('reportContainer').classList.remove('active'); 2494 document.getElementById('reportContainer').classList.remove('active');
2311 - 2495 +
2312 // 追加提示并加载forum日志 2496 // 追加提示并加载forum日志
2313 appendConsoleTextLine('forum', '[系统] 切换到论坛模式'); 2497 appendConsoleTextLine('forum', '[系统] 切换到论坛模式');
2314 loadForumLog(); 2498 loadForumLog();
2315 - 2499 +
2316 } else if (app === 'report') { 2500 } else if (app === 'report') {
2317 // 切换到报告模式 2501 // 切换到报告模式
2318 document.getElementById('embeddedHeader').textContent = 'Report Agent - 最终报告生成'; 2502 document.getElementById('embeddedHeader').textContent = 'Report Agent - 最终报告生成';
2319 - 2503 +
2320 // 显示报告容器,隐藏其他内容 2504 // 显示报告容器,隐藏其他内容
2321 document.getElementById('reportContainer').classList.add('active'); 2505 document.getElementById('reportContainer').classList.add('active');
2322 document.getElementById('forumContainer').classList.remove('active'); 2506 document.getElementById('forumContainer').classList.remove('active');
2323 - 2507 +
2324 // 追加提示并加载report日志 2508 // 追加提示并加载report日志
2325 appendConsoleTextLine('report', '[系统] 切换到报告生成模式'); 2509 appendConsoleTextLine('report', '[系统] 切换到报告生成模式');
2326 loadReportLog(); 2510 loadReportLog();
2327 - 2511 +
2328 // 只在报告界面未初始化时才重新加载 2512 // 只在报告界面未初始化时才重新加载
2329 const reportContent = document.getElementById('reportContent'); 2513 const reportContent = document.getElementById('reportContent');
2330 if (!reportContent || reportContent.children.length === 0) { 2514 if (!reportContent || reportContent.children.length === 0) {
2331 loadReportInterface(); 2515 loadReportInterface();
2332 } 2516 }
2333 - 2517 +
2334 // 切换到report页面时检查是否可以自动生成报告 2518 // 切换到report页面时检查是否可以自动生成报告
2335 setTimeout(() => { 2519 setTimeout(() => {
2336 checkReportLockStatus(); 2520 checkReportLockStatus();
2337 }, 500); 2521 }, 500);
2338 - 2522 +
2339 } else { 2523 } else {
2340 // 切换到普通Engine模式 2524 // 切换到普通Engine模式
2341 document.getElementById('embeddedHeader').textContent = agentTitles[app] || appNames[app]; 2525 document.getElementById('embeddedHeader').textContent = agentTitles[app] || appNames[app];
2342 - 2526 +
2343 // 隐藏论坛和报告容器 2527 // 隐藏论坛和报告容器
2344 document.getElementById('forumContainer').classList.remove('active'); 2528 document.getElementById('forumContainer').classList.remove('active');
2345 document.getElementById('reportContainer').classList.remove('active'); 2529 document.getElementById('reportContainer').classList.remove('active');
2346 - 2530 +
2347 // 追加提示并加载新的控制台输出 2531 // 追加提示并加载新的控制台输出
2348 appendConsoleTextLine(app, '[系统] 切换到 ' + appNames[app]); 2532 appendConsoleTextLine(app, '[系统] 切换到 ' + appNames[app]);
2349 loadConsoleOutput(app); 2533 loadConsoleOutput(app);
  2534 +
  2535 + // 显示该Engine的进度条(如果有)
  2536 + if (engines.includes(app)) {
  2537 + showEngineProgress(app);
  2538 + // 立即更新一次进度,确保显示最新状态
  2539 + updateEngineProgress(app);
  2540 + }
2350 } 2541 }
2351 2542
2352 // 更新嵌入页面 2543 // 更新嵌入页面
@@ -2542,123 +2733,194 @@ @@ -2542,123 +2733,194 @@
2542 // 预加载的iframe存储 2733 // 预加载的iframe存储
2543 let preloadedIframes = {}; 2734 let preloadedIframes = {};
2544 let iframesInitialized = false; 2735 let iframesInitialized = false;
2545 -  
2546 - // 预加载所有iframe(只执行一次)  
2547 - function preloadIframes() {  
2548 - if (iframesInitialized) return;  
2549 - 2736 + let currentVisibleIframe = null; // 跟踪当前可见的iframe
  2737 +
  2738 + // 懒加载iframe - 只在真正需要时才创建
  2739 + function lazyLoadIframe(app) {
  2740 + // 如果iframe已存在,直接返回
  2741 + if (preloadedIframes[app]) {
  2742 + return preloadedIframes[app];
  2743 + }
  2744 +
2550 const ports = { insight: 8501, media: 8502, query: 8503 }; 2745 const ports = { insight: 8501, media: 8502, query: 8503 };
  2746 + if (!ports[app]) {
  2747 + console.warn(`未知的应用: ${app}`);
  2748 + return null;
  2749 + }
  2750 +
2551 const content = document.getElementById('embeddedContent'); 2751 const content = document.getElementById('embeddedContent');
2552 -  
2553 - for (const [app, port] of Object.entries(ports)) {  
2554 - const iframe = document.createElement('iframe');  
2555 - iframe.src = `http://${window.location.hostname}:${port}`;  
2556 - iframe.style.width = '100%';  
2557 - iframe.style.height = '100%';  
2558 - iframe.style.border = 'none';  
2559 - iframe.style.position = 'absolute';  
2560 - iframe.style.top = '0';  
2561 - iframe.style.left = '0';  
2562 - iframe.style.display = 'none';  
2563 - iframe.id = `iframe-${app}`;  
2564 -  
2565 - // 直接添加到content区域  
2566 - content.appendChild(iframe);  
2567 - preloadedIframes[app] = iframe;  
2568 -  
2569 - console.log(`预加载 ${app} 应用完成`); 2752 + const iframe = document.createElement('iframe');
  2753 + iframe.src = `http://${window.location.hostname}:${ports[app]}`;
  2754 + iframe.style.width = '100%';
  2755 + iframe.style.height = '100%';
  2756 + iframe.style.border = 'none';
  2757 + iframe.style.position = 'absolute';
  2758 + iframe.style.top = '0';
  2759 + iframe.style.left = '0';
  2760 + iframe.style.display = 'none';
  2761 + iframe.id = `iframe-${app}`;
  2762 +
  2763 + // 添加加载完成事件
  2764 + iframe.addEventListener('load', () => {
  2765 + console.log(`${app} iframe 加载完成`);
  2766 + });
  2767 +
  2768 + content.appendChild(iframe);
  2769 + preloadedIframes[app] = iframe;
  2770 +
  2771 + console.log(`懒加载 ${app} iframe`);
  2772 + return iframe;
  2773 + }
  2774 +
  2775 + // 卸载不需要的iframe以释放内存
  2776 + function unloadIframe(app) {
  2777 + if (!preloadedIframes[app]) return;
  2778 +
  2779 + const iframe = preloadedIframes[app];
  2780 +
  2781 + // 先隐藏iframe
  2782 + iframe.style.display = 'none';
  2783 +
  2784 + // 清空iframe内容以释放内存
  2785 + if (iframe.contentWindow) {
  2786 + try {
  2787 + // 尝试清空iframe的DOM
  2788 + iframe.src = 'about:blank';
  2789 + } catch (e) {
  2790 + console.warn(`无法清空 ${app} iframe:`, e);
  2791 + }
2570 } 2792 }
2571 - 2793 +
  2794 + // 从DOM中移除iframe
  2795 + if (iframe.parentNode) {
  2796 + iframe.parentNode.removeChild(iframe);
  2797 + }
  2798 +
  2799 + // 从缓存中删除
  2800 + delete preloadedIframes[app];
  2801 +
  2802 + console.log(`卸载 ${app} iframe,释放内存`);
  2803 + }
  2804 +
  2805 + // 卸载所有非当前应用的iframe
  2806 + function unloadInactiveIframes(currentApp) {
  2807 + const apps = ['insight', 'media', 'query'];
  2808 + apps.forEach(app => {
  2809 + if (app !== currentApp && preloadedIframes[app]) {
  2810 + // 延迟卸载,给一些缓冲时间
  2811 + setTimeout(() => {
  2812 + if (currentApp !== app) { // 再次确认没有切换回来
  2813 + unloadIframe(app);
  2814 + }
  2815 + }, 30000); // 30秒后卸载不活跃的iframe
  2816 + }
  2817 + });
  2818 + }
  2819 +
  2820 + // 预加载所有iframe(只执行一次)- 已废弃,改用懒加载
  2821 + function preloadIframes() {
  2822 + // 不再预加载所有iframe,改用懒加载机制
  2823 + console.log('使用懒加载机制,不再预加载所有iframe');
2572 iframesInitialized = true; 2824 iframesInitialized = true;
2573 - console.log('所有iframe预加载完成,准备进行无缝切换');  
2574 } 2825 }
2575 2826
2576 // 更新嵌入页面 2827 // 更新嵌入页面
2577 function updateEmbeddedPage(app) { 2828 function updateEmbeddedPage(app) {
2578 const header = document.getElementById('embeddedHeader'); 2829 const header = document.getElementById('embeddedHeader');
2579 const content = document.getElementById('embeddedContent'); 2830 const content = document.getElementById('embeddedContent');
2580 - 2831 +
2581 // 如果是Forum Engine,直接显示论坛界面 2832 // 如果是Forum Engine,直接显示论坛界面
2582 if (app === 'forum') { 2833 if (app === 'forum') {
2583 header.textContent = 'Forum Engine - 多智能体交流'; 2834 header.textContent = 'Forum Engine - 多智能体交流';
2584 - 2835 +
2585 // 隐藏所有iframe 2836 // 隐藏所有iframe
2586 - if (typeof preloadedIframes !== 'undefined') {  
2587 - Object.values(preloadedIframes).forEach(iframe => {  
2588 - iframe.style.display = 'none';  
2589 - });  
2590 - }  
2591 - 2837 + Object.values(preloadedIframes).forEach(iframe => {
  2838 + iframe.style.display = 'none';
  2839 + });
  2840 +
2592 // 移除占位符 2841 // 移除占位符
2593 const placeholder = content.querySelector('.status-placeholder'); 2842 const placeholder = content.querySelector('.status-placeholder');
2594 if (placeholder) { 2843 if (placeholder) {
2595 placeholder.remove(); 2844 placeholder.remove();
2596 } 2845 }
2597 - 2846 +
2598 // 显示论坛容器,隐藏报告容器 2847 // 显示论坛容器,隐藏报告容器
2599 document.getElementById('forumContainer').classList.add('active'); 2848 document.getElementById('forumContainer').classList.add('active');
2600 document.getElementById('reportContainer').classList.remove('active'); 2849 document.getElementById('reportContainer').classList.remove('active');
  2850 +
  2851 + // 卸载不活跃的iframe
  2852 + unloadInactiveIframes(null);
  2853 +
  2854 + currentVisibleIframe = null;
2601 return; 2855 return;
2602 } 2856 }
2603 - 2857 +
2604 // 如果是Report Engine,显示报告界面 2858 // 如果是Report Engine,显示报告界面
2605 if (app === 'report') { 2859 if (app === 'report') {
2606 header.textContent = 'Report Agent - 最终报告生成'; 2860 header.textContent = 'Report Agent - 最终报告生成';
2607 - 2861 +
2608 // 隐藏所有iframe 2862 // 隐藏所有iframe
2609 - if (typeof preloadedIframes !== 'undefined') {  
2610 - Object.values(preloadedIframes).forEach(iframe => {  
2611 - iframe.style.display = 'none';  
2612 - });  
2613 - }  
2614 - 2863 + Object.values(preloadedIframes).forEach(iframe => {
  2864 + iframe.style.display = 'none';
  2865 + });
  2866 +
2615 // 移除占位符 2867 // 移除占位符
2616 const placeholder = content.querySelector('.status-placeholder'); 2868 const placeholder = content.querySelector('.status-placeholder');
2617 if (placeholder) { 2869 if (placeholder) {
2618 placeholder.remove(); 2870 placeholder.remove();
2619 } 2871 }
2620 - 2872 +
2621 // 显示报告容器,隐藏论坛容器 2873 // 显示报告容器,隐藏论坛容器
2622 document.getElementById('reportContainer').classList.add('active'); 2874 document.getElementById('reportContainer').classList.add('active');
2623 document.getElementById('forumContainer').classList.remove('active'); 2875 document.getElementById('forumContainer').classList.remove('active');
  2876 +
  2877 + // 卸载不活跃的iframe
  2878 + unloadInactiveIframes(null);
  2879 +
  2880 + currentVisibleIframe = null;
2624 return; 2881 return;
2625 } 2882 }
2626 - 2883 +
2627 // 隐藏论坛和报告容器 2884 // 隐藏论坛和报告容器
2628 document.getElementById('forumContainer').classList.remove('active'); 2885 document.getElementById('forumContainer').classList.remove('active');
2629 document.getElementById('reportContainer').classList.remove('active'); 2886 document.getElementById('reportContainer').classList.remove('active');
2630 2887
2631 header.textContent = agentTitles[app] || appNames[app] || app; 2888 header.textContent = agentTitles[app] || appNames[app] || app;
2632 2889
2633 - // 如果应用正在运行,显示对应的iframe 2890 + // 如果应用正在运行,显示对应的iframe(使用懒加载)
2634 if (appStatus[app] === 'running') { 2891 if (appStatus[app] === 'running') {
2635 - // 确保iframe已初始化  
2636 - if (!iframesInitialized) {  
2637 - preloadIframes(); 2892 + // 懒加载当前应用的iframe
  2893 + const iframe = lazyLoadIframe(app);
  2894 +
  2895 + if (!iframe) {
  2896 + console.error(`无法加载 ${app} iframe`);
  2897 + return;
2638 } 2898 }
2639 - 2899 +
2640 // 隐藏所有iframe 2900 // 隐藏所有iframe
2641 - Object.values(preloadedIframes).forEach(iframe => {  
2642 - iframe.style.display = 'none'; 2901 + Object.values(preloadedIframes).forEach(otherIframe => {
  2902 + otherIframe.style.display = 'none';
2643 }); 2903 });
2644 - 2904 +
2645 // 移除占位符 2905 // 移除占位符
2646 const placeholder = content.querySelector('.status-placeholder'); 2906 const placeholder = content.querySelector('.status-placeholder');
2647 if (placeholder) { 2907 if (placeholder) {
2648 placeholder.remove(); 2908 placeholder.remove();
2649 } 2909 }
2650 - 2910 +
2651 // 显示当前应用的iframe 2911 // 显示当前应用的iframe
2652 - if (preloadedIframes[app]) {  
2653 - preloadedIframes[app].style.display = 'block';  
2654 - console.log(`切换到 ${app} 应用 - 无刷新切换`);  
2655 - } 2912 + iframe.style.display = 'block';
  2913 + currentVisibleIframe = app;
  2914 + console.log(`切换到 ${app} 应用 - 懒加载模式`);
  2915 +
  2916 + // 卸载不活跃的iframe(30秒后)
  2917 + unloadInactiveIframes(app);
2656 } else { 2918 } else {
2657 // 隐藏所有iframe 2919 // 隐藏所有iframe
2658 Object.values(preloadedIframes).forEach(iframe => { 2920 Object.values(preloadedIframes).forEach(iframe => {
2659 iframe.style.display = 'none'; 2921 iframe.style.display = 'none';
2660 }); 2922 });
2661 - 2923 +
2662 // 显示状态信息 2924 // 显示状态信息
2663 let placeholder = content.querySelector('.status-placeholder'); 2925 let placeholder = content.querySelector('.status-placeholder');
2664 if (!placeholder) { 2926 if (!placeholder) {
@@ -2667,11 +2929,13 @@ @@ -2667,11 +2929,13 @@
2667 placeholder.style.cssText = 'display: flex; align-items: center; justify-content: center; height: 100%; color: #666; flex-direction: column; position: absolute; top: 0; left: 0; width: 100%;'; 2929 placeholder.style.cssText = 'display: flex; align-items: center; justify-content: center; height: 100%; color: #666; flex-direction: column; position: absolute; top: 0; left: 0; width: 100%;';
2668 content.appendChild(placeholder); 2930 content.appendChild(placeholder);
2669 } 2931 }
2670 - 2932 +
2671 placeholder.innerHTML = ` 2933 placeholder.innerHTML = `
2672 <div style="margin-bottom: 10px;">${appNames[app]} 未运行</div> 2934 <div style="margin-bottom: 10px;">${appNames[app]} 未运行</div>
2673 <div style="font-size: 12px;">状态: ${appStatus[app]}</div> 2935 <div style="font-size: 12px;">状态: ${appStatus[app]}</div>
2674 `; 2936 `;
  2937 +
  2938 + currentVisibleIframe = null;
2675 } 2939 }
2676 } 2940 }
2677 2941
@@ -2840,7 +3104,164 @@ @@ -2840,7 +3104,164 @@
2840 3104
2841 // Forum Engine 相关函数 3105 // Forum Engine 相关函数
2842 let forumLogLineCount = 0; 3106 let forumLogLineCount = 0;
2843 - 3107 +
  3108 + // 更新所有Engine的进度条
  3109 + function updateAllEngineProgress() {
  3110 + // 通过现有的status API获取所有Engine的状态
  3111 + fetch('/api/status')
  3112 + .then(response => response.json())
  3113 + .then(data => {
  3114 + // 为每个需要进度显示的Engine更新状态
  3115 + const engines = ['insight', 'media', 'query'];
  3116 +
  3117 + engines.forEach(engine => {
  3118 + if (data[engine]) {
  3119 + const info = data[engine];
  3120 + const status = info.status === 'running' ? 'running' : 'stopped';
  3121 +
  3122 + // 如果Engine正在运行,显示进度条
  3123 + if (status === 'running') {
  3124 + // 尝试从API获取详细进度,如果失败则显示基本运行状态
  3125 + updateEngineProgress(engine);
  3126 + } else {
  3127 + // Engine未运行,清除进度信息
  3128 + engineProgress[engine] = null;
  3129 + const progressContainer = document.getElementById(`progress-${engine}`);
  3130 + if (progressContainer && progressContainer.parentNode) {
  3131 + progressContainer.parentNode.removeChild(progressContainer);
  3132 + }
  3133 + }
  3134 + }
  3135 + });
  3136 + })
  3137 + .catch(error => {
  3138 + console.log('获取Engine状态失败:', error);
  3139 + });
  3140 + }
  3141 +
  3142 + // 更新单个Engine的进度
  3143 + function updateEngineProgress(engine) {
  3144 + // 先尝试从专用进度API获取
  3145 + fetch(`/api/${engine}/progress`)
  3146 + .then(response => {
  3147 + if (!response.ok) {
  3148 + throw new Error('Progress API not available');
  3149 + }
  3150 + return response.json();
  3151 + })
  3152 + .then(data => {
  3153 + if (data.success && data.progress) {
  3154 + // 存储进度信息
  3155 + engineProgress[engine] = {
  3156 + status: data.progress.status || 'running',
  3157 + progress: data.progress.progress || 0,
  3158 + message: data.progress.message || '正在处理...',
  3159 + updated_at: new Date().toISOString()
  3160 + };
  3161 +
  3162 + // 如果当前正在查看该Engine,更新显示
  3163 + if (currentApp === engine) {
  3164 + displayEngineProgress(engine);
  3165 + }
  3166 + }
  3167 + })
  3168 + .catch(error => {
  3169 + // 如果专用API不可用,使用基本的运行状态
  3170 + if (appStatus[engine] === 'running') {
  3171 + // 使用基本的进度信息
  3172 + if (!engineProgress[engine]) {
  3173 + engineProgress[engine] = {
  3174 + status: 'running',
  3175 + progress: 50, // 默认显示50%表示运行中
  3176 + message: '正在分析中...',
  3177 + updated_at: new Date().toISOString()
  3178 + };
  3179 + }
  3180 +
  3181 + // 如果当前正在查看该Engine,更新显示
  3182 + if (currentApp === engine) {
  3183 + displayEngineProgress(engine);
  3184 + }
  3185 + }
  3186 + });
  3187 + }
  3188 +
  3189 + // 在嵌入页面区域显示Engine进度
  3190 + function displayEngineProgress(engine) {
  3191 + const progress = engineProgress[engine];
  3192 + if (!progress) return;
  3193 +
  3194 + // 查找或创建进度显示容器
  3195 + let progressContainer = document.getElementById(`progress-${engine}`);
  3196 + if (!progressContainer) {
  3197 + // 在嵌入内容区域的顶部创建进度条容器
  3198 + const embeddedContent = document.getElementById('embeddedContent');
  3199 + if (!embeddedContent) return;
  3200 +
  3201 + progressContainer = document.createElement('div');
  3202 + progressContainer.id = `progress-${engine}`;
  3203 + progressContainer.className = 'task-progress-container';
  3204 + progressContainer.style.position = 'absolute';
  3205 + progressContainer.style.top = '10px';
  3206 + progressContainer.style.left = '10px';
  3207 + progressContainer.style.right = '10px';
  3208 + progressContainer.style.zIndex = '100';
  3209 + progressContainer.style.backgroundColor = '#f5f5f0';
  3210 + embeddedContent.insertBefore(progressContainer, embeddedContent.firstChild);
  3211 + }
  3212 +
  3213 + // 更新进度条内容
  3214 + const loadingIndicator = progress.status !== 'completed' && progress.status !== 'error'
  3215 + ? '<span class="report-loading-spinner"></span>'
  3216 + : '';
  3217 +
  3218 + progressContainer.innerHTML = `
  3219 + <div class="task-progress-header">
  3220 + <div class="task-progress-title">
  3221 + ${loadingIndicator}${appNames[engine] || engine} - ${progress.message}
  3222 + </div>
  3223 + <div class="task-progress-bar">
  3224 + <div class="task-progress-fill" style="width: ${Math.min(Math.max(progress.progress || 0, 0), 100)}%"></div>
  3225 + <div class="task-progress-text">${progress.progress || 0}%</div>
  3226 + </div>
  3227 + </div>
  3228 + `;
  3229 +
  3230 + // 如果任务已完成,5秒后淡出进度条
  3231 + if (progress.status === 'completed') {
  3232 + setTimeout(() => {
  3233 + if (progressContainer && progressContainer.parentNode) {
  3234 + progressContainer.style.transition = 'opacity 1s';
  3235 + progressContainer.style.opacity = '0';
  3236 + setTimeout(() => {
  3237 + if (progressContainer && progressContainer.parentNode) {
  3238 + progressContainer.parentNode.removeChild(progressContainer);
  3239 + }
  3240 + }, 1000);
  3241 + }
  3242 + }, 5000);
  3243 + }
  3244 + }
  3245 +
  3246 + // 隐藏指定Engine的进度条(切换时使用)
  3247 + function hideEngineProgress(engine) {
  3248 + const progressContainer = document.getElementById(`progress-${engine}`);
  3249 + if (progressContainer) {
  3250 + progressContainer.style.display = 'none';
  3251 + }
  3252 + }
  3253 +
  3254 + // 显示指定Engine的进度条(切换时使用)
  3255 + function showEngineProgress(engine) {
  3256 + const progressContainer = document.getElementById(`progress-${engine}`);
  3257 + if (progressContainer) {
  3258 + progressContainer.style.display = 'block';
  3259 + } else if (engineProgress[engine]) {
  3260 + // 如果有缓存的进度信息但容器不存在,重新创建
  3261 + displayEngineProgress(engine);
  3262 + }
  3263 + }
  3264 +
2844 // Report Engine 相关函数 3265 // Report Engine 相关函数
2845 let reportLogLineCount = 0; 3266 let reportLogLineCount = 0;
2846 let reportLockCheckInterval = null; 3267 let reportLockCheckInterval = null;