fix(web): SVG 预览强制 image/svg+xml(前端 blob mime + 后端 download)(bump 0.33.1)

SVG 在 <img> 里必须 Content-Type=image/svg+xml 才渲染。前端 preview.js 的
_showImage / mini 图片分支据扩展名强制 blob mime;后端 download 接口对 .svg
显式回 image/svg+xml(部分部署环境 mimetypes 未注册 svg → FileResponse 会猜成
octet-stream → 不显示)。双保险。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
caoqianming 2026-06-30 08:28:11 +08:00
parent e3a432dcdd
commit ff276eb9b3
4 changed files with 18 additions and 4 deletions

View File

@ -2,7 +2,7 @@
> 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 1-2 句:做了啥 + 关键判断;细节查 `git log` / `git diff` / `DESIGN §7.9` > 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 1-2 句:做了啥 + 关键判断;细节查 `git log` / `git diff` / `DESIGN §7.9`
最后更新:2026-06-29(ppt skill 重构为 SVG-first,移植 ppt-master + bump 0.33.0) 最后更新:2026-06-30(web 端 SVG 预览修复:强制 image/svg+xml + bump 0.33.1)
--- ---
@ -21,6 +21,9 @@
## 已完成关键能力 ## 已完成关键能力
### 2026-06-30 / 修复 web 端 SVG 无法预览(bump 0.33.1)
SVG 在 `<img>` 里必须 Content-Type=`image/svg+xml` 才渲染。前端 `preview.js``_showImage` / mini 图片分支据扩展名强制 blob mime(与服务端响应头无关);后端 `download` 接口对 `.svg` 显式回 `image/svg+xml`(部分部署环境 mimetypes 未注册 svg → 会被 FileResponse 猜成 octet-stream)。双保险。
### 2026-06-29 / ppt skill 清空重构为 SVG-first(移植 ppt-master,bump 0.33.0) ### 2026-06-29 / ppt skill 清空重构为 SVG-first(移植 ppt-master,bump 0.33.0)
- 背景:旧 ppt skill 用 python-pptx + 固定组合版式件(`add_card_grid` 等),版面被 helper 框死 → 单调、AI 味重,是架构天花板,调参救不了。用户要求"清空重做,参考 github ppt-master"。 - 背景:旧 ppt skill 用 python-pptx + 固定组合版式件(`add_card_grid` 等),版面被 helper 框死 → 单调、AI 味重,是架构天花板,调参救不了。用户要求"清空重做,参考 github ppt-master"。

View File

@ -1,3 +1,3 @@
# zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。 # zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。
# 改版本只动这一行。 # 改版本只动这一行。
__version__ = "0.33.0" __version__ = "0.33.1"

View File

@ -2755,9 +2755,15 @@ def create_app() -> FastAPI:
# workspace 文件可变, 禁浏览器启发式缓存 (RFC 7234 默认能缓数小时) # workspace 文件可变, 禁浏览器启发式缓存 (RFC 7234 默认能缓数小时)
# 否则文件改了 SPA 预览还是旧内容 # 否则文件改了 SPA 预览还是旧内容
# (Starlette FileResponse 不实现 304, 总是 200 全量; workspace 文件小, 可接受) # (Starlette FileResponse 不实现 304, 总是 200 全量; workspace 文件小, 可接受)
# .svg 显式给 image/svg+xml: 部分部署环境 mimetypes 未注册 svg, FileResponse
# 会猜成 octet-stream, 前端 <img> 就渲染不出 SVG 预览
media_type = None
if target.suffix.lower() == ".svg":
media_type = "image/svg+xml"
return FileResponse( return FileResponse(
path=str(target), path=str(target),
filename=target.name, filename=target.name,
media_type=media_type,
headers={"Cache-Control": "no-cache"}, headers={"Cache-Control": "no-cache"},
) )

View File

@ -228,7 +228,11 @@ export async function openFilePreview(rel) {
} }
function _showImage(blob) { function _showImage(blob) {
const url = _trackBlobUrl(blob); // SVG 在 <img> 里必须 Content-Type=image/svg+xml 才渲染;下载接口靠服务端
// mimetypes 猜类型,部分部署环境(Linux)未注册 .svg → octet-stream → 不显示。
// 这里据扩展名强制纠正,与服务端响应头无关。
const mime = /\.svg$/i.test(_fpCurrentRel || "") ? "image/svg+xml" : "";
const url = _trackBlobUrl(blob, mime);
const body = $("fp-body"); const body = $("fp-body");
body.className = "body center"; body.className = "body center";
body.innerHTML = ""; body.innerHTML = "";
@ -470,7 +474,8 @@ async function openMiniFilePreview(rel) {
body.className = "body center"; body.className = "body center";
const img = document.createElement("img"); const img = document.createElement("img");
img.className = "preview-img"; img.className = "preview-img";
img.src = _trackMiniBlobUrl(blob); const _mime = /\.svg$/i.test(_mpCurrentRel || "") ? "image/svg+xml" : "";
img.src = _trackMiniBlobUrl(blob, _mime);
body.appendChild(img); body.appendChild(img);
_makeImageZoomable(body, img); _makeImageZoomable(body, img);
} else if (cat === "video") { } else if (cat === "video") {