diff --git a/ui/web/app.js b/ui/web/app.js index 8548559..43cd878 100644 --- a/ui/web/app.js +++ b/ui/web/app.js @@ -31,23 +31,40 @@ } catch (e) {} })(); window.__APP_VER = '20260721-v7'; +// 流式逐 token 诊断默认关闭。逐 token 读取 DOM 高度/文本并写 console 会 +// 触发强制布局和跨进程日志传输,长时间运行会明显拖慢渲染器;需要排查时 +// 由 Python 显式调用 setStreamDiagnostics(true) 打开。 +window.__STREAM_DIAG = false; +window.setStreamDiagnostics = function(enabled) { + window.__STREAM_DIAG = !!enabled; + if (window.__STREAM_DIAG && window.__startStreamHeartbeat) { + window.__startStreamHeartbeat(); + } +}; console.log('[JS] ===== app.js 加载 ver=20260721-v7 ====='); // 公式渲染依赖本地 KaTeX(离线);此处确认资源加载结果,缺失时打印告警便于定位 console.log('[JS] KaTeX ' + (typeof katex !== 'undefined' ? katex.version + ' 就绪' : '缺失(公式将退化为纯文本)')); // 渲染器主线程心跳:dt 异常大 = 渲染器被阻塞(截图/重绘/GPU 等) (function() { var _hbLast = Date.now(); - setInterval(function() { + var _started = false; + function tick() { var _now = Date.now(); var _dt = _now - _hbLast; _hbLast = _now; if (_dt >= 1500) { console.log('[JS] 心跳 dt=' + _dt + 'ms (渲染器主线程曾卡顿)'); } - }, 1000); + } + window.__startStreamHeartbeat = function() { + if (_started) return; + _started = true; + setInterval(tick, 1000); + }; })(); window.__diag = { events: [], cap: 300, tokenN: 0, thinkN: 0 }; function diagEvent(name, extra) { + if (!window.__STREAM_DIAG) return; try { var d = window.__diag; d.events.push({ t: Date.now() % 1000000, e: name, x: extra }); @@ -749,7 +766,8 @@ function createMessage(msgId, role, initialText, senderName, branchInfo) { if (role === 'user') { replyDiv.innerText = initialText; } else { - // 🆕 助手纯文本消息(无时间线历史路径)也包进 md-segment → 与时间线路径同样有浅灰气泡背景 + // 🆕 助手纯文本消息(无时间线历史路径)也包进 md-segment, + // 与时间线路径保持相同的透明正文布局 var _tHtml = typeof marked !== 'undefined' ? safeHtml(marked.parse(initialText)) : safeHtml(initialText); replyDiv.innerHTML = '
' + _tHtml + '
'; } @@ -1208,7 +1226,8 @@ function syncRenderThrottled(msgId, minGapMs) { // Qt 看门狗入口:强制渲染(幂等,尾部无变化时开销极小) function forceRenderNow(msgId) { var _frNow = Date.now(); - if (!window.__frLast || _frNow - window.__frLast > 1000) { + if (window.__STREAM_DIAG && + (!window.__frLast || _frNow - window.__frLast > 1000)) { window.__frLast = _frNow; console.log('[JS] forceRenderNow id=' + msgId + ' (看门狗)'); } @@ -1236,7 +1255,7 @@ function _fullRenderSegment(el, c, isThink) { } function doStreamingRender(msgId) { window.__renderN = (window.__renderN || 0) + 1; - if (window.__renderN % 10 === 1) { + if (window.__STREAM_DIAG && window.__renderN % 10 === 1) { console.log('[JS] render#' + window.__renderN + ' 开始 id=' + msgId); } var buf = messageBuffer[msgId]; @@ -1288,9 +1307,11 @@ function appendReasoning(msgId, token) { buf.reasoning += token; var wrapper = document.getElementById(msgId); if (!wrapper) { diagEvent('appendReasoning', 'NO_WRAPPER:' + msgId); return; } - window.__diag.thinkN++; - if (window.__diag.thinkN === 1 || window.__diag.thinkN % 25 === 0) { - diagEvent('appendReasoning', 'n=' + window.__diag.thinkN); + if (window.__STREAM_DIAG) { + window.__diag.thinkN++; + if (window.__diag.thinkN === 1 || window.__diag.thinkN % 25 === 0) { + diagEvent('appendReasoning', 'n=' + window.__diag.thinkN); + } } wrapper.classList.add('streaming'); var tl = ensureTimeline(wrapper, buf); @@ -1308,7 +1329,11 @@ function appendReasoning(msgId, token) { buf.thinkSegs.push(tc); } tc.__buf = (tc.__buf || '') + token; - console.log('[JS] think#' + window.__diag.thinkN + ' 段buf=' + tc.__buf.length + 'c dom=' + (tc.textContent || '').length + 'c h=' + tc.offsetHeight); + if (window.__STREAM_DIAG) { + console.log('[JS] think#' + window.__diag.thinkN + + ' 段buf=' + tc.__buf.length + 'c dom=' + + (tc.textContent || '').length + 'c h=' + tc.offsetHeight); + } syncRenderThrottled(msgId); // ★ 同步通道 scheduleStreamingRender(msgId); } @@ -1319,9 +1344,11 @@ function appendToken(msgId, token) { buf.content += token; var wrapper = document.getElementById(msgId); if (!wrapper) { diagEvent('appendToken', 'NO_WRAPPER:' + msgId); return; } - window.__diag.tokenN++; - if (window.__diag.tokenN === 1 || window.__diag.tokenN % 25 === 0) { - diagEvent('appendToken', 'n=' + window.__diag.tokenN); + if (window.__STREAM_DIAG) { + window.__diag.tokenN++; + if (window.__diag.tokenN === 1 || window.__diag.tokenN % 25 === 0) { + diagEvent('appendToken', 'n=' + window.__diag.tokenN); + } } wrapper.classList.add('streaming'); var tl = ensureTimeline(wrapper, buf); @@ -1345,9 +1372,14 @@ function appendToken(msgId, token) { } else { seg.__buf = (seg.__buf || '') + token; } - console.log('[JS] token#' + window.__diag.tokenN + ' 段buf=' + seg.__buf.length + 'c dom=' + (seg.textContent || '').length + 'c 段数=' + buf.textSegs.length + ' h=' + seg.offsetHeight); - if (window.__diag.tokenN === 1 || window.__diag.tokenN % 25 === 0) { - diagEvent('tokenDOM', { buf: seg.__buf.length, dom: (seg.textContent || '').length }); + if (window.__STREAM_DIAG) { + console.log('[JS] token#' + window.__diag.tokenN + + ' 段buf=' + seg.__buf.length + 'c dom=' + + (seg.textContent || '').length + 'c 段数=' + + buf.textSegs.length + ' h=' + seg.offsetHeight); + if (window.__diag.tokenN === 1 || window.__diag.tokenN % 25 === 0) { + diagEvent('tokenDOM', { buf: seg.__buf.length, dom: (seg.textContent || '').length }); + } } syncRenderThrottled(msgId); // ★ 同步通道:token 到 → 内容必现 scheduleStreamingRender(msgId); // rAF 通道:更平滑(环境允许时) @@ -1357,7 +1389,12 @@ function appendToken(msgId, token) { function finishMessage(msgId) { console.log('[JS] finishMessage id=' + msgId); var wrapper = document.getElementById(msgId); - if (!wrapper) { delete messageBuffer[msgId]; diagEvent('finish', 'NO_WRAPPER:' + msgId); return; } + if (!wrapper) { + delete messageBuffer[msgId]; + delete __lastSyncRender[msgId]; + diagEvent('finish', 'NO_WRAPPER:' + msgId); + return; + } diagEvent('finish', { id: msgId }); var buf = messageBuffer[msgId]; cancelStreamingRender(msgId); @@ -1440,6 +1477,7 @@ function finishMessage(msgId) { if (buf.raf) { cancelAnimationFrame(buf.raf); buf.raf = 0; } } delete messageBuffer[msgId]; + delete __lastSyncRender[msgId]; // --- H. 刷新自定义滚动条 --- reportWebScroll(); @@ -1940,6 +1978,7 @@ function renderTimelineHistory(msgId, timelineJson) { // 删除 buffer → finishMessage 不再全量重渲(避免破坏时间线 DOM), // 但仍会执行代码高亮/滚动等收尾 delete messageBuffer[msgId]; + delete __lastSyncRender[msgId]; } } catch (e) { console.error('[JS]: renderTimelineHistory 解析失败', e); @@ -2063,6 +2102,23 @@ function hideLoadingOverlay() { setTimeout(function() { el.style.display = 'none'; }, 480); } +// 消息从当前窗口移除时一并释放附件缓存。附件使用 msgId-att-N 作为键, +// 只删除父消息键会让长文本和元数据跨会话持续留在内存中。 +function clearMessageStores(msgId) { + if (!msgId) return; + delete messageBuffer[msgId]; + delete __lastSyncRender[msgId]; + delete longTextStore[msgId]; + delete finalContentStore[msgId]; + delete attachmentMetaStore[msgId]; + var prefix = msgId + '-att-'; + [longTextStore, attachmentMetaStore].forEach(function(store) { + Object.keys(store).forEach(function(key) { + if (key.indexOf(prefix) === 0) delete store[key]; + }); + }); +} + // ==================== 历史记录与视图控制 ==================== function clearChat() { var bubbles = chatContainer.querySelectorAll('.message-wrapper, .system-note'); @@ -2073,6 +2129,7 @@ function clearChat() { longTextStore = {}; finalContentStore = {}; attachmentMetaStore = {}; + __lastSyncRender = {}; // 🆕 P1-01:清渲染窗口状态机(游标/缓存/未决请求/代次;配置模式与大小保留) if (typeof rwState !== 'undefined' && rwState && typeof RenderWindowState !== 'undefined') { @@ -2093,9 +2150,7 @@ function deleteMessage(msgId) { wrapper.style.transform = "translateY(-10px)"; setTimeout(function() { wrapper.remove(); }, 300); } - delete messageBuffer[msgId]; - delete longTextStore[msgId]; - delete finalContentStore[msgId]; + clearMessageStores(msgId); setTimeout(reportWebScroll, 350); } @@ -2267,10 +2322,7 @@ function rwCaptureAnchor() { function rwRemoveMessageDom(msgId) { var wrapper = document.getElementById(msgId); if (wrapper) wrapper.remove(); - delete messageBuffer[msgId]; - delete longTextStore[msgId]; - delete finalContentStore[msgId]; - delete attachmentMetaStore[msgId]; + clearMessageStores(msgId); } /* 加载入口:固定在 chat-container 首/尾;无更多消息时隐藏。 */