feat(halo): 添加图片自动上传功能
- 新增图片处理工具模块 `src/utils/image.ts`,包含图片引用提取、绝对路径解析和路径替换功能 - 新增图片上传服务 `src/service/image-uploader.ts`,支持调用 Halo 媒体 API 上传图片并实现缓存机制 - 在设置界面添加图片上传开关和上传路径配置项 - 更新发布流程,在提交到 Halo 前自动检测并上传本地图片,替换为远程 URL - 添加英文、简体中文和繁体中文的国际化文案 - 更新插件版本至 1.1.1 并完善相关配置文件
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
import type { TFile, Vault } from "obsidian";
|
||||
import { requestUrl } from "obsidian";
|
||||
|
||||
export class ImageUploader {
|
||||
private readonly siteUrl: string;
|
||||
private readonly token: string;
|
||||
private readonly headers: Record<string, string>;
|
||||
private readonly uploadedImagesCache: Map<string, string>;
|
||||
|
||||
constructor(siteUrl: string, token: string) {
|
||||
this.siteUrl = siteUrl;
|
||||
this.token = token;
|
||||
this.uploadedImagesCache = new Map();
|
||||
|
||||
this.headers = {
|
||||
Authorization: `Bearer ${this.token}`,
|
||||
};
|
||||
}
|
||||
|
||||
private toBase64(arrayBuffer: ArrayBuffer): string {
|
||||
const bytes = new Uint8Array(arrayBuffer);
|
||||
let binary = "";
|
||||
for (let i = 0; i < bytes.byteLength; i++) {
|
||||
binary += String.fromCharCode(bytes[i]);
|
||||
}
|
||||
return btoa(binary);
|
||||
}
|
||||
|
||||
public async uploadImage(filePath: string, vault: Vault): Promise<string | null> {
|
||||
const normalizedPath = filePath.replace(/\\/g, "/");
|
||||
|
||||
if (this.uploadedImagesCache.has(normalizedPath)) {
|
||||
return this.uploadedImagesCache.get(normalizedPath) || null;
|
||||
}
|
||||
|
||||
try {
|
||||
const file = vault.getAbstractFileByPath(normalizedPath);
|
||||
|
||||
if (!file || !(file instanceof TFile)) {
|
||||
console.error(`[ImageUploader] 文件不存在或不是有效文件: ${normalizedPath}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const arrayBuffer = await vault.readBinary(file);
|
||||
const base64Data = this.toBase64(arrayBuffer);
|
||||
const filename = normalizedPath.split("/").pop() || "image";
|
||||
|
||||
const boundary = `----ObsidianHalo${Date.now()}`;
|
||||
let body = `--${boundary}\r\n`;
|
||||
body += `Content-Disposition: form-data; name="file"; filename="${filename}"\r\n`;
|
||||
body += "Content-Type: application/octet-stream\r\n";
|
||||
body += "Content-Transfer-Encoding: base64\r\n\r\n";
|
||||
body += base64Data;
|
||||
body += `\r\n--${boundary}--\r\n`;
|
||||
|
||||
const response = await requestUrl({
|
||||
url: `${this.siteUrl}/apis/api.console.halo.run/v1alpha1/attachments/upload`,
|
||||
method: "POST",
|
||||
headers: {
|
||||
...this.headers,
|
||||
"Content-Type": `multipart/form-data; boundary=${boundary}`,
|
||||
},
|
||||
body: body,
|
||||
});
|
||||
|
||||
const jsonResponse = response.json;
|
||||
|
||||
if (jsonResponse?.url) {
|
||||
this.uploadedImagesCache.set(normalizedPath, jsonResponse.url);
|
||||
return jsonResponse.url;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error(`[ImageUploader] 上传图片失败: ${filePath}`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async uploadImages(filePaths: string[], vault: Vault): Promise<Map<string, string>> {
|
||||
const result = new Map<string, string>();
|
||||
|
||||
for (const filePath of filePaths) {
|
||||
const url = await this.uploadImage(filePath, vault);
|
||||
if (url) {
|
||||
result.set(filePath, url);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public getCachedUrl(filePath: string): string | undefined {
|
||||
const normalizedPath = filePath.replace(/\\/g, "/");
|
||||
return this.uploadedImagesCache.get(normalizedPath);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user