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