
Picgo 插件开发记录
1 前言
我很喜欢用 Obsidian,我的第一个笔记软件就是用的 Obsidian,在我还没开始写代码的时候,我就开始用 Obsidian 做笔记。
我现在也搭建了一个自己的博客网站,初版,也就是 v1 版的博客端的布局,特别是文章部分的布局,是参考 Obsidian 的。
然而,v1 版的后台系统,使用 Vue3 + Element Plus 搭建的,很简陋,而且超级无敌丑。
在 v1 版,上传笔记的主要方式是,先在 Obsidian 写好,然后复制粘贴到后台管理系统的 markdown 编辑器,最后把图片一一上传。很繁琐,对吧?
说实话,如果只是文字的话,复制粘贴过去就完事了,只是有些 Obsidian 特殊的语法,在 markdown 编辑器没法展示出来而已(博客端是可以的,因为我用了 markdown-it 插件)。
最麻烦的还是图片。如果我写了一篇很长的博客,有大量图片的话,一个个上传简直要我老命。
最开始,我解决该问题的想法是,在后台搭一个和 Obsidian 差不多体验的编辑器,这样写文章和上传图片两不误。这个想法后面放弃了,想想也知道,自己怎么可能搭建出媲美 Obsidian 的编辑体验的编辑器?
就算可以,那也得花超级多的时间,而且可能还会有众多 bug,对一个小博客网站来说,实在是杀鸡用牛刀。
后面,我了解到了图床这个东西。了解到 Obsidian 也可以利用插件连接图床,实现图片上传到图床,然后在笔记中使用。
可以说,这种方式完全解决了我的问题。
我的服务器使用 RustFS 作为对象存储服务,如果把其当作图床,利用插件,在 Obsidian 用到的图片就能直接上传到 RustFS,我的笔记图片就全是上传好的链接,我只需要将文档内容直接复制到后台的 md 编辑器,就移植好了,非常方便。
于是,我开始琢磨怎么在 Obsidian 集成图床。网上大部分都是使用 Picgo,我查了一下,Picgo 还支持 MinIO 。
下载完 Picgo 后,我查了一下插件市场,不出所料,没有 RustFS 的。
其实没有也无所谓,因为有 S3 的,RustFS 是完全遵循 S3 协议的,所以使用 S3 的插件也可以。
但是,我忘了一件事。我的博客系统上传文件是使用预签名上传的。更巧的是,我的博客是前后端分开服务器部署的,并且设置了反向代理,后端服务器完全是藏起来的,任何相关的端口都只开给前端服务器。预签名也是通过前端服务器转发。
不巧的是,里面所有的插件,都是直传对象存储服务的,适配我这个场景的完全没有。
自己写一个?我立刻浮起这种想法。抱着试一试的心态,我找到了 picgo-plugin-s3 插件的仓库,拉来下看了下。
好家伙,居然是用 ts 写的,代码完全看得懂。
于是为了让 Picgo 支持我的服务器,照着官网文档,开始开发一个 Picgo 插件。
参考:
- 插件开发 | PicGo-Core
- 在Node里使用picgo | PicGo-Core
- PicGo 插件开发入门 | 怠惰のコエ - imba久期 BLOG
- GitHub - wayjam/picgo-plugin-s3: PicGo S3 插件
- GitHub - PicGo/picgo-template-plugin: Official plugin template for PicGo-Core
- Obsidian中图床自动上传设置-使用PicGo - 知乎
2 开发前准备
开发之前,需要安装 picgo 官方的项目模板创建工具,不需要直接从 0 搭建。
随便找个目录打开 CMD,输入命令:
pnpm add -g picgo
由于我使用了 nvm,没有全局目录,我需要先运行
pnpm setup创建一个全局目录
安装完成后,输入 picgo -h 看看是不是安装成功了
然后,在你的项目目录上,打开 CMD,输入命令
picgo init plugin <你的插件名称>
我经常创建不了项目,老是提示下载超时,我挂魔法也要试好多次才行,真的令人血压升高。

说几个关键的点
Choose modules you want to develop:指你要开发的模块,这里有五个模块,如果你选择某一个,创建出的项目的 index.ts 就会注册这个模块。当然,不选择的话,后面也可以自己在代码添加,添加也很简单。 Your plugin is just used in CLI?:指你开发的插件要用在命令行吗,我这里选 NO,因为我主要开发的是 GUI。 Your plugin has some shortcut for GUI?:指你的插件需要为 GUI 界面提供快捷键吗?我选的 NO。 项目名称一定要是 picgo-plugin-xxx 这种格式,不然后面安装的时候,PicGo 识别不了。
创建完成后,先不着急执行 npm install,我们先看看目录结构。

相当简单的目录结构。
点开 package.json, 默认配置
{
"name": "picgo-plugin-picgo-plugin-test",
"version": "1.0.0",
"description": "测试插件",
"main": "dist/index.js",
"publishConfig": {
"access": "public"
},
"homepage": "",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "tsc -p .",
"dev": "tsc -w -p .",
"patch": "npm version patch && git push origin master && git push origin --tags",
"minor": "npm version minor && git push origin master && git push origin --tags",
"major": "npm version major && git push origin master && git push origin --tags"
},
"keywords": [
"picgo",
"picgo-gui-plugin",
"picgo-plugin"
],
"author": "Xigrut",
"license": "MIT",
"devDependencies": {
"@types/node": "16.9.1",
"@typescript-eslint/eslint-plugin": "^5.40.0",
"@typescript-eslint/parser": "^5.40.0",
"eslint-config-standard-with-typescript": "^23.0.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-standard": "^3.1.0",
"picgo": "^1.5.0-alpha.13",
"typescript": "^4.8.4",
"eslint": "^8.25.0",
"eslint-config-standard": "^17.0.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-n": "^15.3.0",
"eslint-plugin-promise": "^6.1.0"
}
}
可以看到,配置的各种依赖,版本都很老,甚至连自己的 picgo 库都不是最新版的。我们来简单升级一下。
把依赖改成这样:
"devDependencies": {
"@eslint/js": "^9.39.2",
"@types/node": "22.19.3",
"eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.4",
"picgo": "1.6.0",
"prettier": "^3.7.4",
"typescript": "^5.9.3",
"typescript-eslint": "^8.50.0"
},
"dependencies": {
"file-type": "^21.1.1",
"mime": "^4.1.0"
}
@types/node的版本可以根据自己的 node 版本调整,我的是 22 版。file-type和mime主要是用来判断文件类型的。
然后直接执行 pnpm install 安装。执行完成后,删除项目目录中的 .eslintrc 文件。创建一个 eslint.config.mjs 文件。然后把下面的内容粘进去。
// @ts-check
import eslint from '@eslint/js'
import { defineConfig } from 'eslint/config'
import tseslint from 'typescript-eslint'
import eslintPluginPrettier from 'eslint-plugin-prettier/recommended'
export default defineConfig(
{
ignores: ['**/build/**', '**/dist/**'],
},
eslint.configs.recommended,
tseslint.configs.recommended,
eslintPluginPrettier,
{
rules: {
'prettier/prettier': [
'error',
{
semi: false, // 不使用分号结尾
singleQuote: true, // 使用单引号而非双引号
printWidth: 80, // 代码行最大宽度为80个字符
tabWidth: 2, // 缩进使用2个空格
trailingComma: 'all', // 对象/数组末尾保留逗号
arrowParens: 'always',// 箭头函数参数始终使用括号
endOfLine: 'auto', // 自动选择换行符类型
}
],
},
},
)
'prettier/prettier':部分,可以根据自己的习惯更改,上面是我自己的习惯。
粘完以后,你的编译器可能会报一堆错,那是因为 eslint 配置生效了,但是没有自动帮你纠正,我们得设置一下保存后自动纠正的功能。
我使用的是 WebStorm 的免费版,先点开 eslint 的设置

点击 All actions on save

勾选 Run eslint --fix,Run Prettier 好像勾选不了,如果可以勾选的话就勾选一下。

点击 OK,然后随便打个回车,按下快捷键 ctrl + s 保存, eslint 就会自动纠正了。

其他地方的代码也是,如果有很多报错的话,可以打回车,然后保存一下就行。觉得麻烦的话,就可以像 vue 项目一样,在 package.json 添加一个 format 命令,然后直接执行 pnpm format 就行。
然后点开 tsconfig.json,需要的话,也可以简单更新一下,这个文件主要关于 ts 的编译行为的配置,我直接贴代码了,改的不多。
{
"compilerOptions": {
"target": "es2020",
"module": "commonjs",
"moduleResolution": "node",
"resolveJsonModule": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"sourceMap": true,
"declaration": true,
"outDir": "dist",
"strict": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"baseUrl": "src",
"lib": [
"es2020"
]
},
"include": [
"./src/**/*"
]
}
做到这,项目就更新好了。
3 开发
3.1 各个模块的功能
首先点开 index.ts ,可以看到如下内容:
import { PicGo } from 'picgo'
export = (ctx: PicGo) => {
const register = (): void => {
ctx.helper.uploader.register('picgo-plugin-test', {
handle(ctx) {
console.log(ctx)
},
})
ctx.helper.transformer.register('picgo-plugin-test', {
handle(ctx) {
console.log(ctx)
},
})
ctx.helper.beforeTransformPlugins.register('picgo-plugin-test', {
handle(ctx) {
console.log(ctx)
},
})
ctx.helper.beforeUploadPlugins.register('picgo-plugin-test', {
handle(ctx) {
console.log(ctx)
},
})
ctx.helper.afterUploadPlugins.register('picgo-plugin-test', {
handle(ctx) {
console.log(ctx)
},
})
}
return {
uploader: 'picgo-plugin-test',
transformer: 'picgo-plugin-test',
register,
}
}
命令行创建项目时,选择的模块会在这里被注册。
每个模块的执行流程,可以参照官网的流程图:

每个模块的作用,可以去官网上看,我自己只开发 uploader,其他不太熟悉,就不多说了。 插件开发 | PicGo-Core
我们主要编写的,就是其中的 handle 方法,可以看到,handle 方法是一个接收 PicGo 类型的 ctx 参数,返回的是 void,那我们定义方法的时候,就可以这么定义。
3.2 输入配置编写
了解完每个模块的作用后,就可以开始开发了,由于我只需要开发 uploader,所以其他模块我就直接删了。
import { PicGo } from 'picgo'
export = (ctx: PicGo) => {
const register = (): void => {
ctx.helper.uploader.register('picgo-plugin-test', {
handle(ctx) {
console.log(ctx)
},
})
}
return {
uploader: 'picgo-plugin-test',
transformer: 'picgo-plugin-test',
register,
}
}
然后,我们来捋一下开发的最终目标。我的博客的上传图片的流程是:请求预签名 -> 拿到预签名 -> 使用预签名上传文件。
非常简单吧?不需要任何库,仅仅使用原生的 fetch API 就行。
但由于我博客网站有鉴权,请求预签名需要登录 token,但是我没有开放登录功能,所以我这就不用博客的来演示了,我使用我之前的 RustFS + Springboot 的 Demo 项目来作为需要上传的目标。 rustfs_demo: RustFS Learning Demo
以上只是上传的流程,其实还有前置的流程没有说,我一边说一边给代码。
首先, PicGo 上传一般需要用户填写相应的信息,例如目标地址、Access Key、Secret Key 等等。所以我们得写一个配置信息,主要是配置用户需要填写什么信息,程序怎么获取。
在 src 目录下,创建 core 目录,在 core 目录下创建 types 目录,然后在 types 目录创建一个 config.d.ts 文件,定义一下我们需要填写的信息类型。
说明一下,这些目录没有规定一定要这么创建,这么创建纯粹只是我的编程习惯。
interface ICustomConfig {
// 后端服务地址
backendEndpoint: string
// 预签名接口地址
presignedAPIPath: string
}
export type { ICustomConfig }
然后在 core 目录下, 创建 config.ts 文件,添加一个 buildPluginConfig 方法。
import { ICustomConfig } from './types/config'
import { IPluginConfig } from 'picgo'
function buildPluginConfig(customConfig: ICustomConfig): IPluginConfig[] {
return [
{
name: 'backendEndpoint',
type: 'input',
default: customConfig.backendEndpoint,
required: true,
message: '请输入后端服务地址',
alias: '后端服务地址',
},
{
name: 'presignedAPIPath',
type: 'input',
default: customConfig.presignedAPIPath,
required: true,
message: '请输入预签名获取接口地址',
alias: '预签名获取接口地址',
},
]
}
type 有 input、password、confirm 三种类型,对应不同的输入组件。
然后,我们再添加和导出两个方法,也就是 get 和 set 方法。get 就是获取当前的配置信息,set 就是设置用户输入的值,可以在这里添加默认值。
忘记了一件事,由于有很多地方需要用到插件名称,如果每次都要手打,后面要改的话,一个个改很麻烦,所以我们在 core 目录下创建一个 constants.ts 文件,导出一个 PLUGIN_NAME 常量,在需要用到 Plugin其他地方直接调用就行。
export const PLUGIN_NAME = 'picgo-plugin-test'
添加 set 和 get 方法,我直接给完整的 config.ts 代码。
import { ICustomConfig } from './types/config'
import { IPicGo, IPluginConfig } from 'picgo'
import { PLUGIN_NAME } from './constants'
function buildPluginConfig(customConfig: ICustomConfig): IPluginConfig[] {
return [
{
name: 'backendEndpoint',
type: 'input',
default: customConfig.backendEndpoint,
required: true,
message: '请输入后端服务地址',
alias: '后端服务地址',
},
{
name: 'presignedAPIPath',
type: 'input',
default: customConfig.presignedAPIPath,
required: true,
message: '请输入预签名获取接口地址',
alias: '预签名获取接口地址',
},
]
}
export function setConfig(ctx: IPicGo): IPluginConfig[] {
const defaultConfig: ICustomConfig = {
backendEndpoint: 'http://127.0.0.1:8080',
presignedAPIPath: '/api/presigned',
}
let userConfig = ctx.getConfig<ICustomConfig>(`picBed.${PLUGIN_NAME}`)
userConfig = { ...defaultConfig, ...(userConfig || {}) }
return buildPluginConfig(userConfig)
}
export function getConfig(ctx: IPicGo): ICustomConfig {
const userConfig = ctx.getConfig<ICustomConfig>(`picBed.${PLUGIN_NAME}`)
if (!userConfig) {
ctx.log.error('[uploader]无法找到用户配置')
throw new Error('[uploader]无法找到用户配置')
}
return userConfig
}
ctx.log是 PicGo 的日志方法,使用该方法输出的日志,能够在 PicGo 的日志文件里看到,方便调试。 当然,日志的内容格式没有规定,这么写是我自己的习惯。
3.3 文件前置处理
官网介绍对 PicGo 能获取的文件信息有如下介绍:
[
{
buffer: 'xxx', // 图片的buffer值,buffer和base64值二选一即可
base64Image: 'xxxx', // 图片的base64值,buffer和base64值二选一即可
fileName: 'xxxx', // 图片的文件名
width: 'xxxx', // 图片宽度
height: 'xxxx', // 图片高度
extname: '.xxx' // 图片格式的扩展名 比如.jpg | .png
},
{
...
},
...
]
其中buffer和base64值二选一即可,如果传入的是path数组,PicGo 默认输出是buffer值 。
但是预签名上传的话,是用 put 方法,然后在 body 设置 File 类型的文件来上传的,所以我们得写一个转化方法,将 buffer 或者 base64,转化为 File。
除此之外,File 文件需要知道图片的类型,而且预签名也对文件类型敏感,也需要传递文件的 Mime 类型,所以我们还得获取文件的 Mime 类型,以此设置 contentType。目标就是这两个。
在 core/types 目录下,创建 convert.d.ts 文件,添加如下类型
interface IImageFile {
imageFile: File
contentType: string
}
export type { IImageFile }
在 core 目录下,创建 convert.ts 文件
import { IImgInfo } from 'picgo'
import { IImageFile } from './types/convert'
import mime from 'mime'
import { fileTypeFromBuffer } from 'file-type'
export async function convertImageFile(
imageItem: IImgInfo,
): Promise<IImageFile> {
if (!imageItem) {
throw new Error('[uploader]未获取到文件项')
}
let contentType = ''
let imageBuffer: Buffer
// 统一使用 buffer 来创建 File,base64 也转为 buffer if (imageItem.buffer) {
imageBuffer = imageItem.buffer
} else if (imageItem.base64Image) {
imageBuffer = Buffer.from(imageItem.base64Image, 'base64')
} else {
throw new Error('[uploader]未获取到文件的 buffer 或 base64')
}
// 从 base64Image 中获取 contentType if (imageItem.base64Image) {
const match = imageItem.base64Image.match(/[^:]\w+\/[\w-+\d.]+(?=;|,)/)
if (match?.[0]) {
contentType = match[0]
}
}
// 从 extname 扩展名中获取 contentType if (!contentType && imageItem.extname) {
contentType = mime.getType(imageItem.extname) || ''
}
// 如果都获取不到,使用 file-type 库提取 mime 类型
if (!contentType) {
const fileType = await fileTypeFromBuffer(imageBuffer)
contentType = fileType?.mime || ''
}
// 如果都找不到,抛出错误
if (!contentType) {
throw new Error('[uploader]无法确定文件类型')
}
// 获取文件扩展名(为了设置文件名)
let fileExtension = ''
if (imageItem.extname) {
// 从 extname 获取扩展名
fileExtension = imageItem.extname.startsWith('.')
? imageItem.extname.slice(1)
: imageItem.extname
} else if (contentType) {
// 使用 mime 库获取扩展名
fileExtension = mime.getExtension(contentType) || ''
}
const fileName = fileExtension ? `file.${fileExtension}` : 'file'
// 返回创建结果
return {
imageFile: new File([imageBuffer], fileName, {
type: contentType,
}),
contentType,
}
}
3.4 API 与 工具
为了方便 API 的调用,我们需要手动定义一下我们需要使用的API。
在 core 包下,创建 apis.ts 文件。
import { ICustomConfig } from './types/config'
export const Apis = (userConfig: ICustomConfig) => {
const backendBaseUrl: string = userConfig.backendEndpoint
async function fetchPresignedUrl(mimeType: string, ext: string) {
return fetch(backendBaseUrl + userConfig.presignedAPIPath, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
mimeType: mimeType,
ext: ext,
}),
})
}
async function uploadFileByPresignedUrl(
presignedUrl: string,
imageFile: File,
) {
return fetch(presignedUrl, {
method: 'PUT',
body: imageFile,
headers: {
'Content-Type': imageFile.type,
},
})
}
return {
fetchPresignedUrl,
uploadFileByPresignedUrl,
}
}
另外,预签名是直传 OSS 服务,但是 OSS 服务是不返回 URL 的,我们可以从预签名中直接获取,所以得写个工具方法,从预签名中获取文件访问的 URL。
在 core 目录下,创建 utils.ts 文件,添加如下方法:
export function extractObjectUrlFromPresignedUrl(presignedUrl: string) {
const urlObject = new URL(presignedUrl)
urlObject.search = ''
return urlObject.href
}
到这,前置步骤就完了,接下来就是核心上传方法。
3.5 上传文件
从官网的介绍:
[
{
buffer: 'xxx', // 图片的buffer值,buffer和base64值二选一即可
base64Image: 'xxxx', // 图片的base64值,buffer和base64值二选一即可
fileName: 'xxxx', // 图片的文件名
width: 'xxxx', // 图片宽度
height: 'xxxx', // 图片高度
extname: '.xxx' // 图片格式的扩展名 比如.jpg | .png
},
{
...
},
...
]
可以看到,输入的是一个列表,也就是说,可能会传入多个文件,所以我们得写一个任务方法,将每个上传任务包装起来。
从上面的前置操作可以看到,我们的完整流程大概是:获取配置 -> 初始化 API -> 将文件转化为 File,提取 mime 类型和扩展名 -> 获取预签名 -> 解析访问路径 URL -> 使用预签名上传文件。
我们的每个任务的执行都是这样的,所以只需要将其包装成异步方法,返回 Promise 就行。这样, uploader 就只需要根据文件列表,创建任务,然后 Promise.all() 等待所有任务完成,再解析结果就行。
现在思考一下,任务方法怎么创建?
传入参数需要什么?当然是 PicGo 的 IImgInfo 了。
返回结果需要什么?当然是上传的 url 了。
所以在 core/types 下,创建 task.d.ts 文件。
import { IImgInfo } from 'picgo'
interface ITaskOption {
index: number
imageItem: IImgInfo
}
interface ITaskResult {
index: number
url?: string
error?: Error
}
export type { ITaskOption, ITaskResult }
添加索引的目的是方便 task 列表和 input 的文件列表做对应。
在 core 目录下,创建 task.ts 文件
import { ITaskOption, ITaskResult } from './types/task'
import { ICustomConfig } from './types/config'
import { IPicGo } from 'picgo'
import { Apis } from './apis'
import { IImageFile } from './types/convert'
import { convertImageFile } from './convert'
import { extractObjectUrlFromPresignedUrl } from './utils'
export async function createTask(
option: ITaskOption,
userConfig: ICustomConfig,
ctx: IPicGo,
): Promise<ITaskResult> {
// 初始化结果
const result: ITaskResult = {
index: option.index,
}
// 初始化 api 接口
const apis = Apis(userConfig)
// 初始化图片文件
let imageFile: IImageFile = {} as IImageFile
try {
// 转化为 File imageFile = await convertImageFile(option.imageItem)
ctx.log.debug('[uploader]转换图片文件成功')
} catch (err) {
result.error = new Error(
`[uploader]转换图片文件失败,原因: ${err instanceof Error ? err.message : String(err)}`,
)
return result
}
// 获取预签名
await apis
.fetchPresignedUrl(imageFile.contentType)
.then((value) => {
if (!value.ok) {
result.error = buildErrorLog(
`[uploader]获取预签名地址失败, 原因:${value.status} --- ${value.statusText}`,
ctx,
)
}
return value.json() as Promise<string>
})
.then(async (value: string) => {
const presignedUrl = value
// 提取预签名
const realUrl = extractObjectUrlFromPresignedUrl(presignedUrl)
// 使用预签名上传文件
await apis
.uploadFileByPresignedUrl(presignedUrl, imageFile.imageFile)
.then((value) => {
if (!value.ok) {
result.error = buildErrorLog(
`[uploader]上传文件失败, 状态码:${value.status} --- ${value.statusText}`,
ctx,
)
}
// 返回文件访问地址
result.url = realUrl
})
.catch((err) => {
result.error = buildErrorLog(
`[uploader]上传文件失败, 原因:${err instanceof Error ? err.message : String(err)}`,
ctx,
)
})
})
.catch((err) => {
result.error = buildErrorLog(
`[uploader]获取预签名地址失败, 原因:${err instanceof Error ? err.message : String(err)}`,
ctx,
)
})
// 返回结果
return result
}
// 构建错误日志
function buildErrorLog(msg: string, ctx: IPicGo): Error {
ctx.log.error(msg)
return new Error(msg)
}
接下来,就可以实现上传器了。在 src 目录,创建 uploader.ts 文件
import { IImgInfo, IPicGo } from 'picgo'
import { getConfig } from './core/config'
import { createTask } from './core/task'
import { ITaskResult } from './core/types/task'
export async function uploaderHandler(ctx: IPicGo): Promise<IPicGo> {
// 获取配置
const userConfig = getConfig(ctx)
const output = ctx.output
ctx.log.info('[uploader]开始上传')
// 创建下载任务
const tasks = output.map((item: IImgInfo, index: number) => {
ctx.log.debug('[uploader]开始处理图片: ' + item.fileName)
return createTask(
{
index,
imageItem: item,
},
userConfig,
ctx,
)
})
// 上传任务
let results: ITaskResult[] = []
try {
results = await Promise.all(tasks)
} catch (err) {
throw buildErrorLog(
`[uploader]上传失败,原因: ${err instanceof Error ? err.message : String(err)}`,
ctx,
)
}
// 处理结果
for (const result of results) {
// 释放文件内存
delete output[result.index].buffer
delete output[result.index].base64Image
// 处理错误
if (result.error) {
output[result.index].error = result.error
} else {
// 处理成功的 url ctx.log.debug(`[uploader]文件上传成功,url: ${result.url}`)
output[result.index].url = result.url
output[result.index].imgUrl = result.url
}
}
// 更新输出
ctx.output = output
return ctx
}
function buildErrorLog(msg: string, ctx: IPicGo): Error {
ctx.log.error(msg)
return new Error(msg)
}
更新一下 index.ts 的信息
import { IPicGo, IPluginConfig, PicGo } from 'picgo'
import { PLUGIN_NAME } from './core/constants'
import { uploaderHandler } from './uploader'
import { setConfig } from './core/config'
function getConfig(ctx: IPicGo): IPluginConfig[] {
return setConfig(ctx)
}
export = (ctx: PicGo) => {
const register = (): void => {
ctx.helper.uploader.register(PLUGIN_NAME, {
handle: uploaderHandler,
config: getConfig,
})
}
return {
uploader: PLUGIN_NAME,
transformer: PLUGIN_NAME,
register,
}
}
大功告成了。
4 安装与测试
安装插件的流程,可以参考官网: 插件测试与发布 | PicGo-Core
首先, 在目录下,使用 pnpm build 命令,打包文件。
如果你像我一样,使用 PicGo 的 GUI 版本(Windows),可以直接使用 GUI 的导入功能。

在弹出的文件选择器中,选择 picgo-plugin-test 这个项目文件,点击确定,就能导入了。

注意
如果导入失败的话,就去 PicGo 设置、设置日志文件、点击【设置文件】按钮、弹出窗口、点击【点击打开】按钮,就能打开 PicGo 的日志文件,可以在那里看是什么原因出错了。
现在测试一下插件能不能上传文件。
在图床设置里,找到刚刚导入的插件,新建一个 test 配置。

然后在上传区,上传一个图片。
我这边上传失败了,打开日志,看到下面的信息
2025-12-31 14:10:42 [PicGo INFO] Uploading... Current uploader is [picgo-plugin-test]
2025-12-31 14:10:42 [PicGo INFO] [uploader]开始上传
2025-12-31 14:10:43 [PicGo ERROR] [uploader]获取预签名地址失败, 原因:405 ---
2025-12-31 14:10:43 [PicGo ERROR] [uploader]获取预签名地址失败, 原因:Invalid URL
2025-12-31 14:10:43 [PicGo INFO] afterUploadPlugins: picgo-plugin-xgrblog running
2025-12-31 14:10:43 [PicGo INFO] xgrblog: after upload...
2025-12-31 14:10:43 [PicGo SUCCESS]
我看了一眼,是我后端接口参数有问题,多了一个参数,另外,task.ts 也有问题
// 获取预签名
await apis
.fetchPresignedUrl(imageFile.contentType)
.then((value) => {
if (!value.ok) {
result.error = buildErrorLog(
`[uploader]获取预签名地址失败, 原因:${value.status} --- ${value.statusText}`,
ctx,
)
}
// 我的接口返回的直接就是 url,不需要转化为 Json。
return value.text() as Promise<string>
})
提醒
如果需要联调测试的话,就在项目的命令行,运行
pnpm dev,运行后会自动监听你项目的变化,然后自动打包。 你在项目里改好代码后,保存一下,然后重启一下 PicGo 就行了,不需要重新安装插件。
修改完以后,就上传成功了。

访问一下

完成了。
测试代码 Demo 的仓库:picgo-plugin-test: PicGo 插件开发 Demo
5 总结
- 学习了 PicGo 插件的开发流程。
- 学会了如何开发 PicGo 插件的 Uploader 模块。
- 学会了如何在 PicGo GUI 客户端安装插件。
- 学会了如何联调测试插件。
0 人赞过
评论
评论加载中…