feat(halo): 添加图片自动上传功能

- 新增图片处理工具模块 `src/utils/image.ts`,包含图片引用提取、绝对路径解析和路径替换功能
- 新增图片上传服务 `src/service/image-uploader.ts`,支持调用 Halo 媒体 API 上传图片并实现缓存机制
- 在设置界面添加图片上传开关和上传路径配置项
- 更新发布流程,在提交到 Halo 前自动检测并上传本地图片,替换为远程 URL
- 添加英文、简体中文和繁体中文的国际化文案
- 更新插件版本至 1.1.1 并完善相关配置文件
This commit is contained in:
2026-04-26 16:11:11 +08:00
commit 7d332d3b8c
213 changed files with 128271 additions and 0 deletions
@@ -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);
}
}