优化: aichat效率剩余(压缩后台化防阻塞/审计批量事务/只读缓存轮内去重/流式增量渲染/AiCommandOutput合批/双渲染合并) + 跨端加固(df-project路径保留大小写/tunnel文档更正supervisor重连/relay固定时间比较与帧上限/启动校验) + 销账
This commit is contained in:
@@ -867,13 +867,16 @@ defineExpose({
|
||||
@click="toggleJson(item.msg.id)"
|
||||
><pre>{{ item.msg.content }}</pre></div>
|
||||
<div
|
||||
v-else-if="(item.msg.content && item.msg.content.trim()) || (isLastAi(item.msg) && store.state.streaming && store.state.currentText)"
|
||||
v-else-if="(item.msg.content && item.msg.content.trim()) || (isLastAi(item.msg) && store.state.streaming && streamingBlocks.length > 0)"
|
||||
class="ai-msg-bubble ai-msg-bubble--ai ai-md"
|
||||
:class="{ 'ai-msg-bubble--error': item.msg.isError, 'ai-msg-bubble--collapsed': shouldCollapseLong(item.msg) }"
|
||||
:key="'md-' + item.msg.id + '-' + (isLastAi(item.msg) ? _mdRenderKey : 0)"
|
||||
>
|
||||
<!-- 流式:分块渲染(已完成块key稳定→DOM不重建) -->
|
||||
<template v-if="isLastAi(item.msg) && store.state.streaming && store.state.currentText">
|
||||
<!-- 流式:分块渲染(已完成块key稳定→DOM不重建)。
|
||||
AC-EFF-S2-1(2026-08-09):条件改用 streamingBlocks.length 而非 store.state.currentText,
|
||||
消除「currentText 变化触发模板重渲染 + rAF 更新 streamingBlocks 触发重渲染」的双渲染
|
||||
(每次 delta 只经 rAF 更新 streamingBlocks 触发一次渲染)。 -->
|
||||
<template v-if="isLastAi(item.msg) && store.state.streaming && streamingBlocks.length > 0">
|
||||
<div v-for="block in streamingBlocks" :key="block.key" v-html="block.html"></div>
|
||||
<span v-if="isViewingGenerating" class="ai-cursor"></span>
|
||||
</template>
|
||||
|
||||
@@ -64,6 +64,20 @@ export function useStreamRenderer(opts: UseStreamRendererOptions) {
|
||||
let rafId: number | null = null
|
||||
let lastStreamText = ''
|
||||
|
||||
// ═══ AC-EFF-S1-1 增量渲染状态(2026-08-09) ═══
|
||||
// 流式追加场景(text.startsWith(_prevStreamText))下,已完成块(除末块外)跨帧稳定:
|
||||
// splitBlocks 按 \n\n / 已闭合代码围栏切块,块边界随文本追加保持稳定(末块外不重切)。
|
||||
// 原实现每帧对全文 marked.lexer O(n)(长回复/单块回复病态甚至每帧 O(n) lexer + O(n) parse)。
|
||||
// 现缓存上一帧「已完成块 + 末块文本」,只对「旧末块 + 增长段」增量 lexer,已完成块直接复用
|
||||
// (块内容寻址缓存 _blockCache 本就命中,此处省掉对全文的重复 lexer)。
|
||||
// 非追加(首帧 / 重渲染 / 内容替换)回退全量 splitBlocks(与旧行为一致,渲染结果等价)。
|
||||
let _prevStreamText = ''
|
||||
let _prevCompleted: string[] = []
|
||||
let _prevTail = ''
|
||||
/// 单块回复病态阈值:整个回复无 \n\n/围栏边界(末块=全文)且超此长度时,末块降级纯文本
|
||||
/// (escapeFallback,避免每帧 marked.lexer+parse O(n) 卡顿),AiCompleted 后整段 renderMd 完整渲染。
|
||||
const SINGLE_BLOCK_TEXT_LIMIT = 12000
|
||||
|
||||
/// 切块:代码围栏(```...``` 或 3+ 反引号 + 行首可选空格 + info 字符串)整体一块
|
||||
/// (跨双换行不切),非代码段按双换行切段。
|
||||
/// 流式期末块可能不完整(未闭合围栏/半截段落),交给 parseBlockNoCache 每次重 parse。
|
||||
@@ -124,17 +138,46 @@ export function useStreamRenderer(opts: UseStreamRendererOptions) {
|
||||
return html
|
||||
}
|
||||
|
||||
/// 流式渲染:切块 + 块级 memo(前块缓存/末块重 parse)
|
||||
/// 流式渲染:增量切块 + 块级 memo(前块缓存/末块重 parse)
|
||||
/// 返回 { html, key } 数组:key 用 block 文本 hash(已完成块稳定/末块递增),
|
||||
/// 模板 v-for + v-html 分块渲染,已完成块 DOM 不重建 → 选文字保持。
|
||||
///
|
||||
/// AC-EFF-S1-1 增量:追加场景复用上一帧已完成块,只对「旧末块+增长段」增量 lexer,
|
||||
/// 不再每帧全文 lexer;单块回复(整回复无块边界)超阈值降级纯文本防 O(n) parse 卡顿。
|
||||
function renderStreamingBlocks(text: string): StreamBlock[] {
|
||||
if (!text) return []
|
||||
// AR-1 流式 MD 开关关 → 回退纯文本短路(escapeFallback 全文转义 + <br>,原行为)
|
||||
// mdReady 未就绪也走纯文本兜底(marked 尚未加载完成)。
|
||||
if (!streamingMdEnabled.value || !mdReady.value || !getMarked() || !getPurify()) {
|
||||
_prevStreamText = '' // 纯文本降级路径不增量缓存(下帧回全量,防旧帧基线误命中)
|
||||
return [{ html: escapeFallback(text), key: 'fallback' }]
|
||||
}
|
||||
const blocks = splitBlocks(text)
|
||||
// 增量切块:追加场景复用上一帧已完成块,只对「旧末块 + 增长段」增量 lexer。
|
||||
// 非追加(首帧/重渲染/内容替换)→ 全量 splitBlocks(与旧行为一致)。
|
||||
const appendOnly = _prevStreamText.length > 0 && text.startsWith(_prevStreamText)
|
||||
let blocks: string[]
|
||||
if (appendOnly) {
|
||||
const growth = text.slice(_prevStreamText.length)
|
||||
const combined = _prevTail + growth
|
||||
const combinedBlocks = splitBlocks(combined)
|
||||
blocks = combinedBlocks.length <= 1
|
||||
// 仍单块增长(旧末块未跨边界)→ 已完成块不变,新末块 = 旧末块+增长
|
||||
? _prevCompleted.concat(combinedBlocks)
|
||||
// 旧末块跨边界拆出若干已完成块(围栏闭合/新 \n\n)+ 新末块
|
||||
: _prevCompleted.concat(combinedBlocks.slice(0, -1), combinedBlocks[combinedBlocks.length - 1])
|
||||
} else {
|
||||
blocks = splitBlocks(text)
|
||||
}
|
||||
// 记录下一帧增量基线:已完成块 = 除末块外全部;末块 = 最后一块(可能不完整)。
|
||||
if (blocks.length > 1) {
|
||||
_prevCompleted = blocks.slice(0, -1)
|
||||
_prevTail = blocks[blocks.length - 1]
|
||||
} else {
|
||||
_prevCompleted = []
|
||||
_prevTail = blocks.length === 1 ? blocks[0] : ''
|
||||
}
|
||||
_prevStreamText = text
|
||||
|
||||
const n = blocks.length
|
||||
let tailSeq = 0 // 末块递增序号,确保末块 key 每次不同触发更新
|
||||
return blocks.map((b, i) => {
|
||||
@@ -144,7 +187,10 @@ export function useStreamRenderer(opts: UseStreamRendererOptions) {
|
||||
// 流式过程中每 delta 重 parse 会闪烁/错乱)。降级为纯文本(转义围栏原文),AiCompleted
|
||||
// 后整段 renderMd 会做完整代码块渲染(此时围栏已闭合)。已完成块不受影响(围栏已闭合)。
|
||||
const degrade = isTail && isUnclosedCodeFence(b)
|
||||
const html = degrade
|
||||
// AC-EFF-S1-1:单块回复病态(整回复无 \n\n/围栏边界,末块=全文)超阈值 → 降级纯文本,
|
||||
// 防每帧 marked.lexer+parse O(n) 卡顿(流式期间临时纯文本,AiCompleted 后完整 markdown)。
|
||||
const longSingleBlock = isTail && n === 1 && b.length > SINGLE_BLOCK_TEXT_LIMIT
|
||||
const html = (degrade || longSingleBlock)
|
||||
? escapeFallback(b)
|
||||
: isTail ? parseBlockNoCache(b) : parseBlock(b)
|
||||
// 已完成块用文本做 key(DOM 稳定不重建);末块用递增序号(每帧更新)
|
||||
@@ -194,11 +240,15 @@ export function useStreamRenderer(opts: UseStreamRendererOptions) {
|
||||
}
|
||||
|
||||
/// 清理流式渲染状态(streaming 翻 false / 会话切换 / 卸载前调)
|
||||
/// 取消 pending rAF + 清 streamingBlocks + 重置 lastStreamText。
|
||||
/// 取消 pending rAF + 清 streamingBlocks + 重置 lastStreamText + 清增量基线。
|
||||
function clearStreamingState(): void {
|
||||
if (rafId !== null) { cancelAnimationFrame(rafId); rafId = null }
|
||||
streamingBlocks.value = []
|
||||
lastStreamText = ''
|
||||
// AC-EFF-S1-1:清增量基线(防新轮/新会话误命中旧帧已完成块)。
|
||||
_prevStreamText = ''
|
||||
_prevCompleted = []
|
||||
_prevTail = ''
|
||||
}
|
||||
|
||||
/// 仅取消 pending rAF(不清 streamingBlocks/lastStreamText)。
|
||||
@@ -209,8 +259,12 @@ export function useStreamRenderer(opts: UseStreamRendererOptions) {
|
||||
|
||||
/// 清空实例级块缓存(内容寻址 Map + LRU)。会话切换/开关变化时调用,
|
||||
/// 防旧会话已缓存块被新会话命中(跨会话内容错乱)。仅清缓存,不动 streamingBlocks。
|
||||
/// AC-EFF-S1-1:同步清增量基线(已完成块引用旧会话内容,防跨会话误复用)。
|
||||
function resetStreamCache(): void {
|
||||
_blockCache.clear()
|
||||
_prevStreamText = ''
|
||||
_prevCompleted = []
|
||||
_prevTail = ''
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user