Files
EvoScientist-WebUI/docs/superpowers/specs/2026-08-08-html-preview-toggle-design.md
T
2026-08-08 11:09:24 +08:00

4.1 KiB
Raw Blame History

HTML 文件 Preview/Source 切换 — 设计

日期:2026-08-08

背景

Workspace 文件对话框(WorkspaceFileDialog.tsx)打开 .html 文件时只显示源码(SyntaxHighlighter)。服务端有意以 text/plain + Content-Security-Policy: sandbox + nosniff 返回 HTML(src/app/api/workspace/file/route.ts:53),防止直接打开时渲染执行。

需求

  1. HTML 文件增加 Preview | Source 切换,默认 Preview。
  2. Preview 渲染页面(允许 JavaScript,plotly/echarts 交互图表可用)。
  3. Source 为现有源码高亮视图;现有 Edit(textarea 编辑 + Save)保持不变。
  4. 同目录相对资源(CSS/JS/图片)在 Preview 中可解析。

设计

1. 服务端:?render=1 渲染模式

修改 src/app/api/workspace/file/route.ts 的 GET:

  • 新参数 render=1,仅当解析出的扩展名为 html 时生效;其他扩展名忽略该参数。
  • render 模式下:
    • Content-Type: text/html; charset=utf-8
    • Content-Security-Policy: sandbox allow-scripts(允许脚本;不授予 allow-same-origin,脚本运行在 opaque origin,无法读取应用 cookie/DOM,其 fetch 会被 API 的跨源与会话检查拒绝)
    • Content-Disposition: inline
    • 保留 Cache-Control: no-store;不加 X-Content-Type-Options: nosniff 以外的变化
  • ?download=1 优先于 render=1(仍为 attachment、text/plain)。
  • 非 render 模式行为完全不变(HTML 仍 text/plain + sandbox)。

相对资源 MIME 修正:同目录 .css/.js 当前以 text/plain + nosniff 返回,浏览器在渲染页面中会拒绝应用/执行。CONTENT_TYPES 中:

  • css: text/plain → text/css; charset=utf-8
  • js: text/plain → text/javascript; charset=utf-8

这两个扩展仍受 CSP sandbox(直接打开时脚本不执行)与既有同源检查保护;图片扩展(png/jpg/…)已是正确 MIME,无需改动。

2. 对话框 UI(src/app/components/WorkspaceFileDialog.tsx)

  • 仅对 ext === "html" 且 kind === "text" 且未超大小限制的文件启用。
  • 新 state:htmlView: "preview" | "source",默认 "preview";useEffect 在 path/threadId 变化时重置为 "preview"。
  • 头部工具栏(现有 Edit/Download/Delete 之前)增加分段切换:Preview | Source 两个 ghost Button,激活态用 bg-accent 类区分;进入编辑模式时隐藏该切换(编辑已有 Cancel/Save)。
  • Preview 视图:<iframe src={workspaceFileUrl(threadId, path) + "&render=1"} sandbox="allow-scripts" title={name} className="h-full w-full rounded-md border border-border" />
  • Source 视图:现有 SyntaxHighlighter 分支(对 html 文件从默认改为仅 htmlView === "source" 时显示)。
  • 保存成功后 setEditing(false) 且 htmlView 回到 "preview",iframe 以 key={content}(或保存计数)强制重新加载以显示新内容。
  • 切换状态不持久化,每次打开文件重置为 Preview。

3. 安全边界

  • render=1 仅限 GET、仅 .html;isCrossOrigin 检查已有。
  • iframe sandbox="allow-scripts"(不含 allow-same-origin/allow-forms/allow-modals/allow-popups),与服务端 CSP 双重限制。
  • 渲染的 HTML 来自用户/agent 自己的工作区文件,与现有图片/PDF iframe 预览的信任级别一致。

错误处理

  • render 模式读取失败沿用现有错误 JSON(400/403/404)。
  • Preview iframe 加载失败(文件被删等)由浏览器显示空 iframe;Source 视图的错误展示不变。

测试

  • src/app/api/workspace/file/route.test.ts(新建或扩展,参照现有路由测试模式):
    • render=1 + .html → 200、text/html、CSP sandbox allow-scripts、inline
    • render=1 + .md → 忽略 render,行为不变
    • render=1&download=1 + .html → download 优先(attachment、text/plain)
    • 不带 render 的 .html → 仍 text/plain + CSP sandbox
    • .css/.js 新 MIME
  • UI 手动验证:打开含 plotly 的 HTML → 默认渲染、交互可用;切 Source → 源码;Edit 保存 → 回到 Preview 且内容更新;相对 CSS/JS/图片正常加载。