# 工作区文件预览完整度(表格 / 压缩包 / 表格文件 / 音视频)技术方案 > 版本:v1.0(2026-10-26) > 状态:待评审(提案见 [../proposals/2026-10-26-workspace-file-preview.md](../proposals/2026-10-26-workspace-file-preview.md);第十七批另两件见 [../proposals/README.md](../proposals/README.md)) > 设计红线:**既有五种预览路径(markdown / PDF / 图片 / 文本 / 二进制)行为零变化;后端、admin、agent-cli、mao-cli 零改动零迁移;新库全部动态 chunk 不进首屏。** ## 0. 待确认决策(评审逐条定) | # | 决策点 | 推荐 | 备选 / 说明 | |---|--------|------|------------| | 1 | XLSX 解析库 | **`exceljs`(npm 原生)** | SheetJS 官方 npm 版停留 0.18.5(存在已披露的原型污染类历史漏洞,修复版仅 CDN 分发,违反内网禁 CDN 硬约束);exceljs 维护活跃、体积大但走动态 chunk。实施前过一次 `npm audit` 依赖安全评审 | | 2 | ZIP 解析库 | **`jszip`(中央目录先行 + 按条目懒解压)** | fflate 体积更小但 `unzipSync` 全量解压,zip 炸弹防线要自建;adm-zip 是 Node 向(Buffer/fs 语义),浏览器不可用——不因 desktop/package.json 已有该依赖而选它 | | 3 | 媒体获取方式 | **blob fetch 复用 `workspace-download`**(不新增端点、不改响应头) | 新签 `?token=` 流式端点(省内存但新增对外暴露面,否) | | 4 | CSV 数据口径 | **沿用 `readFile`(5000 行 / 512KB 上限)+ 截断提示**,与文本预览现状一致 | `readFileBytes` 全量解析(取消行数口径但引入大文件内存风险,留作 P3 后评估) | | 5 | 媒体大小阈值 | **50MB**,超限降级下载(阈值集中为常量,后续可配置) | 不做流式 Range 播放(v1 范围外) | | 6 | DOCX 预览 | **P2 伸展项**:mammoth → HTML → DOMPurify → 既有 markdown 容器;数据验证后再定去留 | 直接不做(Agent 产 docx 频率待 P1/P2 数据验证) | | 7 | 代码形态 | **FileViewer 保持调度器,新格式拆独立子组件**(CsvTableView / ArchiveBrowser / MediaPlayer / SpreadsheetView) | 全部内联进 FileViewer(该文件已 627 行,拒绝) | | 8 | `.tar` / `.tar.gz` | **不做**(JSZip 只支持 zip 家族;tar 需另一套解析,下载兜底) | 提案 §1.3 提到 tar.gz,此处收窄为 zip,如实记录偏差 | | 9 | 未知二进制格式的双端不一致(CLOUD 报错态 + 全局 toast vs LOCAL 干净 binary 态) | **不在本方案收口**(避免行为面扩大;新格式已绕过该路径) | 顺手归一(需改后端错误语义或字符串匹配, hack,否) | ## 1. 需求背景 提案已论证:Agent 高频产出 CSV / XLSX 报表、ZIP 打包、音视频,而 `FileViewer.vue` 只有 markdown / PDF / 图片 / 文本 / 二进制五档,其余「无法预览,请下载」。本方案承接提案 §4,细化到文件与函数级,并把「与现有预览链的每一个冲突点」逐项列出(§6)。 ## 2. 现状:预览链全貌(代码事实,file:line) ### 2.1 FileViewer 状态机(`desktop/src/components/center/FileViewer.vue`) - 状态联合:`'loading' | 'ready' | 'error' | 'binary' | 'empty' | 'image' | 'pdf'`(:83)。 - `loadFile()` 分发顺序(:201-260):**扩展名只判 pdf(`isPdfPath` :162)** → `provider.readFile(path, {limit:5000})` → 依次判 `media_type==='image'`(:232)/ 空文件(:239)/ `isBinaryContent` NUL 嗅探(:146)/ 文本 ready(:251)。markdown 判定(`isMarkdown` :102)只影响 ready 态内的「预览 / 源码」切换,不进分发。 - 代际守卫:`loadFileSeq` / `pdfPreviewSeq`(:168-174),切换文件 / 重载时作废过期响应;ChatInput.vue:477 注释亦引此为范式。 - 变更重载:`watch [filePath, provider]`(:266-274)切文件重载;`matchingChangeCount` watch(:278-289)对当前文件的新变更防抖 300ms 重载。 - Monaco:`useMonacoViewer` 的 `enabled = state==='ready' && showSource`(:109)——非 ready 态不激活编辑器。 - 刷新按钮仅在 ready 态模板内(:41-43);「仅显示前 5000 行」截断提示(:54-56)。 ### 2.2 CLOUD 与 LOCAL 的现存行为差异(设计输入) | 场景 | CLOUD | LOCAL(Electron) | |---|---|---| | 二进制文件(如 .zip) | 后端 `readFile` 遇 NUL 抛 `二进制文件,无法预览`(workspace-browse.service.ts:333-341)→ 请求拦截器弹**全局 ElMessage.error**(api/index.ts:63-71)+ FileViewer 进 error 态带重试按钮 | `localReadFile` 以 utf-8 读全文件(main.cjs:1896-1925,NUL 是合法 UTF-8 字符故保留)→ `isBinaryContent` 命中 → 干净 binary 态 | | 无全局 toast | 否(拦截器统一弹) | 是 | **这是本方案最重要的现状事实**:新格式(zip / xlsx / 媒体)今天在 CLOUD 上是「错误态 + 全局错误提示」的双重噪音。扩展名先行分发(§5.1)让这些格式根本不走 `readFile`,噪音路径自然消失。 ### 2.3 Provider 抽象(`desktop/src/composables/workspace-file-provider.ts`) - 三实现:`createLocalProvider`(:135,Electron IPC)、`createCloudProvider`(:175,REST `sessionId`)、`createProjectProvider`(:303,新建态只读)。 - 取数能力现状:`readFile`(文本 / 图片 data_uri)、`previewFile`(**仅 PDF**,:64 注释明示)、`downloadFile` / `downloadDirectory`(**仅 CLOUD**,:78-81 注释)、`searchFiles`、`getAbsolutePath`。 - **二进制字节通道缺口**:LOCAL 的 `localReadFile` 只能返回 utf-8 文本或图片 / PDF 的 data_uri(main.cjs:1896-1925),zip / xlsx / 媒体拿不到原始字节 → 需新增 IPC(§5.2)。CLOUD 侧 `workspace-download`(file.routes.ts:152)本就可取任意文件字节(带鉴权 fetch),**零新端点**。 - `downloadWorkspaceResource`(:397-428)是现成的「带鉴权 fetch + token query 兜底 + Content-Disposition 解析」范式,新 `readFileBytes` 复用其 fetch 模式。 ### 2.4 后端 file 域约束(`backend-ts/src/file/workspace-browse.service.ts`) | 约束 | 值 | 位置 | |---|---|---| | 路径安全 | `resolvePath` 工作区内解析,拒绝 symlink / 非普通文件 | :306-318 | | 文本预览总大小 | `MAX_TEXT_PREVIEW_BYTES` = 10MB | :25 | | 单次内容大小 | `MAX_CONTENT_BYTES` = 512KB | :24 | | 单次行数 | `MAX_READ_LIMIT` = 5000 | :23 | | 目录条目上限 | `MAX_ENTRIES` = 500 | :16 | | `downloadFile` / `readPdfFile` | **无格式限制**,任意字节 | :207-261 | 结论:新格式全部「前端解析 + 既有取数字节」,**后端零改动**。 ### 2.5 消费方与构建 / 测试基建 - 消费方:`CenterTabContainer.vue:5-10` 在 `` 内以 `:key="${activeTabId}-${version}"` 挂载 FileViewer;文件管理器(`FileManagerPanel.vue:301-307` emit open-file → TaskView handleOpenFile)与 markdown 内链(FileViewer:135-139)都经 `openFileTab` 汇入同一 FileViewer——**一个调度器覆盖全部入口**。 - KeepAlive 范式:`PdfViewer.vue` 的 `onActivated` 重载 + `onDeactivated` 销毁文档 / 观察器(:351-384)是新组件生命周期管理的直接先例。 - 构建:`vite.config.ts` 无 manualChunks,动态 import 自动独立 chunk;`scripts/check-main-chunk.mjs` 以**库内部字面量**(`sequenceDiagram` / `katex-mathml`)断言主 chunk 不含 mermaid / katex(脚本注释明确:不能按 chunk 文件名判断)。 - 测试:desktop vitest `environment: 'node'`;DOM 全链用 `@vitest-environment jsdom` 单文件开启(先例:`markdown-sanitize.dom.test.ts`,devDep `jsdom` 已在)。FileViewer 真实渲染 E2E 历史上降级为手动冒烟(message-rendering 方案 §9),`tests/workspace-file-dialog-*.spec.ts` 是「静态 SFC 断言」式轻量 E2E 先例。 - 现存包体事实:`PdfViewer.vue:51` 对 `pdfjs-dist` 为**静态 import**(随主 chunk 加载)。本方案不扩大静态依赖面;是否顺手把 PdfViewer 动态化列为可选清理,**不做**(改加载态有行为风险,§6-C6)。 ## 3. 技术选型 | 项 | 选型 | 理由 | |---|---|---| | CSV / TSV 解析 | **自研纯函数**(`utils/csv-parse.ts`,约 150 行) | 无依赖、node 环境可全测;复制序列化复用既有 `csvEscape` / `tableToCsv`(utils/tableCopy.ts:12-19) | | XLSX | `exceljs` 动态 import | 决策 1 | | ZIP | `jszip` 动态 import | 决策 2;懒解压 = 天然炸弹防线 | | DOCX(P2 伸展) | `mammoth` 动态 import | 输出 HTML 过 DOMPurify 后进既有 markdown 容器 | | 音视频 | 原生 `