feat(halo): 添加图片上传功能并完善发布流程

- 实现图片上传服务,支持检测并上传本地图片到 Halo
- 优化发布流程,添加详细日志和错误处理
- 更新任务清单和检查列表以反映完成状态
- 添加 Halo 博客写作技能文档
This commit is contained in:
2026-04-26 16:47:41 +08:00
parent 7d332d3b8c
commit 8ccc32be0b
13 changed files with 1149 additions and 92 deletions
+12
View File
@@ -120,28 +120,40 @@ export default class HaloPlugin extends Plugin {
}
private async publishCommand() {
console.log("[HaloPlugin] 执行发布命令");
const { activeEditor } = this.app.workspace;
console.log(`[HaloPlugin] activeEditor: ${activeEditor ? '存在' : '不存在'}`);
if (!activeEditor || !activeEditor.file) {
console.log("[HaloPlugin] 没有打开的编辑器,退出");
return;
}
console.log(`[HaloPlugin] 当前文件: ${activeEditor.file.path}`);
const matterData = this.app.metadataCache.getFileCache(activeEditor.file)?.frontmatter;
console.log(`[HaloPlugin] frontmatter: ${JSON.stringify(matterData)}`);
if (matterData?.halo?.site) {
console.log(`[HaloPlugin] 检测到已发布的文章,站点: ${matterData.halo.site}`);
const site = this.settings.sites.find((site) => site.url === matterData.halo.site);
if (!site) {
console.log("[HaloPlugin] 未找到匹配的站点配置");
new Notice(i18next.t("command.publish.error_no_matched_site"));
return;
}
console.log(`[HaloPlugin] 找到站点: ${site.name}, URL: ${site.url}`);
const service = new HaloService(this.app, this.settings, site);
await service.publishPost();
return;
}
console.log("[HaloPlugin] 文章未发布过,需要选择站点");
const site = await openSiteSelectionModal(this);
console.log(`[HaloPlugin] 选择站点: ${site.name}, URL: ${site.url}`);
const service = new HaloService(this.app, this.settings, site);
await service.publishPost();
}
+1 -1
View File
@@ -1,7 +1,7 @@
import type { TFile, Vault } from "obsidian";
import { requestUrl } from "obsidian";
export class ImageUploader {
export default class ImageUploader {
private readonly siteUrl: string;
private readonly token: string;
private readonly headers: Record<string, string>;
+82 -56
View File
@@ -1,4 +1,4 @@
import type { Category, Content, Post, Snapshot, Tag } from "@halo-dev/api-client";
import ImageUploader from "./image-uploader";
import i18next from "i18next";
import { type App, Notice, requestUrl } from "obsidian";
import { randomUUID } from "src/utils/id";
@@ -6,7 +6,6 @@ import markdownIt from "src/utils/markdown";
import { slugify } from "transliteration";
import type { HaloSetting, HaloSite } from "../settings";
import { extractImageReferences, getAbsolutePath, replaceImagePaths } from "../utils/image";
import ImageUploader from "./image-uploader";
class HaloService {
private readonly site: HaloSite;
@@ -114,10 +113,14 @@ class HaloService {
// 检测并上传本地图片
let processedRaw = raw;
if (this.settings.imageUpload?.enabled) {
console.log("[HaloService] 图片上传功能已启用");
const imageReferences = extractImageReferences(raw);
console.log(`[HaloService] 检测到 ${imageReferences.length} 个图片引用`);
if (imageReferences.length > 0) {
const localImages = imageReferences.filter((ref) => !ref.path.startsWith("http://") && !ref.path.startsWith("https://") && !ref.path.startsWith("data:"));
console.log(`[HaloService] 其中 ${localImages.length} 个是本地图片`);
if (localImages.length > 0) {
new Notice(`检测到 ${localImages.length} 个本地图片,正在上传...`);
@@ -128,9 +131,12 @@ class HaloService {
absolute: getAbsolutePath(this.app.vault, ref.path, activeEditor.file.path),
}))
.filter((item) => item.absolute !== null) as { original: string; absolute: string }[];
console.log(`[HaloService] 其中 ${absolutePaths.length} 个图片可以解析为绝对路径`);
if (absolutePaths.length > 0) {
console.log(`[HaloService] 开始上传 ${absolutePaths.length} 个图片到 ${this.site.url}`);
const pathMapping = await imageUploader.uploadImages(absolutePaths.map((item) => item.absolute), this.app.vault);
console.log(`[HaloService] 上传完成,成功 ${pathMapping.size}`);
if (pathMapping.size > 0) {
const mapping = new Map<string, string>();
@@ -157,14 +163,17 @@ class HaloService {
}
}
}
} else {
console.log("[HaloService] 图片上传功能未启用");
}
// check site url
// 检查站点 URL
if (matterData?.halo?.site && matterData.halo.site !== this.site.url) {
new Notice(i18next.t("service.error_site_not_match"));
return;
}
// 如果已发布,获取现有文章信息
if (matterData?.halo?.name) {
const post = await this.getPost(matterData.halo.name);
@@ -177,7 +186,7 @@ class HaloService {
content.raw = processedRaw;
content.content = markdownIt.render(processedRaw);
// restore metadata
// 恢复元数据
if (matterData?.title) {
params.spec.title = matterData.title;
}
@@ -206,44 +215,44 @@ class HaloService {
}
try {
if (params.metadata.name) {
const { name } = params.metadata;
// 设置标题和 slug
params.spec.title = matterData?.title || activeEditor.file.basename;
params.spec.slug = matterData?.slug || slugify(params.spec.title, { trim: true });
// 设置内容注解
params.metadata.annotations = {
...params.metadata.annotations,
"content.halo.run/content-json": JSON.stringify(content),
};
// 设置 metadata.name(如果还没有的话)
if (!params.metadata.name) {
params.metadata.name = randomUUID();
}
console.log(`[HaloService] 开始发布文章,站点: ${this.site.url}`);
console.log(`[HaloService] 文章标题: ${params.spec.title}`);
console.log(`[HaloService] 文章 slug: ${params.spec.slug}`);
console.log(`[HaloService] 文章 name: ${params.metadata.name}`);
// 发送创建/更新请求
const isUpdate = !!matterData?.halo?.name;
if (isUpdate) {
console.log(`[HaloService] 更新现有文章: ${params.metadata.name}`);
await requestUrl({
url: `${this.site.url}/apis/uc.api.content.halo.run/v1alpha1/posts/${name}`,
url: `${this.site.url}/apis/uc.api.content.halo.run/v1alpha1/posts/${params.metadata.name}`,
method: "PUT",
contentType: "application/json",
headers: this.headers,
body: JSON.stringify(params),
});
const snapshot = (await requestUrl({
url: `${this.site.url}/apis/uc.api.content.halo.run/v1alpha1/posts/${name}/draft?patched=true`,
headers: this.headers,
}).json) as Snapshot;
snapshot.metadata.annotations = {
...snapshot.metadata.annotations,
"content.halo.run/content-json": JSON.stringify(content),
};
await requestUrl({
url: `${this.site.url}/apis/uc.api.content.halo.run/v1alpha1/posts/${name}/draft`,
method: "PUT",
contentType: "application/json",
headers: this.headers,
body: JSON.stringify(snapshot),
});
console.log(`[HaloService] 文章基本信息更新成功`);
} else {
params.metadata.name = randomUUID();
params.spec.title = matterData?.title || activeEditor.file.basename;
params.spec.slug = matterData?.slug || slugify(params.spec.title, { trim: true });
params.metadata.annotations = {
...params.metadata.annotations,
"content.halo.run/content-json": JSON.stringify(content),
};
console.log(`[HaloService] 创建新文章`);
const post = await requestUrl({
url: `${this.site.url}/apis/uc.api.content.halo.run/v1alpha1/posts`,
method: "POST",
@@ -251,11 +260,19 @@ class HaloService {
headers: this.headers,
body: JSON.stringify(params),
}).json;
console.log(`[HaloService] 文章创建响应:`, JSON.stringify(post));
if (!post || !post.metadata) {
console.error(`[HaloService] 创建文章响应格式错误:`, post);
throw new Error("创建文章响应格式错误");
}
console.log(`[HaloService] 文章创建成功: ${post.metadata.name}`);
params = post;
}
// Publish post
// 处理发布状态
// biome-ignore lint: no
if (matterData?.halo?.hasOwnProperty("publish")) {
if (matterData?.halo?.publish) {
@@ -269,30 +286,39 @@ class HaloService {
}
}
params = (await this.getPost(params.metadata.name))?.post || params;
const postResult = await this.getPost(params.metadata.name);
if (!postResult || !postResult.post.metadata) {
console.error("[HaloService] 获取文章详情失败");
new Notice(i18next.t("service.error_publish_failed"));
return;
}
params = postResult.post;
const postCategories = await this.getCategoryDisplayNames(params.spec.categories);
const postTags = await this.getTagDisplayNames(params.spec.tags);
this.app.fileManager.processFrontMatter(activeEditor.file, (frontmatter) => {
frontmatter.title = params.spec.title;
frontmatter.slug = params.spec.slug;
frontmatter.cover = params.spec.cover;
frontmatter.excerpt = params.spec.excerpt.autoGenerate ? undefined : params.spec.excerpt.raw;
frontmatter.categories = postCategories;
frontmatter.tags = postTags;
frontmatter.halo = {
site: this.site.url,
name: params.metadata.name,
publish: params.spec.publish,
};
});
new Notice(i18next.t("service.notice_publish_success"));
} catch (error) {
console.error(`[HaloService] 发布失败,错误:`, error);
new Notice(i18next.t("service.error_publish_failed"));
return;
}
const postCategories = await this.getCategoryDisplayNames(params.spec.categories);
const postTags = await this.getTagDisplayNames(params.spec.tags);
this.app.fileManager.processFrontMatter(activeEditor.file, (frontmatter) => {
frontmatter.title = params.spec.title;
frontmatter.slug = params.spec.slug;
frontmatter.cover = params.spec.cover;
frontmatter.excerpt = params.spec.excerpt.autoGenerate ? undefined : params.spec.excerpt.raw;
frontmatter.categories = postCategories;
frontmatter.tags = postTags;
frontmatter.halo = {
site: this.site.url,
name: params.metadata.name,
publish: params.spec.publish,
};
});
new Notice(i18next.t("service.notice_publish_success"));
}
public async changePostPublish(name: string, publish: boolean): Promise<void> {