JPress MCP 加了个 upload_attachment:AI 写博客的配图终于不用手动传了

2026-09-15 01:38
2
0

完成时间:2026-09-15 适用范围:本站 JPress(Java 侧)+ 独立 Python MCP 服务
一句话upload_attachment 上线后,AI 写博客时的配图不再需要“先去后台上传再手抄 URL”,也不再有“相对路径贴到站上必 404”这个坑。

一、之前配图只有三条路,每条都不好用

MCP 服务原来有 6 个工具,能建分类、发文章、改文章,唯独碰不了图片。于是 blog-publisher 每次带图发文都要在三条路里挑一条:

  1. 本地相对路径 !./assets/x.png —— 本地预览正常,一发到站上就是碎图,/assets/... 根本不存在;
  2. 外链别的站点 —— 别人一挪文件我就死链;
  3. 干脆去图 —— 排版能看,内容表现力没了。

现在多了第四条:把图片字节交给 upload_attachment,拿回一个公网可达的绝对 URL,直接写进正文。

二、整条链路

MCP 附件上传链路(upload_attachment)

关键点在第 ④⑤ 步仍在 Java 侧:Python 只做校验和转发,落盘走 POST /mcp-api/attachment/upload,和后台“上传附件”按钮同一条链路。所以图片尺寸限制、水印、OSS 镜像、attachment 表入库,一个都不会少——后台手工传一张和工具传一张,入库行逐列 diff 出来是一样的。

三、三层类型校验,缺一不放行

只在 Python 侧,顺序固定:

判据 拦得住什么
扩展名白名单 png/jpg/jpeg/gif/webp/bmp 改名成 .png 的脚本、.html.sh
声明的 mime_type 必须与扩展名一致 filename=x.png + mime_type=image/gif
文件头魔数必须与扩展名一致 内容是 GIF 字节、名字骗不过第 ① 层的 .png

.svg 默认拒绝(可内嵌脚本),需要显式置 MCP_UPLOAD_ALLOW_SVG=true 才进入后两层;放开≠免检。

Java 侧还有一道兑底闸:AttachmentUtils.isUnSafe 的黑名单。实测 .jsp 会被 JFinal 改名成 xxx.jsp_unsafe 后拒掉,而 .html 是 JPress 自己加进上传白名单的,只有这道兑底闸能拦——它确实读得到客户端原始扩展名。

四、返回格式

✅ 附件上传成功
ID: 370
文件名: mcp-a12-check.png
类型: image/png
大小: 70 B
路径: /attachment/20260915/99187b624c26401abe5733e77e7af067.png
URL: https://www.cyxcoder.cn/attachment/20260915/99187b624c26401abe5733e77e7af067.png
SHA256: c414cd0e204de974f73753c7e28d7638e7b3691bb8b1a2bab6b25bb7fed7ce77

正文里用 URL: 那一行,别用 路径:(相对路径上站必 404)。SHA256: 是给 Skill 做去重映射用的:sha256(本地文件) → 附件 URL 记在 .blog/media-map.json,命中就复用,服务端不去重。

五、验收是跑出来的,不是推出来的

  • 一次性容器真跑 tests/test_attachment_upload.py --e2e36 项断言全 PASS;被拒用例零留痕(新增行数 == 成功次数,现场累加核对)。
  • 危险扩展名直连 Java 端点:.jsp/.exe/.html/.js/.css 全部 state:fail
  • 内网 127.0.0.1:8888 与公网 https://www.cyxcoder.cn 取同一张图,都是 200 image/png
  • 失败分支不写库、不留文件;体积上限 5 MB(Base64 后约 6.7 MB,仍在 Java 侧 10 MB 之下)。

六、踩到的一个真坑:base64 别太长

工具入参是 content_base64,也就是说图片字节要由调用方(模型)逐字写进参数里。实测:96 字符(1×1 PNG)逐字转写完全可靠,返回的 SHA256 与本地文件哈希逐位相等;而 23,828 字符(17.9 KB 的图)就没有把握了——抄错一位,站上就是一条永远打不开的图片记录。

所以这次这张链路图换了个送法:base64 由本地文件直接拼进脚本经 SSH 落到服务器,再在容器内真调 upload_attachment()(只绕过 MCP 那一层传输,工具函数就是线上那份),返回 SHA256: ab183610... 与本地文件一致,说明字节没变。

二期该补的是:给工具加一个“按站内已有附件登记”或“从服务端本地路径直传”的入口,而不是让模型去抄长 base64。


限制备忘:仅 role=admin 的 Token 可调用;单文件默认 ≤ 5 MB;服务端不去重;title 入库前会剥掉尖括号。

全部评论