Note此文章由 LemonPrefect 基于 Creative Commons - CC0 International 协议发布,所有内容仅限于学习交流。

用 jsDelivr 做图床蛮久了,但是每次更新图片都要 release。好像要反复占用空间,这样薅多了不太好,所以就结合最近在 v2ex 上看到的一个接口,给 Typora 写了个上传插件。(其实可以用 SMMS 的服务,但是我觉得有点慢)

文章的内容更新啦!因为换了接口,所以建议跳过后面 BUG 修复的部分,或者仅作为严谨性参考(虽然本身也不严谨)

此文档最后更新于很多天以前,其中的某些方法已经失效啦(其实调整一下就好了,但毕竟是学习研究,就不放出来啦(自己试试看吧(逃x

读 API 文档

顺着 v2ex 的作者的站点,我找到了 API 提供方和相关的文档,相关摘要如下(为了更好理解做了部分修改)。

参数名称必填类型说明
imgurlstring图片链接上传地址
filestring文件流上传方式 必须为“multipart”
Filedatastring文件流

接下来就是 Typora 方面对于图片上传插件的要求(相关要点用高亮标出)。

You could config a custom command to upload images, using tools that is not listed in above options, or event write your own tools / scripts. Typora will append all images that needs to be uploaded after the custom command you filled.

Then, Typora will fetch image urls from the last N lines of the standard output of your custom command. (N is the number of images to upload).

For example, if you write a tool upload-image.sh, then you can input [some path]/upload-image.sh in the command filed. Typora will call [some path]/upload-image.sh "image-path-1" "image-path-2" to upload two images located in image-path-1 and image-path-2. Then the command may return something like:

1
2
3
Upload Success:
http://remote-image-1.png
http://remote-image-2.png

Then Typora will get the two remote image url from the output, and replace the original local images used in the Markdown document.

主要就是上传图片然后依次输出地址以供引用。(没看到报错怎么定义,所以后面就随便写了)

快速开发与踩坑记录

既然需要用可执行的,也就 Poweshell 和 C# 还有 C/C++ 编译出的文件可以这样执行了吧。(也不一定)于是就想着用 C# 写一个简单的 exe,因为 C# 上就可以用我很喜欢的 Flurl 了。

安装好库之后就开始面向文档写请求了,但是 Flurl 网站上给的文档不够完全,导致相关的 PostMultipartAsync() 方法使用方法不详细。好在我在 Stack Overflow 上找到了相关的问题。这里贴上相关的用法。

1
2
3
4
5
6
7
8
9
var resp = await "http://api.com"
.PostMultipartAsync(mp => mp
.AddString("name", "hello!") // individual string
.AddStringParts(new {a = 1, b = 2}) // multiple strings
.AddFile("file1", path1) // local file path
.AddFile("file2", stream, "file.txt") // file stream
.AddJson("json", new { foo = "x" }) // json
.AddUrlEncoded("urlEnc", new { bar = "y" }) // URL-encoded
.Add(content)); // any HttpContent

结合 Typora 的“所见即所得”的特性,我们不需要实现多线程和异步操作,毕竟不太可能存在大量图片同时准备上传的情况(当然粘贴大量图片除外),Typora 可以开启粘贴后实时上传的操作,不过这样就不能粘贴含有隐私的图片了,因为这个图床好像没有给出删除相关图片的接口。

根据文档,我们只需要提供两个 POST 参数即可,因此,请求可以这样构造。(需要注意的是文档中有一个参数是首字母大写的,有点不一样)

1
2
3
4
5
var uploadRespose = uploadUrl.PostMultipartAsync(data => 
data.AddString("file","multipart")
.AddFile("Filedata",args[i])
).Result;
string responseData = uploadRespose.ResponseMessage.Content.ReadAsStringAsync().Result;

然后再从 response 中取出图片链接并次序显示即可。这里加上 Typora 官方文档中的 Upload Success: 以确保跟例子一致。

当遇到上传错误的时候(code != 1),可以再次上传。(具体上传错误原因因为文档没写出,所以不清楚)因此我简单地声明了个标记次数然后重试。同理,在获取响应时也可以通过判断 http code 来判断 API 是否有正常响应以确定要不要重新上传图片。

1
2
3
4
if (flag == 5){
Console.WriteLine("Error:API Failed to upload for 5 times!");
Thread.CurrentThread.Abort();
}

上传完成后,把得到的链接次序输出。

1
2
3
for (int i = 0; i < imageQuantity; i++){
Console.WriteLine(fetchedUrl[i]);
}

完成之后将程序编译并放在喜欢的位置并配置好 Typora 的上传。(有空格的目录需要按 dir /x 下的目录给定,譬如 X:\PROGRA~2\PicAlicdnForTypora\PicAlicdnForTypora.exe)在 Typora 的测试通过后即可正常使用。

BUG 修复 📘

上传请求次数过多导致无法成功

之前的程序没有考虑到一次性粘贴大量图片的上传,结果导致在进行相关操作的时候图片几乎完全无法上传成功。于是在这次的更新中增加了一点小策略。

在每次请求时从准备好的 User-Agent 中随机抽取,同时随机生成 Client-IP,将其二者合并到构造的请求中。

1
2
3
4
5
6
7
8
var uploadRespose = uploadUrl.WithHeaders(new {
User_Agent = userAgents[randomNum.Next(4)],
Client_IP = randomNum.Next(192) + "." + randomNum.Next(255) + "." + randomNum.Next(255) + "." + randomNum.Next(255)

}).PostMultipartAsync(data =>
data.AddString("file","multipart")
.AddFile("Filedata",args[i])
).Result;

同时,使用 Thread.Sleep(300) 在每次处理完请求后使线程暂停 300 毫秒。这样可以有效地防止因为上传请求过于频繁而上传失败的问题。

Content-Type 编码报错

这个 BUG 我感觉比较奇怪,在请求的响应中,有一定概率会把 Content-Type 中的 utf-8 返回成 utf8。但是有的时候却是正常的。因此这需要使用到 .Net 4.6+ 的一个新特性 EncodingProvider 子类,相关的文档在这里。这个新的特性允许自定义一个编码类型,所以我们可以使用它构建一个 utf8 的类型然后返回 UTF-8 的编码类型使得解析可以正常进行。相关参考来自 Stack Overflow

1
2
3
4
5
6
7
8
9
10
public class EquivocalUtf8EncodingProvider : EncodingProvider {
public override Encoding GetEncoding(string name){
return name == "utf8" ? Encoding.UTF8 : null;
}
public override Encoding GetEncoding(int codepage){
return null;
}
}
EncodingProvider provider = new EquivocalUtf8EncodingProvider();
Encoding.RegisterProvider(provider);

文件名包含中文时上传失败

因为一开始文件的上传依赖于文件名(具体见上面的请求部分代码),所以一直不知道怎么解决好。偶然看了一下方法的重载,发现可以上传 Stream。于是就有了先把文件读入成 Stream 上传之后再 Dispose 的思路。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
FileStream uploadImg = new FileStream(args[i],FileMode.Open,FileAccess.Read, FileShare.Read);
byte[] bytesImg = new byte[uploadImg.Length];
uploadImg.Read(bytesImg, 0, bytesImg.Length);
uploadImg.Close();
Stream uploadImgStream = new MemoryStream(bytesImg);

var uploadRespose = uploadUrl.WithHeaders(new {
User_Agent = userAgents[randomNum.Next(4)],
Client_IP = randomNum.Next(192) + "." + randomNum.Next(255) + "." + randomNum.Next(255) + "." + randomNum.Next(255)
}).PostMultipartAsync(data =>
data.AddString("file","multipart")
.AddFile("Filedata",uploadImgStream,WebUtility.UrlEncode(args[i]))
).Result;

uploadImgStream.Dispose();

这样之后,无法上传含有中文字符作文件名的文件的问题迎刃而解。

带协议的文件路径导致上传失败

Typora 支持使用 file:/// 协议来插入图片,但是在调用的时候却只会去掉 file:// 导致一个 / 被保留在文件路径参数中,导致文件无法被正常读取而报错。在尝试过之后发现,以 / 开头的文件路径没有办法成功地插入一张图片,所以这波刚好只需要在尝试在读取文件之前过滤一个字符 /

1
2
3
if ('/' == args[i][0]){
args[i] = args[i].Substring(1);
}

在测试的时候,发现即使是在线引用的图片也有可能被调用上传,所以根据可以引用的几种协议判断一下直接返回原本的地址以避免出错。

1
2
3
4
if (args[i].Contains("http://") || args[i].Contains("https://") || args[i].Contains("ftp://")){
fetchedUrl[i] = args[i];
continue;
}

使用原始接口的再开发

真的不得不说,原来的接口真的,各种奇奇怪怪的问题。这回终于找到了原始的接口 来源,兴奋之余开了个新的 solution 修订一波原来的代码,在这里分析一二。

接口的地址是 https://kfupload.alibaba.com/mupload,主要有这么几个参数。

参数名称必填类型说明
scenestring固定值“aeMessageCenterV2ImageRule”
namestring文件名
filestream文件流

除此之外,必须指定 mediaType 且大概只支持 png/jpg/gif 三种,且 Header 中的 User-Agent 须为 iAliexpress/6.22.1 (iPhone; iOS 12.1.2; Scale/2.00)。其他部分可以依照前文所述构造 POST 请求。


Download Source