
我最早被图片管理恶心到是在连续写了三个月的技术笔记之后。Typora 里贴的截图越来越多图床目录乱成一锅粥——今天上传的图片叫image-20240115-093021.png明天的叫image-20240116-102233.png全部平铺在一个文件夹里。想找某篇文章对应的原图只能靠眼睛硬翻后来实在受不了花了一个周末研究 PicGo 插件机制把图片上传路径改成了「日期 MD 文件名」的自动分类结构。现在每写一篇 Markdown配图自动落位到图床根目录/2024/01/15/这篇文章的标题/下面素材规整、复用方便、备份清晰。这篇文章就把这套方案的完整思路、插件代码和踩坑记录都摊开讲适合被图片管理折腾过、想彻底理顺 Typora PicGo 工作流的同学。1. 方案设计与选型思路1.1 痛点分析图片平铺到底带来哪些麻烦很多人的 Typora 配图流程是Typora 里粘贴截图 → 自动上传到图床 → 本地留一个链接。PicGo 默认的上传路径往往是固定的比如img/或者干脆就是图床根目录。短时间用没问题一旦文章数量超过几十篇问题就全冒出来了。首先是定位困难。你想找某篇文章里用过的某张原图得先回忆起大概的日期和文件名然后在图床的图片列表里一页页翻。如果图床还开了 CDN 缓存删改图片之后刷新不及时写文章时配图出现旧版本排查起来更头大。其次是备份和迁移困难。零散平铺的图片没法按文章粒度打包你如果想迁移某几篇文章到新博客得手动筛选对应图片漏一张就导致文章里的图裂掉。我自己就栽过从 WordPress 迁到 Hugo 的时候大量图片链接失效回图床翻原图翻到怀疑人生。第三是命名冲突和覆盖风险。PicGo 默认在文件名重复时可能追加时间戳或直接覆盖。多篇文章都引用同一张image-2024.png时后期维护根本分不清谁是谁。基于这些真实的痛点我决定给 PicGo 写一个自定义上传插件让文件路径带足上下文信息日期 源 Markdown 文件名。1.2 为什么选 PicGo 自定义插件而非其他方案市面上处理 Typora 图片上传的方案不少。有人用 uPic有人用 iPic也有人直接写脚本监听文件夹。但对我来说 PicGo 有四个不可替代的优势跨平台支持好Windows 和 macOS 下表现一致插件机制成熟官方预留了自定义插件入口改造成本低Typora 对 PicGo 有原生联动设置里填好 PicGo 的监听端口就能无缝对接社区活跃遇到问题搜得到解决方案。也有人会问为什么不直接在 Typora 里设置图片保存路径规则Typora 确实支持诸如./${filename}/之类的路径变量但那只是把图片存到本地最终还是要手动上传到图床。我的需求是“上传时就自动分类”所以必须在上传环节做文章PicGo 自定义插件正好卡在这个关键点上。1.3 目录分类规则为什么用「日期 MD 文件名」先看一下我最终采用的目录结构https://your-cdn.com/ └── 2024/ └── 01/ └── 15/ ├── 用-PicGo-插件实现-Typora-图片自动分类/ │ ├── image-20240115-093021.png │ └── image-20240115-113042.png └── 从零搭建个人博客的完整指南/ └── image-20240115-153012.png根目录按年分年下面按月月下面按日日下面再按文章名分图片。这个结构有几个很直观的好处按时间线回溯素材时能快速锁定期望的月份和日期范围按文章维度整理时打开对应日期的文件夹就能看到当天所有文章的资源配合图床目录 API 或对象存储生命周期规则还能自动做冷热数据分层——比如超过一年的图片自动转低频存储。有人可能觉得按日期分太细做到“年/月/文章名”就够了。我实际体验下来三层日期其实不累赘因为云存储目录层级对访问速度几乎没有影响而更细的层级换来了更强的归档能力。这里按个人偏好选择就好但“日期 MD 文件名”这个组合是必需的——两者分别对应“时间维度”和“内容维度”交叉定位独一无二。2. 环境准备与前置条件2.1 Typora 图片上传设置解析在开始写插件之前先把 Typora 的图片上传行为搞清楚。Typora 的偏好设置里有一项「图像」核心配置是「上传图片」的行为选项可以设置为“上传图片”“上传图片并应用 URL”“上传图片并导出”等。我们通常选择“上传图片”这样粘贴截图后 Typora 会自动调用配置的上传服务。Typora 支持的上传服务包括 PicGo.app、uPic、自定义命令等。这里选 PicGo.app配置方式很简单上传服务选 PicGo.appPicGo 路径填你安装的 PicGo 可执行文件位置Typora 会自动检测 PicGo 是否在监听端口。PicGo 默认监听127.0.0.1:36677这个端口号在后面的自定义脚本里会用到先记下来。还有一个容易被忽略的细节Typora 里插入的图片如果是本地相对路径上传时 PicGo 需要能访问到这个文件。如果文件被移动过、路径失效上传就会失败。建议在写文章之前先把图片保存到本地一个统一目录比如./assets用相对路径引用Typora 和 PicGo 两边都不会出问题。2.2 PicGo 自定义插件开发基础PicGo 的插件机制基于 Node.js插件本质上就是一个 npm 包需要实现固定的接口。PicGo 在启动时会扫描~/.picgo/目录下的配置文件插件一般安装在~/.picgo/下的某个子目录或者通过 npm 全局安装。新版 PicGo 支持从「插件设置」里直接搜索安装也支持手动放置插件文件夹到指定位置。自定义插件的最小结构如下custom-uploader/ ├── package.json ├── index.js └── README.mdpackage.json里要声明插件名称、版本、主入口等字段index.js里通过module.exports导出一个对象PicGo 会调用这个对象的upload方法。方法接收一个ctx上下文参数包含待上传的文件信息、PicGo 的配置项、日志工具等最终把上传后的 URL 数组返回给调用方。module.exports (ctx) { return { uploader: custom-uploader, upload: async (input) { // input 是待上传的文件信息数组 // 返回上传后的 URL 数组 } } }这个接口设计得非常轻量核心逻辑就集中在upload方法里。接下来要做的就是在upload方法里读取当前 Markdown 文件信息、构造目标路径、调用对象存储 SDK 完成上传。2.3 图床与对象存储的选型建议PicGo 支持的图床种类很多包括腾讯云 COS、阿里云 OSS、又拍云、GitHub、七牛云等。我的方案对图床选型不敏感因为自定义插件里调用的上传接口是统一的你只需根据自己用的图床替换 SDK 即可。给新手一个建议优先选带有“目录前缀”或“自定义路径”功能的图床。比如腾讯云 COS 在上传对象时可以指定Key即对象键这个值通俗理解就是云端的完整路径正好满足我们自定义路径的需求。如果用的是 GitHub 图床就要注意仓库体积限制和加速问题GitHub 不适合存放大量图片素材超出 1GB 仓库限制后会很麻烦。我个人目前主力用腾讯云 COS原因有几点上传/下载速度快、CDN 加速配置简单、对象键规则灵活、生命周期管理完善。但这不代表其他图床不行关键是理解“对象键 云端路径”这个等价关系后面的核心逻辑无论换什么图床都成立。3. 核心实现动态获取 MD 文件名与日期3.1 方案一通过 Typora 自定义命令传入文档路径推荐PicGo 插件运行在 PicGo 进程里本身不知道 Typora 当前打开的是哪个 Markdown 文件。要拿到 MD 文件名得让 Typora 在调用上传时主动把文档路径传给 PicGo。最可靠的办法是绕开 PicGo.app 而使用 Typora 的「自定义命令」上传模式。Typora 的「图像」设置里有「自定义命令」选项它允许你指定一个命令行工具Typora 会把待上传的图片路径列表作为参数传给这个命令。借助这个机制我可以写一个 Node.js 脚本它做三件事接收 Typora 传入的图片路径列表读取当前 Typora 文档路径通过 Typora 注入的环境变量把图片路径和文档路径一起转发给 PicGo 的 CLI 上传或直接调用图床 SDK 上传。Typora 自定义命令的调用约定脚本会收到图片文件的绝对路径参数同时环境变量里会有TYPOpora_DOC_PATH具体名称视版本而定。实测里我是通过这种方式拿到文档路径的。这个方法最大的优点完全可控不依赖 PicGo 端口通信。3.2 方案二在 PicGo 插件内读取配置文件备选如果你不想用自定义命令也可以让 PicGo 插件去读一个“当前文档路径”的中间文件。比如在 Typora 的「偏好设置 → 导出」里或者在编辑文章时手动运行一个快捷键脚本把当前文档路径写到一个临时文件~/.picgo/current-md-pathPicGo 插件在上传时读取这个文件就能拿到 MD 文件名。这个方法适合不太想改 Typora 上传设置的同学但有个明显缺陷如果你开了多个 Typora 窗口或者 PicGo 上传速度很快、几次上传之间文档路径有变化临时文件里的路径可能就是过期的。我的实测体验是单窗口场景下基本稳定多窗口偶尔会串台。所以最终主力方案还是推荐自定义命令。3.3 日期格式化与目录拼接规范拿到 MD 文件名后下一步就是把日期拼进路径。日期用 JavaScript 的Date对象处理格式化成YYYY/MM/DD。不要用什么复杂的日期库原生方法几行就够function formatDate(date) { const y date.getFullYear() const m String(date.getMonth() 1).padStart(2, 0) const d String(date.getDate()).padStart(2, 0) return ${y}/${m}/${d} }MD 文件名也要做一次清洗去掉.md后缀把空格替换成短横线避免 URL 中出现空格被转义成%20过滤掉 Windows/Unix 路径里的非法字符/ \ : * ? |。这一步不能省否则某些云存储 API 会直接拒绝请求或者生成的链接里带着乱七八糟的转义符。function cleanFileName(name) { return name .replace(/\.md$/i, ) .replace(/\s/g, -) .replace(/[\\/:*?|]/g, -) }拼接最终对象键时我建议用数组join而不是字符串相加比如[year, month, day, cleanName, imgName].filter(Boolean).join(/)这样能避免多个斜杠叠在一起产生的路径卫生问题。4. 实战插件代码编写与调试4.1 完整插件代码展示基于腾讯云 COS下面这个代码是我实际在用的版本基于腾讯云 COS SDK。先通过 Typora 自定义命令脚本拿到文档路径写入环境变量MD_FILE_PATH然后在 PicGo 插件里读取这个环境变量来构造云端路径。// ~/.picgo/custom-uploader/index.js const COS require(cos-nodejs-sdk-v5) const path require(path) let cosInstance null function getCOSInstance() { if (cosInstance) return cosInstance cosInstance new COS({ SecretId: process.env.COS_SECRET_ID, SecretKey: process.env.COS_SECRET_KEY }) return cosInstance } function formatDate(date) { const y date.getFullYear() const m String(date.getMonth() 1).padStart(2, 0) const d String(date.getDate()).padStart(2, 0) return ${y}/${m}/${d} } function cleanFileName(name) { return name .replace(/\.md$/i, ) .replace(/\s/g, -) .replace(/[\\/:*?|]/g, -) } module.exports (ctx) { return { uploader: custom-uploader, upload: async (input) { const cos getCOSInstance() const bucket ctx.getConfig(picBed.custom-uploader.bucket) const region ctx.getConfig(picBed.custom-uploader.region) const baseFolder ctx.getConfig(picBed.custom-uploader.baseFolder) || const mdFilePath process.env.MD_FILE_PATH || const mdFileName mdFilePath ? path.basename(mdFilePath, .md) : 未命名文档 const dateFolder formatDate(new Date()) const cleanName cleanFileName(mdFileName) const resultUrls [] for (const item of input) { const imgName path.basename(item.fileName || item.path) const objectKey [baseFolder, dateFolder, cleanName, imgName].filter(Boolean).join(/) try { await cos.putObject({ Bucket: bucket, Region: region, Key: objectKey, Body: fs.createReadStream(item.path), ContentType: image/ path.extname(imgName).slice(1) }) const url https://${bucket}.cos.${region}.myqcloud.com/${objectKey} resultUrls.push(url) } catch (err) { ctx.log.error(上传失败, err) resultUrls.push() } } return resultUrls } } }这段代码里值得注意的小细节有几个。fs模块需要在文件顶部引入我写的时候漏过一次导致插件一上传就报fs is not definedContentType一定要显式设置否则 CDN 可能返回错误的 MIME 类型图片在浏览器里无法预览ctx.log.error是 PicGo 注入的日志工具调试时非常有用不要自己console.log否则输出不一定能在 PicGo 的日志面板里看见。4.2 Typora 自定义命令脚本的编写Typora 自定义命令的入口我用的是一个 Python 脚本Node 也可以看个人习惯放在~/.typora-uploader/upload.py#!/usr/bin/env python3 import os import subprocess import sys # Typora 会以「图片路径列表」作为参数调用此脚本 img_paths sys.argv[1:] # 读取环境变量中的当前 Markdown 文档路径 md_path os.environ.get(TYPORA_DOC_PATH, ) # 通过环境变量传给 PicGo 插件 env os.environ.copy() env[MD_FILE_PATH] md_path # 调用 picgo 的命令行上传 cmd [picgo, upload] img_paths subprocess.run(cmd, envenv)然后回 Typora 设置里把上传服务改成「自定义命令」命令填python3 ~/.typora-uploader/upload.py。这里有个很关键的点Typora 自定义命令模式下传给脚本的参数是图片文件的绝对路径而 PicGo CLI 的upload命令接受的也是图片路径列表所以我直接在脚本里把参数原样透传没有做二次处理。验证环境变量是否能正确传入可以写一个临时脚本把所有环境变量打印出来在 Typora 里插入一张图片触发上传再去看日志。我首次配置时发现TYPORA_DOC_PATH并不存在查了 Typora 的更新日志才发现不同版本注入的变量名不一样老版本可能是别的名字务必先做一次环境变量输出测试再写正式逻辑。4.3 配置文件与密钥管理PicGo 插件的配置项通过ctx.getConfig(picBed.custom-uploader.bucket)获取这些配置可以在 PicGo 的「图床设置」里找到自定义插件然后逐项填写。密钥不建议写在代码里更不要写死在插件文件夹中而是放在环境变量里。PicGo 是 GUI 应用macOS 下可以在启动前在 shell 里 exportWindows 下可以用系统环境变量。实际项目中我把密钥放在了一个独立的.env文件里PicGo 启动前用一个小脚本加载。这里没有用 dotenv 库就是简单的fs.readFileSync加split避免多引入一个依赖。目的就一个让插件代码可以安全地提交到 GitHub 仓库做版本管理而不泄露密钥。4.4 调试技巧如何看到插件输出和错误信息PicGo 的「日志」窗口能显示插件的 console 输出和错误堆栈。插件里任何ctx.log.error的信息都会出现在这里。调试时我习惯在upload方法开头加一行ctx.log.info(当前文档路径:, mdFilePath)确认环境变量是否传对。另外 PicGo 提供了一个非常有用的「上传区」功能可以手动拖拽图片触发上传而不用每次都从 Typora 里走一遍。调试插件时我都是先打开 PicGo 窗口拖几张测试图观察日志输出确认路径拼接正确后再回 Typora 里做端到端验证。这两个环节分开来调能大幅减少排查时间。5. 常见问题与排查技巧实录5.1 问题速查表症状可能原因解决办法上传失败日志显示fs is not defined插件文件顶部漏了require(fs)补上const fs require(fs)云端路径是全平铺的image-xxx.png没有分类目录MD_FILE_PATH环境变量为空检查 Typora 自定义命令脚本是否正确设置并传递了环境变量图片 URL 带%20或路径里有空格过滤非法字符时漏了空格处理在cleanFileName里加上空格替换上传后图片无法预览Content-Type 是二进制流未设置ContentType字段根据图片扩展名显式设置 MIME 类型同一文档多次上传后图片名重复被覆盖图片文件名本身冲突在目标图片名中加入时间戳或随机串后再拼接多开 Typora 窗口时图片传到了另一个文档的目录临时文件方案过期切换到自定义命令 环境变量方案这个表是我实际踩坑两天的浓缩。第一行和第二行的问题几乎是每个自定义插件新手都会遇到的尤其是第二行Typora 的自定义命令模式和 PicGo.app 模式在参数传递上有差异没搞清楚之前很容易一头雾水。5.2 同名图片覆盖问题的最终处理即使有了分类目录同一个文档里如果引用了两张不同目录下但文件名相同的图片仍然可能覆盖。我在cleanFileName之外又加了一个辅助策略在上传到云端之前给图片名加上上传时间的时间戳例如image-20240115-093021-001.png。具体做法是const now Date.now() const finalImgName ${path.basename(imgName, path.extname(imgName))}-${now}${path.extname(imgName)}这个措施很大程度上避免了同名文件互相覆盖的隐患尤其在本地图片文件名高度重复比如都叫截屏2024-01-15.png时特别管用。当然也有副作用图片名变长、可读性下降。折中的办法是只在 PicGo 检测到同名内容时才追加时间戳但实现复杂度略高。目前我的使用习惯是分类目录 原文件名因为同一个日期 文档名下几乎不会出现同名图片靠目录隔离基本已经足够了。5.3 路径中含中文的处理原则MD 文件名经常是中文清洗后放到 URL 里会变成百分号编码比如%E7%94%A8-PicGo...。这其实不会影响图片访问但我自己看着难受而且某些博客框架处理中文 URL 时可能有问题。我的处理原则是中文保留在对象键里不强制转拼音或英文。理由有三个第一大多数云存储和 CDN 对中文对象键支持良好第二文件名的可读性很重要日后归档整理时一眼能认出文档内容第三Typora 生成的本地链接如果是中文名上传后的 URL 转换逻辑不需要搞两套映射。如果确实不喜欢中文也可以在cleanFileName里用拼音库转成拼音不过这会引入额外依赖而且多音字问题很麻烦。我个人放弃了这个方向保持简单。5.4 关于 Typora 版本差异的提醒不同 Typora 版本的「图像」设置界面和自定义命令传参行为有差异。旧版本可能不注入TYPORA_DOC_PATH新版本把这个变量名改了也不是没可能。最稳妥的做法是升级 Typora 或换电脑后先跑一次“环境变量探测脚本”把系统环境和调用参数全部打出来确认关键变量是否存在、名称是否正确再继续后续操作。同一套方案在不同操作系统上的表现也有差异。macOS 下 Python 路径可能是/usr/bin/python3Windows 下则是C:\Python39\python.exePicGo 安装路径也不一样这些都要按实际环境调整。我在 Windows 机器上配置时就把自定义命令改成了node C:\typora-uploader\upload.js原理一致。6. 扩展与进阶这套方案还能怎么玩6.1 自动压缩与格式转换上传到云端之前可以先对本地图片做一次压缩处理。图片体积直接关系到页面加载速度和存储费用。我在插件里加了一个可选的压缩步骤用sharp库把大于 500KB 的图片压到 80% 质量超过 2000px 宽度的缩小到 2000px。这样既保留了清晰度又显著降低了成本和加载时间。但这个操作要谨慎压缩会增加 CPU 消耗如果单次上传很多图片上传速度会降低。我通常会设置一个阈值只有超过阈值的图片才走压缩流程小图片直接原图上传。用sharp的话记得在插件文件夹里npm install sharpPicGo 的插件目录是独立的 Node 环境不是全局环境。6.2 给图片打水印很多博主喜欢给图片加水印防搬运。这个也可以在插件里实现同样用sharp在图片右下角合成一个半透明文字水印或叠加一个小 logo。水印要提前生成好 PNG 素材放在本地上传时读取并合成然后把合成后的图片流作为Body上传。注意水印会永久改变图片内容万一之后想去掉水印就麻烦了。我的建议是只在“对外发布”的图床上加在本地保留一份无水印原图需要重新生成时再走一遍上传逻辑。6.3 多图床上传与自动切换如果你的项目需要同时使用多家 CDN 或者图床可以在这个插件基础上做一个分发逻辑根据图片用途选择上传到 COS 还是 OSS或者在主图床不可用时自动切换备用图床。这个场景适用于对稳定性要求较高的正式项目但代码复杂度会成倍增加普通个人博客必要性不大。我的建议是先用一个图床跑顺畅观察一段时间的稳定性和成本再决定是不是要引入多图床方案。过早优化往往会带来不必要的维护负担图片管理这件事尤其如此。6.4 与 Obsidian 等其他 Markdown 编辑器的配合很多读者是从 Obsidian 转过来的或者 Typora 和 Obsidian 混用。Obsidian 的图片上传机制跟 Typora 不太一样但自定义命令的思路是通用的Obsidian 也支持在设置里配置图片上传命令传递当前笔记路径的方式略有不同但最终效果差不多。如果你在 Obsidian 里也想用这套分类规则大致思路是写一个适配 Obsidian 的脚本读取当前笔记文件名把它写入环境变量再调用picgo upload。核心代码可以完全复用只需要改传参那一段。我自己在 Obsidian 里验证过目录分类效果一致只是要注意 Obsidian 传递的路径可能是 URI 编码格式需要先解码再用。写在最后整套方案跑通之后我再也没有因为“找不到某篇文章的配图”而烦躁过。写文章时图片自动落到带日期和文档名的目录里翻图床就像翻自己电脑的文件夹一样有迹可循。最后再分享一个我个人的使用习惯每月月底花十分钟把当月图床目录和本地笔记本对照一遍清理掉不再引用的孤儿图片顺便核对有没有上传失败的漏网之鱼。这个小习惯把这套自动化方案的收益又放大了一圈。如果你也在被 Typora 图片管理困扰照着上面的思路和代码改一版适合自己的大概率能彻底解决这个老问题。