Render chat and previews through the shared HTML cleaner

Why: The conversation and office fallbacks wrote marked output straight into the page, including converted docx HTML.
Evidence: html-sinks ratchet forbids new innerHTML writes outside @ui safeHtml.
Impact: Conversation keeps line-break rendering. Unsafe links are stripped after docx and pptx preview. dompurify is deduped with the UI package.
Refs: feature/sillytavern-review
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-04 17:28:46 +00:00
co-authored by Cursor
parent e26119db89
commit 1f42edf80a
7 changed files with 63 additions and 12 deletions
+9 -4
View File
@@ -1,8 +1,7 @@
// 预览能力装配(裁定 D1–D4):把重解析器经 configureFilePreviewLoaders 注入 @ui。
// 全部 dynamic import——对应格式首次预览时才下载 chunk,主 bundle 不含解析器。
// 链接预览 fetcher 走服务端 /api/url-preview(SSRF 防护在服务端执行端)。
import { configureFilePreviewLoaders, configureLinkPreviewFetcher } from '@ui'
import { marked } from 'marked'
import { configureFilePreviewLoaders, configureLinkPreviewFetcher, renderMarkdown, setSafeHTML, stripUnsafeLinks } from '@ui'
import { auth } from './auth'
import type { LinkPreviewData } from '@ui'
@@ -28,7 +27,7 @@ const renderTextFallback = (el: HTMLElement, buf: ArrayBuffer, name: string | un
note.textContent = `「${name ?? '该文件'}」不是有效的 ${kindLabel} 文件(缺少 ZIP 结构),已按文本内容展示;需要正式格式请让智能体重新生成。`
const body = document.createElement('div')
body.className = 'doc-viewer'
body.innerHTML = marked.parse(new TextDecoder().decode(buf)) as string
setSafeHTML(body, renderMarkdown(new TextDecoder().decode(buf), { breaks: true }))
el.innerHTML = ''
el.append(note, body)
}
@@ -43,13 +42,18 @@ const docxLoader = async (el: HTMLElement, source: { contentUrl?: string; name?:
const { renderAsync } = await import('docx-preview')
el.innerHTML = ''
await renderAsync(buf, el, el, { className: 'docx', inWrapper: true, breakPages: true })
stripUnsafeLinks(el)
return
} catch (err) {
console.error('[preview:docx] page render failed, falling back', source.name, err)
}
const mammoth = await import('mammoth')
const { value } = await mammoth.convertToHtml({ arrayBuffer: buf })
el.innerHTML = `<div class="doc-viewer">${value}</div>`
el.innerHTML = ''
const body = document.createElement('div')
body.className = 'doc-viewer'
setSafeHTML(body, value)
el.append(body)
}
const xlsxLoader = async (el: HTMLElement, source: { contentUrl?: string; name?: string }) => {
@@ -120,6 +124,7 @@ const pptxLoader = async (el: HTMLElement, source: { contentUrl?: string; name?:
el.append(stage)
const previewer = init(stage, { width: stage.clientWidth || 900 })
await previewer.preview(buf)
stripUnsafeLinks(el)
}
export function wirePreviewCapabilities() {