feat(halo): 添加 Halo 客户端、内容服务和图像服务,支持文章发布和管理功能
This commit is contained in:
@@ -1,5 +1,9 @@
|
|||||||
{
|
{
|
||||||
"recentFiles": [
|
"recentFiles": [
|
||||||
|
{
|
||||||
|
"basename": "DeepSeek-V4博客大纲",
|
||||||
|
"path": "博客/其他/DeepSeek-V4博客大纲.md"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"basename": "DeepSeek-V4全面解析",
|
"basename": "DeepSeek-V4全面解析",
|
||||||
"path": "博客/DeepSeek-V4全面解析.md"
|
"path": "博客/DeepSeek-V4全面解析.md"
|
||||||
|
|||||||
Vendored
+11
-27
@@ -11,10 +11,14 @@
|
|||||||
"id": "e7a7b303c61786dc",
|
"id": "e7a7b303c61786dc",
|
||||||
"type": "leaf",
|
"type": "leaf",
|
||||||
"state": {
|
"state": {
|
||||||
"type": "empty",
|
"type": "markdown",
|
||||||
"state": {},
|
"state": {
|
||||||
|
"file": "博客/其他/DeepSeek-V4博客大纲.md",
|
||||||
|
"mode": "source",
|
||||||
|
"source": false
|
||||||
|
},
|
||||||
"icon": "lucide-file",
|
"icon": "lucide-file",
|
||||||
"title": "新标签页"
|
"title": "DeepSeek-V4博客大纲"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -84,7 +88,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"direction": "horizontal",
|
"direction": "horizontal",
|
||||||
"width": 293.5
|
"width": 263.5
|
||||||
},
|
},
|
||||||
"right": {
|
"right": {
|
||||||
"id": "1a950cafdb3ea126",
|
"id": "1a950cafdb3ea126",
|
||||||
@@ -197,30 +201,10 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"currentTab": 3
|
"currentTab": 3
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "909893d80e09e8a3",
|
|
||||||
"type": "tabs",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"id": "e7a7b303c61786dc",
|
|
||||||
"type": "leaf",
|
|
||||||
"state": {
|
|
||||||
"type": "markdown",
|
|
||||||
"state": {
|
|
||||||
"file": "博客/DeepSeek-V4全面解析.md",
|
|
||||||
"mode": "source",
|
|
||||||
"source": false
|
|
||||||
},
|
|
||||||
"icon": "lucide-file",
|
|
||||||
"title": "DeepSeek-V4全面解析"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"direction": "horizontal",
|
"direction": "horizontal",
|
||||||
"width": 1013.5
|
"width": 439.5
|
||||||
},
|
},
|
||||||
"left-ribbon": {
|
"left-ribbon": {
|
||||||
"hiddenItems": {
|
"hiddenItems": {
|
||||||
@@ -248,9 +232,10 @@
|
|||||||
},
|
},
|
||||||
"active": "e7a7b303c61786dc",
|
"active": "e7a7b303c61786dc",
|
||||||
"lastOpenFiles": [
|
"lastOpenFiles": [
|
||||||
"copilot/copilot-conversations/分析@20260426_205456.md",
|
"copilot/copilot-conversations/你好@20260428_130750.md",
|
||||||
"copilot/copilot-conversations",
|
"copilot/copilot-conversations",
|
||||||
"copilot",
|
"copilot",
|
||||||
|
"copilot/copilot-conversations/分析@20260426_205456.md",
|
||||||
"博客/标签分类审查报告.md",
|
"博客/标签分类审查报告.md",
|
||||||
"博客/DeepSeek-V4全面解析.md",
|
"博客/DeepSeek-V4全面解析.md",
|
||||||
"博客/Git团队协作指南(精简版).md",
|
"博客/Git团队协作指南(精简版).md",
|
||||||
@@ -293,7 +278,6 @@
|
|||||||
"copilot/copilot-conversations/分析@20260425_212055.md",
|
"copilot/copilot-conversations/分析@20260425_212055.md",
|
||||||
"copilot/copilot-custom-prompts/halo.md",
|
"copilot/copilot-custom-prompts/halo.md",
|
||||||
"Excalidraw/Drawing 2026-04-25 21.16.49.excalidraw.md",
|
"Excalidraw/Drawing 2026-04-25 21.16.49.excalidraw.md",
|
||||||
"博客/深入解析 Claude Code:Vibe Coding 时代的 AI 编程利器.md",
|
|
||||||
"未命名.canvas"
|
"未命名.canvas"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"git.ignoreLimitWarning": true
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
import { requestUrl } from "obsidian";
|
||||||
|
import type { HaloSite } from "../settings";
|
||||||
|
import { HaloError, HttpError } from "./error";
|
||||||
|
|
||||||
|
export class HaloClient {
|
||||||
|
private readonly baseUrl: string;
|
||||||
|
private readonly headers: Record<string, string>;
|
||||||
|
|
||||||
|
constructor(site: HaloSite) {
|
||||||
|
this.baseUrl = site.url;
|
||||||
|
this.headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${site.token}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleResponse<T>(response: { json: unknown; status: number }): T {
|
||||||
|
const { json, status } = response;
|
||||||
|
|
||||||
|
if (status >= 200 && status < 300) {
|
||||||
|
return json as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw HttpError.fromStatus(status, json);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async request<T>(path: string, options: { method: string; body?: object }): Promise<T> {
|
||||||
|
try {
|
||||||
|
const response = await requestUrl({
|
||||||
|
url: `${this.baseUrl}${path}`,
|
||||||
|
method: options.method,
|
||||||
|
headers: this.headers,
|
||||||
|
body: options.body ? JSON.stringify(options.body) : undefined,
|
||||||
|
contentType: options.body ? "application/json" : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.handleResponse<T>({ json: response.json, status: response.status });
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof HttpError) {
|
||||||
|
throw this.mapHttpError(error);
|
||||||
|
}
|
||||||
|
if (error instanceof TypeError) {
|
||||||
|
// Network error
|
||||||
|
throw HaloError.network(error);
|
||||||
|
}
|
||||||
|
throw HaloError.unknown(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private mapHttpError(error: HttpError): HaloError {
|
||||||
|
switch (error.statusCode) {
|
||||||
|
case 401:
|
||||||
|
return HaloError.unauthorized();
|
||||||
|
case 403:
|
||||||
|
return HaloError.forbidden();
|
||||||
|
case 404:
|
||||||
|
return HaloError.notFound("资源");
|
||||||
|
case 500:
|
||||||
|
case 502:
|
||||||
|
case 503:
|
||||||
|
return HaloError.serverError();
|
||||||
|
default:
|
||||||
|
return new HaloError(error.message, `HTTP_${error.statusCode}`, error.statusCode, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async get<T>(path: string): Promise<T> {
|
||||||
|
return this.request<T>(path, { method: "GET" });
|
||||||
|
}
|
||||||
|
|
||||||
|
async post<T>(path: string, body: object): Promise<T> {
|
||||||
|
return this.request<T>(path, { method: "POST", body });
|
||||||
|
}
|
||||||
|
|
||||||
|
async put<T>(path: string, body: object): Promise<T> {
|
||||||
|
return this.request<T>(path, { method: "PUT", body });
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(path: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
const response = await requestUrl({
|
||||||
|
url: `${this.baseUrl}${path}`,
|
||||||
|
method: "DELETE",
|
||||||
|
headers: this.headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status >= 400) {
|
||||||
|
throw HttpError.fromStatus(response.status, response.json);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof HttpError) {
|
||||||
|
throw this.mapHttpError(error);
|
||||||
|
}
|
||||||
|
if (error instanceof TypeError) {
|
||||||
|
throw HaloError.network(error);
|
||||||
|
}
|
||||||
|
throw HaloError.unknown(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async putVoid(path: string, body?: object): Promise<void> {
|
||||||
|
try {
|
||||||
|
const response = await requestUrl({
|
||||||
|
url: `${this.baseUrl}${path}`,
|
||||||
|
method: "PUT",
|
||||||
|
headers: this.headers,
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
contentType: body ? "application/json" : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status >= 400) {
|
||||||
|
throw HttpError.fromStatus(response.status, response.json);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof HttpError) {
|
||||||
|
throw this.mapHttpError(error);
|
||||||
|
}
|
||||||
|
if (error instanceof TypeError) {
|
||||||
|
throw HaloError.network(error);
|
||||||
|
}
|
||||||
|
throw HaloError.unknown(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { slugify } from "transliteration";
|
||||||
|
import markdownIt from "../utils/markdown";
|
||||||
|
import { readMatter } from "../utils/yaml";
|
||||||
|
import type { Content, Post } from "./types";
|
||||||
|
|
||||||
|
export interface FrontmatterData {
|
||||||
|
title?: string;
|
||||||
|
slug?: string;
|
||||||
|
excerpt?: string;
|
||||||
|
cover?: string;
|
||||||
|
categories?: string[];
|
||||||
|
tags?: string[];
|
||||||
|
halo?: {
|
||||||
|
site?: string;
|
||||||
|
name?: string;
|
||||||
|
publish?: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ContentService {
|
||||||
|
renderMarkdown(raw: string): string {
|
||||||
|
return markdownIt.render(raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
extractFrontmatter(md: string): { frontmatter: FrontmatterData; rawContent: string } {
|
||||||
|
const result = readMatter(md);
|
||||||
|
const frontmatter = result.data as FrontmatterData;
|
||||||
|
const rawContent = result.content;
|
||||||
|
return { frontmatter, rawContent };
|
||||||
|
}
|
||||||
|
|
||||||
|
buildPostSpec(
|
||||||
|
title: string,
|
||||||
|
slug: string,
|
||||||
|
_raw: string,
|
||||||
|
frontmatter: FrontmatterData,
|
||||||
|
_content: Content,
|
||||||
|
): Post["spec"] {
|
||||||
|
return {
|
||||||
|
allowComment: true,
|
||||||
|
baseSnapshot: "",
|
||||||
|
categories: [],
|
||||||
|
cover: frontmatter.cover || "",
|
||||||
|
deleted: false,
|
||||||
|
excerpt: {
|
||||||
|
autoGenerate: !frontmatter.excerpt,
|
||||||
|
raw: frontmatter.excerpt || "",
|
||||||
|
},
|
||||||
|
headSnapshot: "",
|
||||||
|
htmlMetas: [],
|
||||||
|
owner: "",
|
||||||
|
pinned: false,
|
||||||
|
priority: 0,
|
||||||
|
publish: false,
|
||||||
|
publishTime: "",
|
||||||
|
releaseSnapshot: "",
|
||||||
|
slug: slug || slugify(title, { trim: true }),
|
||||||
|
tags: [],
|
||||||
|
template: "",
|
||||||
|
title,
|
||||||
|
visible: "PUBLIC",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
export class HaloError extends Error {
|
||||||
|
constructor(
|
||||||
|
message: string,
|
||||||
|
public readonly code: string,
|
||||||
|
public readonly statusCode?: number,
|
||||||
|
public readonly originalError?: unknown,
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
this.name = "HaloError";
|
||||||
|
}
|
||||||
|
|
||||||
|
static network(error: unknown): HaloError {
|
||||||
|
return new HaloError("网络请求失败,请检查网络连接", "NETWORK_ERROR", undefined, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
static unauthorized(): HaloError {
|
||||||
|
return new HaloError("认证失败,请检查 Token 是否有效", "UNAUTHORIZED", 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
static forbidden(): HaloError {
|
||||||
|
return new HaloError("权限不足,无法执行此操作", "FORBIDDEN", 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
static notFound(resource: string): HaloError {
|
||||||
|
return new HaloError(`${resource} 未找到`, "NOT_FOUND", 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
static serverError(message = "服务器错误"): HaloError {
|
||||||
|
return new HaloError(message, "SERVER_ERROR", 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
static validationError(message: string): HaloError {
|
||||||
|
return new HaloError(message, "VALIDATION_ERROR");
|
||||||
|
}
|
||||||
|
|
||||||
|
static unknown(error: unknown): HaloError {
|
||||||
|
const message = error instanceof Error ? error.message : "未知错误";
|
||||||
|
return new HaloError(message, "UNKNOWN", undefined, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
isNetworkError(): boolean {
|
||||||
|
return this.code === "NETWORK_ERROR";
|
||||||
|
}
|
||||||
|
|
||||||
|
isAuthError(): boolean {
|
||||||
|
return this.code === "UNAUTHORIZED" || this.code === "FORBIDDEN";
|
||||||
|
}
|
||||||
|
|
||||||
|
isNotFound(): boolean {
|
||||||
|
return this.code === "NOT_FOUND";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class HttpError extends Error {
|
||||||
|
constructor(
|
||||||
|
public readonly statusCode: number,
|
||||||
|
message: string,
|
||||||
|
public readonly response?: unknown,
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
this.name = "HttpError";
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromStatus(statusCode: number, response?: unknown): HttpError {
|
||||||
|
const messages: Record<number, string> = {
|
||||||
|
400: "请求参数错误",
|
||||||
|
401: "认证失败",
|
||||||
|
403: "权限不足",
|
||||||
|
404: "资源不存在",
|
||||||
|
500: "服务器内部错误",
|
||||||
|
};
|
||||||
|
const message = messages[statusCode] || `HTTP 错误 (${statusCode})`;
|
||||||
|
return new HttpError(statusCode, message, response);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,402 @@
|
|||||||
|
import { type App, Notice, type TFile } from "obsidian";
|
||||||
|
import { randomUUID } from "./utils/id";
|
||||||
|
import markdownIt from "./utils/markdown";
|
||||||
|
import { slugify } from "transliteration";
|
||||||
|
import type { HaloSetting, HaloSite } from "./settings";
|
||||||
|
import type { HaloClient } from "./services/client";
|
||||||
|
import { PostService } from "./services/post-service";
|
||||||
|
import { ImageService } from "./services/image-service";
|
||||||
|
import { TaxonomyService } from "./services/taxonomy-service";
|
||||||
|
import { ContentService } from "./services/content-service";
|
||||||
|
import type { Content, Post } from "./services/types";
|
||||||
|
import { HaloError } from "./error";
|
||||||
|
|
||||||
|
export class HaloService {
|
||||||
|
private readonly app: App;
|
||||||
|
private readonly siteUrl: string;
|
||||||
|
private readonly client: HaloClient;
|
||||||
|
private readonly postService: PostService;
|
||||||
|
private readonly imageService: ImageService;
|
||||||
|
private readonly taxonomyService: TaxonomyService;
|
||||||
|
private readonly contentService: ContentService;
|
||||||
|
|
||||||
|
constructor(app: App, settings: HaloSetting, site: HaloSite) {
|
||||||
|
this.app = app;
|
||||||
|
this.siteUrl = site.url;
|
||||||
|
this.client = new HaloClient(site);
|
||||||
|
this.postService = new PostService(app, this.client);
|
||||||
|
this.imageService = new ImageService(site);
|
||||||
|
this.taxonomyService = new TaxonomyService(this.client);
|
||||||
|
this.contentService = new ContentService();
|
||||||
|
}
|
||||||
|
|
||||||
|
async publishPost(): Promise<void> {
|
||||||
|
const { activeEditor } = this.app.workspace;
|
||||||
|
|
||||||
|
if (!activeEditor || !activeEditor.file) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let processedRaw = await this.app.vault.read(activeEditor.file);
|
||||||
|
const matterData = this.app.metadataCache.getFileCache(activeEditor.file)?.frontmatter;
|
||||||
|
const frontmatterPosition = this.app.metadataCache.getFileCache(activeEditor.file)?.frontmatterPosition;
|
||||||
|
|
||||||
|
processedRaw = frontmatterPosition ? processedRaw.slice(frontmatterPosition.end.offset) : processedRaw;
|
||||||
|
|
||||||
|
// Process images if enabled
|
||||||
|
if (this.settings.imageUpload?.enabled) {
|
||||||
|
const imageResult = await this.imageService.uploadLocalImages(
|
||||||
|
processedRaw,
|
||||||
|
this.app.vault,
|
||||||
|
activeEditor.file.path,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (imageResult.successCount > 0) {
|
||||||
|
new Notice(`图片上传成功 (${imageResult.successCount}/${imageResult.successCount + imageResult.failCount})`);
|
||||||
|
} else if (imageResult.failCount > 0) {
|
||||||
|
new Notice(`部分图片上传失败 (失败: ${imageResult.failCount})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
processedRaw = imageResult.processedContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build content
|
||||||
|
const content: Content = {
|
||||||
|
rawType: "markdown",
|
||||||
|
raw: processedRaw,
|
||||||
|
content: markdownIt.render(processedRaw),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get existing post or create new params
|
||||||
|
let params: Post;
|
||||||
|
let isUpdate = false;
|
||||||
|
|
||||||
|
if (matterData?.halo?.name) {
|
||||||
|
const existingPost = await this.postService.getPost(matterData.halo.name);
|
||||||
|
if (existingPost) {
|
||||||
|
params = existingPost.post;
|
||||||
|
content.raw = processedRaw;
|
||||||
|
content.content = markdownIt.render(processedRaw);
|
||||||
|
isUpdate = true;
|
||||||
|
} else {
|
||||||
|
params = this.createDefaultPostParams();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
params = this.createDefaultPostParams();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update params from frontmatter
|
||||||
|
params.spec.title = matterData?.title || activeEditor.file.basename;
|
||||||
|
params.spec.slug = matterData?.slug || slugify(params.spec.title, { trim: true });
|
||||||
|
|
||||||
|
if (matterData?.excerpt) {
|
||||||
|
params.spec.excerpt.raw = matterData.excerpt;
|
||||||
|
params.spec.excerpt.autoGenerate = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matterData?.cover) {
|
||||||
|
params.spec.cover = matterData.cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matterData?.categories) {
|
||||||
|
params.spec.categories = await this.taxonomyService.getCategoryNames(matterData.categories);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matterData?.tags) {
|
||||||
|
params.spec.tags = await this.taxonomyService.getTagNames(matterData.tags);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set content annotation
|
||||||
|
params.metadata.annotations = {
|
||||||
|
...params.metadata.annotations,
|
||||||
|
"content.halo.run/content-json": JSON.stringify(content),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ensure metadata.name exists
|
||||||
|
if (!params.metadata.name) {
|
||||||
|
params.metadata.name = randomUUID();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isUpdate) {
|
||||||
|
await this.postService.updatePost(params.metadata.name, params);
|
||||||
|
} else {
|
||||||
|
const newPost = await this.postService.createPost(params);
|
||||||
|
params.metadata.name = newPost.metadata.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle publish status
|
||||||
|
if (matterData?.halo?.hasOwnProperty("publish")) {
|
||||||
|
await this.postService.changePublishStatus(params.metadata.name, matterData.halo.publish);
|
||||||
|
} else if (this.settings.publishByDefault) {
|
||||||
|
await this.postService.changePublishStatus(params.metadata.name, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh post to get latest data
|
||||||
|
const postResult = await this.postService.getPost(params.metadata.name);
|
||||||
|
if (!postResult || !postResult.post.metadata) {
|
||||||
|
new Notice("发布失败:无法获取文章详情");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
params = postResult.post;
|
||||||
|
|
||||||
|
// Update frontmatter
|
||||||
|
const postCategories = await this.taxonomyService.getCategoryDisplayNames(params.spec.categories);
|
||||||
|
const postTags = await this.taxonomyService.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.siteUrl,
|
||||||
|
name: params.metadata.name,
|
||||||
|
publish: params.spec.publish,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
new Notice("发布成功");
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof HaloError) {
|
||||||
|
console.error(`[HaloService] 发布失败 [${error.code}]:`, error.message);
|
||||||
|
new Notice(error.message);
|
||||||
|
} else {
|
||||||
|
console.error("[HaloService] 发布失败:", error);
|
||||||
|
new Notice("发布失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updatePost(): Promise<void> {
|
||||||
|
const { activeEditor } = this.app.workspace;
|
||||||
|
|
||||||
|
if (!activeEditor || !activeEditor.file) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const matterData = this.app.metadataCache.getFileCache(activeEditor.file)?.frontmatter;
|
||||||
|
|
||||||
|
if (!matterData?.halo?.name) {
|
||||||
|
new Notice("文章尚未发布到 Halo");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const post = await this.postService.getPost(matterData.halo.name);
|
||||||
|
|
||||||
|
if (!post) {
|
||||||
|
new Notice("在 Halo 中未找到文章");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const postCategories = await this.taxonomyService.getCategoryDisplayNames(post.post.spec.categories);
|
||||||
|
const postTags = await this.taxonomyService.getTagDisplayNames(post.post.spec.tags);
|
||||||
|
|
||||||
|
await this.app.vault.modify(activeEditor.file, post.content.raw);
|
||||||
|
|
||||||
|
this.app.fileManager.processFrontMatter(activeEditor.file, (frontmatter) => {
|
||||||
|
frontmatter.title = post.post.spec.title;
|
||||||
|
frontmatter.slug = post.post.spec.slug;
|
||||||
|
frontmatter.cover = post.post.spec.cover;
|
||||||
|
frontmatter.excerpt = post.post.spec.excerpt.autoGenerate ? undefined : post.post.spec.excerpt.raw;
|
||||||
|
frontmatter.categories = postCategories;
|
||||||
|
frontmatter.tags = postTags;
|
||||||
|
frontmatter.halo = {
|
||||||
|
site: this.siteUrl,
|
||||||
|
name: post.post.metadata.name,
|
||||||
|
publish: post.post.spec.publish,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async pullPost(name: string): Promise<void> {
|
||||||
|
const post = await this.postService.getPost(name);
|
||||||
|
|
||||||
|
if (!post) {
|
||||||
|
new Notice("在 Halo 中未找到文章");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const postCategories = await this.taxonomyService.getCategoryDisplayNames(post.post.spec.categories);
|
||||||
|
const postTags = await this.taxonomyService.getTagDisplayNames(post.post.spec.tags);
|
||||||
|
|
||||||
|
const file = await this.app.vault.create(`${post.post.spec.title}.md`, post.content.raw);
|
||||||
|
this.app.workspace.getLeaf().openFile(file);
|
||||||
|
|
||||||
|
this.app.fileManager.processFrontMatter(file, (frontmatter) => {
|
||||||
|
frontmatter.title = post.post.spec.title;
|
||||||
|
frontmatter.slug = post.post.spec.slug;
|
||||||
|
frontmatter.cover = post.post.spec.cover;
|
||||||
|
frontmatter.excerpt = post.post.spec.excerpt.autoGenerate ? undefined : post.post.spec.excerpt.raw;
|
||||||
|
frontmatter.categories = postCategories;
|
||||||
|
frontmatter.tags = postTags;
|
||||||
|
frontmatter.halo = {
|
||||||
|
site: this.siteUrl,
|
||||||
|
name: name,
|
||||||
|
publish: post.post.spec.publish,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async importPost(file: TFile, publishImmediately: boolean = false): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
let raw = await this.app.vault.read(file);
|
||||||
|
const matterData = this.app.metadataCache.getFileCache(file)?.frontmatter;
|
||||||
|
const frontmatterPosition = this.app.metadataCache.getFileCache(file)?.frontmatterPosition;
|
||||||
|
|
||||||
|
raw = frontmatterPosition ? raw.slice(frontmatterPosition.end.offset) : raw;
|
||||||
|
|
||||||
|
// Process images if enabled
|
||||||
|
if (this.settings.imageUpload?.enabled) {
|
||||||
|
const imageResult = await this.imageService.uploadLocalImages(raw, this.app.vault, file.path);
|
||||||
|
|
||||||
|
if (imageResult.successCount > 0) {
|
||||||
|
new Notice(`图片上传成功 (${imageResult.successCount}/${imageResult.successCount + imageResult.failCount})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
raw = imageResult.processedContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
const content: Content = {
|
||||||
|
rawType: "markdown",
|
||||||
|
raw: raw,
|
||||||
|
content: markdownIt.render(raw),
|
||||||
|
};
|
||||||
|
|
||||||
|
const params: Post = {
|
||||||
|
apiVersion: "content.halo.run/v1alpha1",
|
||||||
|
kind: "Post",
|
||||||
|
metadata: {
|
||||||
|
annotations: {
|
||||||
|
"content.halo.run/content-json": JSON.stringify(content),
|
||||||
|
},
|
||||||
|
name: randomUUID(),
|
||||||
|
},
|
||||||
|
spec: {
|
||||||
|
allowComment: true,
|
||||||
|
baseSnapshot: "",
|
||||||
|
categories: [],
|
||||||
|
cover: matterData?.cover || "",
|
||||||
|
deleted: false,
|
||||||
|
excerpt: {
|
||||||
|
autoGenerate: matterData?.excerpt ? false : true,
|
||||||
|
raw: matterData?.excerpt || "",
|
||||||
|
},
|
||||||
|
headSnapshot: "",
|
||||||
|
htmlMetas: [],
|
||||||
|
owner: "",
|
||||||
|
pinned: false,
|
||||||
|
priority: 0,
|
||||||
|
publish: publishImmediately,
|
||||||
|
publishTime: "",
|
||||||
|
releaseSnapshot: "",
|
||||||
|
slug: matterData?.slug || slugify(matterData?.title || file.basename, { trim: true }),
|
||||||
|
tags: [],
|
||||||
|
template: "",
|
||||||
|
title: matterData?.title || file.basename,
|
||||||
|
visible: "PUBLIC",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle categories
|
||||||
|
if (matterData?.categories) {
|
||||||
|
params.spec.categories = await this.taxonomyService.getCategoryNames(matterData.categories);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle tags
|
||||||
|
if (matterData?.tags) {
|
||||||
|
params.spec.tags = await this.taxonomyService.getTagNames(matterData.tags);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create post
|
||||||
|
const post = await this.postService.createPost(params);
|
||||||
|
|
||||||
|
return !!post && !!post.metadata;
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof HaloError) {
|
||||||
|
console.error(`[HaloService] 导入文章失败 [${error.code}]:`, error.message);
|
||||||
|
} else {
|
||||||
|
console.error("[HaloService] 导入文章失败:", error);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deletePost(name: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await this.postService.deletePost(name);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof HaloError) {
|
||||||
|
console.error(`[HaloService] 删除文章失败 [${error.code}]:`, error.message);
|
||||||
|
} else {
|
||||||
|
console.error("[HaloService] 删除文章失败:", error);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async createTag(displayName: string, slug: string, color: string): Promise<void> {
|
||||||
|
await this.taxonomyService.createTag(displayName, slug, color);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateTag(name: string, displayName: string, slug: string, color: string): Promise<void> {
|
||||||
|
await this.taxonomyService.updateTag(name, displayName, slug, color);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteTag(name: string): Promise<void> {
|
||||||
|
await this.taxonomyService.deleteTag(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createCategory(displayName: string, slug: string, priority: number): Promise<void> {
|
||||||
|
await this.taxonomyService.createCategory(displayName, slug, priority);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateCategory(name: string, displayName: string, slug: string, priority: number): Promise<void> {
|
||||||
|
await this.taxonomyService.updateCategory(name, displayName, slug, priority);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteCategory(name: string): Promise<void> {
|
||||||
|
await this.taxonomyService.deleteCategory(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private createDefaultPostParams(): Post {
|
||||||
|
return {
|
||||||
|
apiVersion: "content.halo.run/v1alpha1",
|
||||||
|
kind: "Post",
|
||||||
|
metadata: {
|
||||||
|
annotations: {},
|
||||||
|
name: "",
|
||||||
|
},
|
||||||
|
spec: {
|
||||||
|
allowComment: true,
|
||||||
|
baseSnapshot: "",
|
||||||
|
categories: [],
|
||||||
|
cover: "",
|
||||||
|
deleted: false,
|
||||||
|
excerpt: {
|
||||||
|
autoGenerate: true,
|
||||||
|
raw: "",
|
||||||
|
},
|
||||||
|
headSnapshot: "",
|
||||||
|
htmlMetas: [],
|
||||||
|
owner: "",
|
||||||
|
pinned: false,
|
||||||
|
priority: 0,
|
||||||
|
publish: false,
|
||||||
|
publishTime: "",
|
||||||
|
releaseSnapshot: "",
|
||||||
|
slug: "",
|
||||||
|
tags: [],
|
||||||
|
template: "",
|
||||||
|
title: "",
|
||||||
|
visible: "PUBLIC",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HaloService;
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import type { Vault } from "obsidian";
|
||||||
|
import ImageUploader from "../service/image-uploader";
|
||||||
|
import type { HaloSite } from "../settings";
|
||||||
|
import { extractImageReferences, getAbsolutePath, replaceImagePaths } from "../utils/image";
|
||||||
|
|
||||||
|
export class ImageService {
|
||||||
|
private uploader: ImageUploader;
|
||||||
|
|
||||||
|
constructor(site: HaloSite) {
|
||||||
|
this.uploader = new ImageUploader(site.url, site.token);
|
||||||
|
}
|
||||||
|
|
||||||
|
async uploadLocalImages(
|
||||||
|
content: string,
|
||||||
|
vault: Vault,
|
||||||
|
currentFilePath: string,
|
||||||
|
): Promise<{ processedContent: string; successCount: number; failCount: number }> {
|
||||||
|
const imageReferences = extractImageReferences(content);
|
||||||
|
|
||||||
|
if (imageReferences.length === 0) {
|
||||||
|
return { processedContent: content, successCount: 0, failCount: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const localImages = imageReferences.filter(
|
||||||
|
(ref) => !ref.path.startsWith("http://") && !ref.path.startsWith("https://") && !ref.path.startsWith("data:"),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (localImages.length === 0) {
|
||||||
|
return { processedContent: content, successCount: 0, failCount: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const absolutePaths = localImages
|
||||||
|
.map((ref) => ({
|
||||||
|
original: ref.path,
|
||||||
|
absolute: getAbsolutePath(vault, ref.path, currentFilePath),
|
||||||
|
}))
|
||||||
|
.filter((item) => item.absolute !== null) as { original: string; absolute: string }[];
|
||||||
|
|
||||||
|
if (absolutePaths.length === 0) {
|
||||||
|
return { processedContent: content, successCount: 0, failCount: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const pathMapping = await this.uploader.uploadImages(
|
||||||
|
absolutePaths.map((item) => item.absolute),
|
||||||
|
vault,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (pathMapping.size === 0) {
|
||||||
|
return { processedContent: content, successCount: 0, failCount: absolutePaths.length };
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapping = new Map<string, string>();
|
||||||
|
absolutePaths.forEach((item) => {
|
||||||
|
const remoteUrl = pathMapping.get(item.absolute);
|
||||||
|
if (remoteUrl) {
|
||||||
|
mapping.set(item.original, remoteUrl);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const processedContent = replaceImagePaths(content, mapping);
|
||||||
|
const successCount = mapping.size;
|
||||||
|
const failCount = absolutePaths.length - successCount;
|
||||||
|
|
||||||
|
return { processedContent, successCount, failCount };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import type { App, TFile } from "obsidian";
|
||||||
|
import { randomUUID } from "../utils/id";
|
||||||
|
import type { HaloClient } from "./client";
|
||||||
|
import type { Post, Snapshot, Content } from "./types";
|
||||||
|
import { HaloError } from "./error";
|
||||||
|
|
||||||
|
export class PostService {
|
||||||
|
constructor(
|
||||||
|
private app: App,
|
||||||
|
private client: HaloClient,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async getPost(name: string): Promise<{ post: Post; content: Content } | undefined> {
|
||||||
|
try {
|
||||||
|
const post = await this.client.get<Post>(`/apis/uc.api.content.halo.run/v1alpha1/posts/${name}`);
|
||||||
|
|
||||||
|
const snapshot = await this.client.get<Snapshot>(
|
||||||
|
`/apis/uc.api.content.halo.run/v1alpha1/posts/${name}/draft?patched=true`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { "content.halo.run/patched-content": patchedContent, "content.halo.run/patched-raw": patchedRaw } =
|
||||||
|
snapshot.metadata.annotations || {};
|
||||||
|
|
||||||
|
const { rawType } = snapshot.spec || {};
|
||||||
|
|
||||||
|
const content: Content = {
|
||||||
|
content: patchedContent,
|
||||||
|
raw: patchedRaw,
|
||||||
|
rawType,
|
||||||
|
};
|
||||||
|
|
||||||
|
return { post, content };
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof HaloError && error.isNotFound()) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async createPost(params: Post): Promise<Post> {
|
||||||
|
return await this.client.post<Post>("/apis/uc.api.content.halo.run/v1alpha1/posts", params);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updatePost(name: string, params: Post): Promise<void> {
|
||||||
|
await this.client.put(`/apis/uc.api.content.halo.run/v1alpha1/posts/${name}`, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deletePost(name: string): Promise<void> {
|
||||||
|
await this.client.delete(`/apis/uc.api.content.halo.run/v1alpha1/posts/${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async changePublishStatus(name: string, publish: boolean): Promise<void> {
|
||||||
|
await this.client.putVoid(
|
||||||
|
`/apis/uc.api.content.halo.run/v1alpha1/posts/${name}/${publish ? "publish" : "unpublish"}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createPostFromFile(
|
||||||
|
file: TFile,
|
||||||
|
content: Content,
|
||||||
|
frontmatter: Record<string, unknown>,
|
||||||
|
): Promise<Post> {
|
||||||
|
const params: Post = {
|
||||||
|
apiVersion: "content.halo.run/v1alpha1",
|
||||||
|
kind: "Post",
|
||||||
|
metadata: {
|
||||||
|
annotations: {
|
||||||
|
"content.halo.run/content-json": JSON.stringify(content),
|
||||||
|
},
|
||||||
|
name: randomUUID(),
|
||||||
|
},
|
||||||
|
spec: {
|
||||||
|
allowComment: true,
|
||||||
|
baseSnapshot: "",
|
||||||
|
categories: [],
|
||||||
|
cover: (frontmatter.cover as string) || "",
|
||||||
|
deleted: false,
|
||||||
|
excerpt: {
|
||||||
|
autoGenerate: frontmatter.excerpt ? false : true,
|
||||||
|
raw: (frontmatter.excerpt as string) || "",
|
||||||
|
},
|
||||||
|
headSnapshot: "",
|
||||||
|
htmlMetas: [],
|
||||||
|
owner: "",
|
||||||
|
pinned: false,
|
||||||
|
priority: 0,
|
||||||
|
publish: false,
|
||||||
|
publishTime: "",
|
||||||
|
releaseSnapshot: "",
|
||||||
|
slug: (frontmatter.slug as string) || "",
|
||||||
|
tags: [],
|
||||||
|
template: "",
|
||||||
|
title: (frontmatter.title as string) || file.basename,
|
||||||
|
visible: "PUBLIC",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return await this.createPost(params);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
import { slugify } from "transliteration";
|
||||||
|
import type { HaloClient } from "./client";
|
||||||
|
import type { Category, Tag } from "./types";
|
||||||
|
|
||||||
|
export class TaxonomyService {
|
||||||
|
constructor(private client: HaloClient) {}
|
||||||
|
|
||||||
|
async getCategories(): Promise<Category[]> {
|
||||||
|
const data = await this.client.get<{ items: Category[] }>("/apis/content.halo.run/v1alpha1/categories");
|
||||||
|
return data.items;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTags(): Promise<Tag[]> {
|
||||||
|
const data = await this.client.get<{ items: Tag[] }>("/apis/content.halo.run/v1alpha1/tags");
|
||||||
|
return data.items;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCategoryNames(displayNames: string[]): Promise<string[]> {
|
||||||
|
const allCategories = await this.getCategories();
|
||||||
|
|
||||||
|
const notExistDisplayNames = displayNames.filter(
|
||||||
|
(name) => !allCategories.find((item) => item.spec.displayName === name),
|
||||||
|
);
|
||||||
|
|
||||||
|
const newCategories = await Promise.all(
|
||||||
|
notExistDisplayNames.map((name, index) =>
|
||||||
|
this.createCategory(name, slugify(name, { trim: true }), allCategories.length + index),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const existNames = displayNames
|
||||||
|
.map((name) => {
|
||||||
|
const found = allCategories.find((item) => item.spec.displayName === name);
|
||||||
|
return found ? found.metadata.name : undefined;
|
||||||
|
})
|
||||||
|
.filter(Boolean) as string[];
|
||||||
|
|
||||||
|
return [...existNames, ...newCategories.map((item) => item.metadata.name)];
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCategoryDisplayNames(names?: string[]): Promise<string[]> {
|
||||||
|
if (!names || names.length === 0) return [];
|
||||||
|
const categories = await this.getCategories();
|
||||||
|
return names
|
||||||
|
.map((name) => {
|
||||||
|
const found = categories.find((item) => item.metadata.name === name);
|
||||||
|
return found ? found.spec.displayName : undefined;
|
||||||
|
})
|
||||||
|
.filter(Boolean) as string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
async createCategory(displayName: string, slug: string, priority: number): Promise<Category> {
|
||||||
|
return await this.client.post<Category>("/apis/content.halo.run/v1alpha1/categories", {
|
||||||
|
spec: {
|
||||||
|
displayName,
|
||||||
|
slug,
|
||||||
|
description: "",
|
||||||
|
cover: "",
|
||||||
|
template: "",
|
||||||
|
priority,
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
apiVersion: "content.halo.run/v1alpha1",
|
||||||
|
kind: "Category",
|
||||||
|
metadata: { name: "", generateName: "category-" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateCategory(name: string, displayName: string, slug: string, priority: number): Promise<void> {
|
||||||
|
const category = await this.client.get<Category>(`/apis/content.halo.run/v1alpha1/categories/${name}`);
|
||||||
|
category.spec.displayName = displayName;
|
||||||
|
category.spec.slug = slug;
|
||||||
|
category.spec.priority = priority;
|
||||||
|
await this.client.put(`/apis/content.halo.run/v1alpha1/categories/${name}`, category);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteCategory(name: string): Promise<void> {
|
||||||
|
await this.client.delete(`/apis/content.halo.run/v1alpha1/categories/${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTagNames(displayNames: string[]): Promise<string[]> {
|
||||||
|
const allTags = await this.getTags();
|
||||||
|
|
||||||
|
const notExistDisplayNames = displayNames.filter(
|
||||||
|
(name) => !allTags.find((item) => item.spec.displayName === name),
|
||||||
|
);
|
||||||
|
|
||||||
|
const newTags = await Promise.all(
|
||||||
|
notExistDisplayNames.map((name) => this.createTag(name, slugify(name, { trim: true }), "#ffffff")),
|
||||||
|
);
|
||||||
|
|
||||||
|
const existNames = displayNames
|
||||||
|
.map((name) => {
|
||||||
|
const found = allTags.find((item) => item.spec.displayName === name);
|
||||||
|
return found ? found.metadata.name : undefined;
|
||||||
|
})
|
||||||
|
.filter(Boolean) as string[];
|
||||||
|
|
||||||
|
return [...existNames, ...newTags.map((item) => item.metadata.name)];
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTagDisplayNames(names?: string[]): Promise<string[]> {
|
||||||
|
if (!names || names.length === 0) return [];
|
||||||
|
const tags = await this.getTags();
|
||||||
|
return names
|
||||||
|
.map((name) => {
|
||||||
|
const found = tags.find((item) => item.metadata.name === name);
|
||||||
|
return found ? found.spec.displayName : undefined;
|
||||||
|
})
|
||||||
|
.filter(Boolean) as string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
async createTag(displayName: string, slug: string, color: string): Promise<Tag> {
|
||||||
|
return await this.client.post<Tag>("/apis/content.halo.run/v1alpha1/tags", {
|
||||||
|
spec: {
|
||||||
|
displayName,
|
||||||
|
slug,
|
||||||
|
color: color || "#ffffff",
|
||||||
|
cover: "",
|
||||||
|
},
|
||||||
|
apiVersion: "content.halo.run/v1alpha1",
|
||||||
|
kind: "Tag",
|
||||||
|
metadata: { name: "", generateName: "tag-" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateTag(name: string, displayName: string, slug: string, color: string): Promise<void> {
|
||||||
|
const tag = await this.client.get<Tag>(`/apis/content.halo.run/v1alpha1/tags/${name}`);
|
||||||
|
tag.spec.displayName = displayName;
|
||||||
|
tag.spec.slug = slug;
|
||||||
|
tag.spec.color = color || "#ffffff";
|
||||||
|
await this.client.put(`/apis/content.halo.run/v1alpha1/tags/${name}`, tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteTag(name: string): Promise<void> {
|
||||||
|
await this.client.delete(`/apis/content.halo.run/v1alpha1/tags/${name}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
export interface Post {
|
||||||
|
apiVersion: string;
|
||||||
|
kind: string;
|
||||||
|
metadata: {
|
||||||
|
annotations: Record<string, string>;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
spec: {
|
||||||
|
allowComment: boolean;
|
||||||
|
baseSnapshot: string;
|
||||||
|
categories: string[];
|
||||||
|
cover: string;
|
||||||
|
deleted: boolean;
|
||||||
|
excerpt: {
|
||||||
|
autoGenerate: boolean;
|
||||||
|
raw: string;
|
||||||
|
};
|
||||||
|
headSnapshot: string;
|
||||||
|
htmlMetas: unknown[];
|
||||||
|
owner: string;
|
||||||
|
pinned: boolean;
|
||||||
|
priority: number;
|
||||||
|
publish: boolean;
|
||||||
|
publishTime: string;
|
||||||
|
releaseSnapshot: string;
|
||||||
|
slug: string;
|
||||||
|
tags: string[];
|
||||||
|
template: string;
|
||||||
|
title: string;
|
||||||
|
visible: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Snapshot {
|
||||||
|
metadata: {
|
||||||
|
annotations?: Record<string, string>;
|
||||||
|
};
|
||||||
|
spec?: {
|
||||||
|
rawType?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Content {
|
||||||
|
rawType: string;
|
||||||
|
raw: string;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Category {
|
||||||
|
metadata: {
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
spec: {
|
||||||
|
displayName: string;
|
||||||
|
slug: string;
|
||||||
|
description: string;
|
||||||
|
cover: string;
|
||||||
|
template: string;
|
||||||
|
priority: number;
|
||||||
|
children: unknown[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Tag {
|
||||||
|
metadata: {
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
spec: {
|
||||||
|
displayName: string;
|
||||||
|
slug: string;
|
||||||
|
color: string;
|
||||||
|
cover: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user