docs: design spec for HTML preview/source toggle in workspace dialog

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
m4
2026-08-08 11:09:24 +08:00
parent b36da152a3
commit 31aa1608af
@@ -0,0 +1,67 @@
# 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/图片正常加载。