Why: D1-D5 were settled by the user (deps in, full-fidelity pptx, OG card plus optional embed, dual placement); the proposal status must reflect that before implementation starts. Evidence: decision section rewritten with the settled choices and the accepted trade-off on D2 (fall back to outline level with evidence only if no Vite-compatible renderer builds). Impact: documentation only. Refs: task feature/file-preview-caps.
114 lines
8.8 KiB
Markdown
114 lines
8.8 KiB
Markdown
# 文件预览与保留能力(工件删除 / fs 工具)方案
|
||
|
||
日期:2026-09-30;状态:已裁定(§5),实施任务 feature/file-preview-caps;作者:weichen + Agent
|
||
关联:2026-09-30-ui-redesign-and-auth.md §9(上轮"下载为主"的非目标,本提案补齐)
|
||
任务:feature/file-preview-proposal(本仓 PR 见任务卡)
|
||
|
||
## 1. 背景与诉求(用户原话归纳)
|
||
|
||
1. **文件预览补齐**:PDF、Word、PowerPoint、Excel、图像、markdown、代码文件等要有预览;可考虑做网页链接的预览;落位可以考虑右侧栏,或系统提供的弹出窗口。
|
||
2. **工件删除**:接口和功能加入,但系统先不提供此类操作——保留能力,不暴露入口。
|
||
3. **fs 类工具**:同样实现,但不提供(默认不可用)。
|
||
|
||
上轮交付后现状(chat-order-artifact-files,PR#71):
|
||
- DocViewer(OverlayHost 单层):markdown 渲染、图片、pdf iframe、csv/json 等宽、二进制信息卡+下载;docx/xlsx/pptx 仅下载(当时定为非目标)。
|
||
- `DELETE /api/artifacts/{id}` 后端端点已存在,前端工件面板有行内删除按钮(二次确认)。
|
||
- 后端无任何 fs 类工具;文件面唯一入口是 artifacts(上传 + write_artifact)。
|
||
|
||
## 2. 合理性评估(逐条)
|
||
|
||
### 2.1 文件预览 —— 合理,方向正确;两点边界须裁定
|
||
|
||
**赞同**:
|
||
- 预览是上轮"工作区文件管理"的自然完成项,闭环"浏览/显示/下载/回看"中的"显示"。
|
||
- 落位思路与系统架构一致:预览应做成 @ui 能力组件(FilePreview),右栏轻预览与弹窗深预览复用同一组件——契合能力平台原则(系统基本功能做完善,插件直接复用)。
|
||
|
||
**边界与风险**:
|
||
- docx/xlsx 的浏览器端解析**必须引入前端依赖**(mammoth / SheetJS),突破此前"无新依赖"约定——需 D1 裁定。缓解:全部 `dynamic import()` 按需加载,不进主 bundle,不预览不下载解析器。
|
||
- pptx 没有轻量高保真方案(社区库重且还原度差)。推荐降级为**文本大纲级预览**(JSZip 解 slide XML,逐页标题+正文),全保真不做(D2)。
|
||
- 网页链接预览:iframe 直嵌会被多数站点的 X-Frame-Options/CSP 拒绝,不可靠。推荐 **OG 元数据卡为主 + 可选"尝试内嵌"**(D3)。服务端抓取属 SSRF 高危面,硬边界:仅 http/https、解析后拒绝 localhost/环回/私有/保留地址、重定向逐跳同校验、超时与响应体上限。
|
||
|
||
### 2.2 工件删除:保留能力、不暴露入口 —— 合理,成本极低
|
||
|
||
- 后端端点**已存在**(无需新接口);动作是回退上轮加的行内删除按钮,保留 `api.deleteArtifact` 前端方法(插件可用)。
|
||
- 与 API-first 原则完全一致:能力在公开 API,UI 只是第一个客户端;将来要暴露可走设置开关或管理端,不需要任何后端改动。
|
||
- 文档与代码注释标注"保留能力,默认不暴露",防止后续误当死代码清理。
|
||
|
||
### 2.3 fs 工具:实现但不提供 —— 合理,契合能力预留;三点设计约束
|
||
|
||
**赞同**:实现合入(有测试、有沙箱、有门控),默认关闭=模型不可见、不进 `/api/tools`,插件/配置可开启。这正是"为插件系统预留能力"的形态。
|
||
|
||
**约束(缺一不可)**:
|
||
1. **门控**:设置 `runtime.fsTools`(默认 false)。关闭时不注册工具——`/api/tools` 列表、模型可见工具集均不含;开启后仍受画像 tools.allow/deny/confirm 约束(现有机制)。
|
||
2. **沙箱**:根目录 `<DataDir>/workspaces/<owner>/<conversationId>/`;路径规范化(filepath.Clean + Rel 校验)后必须仍在根内,拒绝 `..`、绝对路径与符号链接逃逸;单文件读写上限(如 2MB/1MB);list 深度与条数上限。
|
||
3. **不变式**:write_file 落盘后**同步登记 artifact**(kind `file`,内容指针指向沙箱文件)——保住上轮确立的"工作区文件 = 工件面板可见可下载"不变式;read/list 仅限沙箱内。
|
||
4. **测试**:路径穿越负例(`../`、绝对路径、URL 编码变体、符号链接)、上限、门控正负例。
|
||
|
||
## 3. 设计方案
|
||
|
||
### 3.1 FilePreview 能力组件(modules/ui)
|
||
|
||
```
|
||
FilePreview({ source, compact? })
|
||
source = { kind:'artifact', id, name, mime?, contentUrl, downloadUrl }
|
||
| { kind:'url', href }
|
||
```
|
||
|
||
- 按扩展名/mime 分派内部渲染器;渲染器注册表 `registerFilePreview(ext, renderer)` 预留插件自定义预览能力。
|
||
- 格式矩阵(v1):
|
||
|
||
| 格式 | 方案 | 新依赖 |
|
||
|---|---|---|
|
||
| md/txt | marked 渲染(现成) | 无 |
|
||
| 图像 | `<img>` + 点击放大 | 无 |
|
||
| pdf | 同源 iframe(现成) | 无 |
|
||
| csv/json/日志 | 等宽 pre(现成) | 无 |
|
||
| **代码文件**(go/py/ts/js/rs/java/sh/sql/yaml/toml/html/css 等) | 等宽 + 行号(不做语法高亮;高亮若做需 highlight.js,另行裁定) | 无 |
|
||
| **docx** | mammoth → HTML(样式收敛进 tokens) | mammoth(动态) |
|
||
| **xlsx/xls** | SheetJS → 表格(sheet 切换,默认首 200 行分页) | xlsx(动态) |
|
||
| **pptx** | JSZip 解 slide XML → 逐页文本大纲卡 | jszip(动态) |
|
||
| **url** | OG 卡(标题/描述/站点/图标/首图)+「尝试内嵌」按钮(iframe 加载失败回退提示) | 服务端 `/api/url-preview` |
|
||
|
||
- **双落位**(D4):右栏新增"预览"页签(compact 模式:窄栏排版,边聊边看,可随消息工件 chip 一键换内容)+ OverlayHost 深预览(现 DocViewer 收敛为 FilePreview 的容器壳:工具栏=编辑/保存/下载/关闭)。两处同一组件、同一注册表。
|
||
|
||
### 3.2 后端(modules/backend)
|
||
|
||
- `GET /api/url-preview?url=`:SSRF 硬边界(见 §2.1);返回 `{title, description, siteName, favicon, image, finalUrl}`;解析 og:title/og:description/og:image/og:site_name,回退 `<title>`/meta description。5s 超时、256KB 截断、Content-Type 须 text/html。缓存(内存 LRU,15 分钟)。
|
||
- fs 工具族 `internal/tools/fs.go`:`fs_read_file` / `fs_write_file` / `fs_list_dir`,参数与返回结构走既有工具契约;门控与沙箱见 §2.3。
|
||
- 工件删除:**零后端改动**。
|
||
|
||
### 3.3 前端(modules/frontend)
|
||
|
||
- DocViewer 收敛为 FilePreview 容器(删自有格式分支);右栏 Workbench 增"预览"页签 + `store.previewSource`。
|
||
- 消息工件 chip 与 ArtifactsPanel 行提供两个入口:默认弹窗深预览,"右栏"按钮轻预览;**移除删除按钮**(保留 api 方法与注释说明)。
|
||
- URL 入口(P3):会话输入框粘贴纯 URL → 消息侧生成链接预览卡(点击进右栏/弹窗预览);插件卡 doc-ref 同样可指向 url 工件。
|
||
|
||
### 3.4 依赖与体积
|
||
|
||
mammoth + xlsx + jszip 全部动态 import,仅对应格式首次预览时加载;主 bundle 不变。版本随 frontend package-lock 锁定。
|
||
|
||
## 4. 分期与验收
|
||
|
||
| 期 | 内容 | 验收 |
|
||
|---|---|---|
|
||
| P1 预览基座 | FilePreview 组件+注册表;双落位;md/图片/pdf/csv/json/代码文件迁移收敛;删除按钮回退 | @ui vitest(组件矩阵);头链:右栏预览页签+弹窗各断言;面板无删除按钮、API 仍在 |
|
||
| P2 Office | docx/xlsx/pptx 渲染器(动态加载) | 样例文件矩阵头链断言(docx 出正文、xlsx 出表格、pptx 出大纲);懒加载不进主 bundle(构建产物体积对比) |
|
||
| P3 URL 预览 | /api/url-preview + LinkPreview + 输入框 URL 卡 | go 测试:OG 解析正例 + SSRF 负例(localhost/私网/非 http 协议/超时);头链:链接卡→右栏预览 |
|
||
| P4 fs 工具 | 实现+门控+沙箱+测试(默认关) | go 测试:穿越负例全红、门控关时不注册;开启后 write_file 产物出现在工件面板 |
|
||
|
||
## 5. 决策点(2026-09-30 用户已裁定)
|
||
|
||
- **D1 Office 依赖**:✅ **引入** mammoth/SheetJS(动态加载,不进主包)。
|
||
- **D2 pptx 深度**:✅ **全保真渲染**(用户选择;接受重型依赖与还原度折衷——实施时在 pptx-preview / pptx2html 候选中取与 Vite 兼容者,若均不可构建则带回证据退回文本大纲级再裁定)。
|
||
- **D3 URL 预览形态**:✅ **OG 卡 + 可选尝试内嵌**。
|
||
- **D4 预览落位**:✅ **右栏 + 弹窗双落位**。
|
||
- **D5 fs 开关可见性**:按推荐默认执行——仅配置/环境可开(runtime.fsTools 默认 false),设置页不显示开关。
|
||
- 实施任务:feature/file-preview-caps(P1–P4 一体交付)。
|
||
|
||
## 6. 边界与不做
|
||
|
||
- 不做 Office 文件的在线**编辑**(预览只读;md/txt/csv/json 编辑能力维持现状)。
|
||
- 不做语法高亮(如需引入 highlight.js 另行裁定)。
|
||
- 不做服务端格式转换(LibreOffice/onlyoffice 等违反单二进制非容器化部署约束)。
|
||
- 删除能力不做确认流之外的新入口形态;fs 工具不做 shell/exec 类操作(只做文件读写列目录)。
|