Files
deepseek-harness/packages/client/ui-sidebar-documentpreview/README.zh.md
T

19 KiB
Raw Blame History

description, kind
description kind
右侧 Sidebar 的文档预览:共享文件加载与控件,可选 Markdown、代码、图片、PDF、Office 和 HTML 渲染器,并以纯文本兜底。 package-reference

@deepseek-ai/dsh-client-ui-sidebar-documentpreview

English | 中文

概述

在右侧 Sidebar 预览文件,并切换已注册的渲染器。Markdown 和代码支持分页文本;PDF、HTML 和常见图片接收完整字节;未知扩展名使用纯文本。Office 文档在本地转换为 PDF。tab 提供文件状态、渲染器选择、换行以及自动或手动重新载入。插件可在头部和不支持预览的空态中添加本地打开控件。

目录


注册了什么

  • 类型 —— ctx.sidebarRightTabs.register(...),id 为 @deepseek-ai/dsh-client-ui-sidebar-documentpreview(这个实现在 tab 系统中的身份,也是其正文注册所用的键),kind text,pattern dsh-resource://file/**,档位 fallback。canOpen 只接受 Session 地址,其中路径可为相对或绝对路径;不认领裸 absolute 地址。在 extension 或 builtin 档以更窄 pattern(比如 *.png)注册的类型接走那些地址;其他受支持文件落到这里。整个地址就是内容身份,所以不同目录下同名的两个文件、或同一路径在两个会话之下,是两个 tab;解码后的 basename 是 tab 标题,keyed slot sidebar.right.pane.tab.title 会在标题前放置按扩展名选择的 FileTypeIcon。
  • 正文 —— keyed slot sidebar.right.pane.tab,键为类型的 id。固定头部在可用时显示 Host 的绝对路径,否则显示请求路径;目录使用三级标签色,文件名使用一级标签色,路径过长时保留末段并向开头淡出,提示中仍提供完整值。有多个受支持的渲染器时才显示下拉菜单。仅文本兼容的源文件提供纯文本选项;只有一个渲染器时不显示查看器控件。已知的二进制容器后缀没有注册渲染器时,在路径头部下方显示文件类型图标和不支持预览的说明,并且不会发起读取。两个 Session 作用域的 list 子 slot 在文件元数据报出 Host 路径后把当前文件交给其他插件,owner prop 为 absolutePath:sidebar.right.tab.document.actions 在每个显示头部的状态里渲染在头部自有控件之后;sidebar.right.tab.document.unpreviewable 渲染在不支持预览的空态里,以及可读但本预览无法渲染的文件(not-text、too-large)的空态里,占据原本放重试的位置;载体或未分类的读取失败仍保留重试,不存在或非普通文件的路径只给说明。两者已发布的占位者都是 ui-open-in-app。仅当所选渲染器声明 wrap: true 时显示换行开关;图标表示点击后切换到的模式,该偏好按 tab 保存,初始开启。重新载入仍在此头部,不放入 Sidebar 的 tab 条。自动刷新默认开启;独立开关暂时隐藏,状态、文案、样式与切换逻辑保留。正文贴合格的每条边,各渲染器自行提供内容留白,并可拥有内部滚动区。这与 Files tab 右侧预留 2px 滚动条间距的布局有意不同:Preview 使用格的完整宽度,使贴边 HTML 与代码滚动区终止于格的边缘。
  • 共享加载与视图状态,会话作用域、按 tab id 分桶。store 持有累计页或完整字节、读取与观察版本、待处理 Resource 变化、自动刷新状态、加载/失败状态、渲染器选择、滚动位置、换行和已响应的导航 revision。普通 inject face 调用 Remote 读取,并经声明的 store action 写入。重新载入和加载模式变化会淘汰旧请求;tab 的中止信号清理其状态。

文档实现在 ctx.documentPreviews.register({ id, extensions, binaryExtensions?, priority, title, loading, wrap? }) 注册元数据,并以相同 id 向 keyed、Session 作用域的子 slot sidebar.right.tab.document 注册正文。binaryExtensions 列出 extensions 中不可按文本阅读的后缀,这些后缀不提供纯文本选项。两处注册都由 effect 持有,通过 ctx.slots.inject 等待子 slot。正文接收 resourceAddress、content、wrap、scrollportRef、addResource、setResources 和标准 useTabInfo/useResource 钩子。依赖成员由 tab 持有的 ResourceGroup 管理;替换依赖始终保留根 Resource。内部滚动元素挂载 scrollportRef;卸载时恢复共享正文的滚动职责。注册表保留所有匹配备选:extension(默认)优先于 builtin,随后按更长的后缀、再按注册顺序排列。所选实现仍可用时,下拉选择保持不变。HTML、SVG 和未匹配的扩展名保留纯文本回退,与加载方式无关。

loading: 'text-pages' 和 'bytes-complete' 使用共享文件读取器。选择 'renderer' 时,所选正文在读取任何字节前挂载,并接收 content: { kind: 'renderer', revision, loaded, failed, reload }。其注入回调负责内容加载、错误和取消。loaded(version) 记录已展示的源版本并结束加载。failed() 结束加载但不记录成功版本,使后续文件变化可以重试;已被替换的 revision 所发出的报告会被忽略。reload() 增加 revision,正文据此取消并替换当前请求。正文也在卸载和 tab 关闭时取消请求,将已完成内容保留在自己声明的 tab store 中,并在 tab 结束时释放。Office 预览 使用此模式,转换后的字节和字体元数据不会进入共享文件 store。

地址

tab 使用 fileAddressFor 构造的 Session 地址,携带相对或绝对路径。hostFileOf(address) 仅从地址取得 Session,不接收外部 Session 参数,也不借用当前或 Tab Session。Host 通过 Session 文件系统解析文件及关联路径,由该后端控制读取权限。任何 UI(包括 Global 组件)都共享同一完整地址的元数据。Workspace Files README 定义这些规则;渲染器选择不改变导航地址。

全部文本分页加载完成后,Markdown 图片通过已鉴权的 /api/file 路由读取绝对文件路径,以及相对于源文档所在目录的路径。相对图片等待 Host 返回文档绝对路径。URL 转义只解码一次;查询参数与片段后缀不计入文件名。HTTP(S) 图片保留原始 URL,加载失败的图片显示 alt 文本。本地图片要求应用基址为 HTTP(S) URL;图片文件不加入自动刷新依赖。

怎么读

Web 和桌面端均通过开发者工具选择 HTML 预览策略。渲染器从插件组装层接收 interactivePreview。关闭时,将经 DOMPurify 清理的完整静态文档放入不授予沙箱权限的 iframe:CSP 禁止脚本、外部资源、连接、表单和嵌套框架;所有 href 和 xlink:href 属性、刷新指令及声明式 Shadow DOM 均在重新解析前被移除。行内样式和 data 图片仍可显示,不读取关联文件。开启时使用下述支持脚本的 Blob 预览。切换模式会卸载之前的框架并中止其待处理关联文件读取。静态预览释放 CSS/JS 的 Resource 订阅,根文件继续监听。其他文档格式保持各自策略。

正文通过 useTabInfo().tab 读取记录、导航和生命周期。useResource<'file'>(tab.contentId) 提供元数据,普通 inject 回调提供内容读取:

  • 资源快照仅包含 status、value 和 failure;value 是 WorkspaceFileStat 元数据。提供方可用后,内容读取无需等待首个元数据帧。观察失败优先于 Preview 的变更提示显示;元数据不可用时保留已加载内容。
  • 文本页 —— 纯文本、Markdown 和代码通过 inject 回调调用 remote.workspaceFiles.read(sessionId, path, { offset }, signal)。首次挂载读取第一页;滚动到正文末尾或点击 加载更多 会读取下一页,直到 eof。owner 以 { kind: 'text', text, pages, eof } 提供累计前缀,包含源码偏移和行数。Markdown 和代码增量渲染此前缀,不把每页当成独立文档。第一页之后到达的更新版本页会使读取从头开始,避免混合版本。尚无内容时,失败会以文件类型图标、说明和该失败对应的出路(重试、unpreviewable 子 slot,或什么都不给)填满正文;较晚的失败保留已有内容并在其下提供重试。
  • 完整字节 —— PDF、HTML 和常见图片通过 inject 回调调用 remote.workspaceFiles.readAll(sessionId, path, signal)。rpc.ts 将线路上的 base64 解码为 data: Uint8Array<ArrayBuffer>,供 { kind: 'bytes', data } 使用。Host 的 maxFileBytes 上限拒绝超大文件,不截断。PDF 在传给 worker 前复制保留的字节,使 Preview 缓冲区仍可使用。字节仅保存在临时视图状态中,绝不进入持久布局或 Session JSONL。加载模式变化会淘汰先前结果。
  • 重新载入 —— 手动重新载入仅让当前 Preview tab 通过自己的 Remote 回调重读,保留滚动偏好并淘汰旧请求。ResourceGroup 成员变化后,自动刷新使用相同回调。各成员首次元数据仅建立基线,不触发重新载入或首读版本对账;后续在读取期间收到的变化仍会留待下一次刷新。读取既不刷新共享元数据,也不清除其它 tab 的提示。

开启开发者工具时,HTML 以贴合正文四边的 Blob iframe 运行,沙箱属性严格为 sandbox="allow-scripts",不含 allow-same-origin;脚本无法访问父应用的源或文件读取接口。渲染器通过普通 inject 回调调用 remote.workspaceFiles.readRelated,加载直接声明的相对 .js 经典脚本和 .css 样式表;固定安全上限为单个资源 4 MiB、总计 32 MiB、64 个不同资源。Host 代码解析关联路径,rpc.ts 解码返回的字节。依赖 Resource 在 readRelated 返回后加入,使用返回的 absolutePath,失败时则使用字符串类型的 error.details.path;没有 Host 路径时,Client 不自行猜测。在渲染器内部,base64 仅用于把 iframe 引导载荷嵌入脚本文本。<base href> 将依赖解析交给浏览器,HTTPS 资源也由浏览器处理。本地模块 import、CSS url()/@import 和动态 fetch 不使用 Host 文件访问。读取失败、无效 UTF-8 或超出上限都使预览失败,不发布部分资源包。替换或卸载文档会释放其 Blob URL。

PNG、JPEG、GIF、WebP、BMP、ICO 和 SVG 通过 Blob URL 在 <img> 静态图片上下文中渲染,带 12px 内边距和圆角。宽图按固有纵横比缩小到面板宽度,小图按固有 CSS 像素尺寸居中,超高图纵向滚动。渲染器既不提供缩放,也不提供拖拽平移。SVG 标记绝不进入应用 DOM 或 iframe,因此其中的脚本无法执行,也无法访问父页面。替换或卸载图片会撤销其 Blob URL。

共享文案来自 sidebarDocumentPreview;各内置渲染器拥有自己的本地化标签。PDF 与转换后的 Office 预览在浅色模式下使用石墨灰底色,在深色模式下使用哑黑底色,页面带有轻微阴影并保留文档原色。

首次读取、追加页及 HTML/PDF/图片准备共用 ongoing StateDot loading,其标签暴露给辅助技术,并遵循减少动态效果偏好;内容出现前的每个等待都把 loading 居中在面板中,打开文件到正文出现始终是同一位置的一个标记。下一页加载期间保留已显示的内容。PDF 正文仅在 PDF 预览挂载时加载包内 client.pdf.js chunk;PDF.js、Worker 源码和内嵌支持数据不会进入启动 client.js。PDF 页面贴边占满面板宽度,组成一个纵向连续序列并在接近视口时惰性渲染;未渲染的页以安静的 3:4 占位块保持位置。PDF.js 官方 TextLayerBuilder 在与画面重合的文字层上管理选区边界和复制文本规范化。配套样式不高亮空白换行;对齐同时考虑 PDF 页面单位、页面旋转与视口宽度变化,页面释放时取消两层渲染。纯图片 PDF 不包含可选取的文字。代码预览默认显示源码行号,但复制文本不包含行号;纯文本与代码使用相同字号和行高。代码直接坐在分栏自身的背景上,而不是会话卡片的填充色;复制条与占满剩余高度的内部滚动区相邻,因此横纵滚动条都从复制控件下方开始。

Office 预览

将 .doc、.docx、.xls、.xlsx、.ppt 和 .pptx 打开为 PDF 预览,使用与 PDF 文件相同的加载状态、控件、取消和文本选择能力。Host 提供方负责本地转换;无效文件、转换失败和超时会显示本地化消息。缺少 Host 服务时显示配置引导。

Web bundle 以 ui-sidebar-documentpreview 挂载本包。通过该条目的 office 设置配置临时 Office 缓存;配置目录定义可接受的值。设置注入到每个页面;修改 YAML 后重新加载浏览器页面。

字段 默认值 含义
office.maxCachedEntries 8 最多保留的已完成 PDF 数量
office.maxCachedBytes 67108864(64 MiB) 最多保留的二进制 PDF 字节数,按缓冲区 byteLength 计算
office.maxPending / office.maxReaders 8 / 32 未完成转换 RPC 数量 / 含元数据查询的读取方数量

打开 Office 文件时按需请求转换。每次读取都先检查渲染 generation 和已授权的源文件元数据,再共享进行中的转换或复用成功的 PDF。缓存以渲染 generation、Session、源文件绝对路径和源版本作为身份;通过淘汰最久未使用的 PDF,使保留量符合两项限制。失败和超过字节限制的 PDF 不会被保留。转换后 PDF 的返回渲染 generation 与缓存身份一致时,才会保留该 PDF。取消一个读取方后,共享转换会继续运行,直到最后一个读取方离开。连接重置会清空缓存字节并取消待完成读取;插件卸载还会等待未结束的请求完成。PDF 从不持久化;传输字符串、解码存储和 PDF.js 渲染内存不计入缓存限制。

后台请求为前台工作保留最后一个在途请求槽和读者槽;将任一限额设为一会拒绝后台读取。渲染器替换后会重新执行一次代次查询与授权检查。重试期间再次替换会显示本地化的繁忙提示。

当前 Office 预览缺失字体时,文档工具栏的刷新按钮前显示黄色圆角三角形警告。悬停或键盘聚焦时显示缺失字体数量;点击后打开锚定字体列表。按 Escape、点击关闭或点击外部会关闭列表,警告图标和文档位置保持不变。重新加载会关闭旧详情;没有缺失字体的预览不显示警告。不保留已读或关闭提示的状态。

Office 实现——点击展开

Office 注册、加载、缓存和字体提示位于 src/client/office/。Office 正文持有转换后的 PDF 字节和字体元数据,并声明嵌套 PDF slot,复用惰性 PDF 正文及其 tab 阅读状态。keyed slot sidebar.right.tab.document.action 将渲染器操作放在刷新按钮前。Office 操作与正文共享 store,仅读取当前 revision 的字体元数据。Host 渲染器缺失时,注册仍然可用;可选的 remote.officeToPdf 和 remote.workspaceFiles 注入提供转换与版本检查回调,移除后恢复不可用提示。注册和 tab 状态保留都遵循 effect 生命周期。转换服务拥有 Host Remote 方法,由 api/remotes 挂载。

共享 documentFileBytes() 辅助函数将普通文件与转换后 PDF 的响应解码到一个独立持有的字节缓冲区,不会将字节展开为 JavaScript 数组元素。渲染器以只读方式借用保留的字节,并在传给 Worker 前复制。

导航

ctx.sidebarRight.openResource(address, { params: { line } }) 通过 file 参数携带 1 起算的源码行号。在 text-pages 模式下,owner 顺序加载到该行或 EOF。纯文本与代码渲染器提供源码行锚点;Markdown 不提供。所选渲染器没有锚点时,导航保持待处理;用户切换到纯文本或代码后执行。代码导航直接滚动内部源码视口。字节模式渲染器不消费源码行导航。每个完成的导航 revision 只响应一次。不带 revealIfOpened: false 打开同一文件时聚焦已有 tab,并送达新 revision。

模型体验

无,因为预览是纯浏览器侧的查看器,不注册工具、提示词段或会话事件。

KV Cache 影响

无直接影响;用户在这里读到的东西永不进入模型请求。

已知限制与延期工作

  • 预览而非编辑。 查看器不提供文件编辑或共享搜索接口;目录地址以 not-regular-file 失败。未知扩展名使用纯文本读取,仍受其 UTF-8/NUL 检查限制。
  • Office 转换限制。 预览不启动原生 Office 编辑器,也不下载引擎。二进制 .doc、.xls 和 .ppt 文件不返回缺失字体诊断。转换保真度与资源限制由 LibreOffice 提供方负责。
  • 文本顺序分页,完整文件受限。 定位到较深处的源码行需要先加载此前各页;PDF、HTML 和图片必须取得 Host maxFileBytes 上限内的完整结果。
  • 字节视图不恢复滚动位置。 PDF、HTML 与图片的渲染器重新挂载或重新载入时可能回到顶部;图片适配面板宽度、不产生横向滚动,HTML iframe 的滚动属于其不透明浏览上下文。
  • 本地 HTML 依赖集合有限。 只打包直接引用的经典 .js 脚本和 .css 样式表。浏览器解析的资源仍受浏览器源与网络规则限制;iframe 不获得运行时文件读取桥接。
  • 滚动写入未节流。 每次滚动事件都把偏移记进 store;行块已 memo 化,于是由此引发的重渲染交还给 React 的是同一批元素。
  • PDF chunk 加载失败后需要刷新页面。 React 会在页面生命周期内缓存被拒绝的 lazy import;已加载正文中的普通 PDF 打开或渲染失败仍可重试。

开发备注

维护者工作上下文——点击展开

无。

运行时不变式: 不发布伴生入口。渲染器元数据、文档加载和视图状态归本地注册表与声明的 Slot store 所有,没有可比对的独立运行时来源;注册释放和 tab 生命周期由行为测试覆盖。