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.
8.8 KiB
8.8 KiB
文件预览与保留能力(工件删除 / 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. 背景与诉求(用户原话归纳)
- 文件预览补齐:PDF、Word、PowerPoint、Excel、图像、markdown、代码文件等要有预览;可考虑做网页链接的预览;落位可以考虑右侧栏,或系统提供的弹出窗口。
- 工件删除:接口和功能加入,但系统先不提供此类操作——保留能力,不暴露入口。
- 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,插件/配置可开启。这正是"为插件系统预留能力"的形态。
约束(缺一不可):
- 门控:设置
runtime.fsTools(默认 false)。关闭时不注册工具——/api/tools列表、模型可见工具集均不含;开启后仍受画像 tools.allow/deny/confirm 约束(现有机制)。 - 沙箱:根目录
<DataDir>/workspaces/<owner>/<conversationId>/;路径规范化(filepath.Clean + Rel 校验)后必须仍在根内,拒绝..、绝对路径与符号链接逃逸;单文件读写上限(如 2MB/1MB);list 深度与条数上限。 - 不变式:write_file 落盘后同步登记 artifact(kind
file,内容指针指向沙箱文件)——保住上轮确立的"工作区文件 = 工件面板可见可下载"不变式;read/list 仅限沙箱内。 - 测试:路径穿越负例(
../、绝对路径、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> + 点击放大 |
无 |
| 同源 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 类操作(只做文件读写列目录)。