重构+优化: CR-04+06 AiChat 流式渲染改进 + CR-10 useRendered DRY 抽
- CR-04: splitBlocks 正则切块改 marked.lexer() tokens 遍历(type=code 的 .raw 整块,其余按双换行切),正确处理行首空格/4+反引号嵌套/未闭合围栏——lexer 与 parse 同 marked 实例同 gfm 规则,切块边界与渲染边界一致。 - CR-06: watch(mdReady) 翻 true 时主动 scheduleStreamParse 重算,修首屏 marked 慢+暂无 delta 致末段停留 escapeFallback 纯文本。 - CR-10: useMarkdown 抽 useRendered(computed 读 mdReady+renderMd + ensureLoaded 幂等预热),4 view(TaskDetail/ProjectDetail/Ideas/Knowledge)删手写 renderedDesc computed + onMounted loadMarkdown 重复,loadMarkdown 提模块级。DRY 行为零变化。 批4 wlnazlvdc,vue-tsc 0 err
This commit is contained in:
@@ -353,22 +353,34 @@ const streamingHtml = ref('')
|
||||
let rafId: number | null = null
|
||||
let lastStreamText = ''
|
||||
|
||||
/// 切块:代码围栏(```...```)整体一块(跨双换行不切),非代码段按双换行切段。
|
||||
/// 切块:代码围栏(```...``` 或 3+ 反引号 + 行首可选空格 + info 字符串)整体一块
|
||||
/// (跨双换行不切),非代码段按双换行切段。
|
||||
/// 流式期末块可能不完整(未闭合围栏/半截段落),交给 parseBlockNoCache 每次重 parse。
|
||||
///
|
||||
/// CR-260615-04:旧实现用正则 /```[^\n]*\n[\s\S]*?(?:```|$)/ 切块,与 marked 围栏规则
|
||||
/// 不一致——不要求行首、固定 3 反引号,导致行中裸三反引号 / 4+ 反引号嵌套围栏切错,
|
||||
/// 前块缓存固化错误 html。改用 marked.lexer()(经 getMarked() 取 marked 实例)做围栏
|
||||
/// 感知切块:lexer 把代码围栏识别为单个 type==='code' token(.raw 含整段围栏),
|
||||
/// 段落/标题等其余 token 的 .raw 按 \n\n 切段。lexer 与 parse 用同一份 marked 实例 +
|
||||
/// 同一份围栏规则,切块边界与渲染边界一致。
|
||||
function splitBlocks(text: string): string[] {
|
||||
const marked = getMarked()
|
||||
const blocks: string[] = []
|
||||
const fenceRe = /```[^\n]*\n[\s\S]*?(?:```|$)/g
|
||||
let last = 0
|
||||
let m: RegExpExecArray | null
|
||||
while ((m = fenceRe.exec(text)) !== null) {
|
||||
if (m.index > last) {
|
||||
for (const b of text.slice(last, m.index).split(/\n{2,}/)) if (b.trim()) blocks.push(b)
|
||||
// marked 未就绪时 splitBlocks 不应被调用(renderStreamingMd 已 mdReady 守卫),
|
||||
// 此处兜底返回原文,不引入正则。
|
||||
if (!marked) return [text]
|
||||
const tokens = marked.lexer(text)
|
||||
for (const tok of tokens) {
|
||||
if (!tok || typeof (tok as { raw?: string }).raw !== 'string') continue
|
||||
const raw = (tok as { raw: string }).raw
|
||||
if ((tok as { type: string }).type === 'code') {
|
||||
// 代码围栏 token:整段一块(含行首可选空格 + 3+ 反引号 + info 字符串)
|
||||
if (raw.trim()) blocks.push(raw)
|
||||
continue
|
||||
}
|
||||
blocks.push(m[0])
|
||||
last = m.index + m[0].length
|
||||
}
|
||||
if (last < text.length) {
|
||||
for (const b of text.slice(last).split(/\n{2,}/)) if (b.trim()) blocks.push(b)
|
||||
// 非代码 token(paragraph/space/heading/list/blockquote/...):
|
||||
// 按 \n\n 切段(段落内单换行不切,inline code 自然保留在段内)
|
||||
for (const b of raw.split(/\n{2,}/)) if (b.trim()) blocks.push(b)
|
||||
}
|
||||
return blocks.length ? blocks : [text]
|
||||
}
|
||||
@@ -718,9 +730,16 @@ watch(() => store.state.currentText, onContentChange)
|
||||
// 流式 Markdown 渲染驱动(ARC-260615-08 块级 memo):
|
||||
// - currentText 变化(每 delta):rAF 节流 scheduleStreamParse,块级 memo 只重 parse 末块
|
||||
// - streaming 翻转:结束清 rAF + 清 streamingHtml(回 renderMd 走 final marked + 缓存)
|
||||
// - mdReady 翻转(CR-260615-06):首屏 marked 慢 + 暂无新 delta 时末段停留 escapeFallback
|
||||
// 纯文本;marked 就绪后主动重算一次,使纯文本转格式化
|
||||
watch(() => store.state.currentText, (text) => {
|
||||
if (store.state.streaming && text) scheduleStreamParse(text)
|
||||
})
|
||||
watch(mdReady, (ready) => {
|
||||
if (ready && store.state.streaming && store.state.currentText) {
|
||||
scheduleStreamParse(store.state.currentText)
|
||||
}
|
||||
})
|
||||
watch(() => store.state.streaming, (s) => {
|
||||
if (rafId !== null) { cancelAnimationFrame(rafId); rafId = null }
|
||||
if (!s) { streamingHtml.value = ''; lastStreamText = '' }
|
||||
|
||||
Reference in New Issue
Block a user