明天你会感谢今天奋力拼搏的你。
ヾ(o◕∀◕)ノヾ
完成时间:2026-09-15 适用范围:本站 JPress(Java 侧)+ 独立 Python MCP 服务
一句话:upload_attachment上线后,AI 写博客时的配图不再需要“先去后台上传再手抄 URL”,也不再有“相对路径贴到站上必 404”这个坑。
MCP 服务原来有 6 个工具,能建分类、发文章、改文章,唯独碰不了图片。于是 blog-publisher 每次带图发文都要在三条路里挑一条:
!./assets/x.png —— 本地预览正常,一发到站上就是碎图,/assets/... 根本不存在;现在多了第四条:把图片字节交给 upload_attachment,拿回一个公网可达的绝对 URL,直接写进正文。

关键点在第 ④⑤ 步仍在 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 自己加进上传白名单的,只有这道兑底闸能拦——它确实读得到客户端原始扩展名。
正文里用 URL: 那一行,别用 路径:(相对路径上站必 404)。SHA256: 是给 Skill 做去重映射用的:sha256(本地文件) → 附件 URL 记在 .blog/media-map.json,命中就复用,服务端不去重。
tests/test_attachment_upload.py --e2e:36 项断言全 PASS;被拒用例零留痕(新增行数 == 成功次数,现场累加核对)。.jsp/.exe/.html/.js/.css 全部 state:fail。127.0.0.1:8888 与公网 https://www.cyxcoder.cn 取同一张图,都是 200 image/png。工具入参是 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 入库前会剥掉尖括号。
全部评论