Blog 图床方案:Backblaze B2 (私密桶) + Cloudflare Workers + PicGo

常用公共图床盘点

图床 说明
sm.ms 要注册,海外服务器【免费】
meotu 要注册,国内服务器【免费】
z4a 图床 要注册,国内服务器【免费】
imgtp 无需注册,国内服务器,最大支持 15MB【免费】
imgbed 无需注册,国内服务器,最大支持 20MB【免费】
imgbb 无需注册,亚太服务器【免费】
聚合图床 要注册,国内服务器【免费】
牛图网 无需注册,日本服务器 【免费】
upload 无需注册,海外服务器 【免费】
catbox 无需注册,海外服务器【免费】
路过图床 要注册,海外服务器【免费】
TG 图床 要注册,国内外服务器【免费】
Postimages 无需注册,国外服务器速度快【免费】
imgloc 无需注册,国内服务器,最大支持 6MB【免费】
映画の妖火图床 无需注册,聚合式 CDN 图床【免费】
遇见图床 无需注册,聚合式 CDN 图床【免费】
hello 图床 要注册,国内 CDN【免费】
IM.GE 无需注册香港 CDN【免费】
云图床 全球 CDN【免费】

Backblaze B2 (私密桶) + Cloudflare Workers + PicGo插件

网上介绍这种方案的比较多。

这种方案有以下好处:

Backblaze 免费账户提供 10 GB 的存储空间以及每天 1G 的下载流量。
Backblaze 和 Cloudflare 同属带宽联盟 (Bandwidth Alliance),两者之间的流量免费。也就是说,通过 Cloudflare 访问 Backblaze 存储的图片,不计流量费。
Cloudflare 全球有310个数据中心,有CDN加持,从任何地方访问图片都不会太慢。
Backblaze B2 支持多种上传方式。除了 Web 界面外,还有 CLI 工具 和 Picgo 等第三方客户端。

这种方案的思路:
Backblaze B2 公开桶 (Public Bucket) 存储图片。
Cloudflare DNS 设置 CNAME 指向存储服务器域名 (如: f004.backblazeb2.com),实现自定义域名访问。
Cloudflare 设置 Page Rules ,实现 URL Rewrite 隐藏真实路径。还可设置缓存时间、去除 x-bz 信息等。

现在 Backblaze 创建桶的策略发生了变化:必须支付一次性的费用(1 美刀),或者账户有过付费记录才可以创建公开桶(应该是防止滥用),否则只能创建私密桶 (Private Bucket)。

储存在公开桶的图片,直接访问 URL 就能看到。但私密桶的图片不行,访问 URL 是这个结果:

<Error>
<Code>UnauthorizedAccess</Code>
<Message>bucket is not authorized: Backblaze-cloudflare-img</Message>
</Error>
<Error>

所以,如果不想付费的话,上述方案无效了。
直到我看到了这篇 官方文档 才知道,原来 Backblaze 早就提出新方案了。

新方案

新方案的思路是:

Backblaze B2 私密桶存储图片。
Cloudflare Worker 通过应用程序密钥 (Application Key) 实现认证访问 Backblaze B2 私密桶,并缓存图片。

具体流程:

用户访问页面,请求图片地址,触发 Cloudflare Worker ,
Worker 用应用程序密钥签名,访问 Backblaze B2 私密桶,
Backblaze B2 验证签名,返回图片给 Worker ,
Worker 显示图片到页面,用户看到图片。

前提条件:

注册好 Cloudflare 账户
注册好 Backblaze 账户
在主页中 ——》点击 Products ——》 B2 Cloud Storage(Always-hot S3 compatible object storage),在接下来出来的页面中 填入你的 邮箱地址 ——》点击Get Stared Free 进入接下来的注册工作。
Backblaze注册

有域名托管在 Cloudflare
安装好 PicGo (支持 Win/Mac/Linux 平台)

我们开始吧!

创建 Backblaze B2 私密桶

登录 Backblaze ,默认来到桶 (Buckets) 页面。
提示:Backblaze 的中文界面机翻味道实在太浓,请先右下角切换为 English 。
点击 Create a Bucket ,在 Bucket Unique Name 项填入桶名称(我填的 it-is-just-a-test-bucket),其余项保持默认即可。然后点击 Create a Bucket 按钮。
Create a Bucket
提示:虽然是私密桶,但桶名应尽可能复杂,避免被他人猜测到,产生不必要的麻烦。
私密桶生成了。记下 Endpoint 的值,后面要用到两次。

点击 Lifecycle Settings ,选择 Keep only the last version of the file 。点击 Update Bucket 按钮。

创建应用程序密钥 (Application Key)

点击页面左侧菜单 B2 Cloud Storage 项下的 Application Keys ,然后点击 Add a New Application Key ,在 Name of Key 项填写应用程序密钥名称(我填的 my-key-for-uploading),其余保持默认即可。点击 Create New Key 按钮。

创建应用程序密钥后,复制信息保存好(关掉就不再显示了!),后面要用到两次。

创建 Cloudflare Worker

登录 Cloudflare ,点击页面左侧菜单中 Workers 和 Pages ——》在右侧 概述 页面中点击 创建 ——》点击 创建Worker ——》 在(创建“”Hello World” Worker”Worker)下的框中 填上 你应用程序的 名称 ——》点击右下角的 部署,一个简单的 Worker 就部署好了。
点击 编辑代码 按钮,在页面中把左侧编辑区中的代码替换为以下代码:

// node_modules/aws4fetch/dist/aws4fetch.esm.mjs
var encoder = new TextEncoder();
var HOST_SERVICES = {
appstream2: "appstream",
cloudhsmv2: "cloudhsm",
email: "ses",
marketplace: "aws-marketplace",
mobile: "AWSMobileHubService",
pinpoint: "mobiletargeting",
queue: "sqs",
"git-codecommit": "codecommit",
"mturk-requester-sandbox": "mturk-requester",
"personalize-runtime": "personalize"
};
var UNSIGNABLE_HEADERS = /* @__PURE__ */ new Set([
"authorization",
"content-type",
"content-length",
"user-agent",
"presigned-expires",
"expect",
"x-amzn-trace-id",
"range",
"connection"
]);
var AwsClient = class {
constructor({ accesskeyID, secretAccessKey, sessionToken, service, region, cache, retries, initRetryMs }) {
if (accesskeyID == null)
throw new TypeError("accesskeyID is a required option");
if (secretAccessKey == null)
throw new TypeError("secretAccessKey is a required option");
this.accesskeyID = accesskeyID;
this.secretAccessKey = secretAccessKey;
this.sessionToken = sessionToken;
this.service = service;
this.region = region;
this.cache = cache || /* @__PURE__ */ new Map();
this.retries = retries != null ? retries : 10;
this.initRetryMs = initRetryMs || 50;
}
async sign(input, init) {
if (input instanceof Request) {
const { method, url, headers, body } = input;
init = Object.assign({ method, url, headers }, init);
if (init.body == null && headers.has("Content-Type")) {
init.body = body != null && headers.has("X-Amz-Content-Sha256") ? body : await input.clone().arrayBuffer();
}
input = url;
}
const signer = new AwsV4Signer(Object.assign({ url: input }, init, this, init && init.aws));
const signed = Object.assign({}, init, await signer.sign());
delete signed.aws;
try {
return new Request(signed.url.toString(), signed);
} catch (e) {
if (e instanceof TypeError) {
return new Request(signed.url.toString(), Object.assign({ duplex: "half" }, signed));
}
throw e;
}
}
async fetch(input, init) {
for (let i = 0; i <= this.retries; i++) {
const fetched = fetch(await this.sign(input, init));
if (i === this.retries) {
return fetched;
}
const res = await fetched;
if (res.status < 500 && res.status !== 429) {
return res;
}
await new Promise((resolve) => setTimeout(resolve, Math.random() * this.initRetryMs * Math.pow(2, i)));
}
throw new Error("An unknown error occurred, ensure retries is not negative");
}
};
var AwsV4Signer = class {
constructor({ method, url, headers, body, accesskeyID, secretAccessKey, sessionToken, service, region, cache, datetime, signQuery, appendSessionToken, allHeaders, singleEncode }) {
if (url == null)
throw new TypeError("url is a required option");
if (accesskeyID == null)
throw new TypeError("accesskeyID is a required option");
if (secretAccessKey == null)
throw new TypeError("secretAccessKey is a required option");
this.method = method || (body ? "POST" : "GET");
this.url = new URL(url);
this.headers = new Headers(headers || {});
this.body = body;
this.accesskeyID = accesskeyID;
this.secretAccessKey = secretAccessKey;
this.sessionToken = sessionToken;
let guessedService, guessedRegion;
if (!service || !region) {
[guessedService, guessedRegion] = guessServiceRegion(this.url, this.headers);
}
this.service = service || guessedService || "";
this.region = region || guessedRegion || "us-east-1";
this.cache = cache || /* @__PURE__ */ new Map();
this.datetime = datetime || (/* @__PURE__ */ new Date()).toISOString().replace(/[:-]|\.\d{3}/g, "");
this.signQuery = signQuery;
this.appendSessionToken = appendSessionToken || this.service === "iotdevicegateway";
this.headers.delete("Host");
if (this.service === "s3" && !this.signQuery && !this.headers.has("X-Amz-Content-Sha256")) {
this.headers.set("X-Amz-Content-Sha256", "UNSIGNED-PAYLOAD");
}
const params = this.signQuery ? this.url.searchParams : this.headers;
params.set("X-Amz-Date", this.datetime);
if (this.sessionToken && !this.appendSessionToken) {
params.set("X-Amz-Security-Token", this.sessionToken);
}
this.signableHeaders = ["host", ...this.headers.keys()].filter((header) => allHeaders || !UNSIGNABLE_HEADERS.has(header)).sort();
this.signedHeaders = this.signableHeaders.join(";");
this.canonicalHeaders = this.signableHeaders.map((header) => header + ":" + (header === "host" ? this.url.host : (this.headers.get(header) || "").replace(/\s+/g, " "))).join("\n");
this.credentialString = [this.datetime.slice(0, 8), this.region, this.service, "aws4_request"].join("/");
if (this.signQuery) {
if (this.service === "s3" && !params.has("X-Amz-Expires")) {
params.set("X-Amz-Expires", "86400");
}
params.set("X-Amz-Algorithm", "AWS4-HMAC-SHA256");
params.set("X-Amz-Credential", this.accesskeyID + "/" + this.credentialString);
params.set("X-Amz-SignedHeaders", this.signedHeaders);
}
if (this.service === "s3") {
try {
this.encodedPath = decodeURIComponent(this.url.pathname.replace(/\+/g, " "));
} catch (e) {
this.encodedPath = this.url.pathname;
}
} else {
this.encodedPath = this.url.pathname.replace(/\/+/g, "/");
}
if (!singleEncode) {
this.encodedPath = encodeURIComponent(this.encodedPath).replace(/%2F/g, "/");
}
this.encodedPath = encodeRfc3986(this.encodedPath);
const seenKeys = /* @__PURE__ */ new Set();
this.encodedSearch = [...this.url.searchParams].filter(([k]) => {
if (!k)
return false;
if (this.service === "s3") {
if (seenKeys.has(k))
return false;
seenKeys.add(k);
}
return true;
}).map((pair) => pair.map((p) => encodeRfc3986(encodeURIComponent(p)))).sort(([k1, v1], [k2, v2]) => k1 < k2 ? -1 : k1 > k2 ? 1 : v1 < v2 ? -1 : v1 > v2 ? 1 : 0).map((pair) => pair.join("=")).join("&");
}
async sign() {
if (this.signQuery) {
this.url.searchParams.set("X-Amz-Signature", await this.signature());
if (this.sessionToken && this.appendSessionToken) {
this.url.searchParams.set("X-Amz-Security-Token", this.sessionToken);
}
} else {
this.headers.set("Authorization", await this.authHeader());
}
return {
method: this.method,
url: this.url,
headers: this.headers,
body: this.body
};
}
async authHeader() {
return [
"AWS4-HMAC-SHA256 Credential=" + this.accesskeyID + "/" + this.credentialString,
"SignedHeaders=" + this.signedHeaders,
"Signature=" + await this.signature()
].join(", ");
}
async signature() {
const date = this.datetime.slice(0, 8);
const cacheKey = [this.secretAccessKey, date, this.region, this.service].join();
let kCredentials = this.cache.get(cacheKey);
if (!kCredentials) {
const kDate = await hmac("AWS4" + this.secretAccessKey, date);
const kRegion = await hmac(kDate, this.region);
const kService = await hmac(kRegion, this.service);
kCredentials = await hmac(kService, "aws4_request");
this.cache.set(cacheKey, kCredentials);
}
return buf2hex(await hmac(kCredentials, await this.stringToSign()));
}
async stringToSign() {
return [
"AWS4-HMAC-SHA256",
this.datetime,
this.credentialString,
buf2hex(await hash(await this.canonicalString()))
].join("\n");
}
async canonicalString() {
return [
this.method.toUpperCase(),
this.encodedPath,
this.encodedSearch,
this.canonicalHeaders + "\n",
this.signedHeaders,
await this.hexBodyHash()
].join("\n");
}
async hexBodyHash() {
let hashHeader = this.headers.get("X-Amz-Content-Sha256") || (this.service === "s3" && this.signQuery ? "UNSIGNED-PAYLOAD" : null);
if (hashHeader == null) {
if (this.body && typeof this.body !== "string" && !("byteLength" in this.body)) {
throw new Error("body must be a string, ArrayBuffer or ArrayBufferView, unless you include the X-Amz-Content-Sha256 header");
}
hashHeader = buf2hex(await hash(this.body || ""));
}
return hashHeader;
}
};
async function hmac(key, string) {
const cryptoKey = await crypto.subtle.importKey(
"raw",
typeof key === "string" ? encoder.encode(key) : key,
{ name: "HMAC", hash: { name: "SHA-256" } },
false,
["sign"]
);
return crypto.subtle.sign("HMAC", cryptoKey, encoder.encode(string));
}
async function hash(content) {
return crypto.subtle.digest("SHA-256", typeof content === "string" ? encoder.encode(content) : content);
}
function buf2hex(buffer) {
return Array.prototype.map.call(new Uint8Array(buffer), (x) => ("0" + x.toString(16)).slice(-2)).join("");
}
function encodeRfc3986(urlEncodedStr) {
return urlEncodedStr.replace(/[!'()*]/g, (c) => "%" + c.charCodeAt(0).toString(16).toUpperCase());
}
function guessServiceRegion(url, headers) {
const { hostname, pathname } = url;
if (hostname.endsWith(".r2.cloudflarestorage.com")) {
return ["s3", "auto"];
}
if (hostname.endsWith(".backblazeb2.com")) {
const match2 = hostname.match(/^(?:[^.]+\.)?s3\.([^.]+)\.backblazeb2\.com$/);
return match2 != null ? ["s3", match2[1]] : ["", ""];
}
const match = hostname.replace("dualstack.", "").match(/([^.]+)\.(?:([^.]*)\.)?amazonaws\.com(?:\.cn)?$/);
let [service, region] = (match || ["", ""]).slice(1, 3);
if (region === "us-gov") {
region = "us-gov-west-1";
} else if (region === "s3" || region === "s3-accelerate") {
region = "us-east-1";
service = "s3";
} else if (service === "iot") {
if (hostname.startsWith("iot.")) {
service = "execute-api";
} else if (hostname.startsWith("data.jobs.iot.")) {
service = "iot-jobs-data";
} else {
service = pathname === "/mqtt" ? "iotdevicegateway" : "iotdata";
}
} else if (service === "autoscaling") {
const targetPrefix = (headers.get("X-Amz-Target") || "").split(".")[0];
if (targetPrefix === "AnyScaleFrontendService") {
service = "application-autoscaling";
} else if (targetPrefix === "AnyScaleScalingPlannerFrontendService") {
service = "autoscaling-plans";
}
} else if (region == null && service.startsWith("s3-")) {
region = service.slice(3).replace(/^fips-|^external-1/, "");
service = "s3";
} else if (service.endsWith("-fips")) {
service = service.slice(0, -5);
} else if (region && /-\d$/.test(service) && !/-\d$/.test(region)) {
[service, region] = [region, service];
}
return [HOST_SERVICES[service] || service, region];
}

// index.js
var UNSIGNABLE_HEADERS2 = [
// These headers appear in the request, but are not passed upstream
"x-forwarded-proto",
"x-real-ip",
// We can't include accept-encoding in the signature because Cloudflare
// sets the incoming accept-encoding header to "gzip, br", then modifies
// the outgoing request to set accept-encoding to "gzip".
// Not cool, Cloudflare!
"accept-encoding"
];
var HTTPS_PROTOCOL = "https:";
var HTTPS_PORT = "443";
var RANGE_RETRY_ATTEMPTS = 3;
function filterHeaders(headers, env) {
return new Headers(Array.from(headers.entries()).filter(
(pair) => !UNSIGNABLE_HEADERS2.includes(pair[0]) && !pair[0].startsWith("cf-") && !("ALLOWED_HEADERS" in env && !env.ALLOWED_HEADERS.includes(pair[0]))
));
}
var my_proxy_default = {
async fetch(request, env) {
if (!["GET", "HEAD"].includes(request.method)) {
return new Response(null, {
status: 405,
statusText: "Method Not Allowed"
});
}
const url = new URL(request.url);
url.protocol = HTTPS_PROTOCOL;
url.port = HTTPS_PORT;
let path = url.pathname.replace(/^\//, "");
path = path.replace(/\/$/, "");
const pathSegments = path.split("/");
if (env.ALLOW_LIST_BUCKET !== "true") {
if (env.BUCKET_NAME === "$path" && pathSegments.length < 2 || env.BUCKET_NAME !== "$path" && path.length === 0) {
return new Response(null, {
status: 404,
statusText: "Not Found"
});
}
}
switch (env.BUCKET_NAME) {
case "$path":
url.hostname = env.B2_ENDPOINT;
break;
case "$host":
url.hostname = url.hostname.split(".")[0] + "." + env.B2_ENDPOINT;
break;
default:
url.hostname = env.BUCKET_NAME + "." + env.B2_ENDPOINT;
break;
}
const headers = filterHeaders(request.headers, env);
const endpointRegex = /^s3\.([a-zA-Z0-9-]+)\.backblazeb2\.com$/;
const [, aws_region] = env.B2_ENDPOINT.match(endpointRegex);
const client = new AwsClient({
"accesskeyID": env.B2_APPLICATION_KEY_ID,
"secretAccessKey": env.B2_APPLICATION_KEY,
"service": "s3",
"region": aws_region
});
const signedRequest = await client.sign(url.toString(), {
method: request.method,
headers
});
if (signedRequest.headers.has("range")) {
let attempts = RANGE_RETRY_ATTEMPTS;
let response;
do {
let controller = new AbortController();
response = await fetch(signedRequest.url, {
method: signedRequest.method,
headers: signedRequest.headers,
signal: controller.signal
});
if (response.headers.has("content-range")) {
if (attempts < RANGE_RETRY_ATTEMPTS) {
console.log(`Retry for ${signedRequest.url} succeeded - response has content-range header`);
}
break;
} else if (response.ok) {
attempts -= 1;
console.error(`Range header in request for ${signedRequest.url} but no content-range header in response. Will retry ${attempts} more times`);
if (attempts > 0) {
controller.abort();
}
} else {
break;
}
} while (attempts > 0);
if (attempts <= 0) {
console.error(`Tried range request for ${signedRequest.url} ${RANGE_RETRY_ATTEMPTS} times, but no content-range in response.`);
}
return response;
}
return fetch(signedRequest);
}
};
export {
my_proxy_default as default
};
/*! Bundled license information:

aws4fetch/dist/aws4fetch.esm.mjs:
(**
* @license MIT <https://opensource.org/licenses/MIT>
* @copyright Michael Hart 2022
*)
*/
//# sourceMappingURL=index.js.map

然后点击 保存并部署 按钮。

配置 Cloudflare Worker 环境变量

返回新创建的 Worker 页面,点击上方 设置 选项卡,再点击左侧 变量 。
点击 添加变量 按钮,依次添加 5 个变量,变量名称 和 值 分别为:

ALLOW_LIST_BUCKET = false
B2_APPLICATION_KEY = 第二步保存的 applicationKey
B2_APPLICATION_KEY_ID = 第二步保存的 keyID
B2_ENDPOINT = 第一步记下的 Endpoint 值(如 s3.us-west-004.backblazeb2.com)
BUCKET_NAME = 私密桶名(如 it-is-just-a-test-bucket)

添加完后,如下图所示:
添加环境变量
为安全起见,输入完 B2_APPLICATION_KEY 的值后,点击右侧的 加密 按钮,会显示 此值在保存后无法再进行查看 。

添加自定义域名

在设置中 点击 域和路由 ——》点击 + 添加 ,在右侧出现的 域和路由 页面中 ——》选中 自定义域 填上你托管在 Cloudflare 中的域名的 字域名,(我填的 test.standat42.com),然后点击 下面的 添加域 按钮。

配置 Backblaze B2 私密桶

回到 Backblaze ,定位到新建的私密桶,点击 Bucket Settings :

桶设置)
在 Bucket Info 项填入:

{"Cache-Control": "public, max-age=5184000"} 

提示:86400 表示缓存一天,可以设置更大。
桶设置
最后点击 Update Bucket 按钮。

访问测试

上传图片到私密桶:

定位到新建的私密桶,点击 Upload/Download 按钮,然后继续点击 Upload 按钮,上传一个图片(如 test-image.jpg)。

查看图片 URL:

点击图片文件名,查看图片 URL:
查看图片

访问测试:

直接访问 S3 URL (https://it-is-just-a-test-bucket.s3.us-west-004.backblazeb2.com/test-image.jpg) 是看不到图片的。

访问自定义域名的 URl (https://test.standat42.com/test-image.jpg) 则可以看到图片。
测试图片

说明 Cloudflare Worker 生效了。

打开开发者工具,刷新页面,在 网络 选项卡中点击 test-image.jpg 查看,Cf-Cache-Status 的值为 HIT ,说明 Cloudflare 已经缓存成功。Cf-Cache-Status: HIT
Cf-Cache-Status: HIT

配置 PicGo

安装 S3 插件 点击左侧 插件设置 ,右边搜索 S3 ,安装即可。安装 S3 插件
安装 S3

配置 Amazon S3 图床 在左侧 图床设置 里,点击 Amazon S3 :配置 Amazon S3

右边这样配置: 在 PicGo 设置 中 找到 打开配置文件 在打开的文件中,可以看到 下面这样的 配置。

"aws-s3": {
"_configName": "test-b2", 图床配置名: 随便起 (我的是 test)
"_id": "cf166d49-c702-4d97-8cdf-1594b65ee292",
"_createdAt": 1729479715614,
"_updatedAt": 1729496911190,
"accessKeyID": "**************", 应用密钥ID: 第二步保存的 keyID
"secretAccessKey": "****************", 应用密钥: 第二步保存的 applicationKey
"bucketName": "it-is-just-a-test-bucket",桶名: 第一步创建的私密桶名 (it-is-just-a-test-bucket)
"uploadPath": "{year}/{month}/{fullName}", 文件路径: (我的是 {fullName} ,不带后缀)
"region": null, 地区: 留空
"endpoint": "https://s3.us-east-004.backblazeb2.com",自定义节点: 第一步记下的 EndPoint (https://s3.us-west-004.backblazeb2.com)
"proxy": null, 代理: 留空
"urlPrefix": "https://b2.bere.top", 自定义域名: 第五步自定义的域名 (我的是https://test.standat42.com)
"urlSuffix": "",
"pathStyleAccess": false,
"rejectUnauthorized": true,
"acl": "private",
"disableBucketPrefixToURL": false
}

picogo设置
相关文章
Free Image Hosting With Cloudflare Transform Rules and Backblaze B2
Deliver Public Backblaze B2 Content Through Cloudflare CDN
Deliver Private Backblaze B2 Content Through Cloudflare CDN
https://blog.standat42.com/posts/image-hosting-backblaze-b2-private-cloudflare-workers-picgo

PicGo+SMSM 简单、快速的打造markdown图床

smms 情况说明

smms 本身免费,而且不折腾,免费版本的 5GB 也能满足大多数人的需要,但是部分显示图片时带有广告 ,因此选择smms。当然有更多需求可以付费升级,SM.MS 终身高级会员 100GB 存储空间,每天 10000 次 上传 无广告 无退款,订单 是 $59.00 美元

注册smms

访问 smms.app,这是sm.ms在中国大陆的专用域名,保证了网络通信顺畅。在smms.app上注册账号,注册账号比较简单。

获取个人Token

登录smms后,在 smms 主页 选中 User 点击 ——》点击 Dashboard 进入Dashboard后选择 API Token 。正常来说你需要再Generate 一个Token(笔者已经生成过了),复制你的Secret Token 。
获取Token

在PigGo中安装SMMS插件

进入安装好的PicGo,点击左侧图床设置的SM.MS,点击加号按钮,新增配置。

smms

需要设置的配置有三个,第一个是配置名称,根据你的喜好命名,笔者命名为smms。第二个是 设定Token,将刚才生成的Token复制进来。第三个是备用上传域名,使用这个域名 smms.app。最后确定即可。

smms

使用smms图床

在正确配置后,我们就可以在PicGo的上传区上传图片了,支持拖拽文件,选择文件,剪切板图片上传。剪切板图片上传应该是最实用的。

上传成功后,会自动复制Marddown语法的图片链接格式,非常方便~。当然也可以在左下角调整你需要的链接格式。
使用smms