优化: aichat效率剩余(压缩后台化防阻塞/审计批量事务/只读缓存轮内去重/流式增量渲染/AiCommandOutput合批/双渲染合并) + 跨端加固(df-project路径保留大小写/tunnel文档更正supervisor重连/relay固定时间比较与帧上限/启动校验) + 销账

This commit is contained in:
lxy
2026-08-09 21:35:59 +08:00
parent fbd8fae44b
commit 11f4978ec1
15 changed files with 841 additions and 193 deletions
+58 -4
View File
@@ -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 {