Fall back to text when xlsx and pptx are not ZIP files

Why: docx preview already sniffs ZIP magic so a text file renamed to
.docx stays readable. xlsx and pptx loaders still fed those bytes to
the parser and surfaced a raw library error.

Evidence: npx tsc --noEmit and npx vitest run (40 tests) on this branch
after merging origin/main. Legacy .xls/.ppt skip the sniff because they
are not OOXML.

Impact: pseudo xlsx/pptx render as text with a short note. Real ZIP
workbooks and decks still use the existing parsers.

Refs: Dev PR #96, task feature/preview-sniff-fallback
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-01 04:07:05 +00:00
co-authored by Cursor
parent b569d8d7b3
commit bfa634d2a9
+19 -7
View File
@@ -22,10 +22,10 @@ const isZipBytes = (buf: ArrayBuffer): boolean => {
((h[2] === 0x03 && h[3] === 0x04) || (h[2] === 0x05 && h[3] === 0x06))
}
const renderTextFallback = (el: HTMLElement, buf: ArrayBuffer, name?: string) => {
const renderTextFallback = (el: HTMLElement, buf: ArrayBuffer, name: string | undefined, kindLabel: string) => {
const note = document.createElement('div')
note.className = 'ui-fp-note'
note.textContent = `「${name ?? '该文件'}」不是有效的 docx 文件(缺少 ZIP 结构),已按文本内容展示;需要正式 Word 格式请让智能体以 docx 格式重新生成。`
note.textContent = `「${name ?? '该文件'}」不是有效的 ${kindLabel} 文件(缺少 ZIP 结构),已按文本内容展示;需要正式格式请让智能体重新生成。`
const body = document.createElement('div')
body.className = 'doc-viewer'
body.innerHTML = marked.parse(new TextDecoder().decode(buf)) as string
@@ -36,7 +36,7 @@ const renderTextFallback = (el: HTMLElement, buf: ArrayBuffer, name?: string) =>
const docxLoader = async (el: HTMLElement, source: { contentUrl?: string; name?: string }) => {
const buf = await fetchBuf(source.contentUrl)
if (!isZipBytes(buf)) {
renderTextFallback(el, buf, source.name)
renderTextFallback(el, buf, source.name, 'DOCX')
return
}
const mammoth = await import('mammoth')
@@ -44,9 +44,15 @@ const docxLoader = async (el: HTMLElement, source: { contentUrl?: string; name?:
el.innerHTML = value
}
const xlsxLoader = async (el: HTMLElement, source: { contentUrl?: string }) => {
const xlsxLoader = async (el: HTMLElement, source: { contentUrl?: string; name?: string }) => {
const buf = await fetchBuf(source.contentUrl)
// .xls 是 OLE 复合文档,不是 ZIP;只对 OOXML(xlsx/xlsm,或未给文件名)做魔数嗅探。
if (!/\.xls$/i.test(source.name ?? '') && !isZipBytes(buf)) {
renderTextFallback(el, buf, source.name, 'XLSX')
return
}
const XLSX = await import('xlsx')
const wb = XLSX.read(await fetchBuf(source.contentUrl), { type: 'array' })
const wb = XLSX.read(buf, { type: 'array' })
el.innerHTML = ''
const tabs = document.createElement('div')
tabs.className = 'ui-fp-sheet-tabs'
@@ -90,7 +96,13 @@ function renderSheet(wrap: HTMLElement, rows: unknown[][]) {
wrap.append(table)
}
const pptxLoader = async (el: HTMLElement, source: { contentUrl?: string }) => {
const pptxLoader = async (el: HTMLElement, source: { contentUrl?: string; name?: string }) => {
const buf = await fetchBuf(source.contentUrl)
// .ppt 是老版二进制;OOXML(pptx/ppsx)才要求 ZIP 魔数。
if (!/\.ppt$/i.test(source.name ?? '') && !isZipBytes(buf)) {
renderTextFallback(el, buf, source.name, 'PPTX')
return
}
// pptx2html@0.3.4 的 module 入口引用了大小写错配的 ./txml(Linux 打包必炸),
// 按 D2 备选条款改用 pptx-preview(init(dom).preview(arrayBuffer) 全保真渲染)。
const { init } = await import('pptx-preview')
@@ -99,7 +111,7 @@ const pptxLoader = async (el: HTMLElement, source: { contentUrl?: string }) => {
stage.className = 'ui-fp-slide-stage'
el.append(stage)
const previewer = init(stage, { width: stage.clientWidth || 900 })
await previewer.preview(await fetchBuf(source.contentUrl))
await previewer.preview(buf)
}
export function wirePreviewCapabilities() {