feat(halo-plugin): 新增文章导入、删除和标签分类管理功能

添加从本地 Markdown 文件导入创建文章的功能,支持文件预览和自动发布选项
新增文章删除命令,支持选择性删除 Halo 文章或本地文件
添加标签和分类管理功能,支持创建、编辑和删除操作
更新国际化文案,支持新功能的多种语言界面
扩展服务层以支持文章导入、删除和标签分类管理 API 调用
更新插件版本至 2.1.1 并更新作者信息
This commit is contained in:
2026-04-26 17:23:35 +08:00
parent 8ccc32be0b
commit 5c4a16dc3a
28 changed files with 2161 additions and 99 deletions
+7 -2
View File
@@ -2,10 +2,15 @@
"sites": [
{
"name": "Serendipity",
"url": "https://blog.metarl.cc.cd",
"url": "http://101.133.128.193:8091",
"default": true,
"token": "pat_eyJraWQiOiI3REcwM05yaUJ0VVBPM2oxbkN4T0hUNXZsSWlJRTVJYndGNWo2NW43eTRBIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwczovL2Jsb2cubWV0YXJsLmNjLmNkLyIsInN1YiI6ImxpdWhhbmd5diIsImlhdCI6MTc3NzE5MDgyNSwianRpIjoiMTI4NDdhZWEtY2Q0YS04OTZjLTQ3ODMtYTc4ZDc5MTRhN2Y1IiwicGF0X25hbWUiOiJwYXQteGF1NWJoZmsifQ.WYXv8eXrc9C5PXMbC58LpjzhC1OFgZyOQ2iKc-_M8xfbhD4pgsdb1Q1uGK16_h45x7OAHcA8eThfdqUJGcuYeiYasoffBfOIVRj2l-YjHHP4RqE_tks_LP7GnZoE6jzz52ZkvmiiPwHt7eusIF563CYSr2vHOq8iAcFZPLHtoviokwG8cYmxKntIkdDU3MtvhzKOrUwEnQWjzbNwYeoYDr_9jLvQegoxrX7uosJPmVbMvw2IkD1xFvTKITxreMRiPzZIH2HSBhVMW9vpu_l7FaOFeQU2Cz_hAuuayCj8FIgZ55zA0lcHPYksHWMTa5f5BelPJfms_Ksn9rpVXfiggC6R7TfCRUY2QPY1IDZCS6v3omWjBBTfC2CgYDtiVE2Omec5kQ7XIgIAkp_4tFdVay1_1qLkJXcl9hDVs9gPNgyNMDKlw30mhU0vxM9SbEHR1WtCFEChYyPso84dWcy23NDY5aCbn-49Xr2EQXhjD-3EZ4rs1V13j-LGEQdsKzB72ERhq2Cv9YPjRR0qsvk9rZ5w4Mj8xQ_3ns57vHinmz4jsm7BDa1FyEd-hmgZXt6P2pkyAH3kQzJLaDjglisEej90hhXvQbImf4hDldNpLaKDH3z3cLFqGrujrGtwEE7x0czG5rfxWutUDXcgztSgQppZfk3H71147mChEpZSW_o"
}
],
"publishByDefault": true
"publishByDefault": true,
"imageUpload": {
"enabled": true,
"uploadPath": "",
"preserveOriginal": false
}
}
+6 -6
View File
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -4,7 +4,7 @@
"version": "1.1.1",
"minAppVersion": "0.15.0",
"description": "Halo's Obsidian integration supports publishing content to Halo sites",
"author": "Ryan Wang",
"authorUrl": "https://github.com/ruibaby",
"author": "刘航宇 (LHY)",
"authorUrl": "https://github.com/LHY0125/obsidian-halo",
"isDesktopOnly": false
}
+38 -10
View File
@@ -1,24 +1,56 @@
{
"recentFiles": [
{
"basename": "SEO分析报告",
"path": "博客/SEO分析报告.md"
"basename": "Git团队协作指南(精简版)",
"path": "博客/Git团队协作指南(精简版).md"
},
{
"basename": "Git团队协作指南大纲",
"path": "博客/Git团队协作指南大纲.md"
},
{
"basename": "uv工具推荐博客",
"path": "博客/uv工具推荐博客.md"
},
{
"basename": "OpenClaw安装教程",
"path": "博客/OpenClaw安装教程.md"
},
{
"basename": "LinearRegression线性回归",
"path": "博客/LinearRegression线性回归.md"
},
{
"basename": "Git团队协作指南",
"path": "博客/Git团队协作指南.md"
},
{
"basename": "大数据技术栈",
"path": "博客/大数据技术栈.md"
},
{
"basename": "大模型赋能架构设计",
"path": "博客/大模型赋能架构设计.md"
},
{
"basename": "博客爬取报告",
"path": "博客/博客爬取报告.md"
},
{
"basename": "文章列表",
"path": "博客/文章列表.md"
},
{
"basename": "从全连接层到卷积",
"path": "博客/从全连接层到卷积.md"
},
{
"basename": "CLI在AI时代的浴火重生",
"path": "博客/CLI在AI时代的浴火重生.md"
"basename": "SEO分析报告",
"path": "博客/SEO分析报告.md"
},
{
"basename": "Git团队协作指南(精简版)",
"path": "博客/Git团队协作指南(精简版).md"
"basename": "CLI在AI时代的浴火重生",
"path": "博客/CLI在AI时代的浴火重生.md"
},
{
"basename": "halo",
@@ -40,10 +72,6 @@
"basename": "视觉语言模型技术综述",
"path": "博客/视觉语言模型技术综述.md"
},
{
"basename": "大数据技术栈",
"path": "博客/大数据技术栈.md"
},
{
"basename": "小组会议",
"path": "小组会议/26.04.04/小组会议.md"
+19 -20
View File
@@ -13,12 +13,12 @@
"state": {
"type": "markdown",
"state": {
"file": "博客/SEO分析报告.md",
"file": "博客/Git团队协作指南(精简版).md",
"mode": "source",
"source": false
},
"icon": "lucide-file",
"title": "SEO分析报告"
"title": "Git团队协作指南(精简版)"
}
}
]
@@ -193,8 +193,7 @@
}
],
"direction": "horizontal",
"width": 426.5,
"collapsed": true
"width": 426.5
},
"left-ribbon": {
"hiddenItems": {
@@ -221,20 +220,29 @@
},
"active": "e7a7b303c61786dc",
"lastOpenFiles": [
"博客/Git团队协作指南大纲.md",
"博客/uv工具推荐博客.md",
"博客/OpenClaw安装教程.md",
"博客/LinearRegression线性回归.md",
"博客/Git团队协作指南.md",
"博客/大数据技术栈.md",
"博客/大模型赋能架构设计.md",
"博客/博客爬取报告.md",
"obsidian-halo/src/commands/manage-taxonomy.ts",
"博客/文章列表.md",
"博客/从全连接层到卷积.md",
"obsidian-halo/src/modals/category-manager-modal.ts",
"obsidian-halo/src/modals/tag-manager-modal.ts",
"博客/SEO分析报告.md",
"obsidian-halo/src/commands/import-markdown.ts",
"obsidian-halo/src/modals/file-preview-modal.ts",
"obsidian-halo/src/modals",
"obsidian-halo/src/commands",
"博客/CLI在AI时代的浴火重生.md",
"博客/Git团队协作指南(精简版).md",
"obsidian-halo/src/service/image-uploader.ts",
"obsidian-halo/src/utils/image.ts",
"博客/check-credentials.ps1",
"博客/get-halo-token.js",
"博客/halo-upload.py",
"obsidian-halo/node_modules/@halo-dev",
"obsidian-halo/node_modules/@rslib",
"obsidian-halo/node_modules/@biomejs",
"obsidian-halo/node_modules/@types",
"obsidian-halo/node_modules",
"obsidian-halo-zip-test/images/pat-zh.png",
"obsidian-halo-zip-test/images/settings-zh.png",
"obsidian-halo-zip-test/images/settings-en.png",
@@ -255,18 +263,9 @@
"博客/articles/article-02-python-efficiency.md",
"博客/articles/article-01-ai-beginner-guide.md",
"博客/视觉语言模型技术综述.md",
"博客/大数据技术栈.md",
"find-skills-0.1.0/SKILL.md",
"小组会议/26.04.04/小组会议.md",
"小组会议/26.03.21 - 副本/小组会议.md",
"小组会议/26.03.21/小组会议.md",
"小组会议/26.02.28/小组会议.md",
"小组会议/25.10.26/小组成员基本信息/小组成员信息/王宇晗.md",
"小组会议/25.10.26/小组成员基本信息/小组成员信息/彭依萍.md",
"小组会议/25.10.26/小组成员基本信息/小组成员信息/赵帅尧.md",
"小组会议/25.10.26/小组成员基本信息/职位安排.md",
"小组会议/25.10.26/小组成员基本信息/小组成员信息/李俊杰.md",
"小组会议/26.02.06/小组会议.md",
"未命名.canvas"
]
}
@@ -0,0 +1,211 @@
# Obsidian Halo 插件功能增强计划
## 一、功能对比分析
### Halo CLI 已有功能
| 分类 | 功能 | CLI 支持 |
|------|------|---------|
| **文章管理** | 发布文章 | ✅ 完整 |
| | 更新文章 | ✅ 完整 |
| | 删除文章 | ✅ 完整 |
| | 列出文章列表 | ✅ 完整 |
| | 查看文章详情 | ✅ 完整 |
| | 批量操作 | ✅ PowerShell 脚本 |
| **导出/导入** | 导出为 JSON | ✅ 完整 |
| | 导出为 Markdown | ✅ 完整 |
| | 从 JSON 导入 | ✅ 完整 |
| | 从 Markdown 导入 | ✅ 完整 |
| **标签管理** | 列出标签 | ✅ 完整 |
| | 创建标签 | ✅ 完整 |
| | 更新标签 | ✅ 完整 |
| | 删除标签 | ✅ 完整 |
| | 为文章设置标签 | ✅ 完整 |
| **分类管理** | 列出分类 | ✅ 完整 |
| | 创建分类 | ✅ 完整 |
| | 更新分类 | ✅ 完整 |
| | 删除分类 | ✅ 完整 |
| | 设置优先级 | ✅ 完整 |
| **其他内容** | 单页管理 | ✅ 完整 |
| | 搜索内容 | ✅ 完整 |
| **系统管理** | 附件管理 | ✅ 完整 |
| | 备份管理 | ✅ 完整 |
| | 动态管理 | ✅ 完整 |
| | 评论管理 | ✅ 完整 |
| | 插件管理 | ✅ 完整 |
| | 主题管理 | ✅ 完整 |
---
## 二、Obsidian 插件当前功能
| 分类 | 功能 | 状态 |
|------|------|------|
| **文章管理** | 发布文章 | ✅ 已实现 |
| | 更新文章(同步 Halo 到本地) | ✅ 已实现 |
| | 列出文章列表 | ❌ 缺失 |
| | 查看文章详情 | ❌ 缺失 |
| | 删除文章 | ❌ 缺失 |
| | 批量发布 | ❌ 缺失 |
| **图片上传** | 自动上传图片 | ✅ 已实现(新增) |
| **标签/分类** | 为文章设置标签 | ✅ 已实现 |
| | 为文章设置分类 | ✅ 已实现 |
| | 创建标签 | ❌ 缺失 |
| | 创建分类 | ❌ 缺失 |
| | 管理标签/分类 | ❌ 缺失 |
| **导出/导入** | 从 Markdown 导入 | ❌ 缺失 |
| | 导出为 Markdown | ❌ 缺失 |
| | 导出为 JSON | ❌ 缺失 |
| **其他** | 单页管理 | ❌ 缺失 |
| | 搜索 | ❌ 缺失 |
---
## 三、功能优先级建议
### 高优先级(核心功能)
1. **文章列表查看**
- 在命令面板中显示 Halo 文章列表
- 支持分页、筛选(草稿/已发布)
- 快速预览文章信息
2. **导入功能增强**
- 支持从本地 Markdown 文件导入创建文章
- 智能解析文件路径和文件名
3. **文章删除功能**
- 添加删除命令(需二次确认)
- 支持删除本地文件或仅删除 Halo 文章
### 中优先级(常用功能)
4. **批量操作**
- 批量发布多篇文章
- 批量同步已发布文章
- 批量管理(删除、更新标签等)
5. **标签/分类管理**
- 创建新标签/分类
- 查看现有标签/分类列表
- 更新/删除标签/分类
6. **导出功能**
- 导出文章为 Markdown
- 导出文章为 JSON(备份用)
### 低优先级(增强功能)
7. **搜索功能**
- 搜索 Halo 文章
- 快速定位文章
8. **单页管理**
- 发布/管理独立页面
- 与文章类似的工作流
---
## 四、实施步骤
### 第一阶段:核心补充(1-2 周)
#### 任务 1:文章列表功能
- [ ] 创建文章列表 Modal
- [ ] 实现分页、筛选功能
- [ ] 添加快速操作按钮(查看、编辑、删除)
- [ ] 集成到命令面板
#### 任务 2:导入功能
- [ ] 实现从文件导入创建文章
- [ ] 智能解析 Markdown 文件
- [ ] 支持选择文件对话框
#### 任务 3:删除功能
- [ ] 添加删除命令
- [ ] 二次确认机制
- [ ] 删除选项(仅 Halo / 仅本地 / 全部)
### 第二阶段:常用功能(2-4 周)
#### 任务 4:批量操作
- [ ] 批量选择界面
- [ ] 批量发布队列
- [ ] 进度显示和错误处理
#### 任务 5:标签/分类管理
- [ ] 标签管理 Modal
- [ ] 分类管理 Modal
- [ ] 创建/编辑/删除功能
#### 任务 6:导出功能
- [ ] 导出为 Markdown
- [ ] 导出为 JSON(备份)
- [ ] 选择保存路径
### 第三阶段:增强功能(4+ 周)
#### 任务 7:搜索功能
- [ ] 文章搜索 Modal
- [ ] 快速筛选和定位
#### 任务 8:单页管理
- [ ] 单页发布命令
- [ ] 单页列表查看
- [ ] 单页导入/导出
---
## 五、技术实现建议
### API 端点
| 功能 | API 端点 |
|------|----------|
| 列出文章 | `GET /apis/uc.api.content.halo.run/v1alpha1/posts` |
| 获取文章 | `GET /apis/uc.api.content.halo.run/v1alpha1/posts/{name}` |
| 删除文章 | `DELETE /apis/uc.api.content.halo.run/v1alpha1/posts/{name}` |
| 列出标签 | `GET /apis/content.halo.run/v1alpha1/tags` |
| 创建标签 | `POST /apis/content.halo.run/v1alpha1/tags` |
| 列出分类 | `GET /apis/content.halo.run/v1alpha1/categories` |
| 创建分类 | `POST /apis/content.halo.run/v1alpha1/categories` |
### 文件结构建议
```
src/
├── commands/ # 命令相关
│ ├── publish.ts
│ ├── delete.ts
│ ├── list.ts
│ ├── import.ts
│ └── export.ts
├── modals/ # 弹窗相关
│ ├── post-list-modal.ts
│ ├── tag-manager-modal.ts
│ ├── category-manager-modal.ts
│ └── search-modal.ts
├── service/ # 服务层
│ ├── index.ts # 现有
│ └── image-uploader.ts # 现有
└── utils/ # 工具层
├── image.ts # 现有
└── ...
```
---
## 六、建议实施顺序
1. **先实现文章列表**(最常用)
2. **再实现导入功能**(补全工作流)
3. **实现删除功能**(完整性)
4. **实现批量操作**(效率提升)
5. **实现标签/分类管理**(内容组织)
6. **实现导出功能**(备份需求)
7. **实现搜索功能**(快速定位)
8. **实现单页管理**(可选)
---
请确认需要实现哪些功能,我将制定详细的技术方案。
+33
View File
@@ -0,0 +1,33 @@
# Markdown 文件导入功能检查清单
## 代码实现检查
- [ ] `src/commands/import-markdown.ts` 文件创建完成
- [ ] 文件选择器正确实现(仅显示 .md 文件)
- [ ] 文件读取和解析逻辑正确
- [ ] 文章创建逻辑正确调用 Halo API
- [ ] `src/modals/file-preview-modal.ts` 文件创建完成
- [ ] 文件内容预览正确显示
- [ ] frontmatter 信息正确显示
- [ ] 导入/取消按钮功能正常
- [ ] "导入后发布"选项功能正常
## 集成检查
- [ ] `src/main.ts` 中命令注册完成
- [ ] 命令名称符合国际化规范
- [ ] 命令在命令面板中可见
## 国际化检查
- [ ] 英文文案添加完成
- [ ] 简体中文文案添加完成
- [ ] 繁体中文文案添加完成
## 测试检查
- [ ] 编译通过无错误
- [ ] 插件文件正确复制到 Obsidian 插件目录
- [ ] 文件选择器测试通过
- [ ] 导入功能测试通过(创建草稿)
- [ ] 导入功能测试通过(导入并发布)
+96
View File
@@ -0,0 +1,96 @@
# Markdown 文件导入功能规格说明
## Why
当前 Obsidian Halo 插件只支持从 Obsidian 发布文章到 Halo,但没有提供从本地 Markdown 文件导入创建文章的功能。用户可能希望:
1. 直接从文件系统导入已存在的 Markdown 文件
2. 从其他平台迁移文章时批量导入
3. 快速创建新文章时选择本地文件而非在编辑器中编写
## What Changes
- 新增命令:直接从 Markdown 文件导入创建 Halo 文章
- 新增文件选择器:支持选择本地 .md 文件
- 新增预览功能:导入前预览文件内容和 frontmatter
- 新增选项:导入时是否立即发布
## Impact
- Affected specs:
- 文章发布功能:提供新的文章创建入口
- 设置功能:可能需要添加导入相关配置
- Affected code:
- 新增 `src/commands/import-markdown.ts`:导入命令
- 新增 `src/modals/file-preview-modal.ts`:文件预览弹窗
- 修改 `src/main.ts`:注册新命令
- 修改 `src/i18n/locales/*.json`:国际化文案
## ADDED Requirements
### Requirement: 从 Markdown 文件导入
系统 SHALL 提供从本地 Markdown 文件导入创建 Halo 文章的功能。
#### Scenario: 选择文件导入
- **WHEN** 用户执行"从 Markdown 文件导入"命令
- **THEN** 系统显示文件选择器,允许用户选择 .md 文件
#### Scenario: 预览和确认
- **WHEN** 用户选择了一个 Markdown 文件
- **THEN** 系统显示文件内容预览(标题、内容摘要、frontmatter
- **AND** 显示"导入"和"取消"按钮
#### Scenario: 导入成功
- **WHEN** 用户点击"导入"按钮
- **THEN** 系统读取文件内容
- **AND** 解析 frontmatter 和正文
- **AND** 调用 Halo API 创建文章
- **AND** 显示导入成功通知
#### Scenario: 导入选项
- **WHEN** 用户可以勾选"导入后立即发布"选项
- **THEN** 文章创建后自动发布
## MODIFIED Requirements
### Requirement: 命令面板扩展
现有命令列表扩展,新增:
- `Halo: 从 Markdown 文件导入` - 打开文件选择器进行导入
## REMOVED Requirements
---
## 技术实现细节
### Obsidian 文件选择器 API
```typescript
const filePicker = app.createFilePicker();
filePicker.setFilter(file => file.extension === 'md');
filePicker.onChange = async (files) => {
// 处理选中的文件
};
filePicker.open();
```
### 文件读取
```typescript
const content = await app.vault.read(file);
```
### Frontmatter 解析
参考现有的 `src/utils/yaml.ts` 实现,或使用 gray-matter 库。
### API 端点
```
POST /apis/uc.api.content.halo.run/v1alpha1/posts
```
请求体格式与发布功能相同。
+47
View File
@@ -0,0 +1,47 @@
# Markdown 文件导入功能任务清单
## 任务列表
- [ ] 任务 1:创建导入命令模块 `src/commands/import-markdown.ts`
- [ ] 子任务 1.1:实现 Obsidian 文件选择器调用
- [ ] 子任务 1.2:实现文件读取和解析
- [ ] 子任务 1.3:实现文章创建逻辑
- [ ] 任务 2:创建文件预览弹窗 `src/modals/file-preview-modal.ts`
- [ ] 子任务 2.1:显示文件内容预览
- [ ] 子任务 2.2:显示 frontmatter 信息
- [ ] 子任务 2.3:添加导入/取消按钮
- [ ] 子任务 2.4:添加"导入后发布"选项
- [ ] 任务 3:更新主程序注册命令 `src/main.ts`
- [ ] 子任务 3.1:导入新命令模块
- [ ] 子任务 3.2:注册"从 Markdown 文件导入"命令
- [ ] 任务 4:添加国际化文案
- [ ] 子任务 4.1:更新英文文案 `src/i18n/locales/en.json`
- [ ] 子任务 4.2:更新简体中文文案 `src/i18n/locales/zh-cn.json`
- [ ] 子任务 4.3:更新繁体中文文案 `src/i18n/locales/zh-tw.json`
- [ ] 任务 5:编译和测试
- [ ] 子任务 5.1:运行 `pnpm build` 确保编译通过
- [ ] 子任务 5.2:复制编译产物到插件目录
- [ ] 子任务 5.3:手动测试导入功能
---
## 任务依赖关系
- [任务 2] 和 [任务 3] 可并行开发
- [任务 1] 依赖 [任务 2] 完成
- [任务 4] 可与任何任务并行
- [任务 5] 需要 [任务 1]、[任务 3] 完成
---
## 实现顺序建议
1. 先完成 [任务 2](文件预览弹窗)- 核心 UI
2. 再完成 [任务 1](导入命令模块)- 核心逻辑
3. 然后完成 [任务 3](注册命令)
4. [任务 4] 可与 [任务 1] 并行
5. 最后 [任务 5](测试)
+2 -2
View File
@@ -4,7 +4,7 @@
"version": "1.1.1",
"minAppVersion": "0.15.0",
"description": "Halo's Obsidian integration supports publishing content to Halo sites",
"author": "Ryan Wang",
"authorUrl": "https://github.com/ruibaby",
"author": "刘航宇 (LHY)",
"authorUrl": "https://github.com/LHY0125/obsidian-halo",
"isDesktopOnly": false
}
+5 -5
View File
@@ -1,7 +1,7 @@
{
"name": "@halo-dev/obsidian-halo",
"private": true,
"version": "1.1.1",
"version": "2.1.1",
"description": "Halo's Obsidian integration supports publishing content to Halo sites",
"main": "main.js",
"scripts": {
@@ -10,12 +10,12 @@
"version": "node version-bump.mjs && git add manifest.json versions.json",
"check": "biome check --write src/"
},
"author": "@halo-dev",
"author": "LHY",
"maintainers": [
{
"name": "Ryan Wang",
"email": "i@ryanc.cc",
"url": "https://github.com/ruibaby"
"name": "刘航宇",
"email": "3364451258@qq.com",
"url": "https://github.com/LHY0125/obsidian-halo"
}
],
"license": "MIT",
+96
View File
@@ -0,0 +1,96 @@
import i18next from "i18next";
import { Notice } from "obsidian";
import HaloService from "../service";
import { openSiteSelectionModal } from "../site-selection-modal";
import { openDeleteConfirmModal } from "../modals/delete-confirm-modal";
import type HaloPlugin from "../main";
import type { HaloSite } from "../settings";
export interface DeleteOptions {
deleteHalo: boolean;
deleteLocal: boolean;
}
export async function deletePost(plugin: HaloPlugin): Promise<void> {
try {
const { activeEditor } = plugin.app.workspace;
if (!activeEditor || !activeEditor.file) {
new Notice(i18next.t("common.error_no_active_editor"));
return;
}
const matterData = plugin.app.metadataCache.getFileCache(activeEditor.file)?.frontmatter;
if (!matterData?.halo?.name) {
new Notice(i18next.t("command.delete_post.error_not_published"));
return;
}
if (!matterData?.halo?.site) {
new Notice(i18next.t("service.error_site_not_match"));
return;
}
const site = plugin.settings.sites.find((s) => s.url === matterData.halo.site);
if (!site) {
new Notice(i18next.t("command.delete_post.error_no_matched_site"));
return;
}
const title = matterData.title || activeEditor.file.basename;
const result = await openDeleteConfirmModal(plugin, site, title, matterData.halo.name);
if (!result.success) {
return;
}
const service = new HaloService(plugin.app, plugin.settings, site);
if (result.options.deleteHalo) {
const success = await service.deletePost(matterData.halo.name);
if (!success) {
new Notice(i18next.t("service.error_delete_failed"));
return;
}
}
if (result.options.deleteLocal) {
await plugin.app.vault.delete(activeEditor.file);
}
new Notice(i18next.t("service.notice_delete_success"));
} catch (error) {
console.error("[HaloPlugin] Delete post failed:", error);
new Notice(i18next.t("service.error_delete_failed"));
}
}
export async function deletePostByName(plugin: HaloPlugin, site: HaloSite, postName: string, title: string): Promise<boolean> {
try {
const result = await openDeleteConfirmModal(plugin, site, title, postName);
if (!result.success) {
return false;
}
const service = new HaloService(plugin.app, plugin.settings, site);
if (result.options.deleteHalo) {
const success = await service.deletePost(postName);
if (!success) {
new Notice(i18next.t("service.error_delete_failed"));
return false;
}
}
new Notice(i18next.t("service.notice_delete_success"));
return true;
} catch (error) {
console.error("[HaloPlugin] Delete post by name failed:", error);
new Notice(i18next.t("service.error_delete_failed"));
return false;
}
}
@@ -0,0 +1,76 @@
import i18next from "i18next";
import { Notice, TFile } from "obsidian";
import { openSiteSelectionModal } from "../site-selection-modal";
import { openFilePreviewModal, type ImportOptions } from "../modals/file-preview-modal";
import HaloService from "../service";
import type HaloPlugin from "../main";
import type { HaloSite } from "../settings";
export async function importFromMarkdownFile(plugin: HaloPlugin): Promise<void> {
try {
if (plugin.settings.sites.length === 0) {
new Notice(i18next.t("command.pull_post.error_no_sites"));
return;
}
let site: HaloSite = plugin.settings.sites[0];
if (plugin.settings.sites.length > 1) {
site = await openSiteSelectionModal(plugin);
}
const options: ImportOptions = {
publishImmediately: plugin.settings.publishByDefault,
};
const file = await new Promise<TFile | null>((resolve) => {
const input = document.createElement("input");
input.type = "file";
input.accept = ".md";
input.onchange = async () => {
const selectedFile = input.files?.[0];
if (!selectedFile) {
resolve(null);
return;
}
const allFiles = plugin.app.vault.getFiles();
const matchedFile = allFiles.find(f => f.name === selectedFile.name);
if (matchedFile) {
resolve(matchedFile);
} else {
new Notice(i18next.t("import_modal.error_file_not_in_vault"));
resolve(null);
}
};
input.oncancel = () => resolve(null);
input.click();
});
if (!file) {
return;
}
const result = await openFilePreviewModal(plugin, site, file, options);
if (!result.success) {
return;
}
const service = new HaloService(plugin.app, plugin.settings, site);
const success = await service.importPost(file, result.options.publishImmediately);
if (success) {
new Notice(i18next.t("service.notice_import_success"));
} else {
new Notice(i18next.t("service.error_import_failed"));
}
} catch (error) {
console.error("[HaloPlugin] Import from markdown file failed:", error);
new Notice(i18next.t("service.error_import_failed"));
}
}
@@ -0,0 +1,44 @@
import i18next from "i18next";
import { Notice } from "obsidian";
import { openSiteSelectionModal } from "../site-selection-modal";
import { openTagManagerModal } from "../modals/tag-manager-modal";
import { openCategoryManagerModal } from "../modals/category-manager-modal";
import type HaloPlugin from "../main";
export async function manageTags(plugin: HaloPlugin): Promise<void> {
try {
if (plugin.settings.sites.length === 0) {
new Notice(i18next.t("command.pull_post.error_no_sites"));
return;
}
let site = plugin.settings.sites[0];
if (plugin.settings.sites.length > 1) {
site = await openSiteSelectionModal(plugin);
}
openTagManagerModal(plugin, site);
} catch (error) {
console.error("[HaloPlugin] Manage tags failed:", error);
new Notice(i18next.t("common.error_connection_failed"));
}
}
export async function manageCategories(plugin: HaloPlugin): Promise<void> {
try {
if (plugin.settings.sites.length === 0) {
new Notice(i18next.t("command.pull_post.error_no_sites"));
return;
}
let site = plugin.settings.sites[0];
if (plugin.settings.sites.length > 1) {
site = await openSiteSelectionModal(plugin);
}
openCategoryManagerModal(plugin, site);
} catch (error) {
console.error("[HaloPlugin] Manage categories failed:", error);
new Notice(i18next.t("common.error_connection_failed"));
}
}
+125 -4
View File
@@ -20,6 +20,22 @@
"pull_post": {
"name": "Pull posts from Halo",
"error_no_sites": "Please configure sites first"
},
"list_posts": {
"name": "View Halo posts list"
},
"delete_post": {
"name": "Delete Halo post",
"error_not_published": "This document is not published to Halo yet"
},
"import_markdown": {
"name": "Import from Markdown file"
},
"manage_tags": {
"name": "Manage tags"
},
"manage_categories": {
"name": "Manage categories"
}
},
"settings": {
@@ -53,7 +69,50 @@
},
"post_selection_modal": {
"title": "Pull posts from Halo",
"button_pull": "Pull"
"button_pull": "Pull",
"button_view": "View",
"filter_all": "All",
"filter_published": "Published",
"filter_draft": "Draft",
"status_published": "Published",
"status_draft": "Draft",
"total_items": "posts",
"button_prev": "Previous",
"button_next": "Next",
"untitled": "Untitled",
"empty": "No posts found"
},
"post_list_modal": {
"title": "Halo Posts List",
"button_pull": "Pull",
"button_view": "View",
"button_edit": "Edit",
"button_delete": "Delete",
"filter_all": "All",
"filter_published": "Published",
"filter_draft": "Draft",
"status_published": "Published",
"status_draft": "Draft",
"total_items": "posts",
"button_prev": "Previous",
"button_next": "Next",
"untitled": "Untitled",
"empty": "No posts found",
"confirm_delete": "Confirm Delete",
"confirm_delete_message": "Are you sure you want to delete the post \"{title}\"? This action cannot be undone."
},
"post_delete_modal": {
"title": "Delete Post",
"message": "Are you sure you want to delete the post \"{title}\"?",
"message_halo_only": "Only delete the post on Halo, keep local file",
"message_local_only": "Only delete local file, keep Halo post",
"message_both": "Delete both Halo post and local file",
"button_delete_halo": "Delete Halo only",
"button_delete_local": "Delete local only",
"button_delete_both": "Delete both",
"success": "Post deleted",
"error_failed": "Delete failed",
"error_no_option_selected": "Please select at least one delete option"
},
"site_editing_modal": {
"title": "Halo site",
@@ -105,10 +164,72 @@
"error_post_not_found": "Post does not exist",
"image_upload_success": "Image uploaded successfully",
"image_upload_failed": "Image upload failed",
"image_uploading": "Uploading image..."
"image_uploading": "Uploading image...",
"error_delete_failed": "Failed to delete post",
"error_import_failed": "Failed to import post",
"notice_import_success": "Imported successfully",
"notice_delete_success": "Deleted successfully"
},
"file_preview_modal": {
"title": "File Preview",
"frontmatter": "Frontmatter Metadata",
"content_preview": "Content Preview",
"publish_immediately": "Publish immediately after import",
"publish_immediately_desc": "If checked, the created post will be published directly",
"button_import": "Import",
"error_file_not_in_vault": "File is not in the vault"
},
"tag_manager_modal": {
"title": "Tag Manager",
"button_create": "Create Tag",
"loading": "Loading...",
"empty": "No tags",
"column_name": "Name",
"column_slug": "Slug",
"column_color": "Color",
"column_actions": "Actions",
"button_edit": "Edit",
"button_delete": "Delete",
"prompt_name": "Enter tag name:",
"prompt_slug": "Enter tag slug:",
"prompt_color": "Enter tag color (hex):",
"notice_create_success": "Tag created successfully",
"error_create_failed": "Failed to create tag",
"notice_update_success": "Tag updated successfully",
"error_update_failed": "Failed to update tag",
"confirm_delete": "Are you sure you want to delete tag \"{name}\"?",
"notice_delete_success": "Tag deleted successfully",
"error_delete_failed": "Failed to delete tag"
},
"category_manager_modal": {
"title": "Category Manager",
"button_create": "Create Category",
"loading": "Loading...",
"empty": "No categories",
"column_name": "Name",
"column_slug": "Slug",
"column_priority": "Priority",
"column_actions": "Actions",
"button_edit": "Edit",
"button_delete": "Delete",
"prompt_name": "Enter category name:",
"prompt_slug": "Enter category slug:",
"prompt_priority": "Enter category priority:",
"notice_create_success": "Category created successfully",
"error_create_failed": "Failed to create category",
"notice_update_success": "Category updated successfully",
"error_update_failed": "Failed to update category",
"confirm_delete": "Are you sure you want to delete category \"{name}\"?",
"notice_delete_success": "Category deleted successfully",
"error_delete_failed": "Failed to delete category"
},
"common": {
"error_connection_failed": "Connection failed",
"button_close": "Close"
"button_close": "Close",
"button_cancel": "Cancel",
"button_confirm": "Confirm",
"button_delete": "Delete",
"button_import": "Import",
"error_no_active_editor": "No file is open"
}
}
}
+125 -4
View File
@@ -20,6 +20,22 @@
"pull_post": {
"name": "从 Halo 拉取文档",
"error_no_sites": "请先配置站点"
},
"list_posts": {
"name": "查看 Halo 文章列表"
},
"delete_post": {
"name": "删除 Halo 文章",
"error_not_published": "此文档还未发布到 Halo"
},
"import_markdown": {
"name": "从 Markdown 文件导入"
},
"manage_tags": {
"name": "管理标签"
},
"manage_categories": {
"name": "管理分类"
}
},
"settings": {
@@ -53,7 +69,50 @@
},
"post_selection_modal": {
"title": "从 Halo 拉取文章",
"button_pull": "拉取"
"button_pull": "拉取",
"button_view": "查看",
"filter_all": "全部",
"filter_published": "已发布",
"filter_draft": "草稿",
"status_published": "已发布",
"status_draft": "草稿",
"total_items": "篇",
"button_prev": "上一页",
"button_next": "下一页",
"untitled": "无标题",
"empty": "没有找到文章"
},
"post_list_modal": {
"title": "Halo 文章列表",
"button_pull": "拉取",
"button_view": "查看",
"button_edit": "编辑",
"button_delete": "删除",
"filter_all": "全部",
"filter_published": "已发布",
"filter_draft": "草稿",
"status_published": "已发布",
"status_draft": "草稿",
"total_items": "篇",
"button_prev": "上一页",
"button_next": "下一页",
"untitled": "无标题",
"empty": "没有找到文章",
"confirm_delete": "确认删除",
"confirm_delete_message": "确定要删除文章「{title}」吗?此操作不可撤销。"
},
"post_delete_modal": {
"title": "删除文章",
"message": "确定要删除文章「{title}」吗?",
"message_halo_only": "仅删除 Halo 上的文章,本地文件保留",
"message_local_only": "仅删除本地文件,Halo 文章保留",
"message_both": "同时删除 Halo 文章和本地文件",
"button_delete_halo": "仅删除 Halo",
"button_delete_local": "仅删除本地",
"button_delete_both": "全部删除",
"success": "文章已删除",
"error_failed": "删除失败",
"error_no_option_selected": "请至少选择一个删除选项"
},
"site_editing_modal": {
"title": "Halo 站点",
@@ -105,10 +164,72 @@
"error_post_not_found": "文章不存在",
"image_upload_success": "图片上传成功",
"image_upload_failed": "图片上传失败",
"image_uploading": "正在上传图片..."
"image_uploading": "正在上传图片...",
"error_delete_failed": "删除文章失败",
"error_import_failed": "导入文章失败",
"notice_import_success": "导入成功",
"notice_delete_success": "删除成功"
},
"file_preview_modal": {
"title": "文件预览",
"frontmatter": "Frontmatter 元数据",
"content_preview": "内容预览",
"publish_immediately": "导入后立即发布",
"publish_immediately_desc": "勾选后,创建的文章将直接发布",
"button_import": "导入",
"error_file_not_in_vault": "文件不在 Vault 中"
},
"tag_manager_modal": {
"title": "标签管理",
"button_create": "创建标签",
"loading": "加载中...",
"empty": "暂无标签",
"column_name": "名称",
"column_slug": "别名",
"column_color": "颜色",
"column_actions": "操作",
"button_edit": "编辑",
"button_delete": "删除",
"prompt_name": "请输入标签名称:",
"prompt_slug": "请输入标签别名:",
"prompt_color": "请输入标签颜色(hex):",
"notice_create_success": "标签创建成功",
"error_create_failed": "标签创建失败",
"notice_update_success": "标签更新成功",
"error_update_failed": "标签更新失败",
"confirm_delete": "确定要删除标签「{name}」吗?",
"notice_delete_success": "标签删除成功",
"error_delete_failed": "标签删除失败"
},
"category_manager_modal": {
"title": "分类管理",
"button_create": "创建分类",
"loading": "加载中...",
"empty": "暂无分类",
"column_name": "名称",
"column_slug": "别名",
"column_priority": "优先级",
"column_actions": "操作",
"button_edit": "编辑",
"button_delete": "删除",
"prompt_name": "请输入分类名称:",
"prompt_slug": "请输入分类别名:",
"prompt_priority": "请输入分类优先级:",
"notice_create_success": "分类创建成功",
"error_create_failed": "分类创建失败",
"notice_update_success": "分类更新成功",
"error_update_failed": "分类更新失败",
"confirm_delete": "确定要删除分类「{name}」吗?",
"notice_delete_success": "分类删除成功",
"error_delete_failed": "分类删除失败"
},
"common": {
"error_connection_failed": "连接失败",
"button_close": "关闭"
"button_close": "关闭",
"button_cancel": "取消",
"button_confirm": "确认",
"button_delete": "删除",
"button_import": "导入",
"error_no_active_editor": "没有打开的文件"
}
}
}
+125 -4
View File
@@ -20,6 +20,22 @@
"pull_post": {
"name": "從 Halo 拉取文件",
"error_no_sites": "請先配置網站"
},
"list_posts": {
"name": "查看 Halo 文章列表"
},
"delete_post": {
"name": "刪除 Halo 文章",
"error_not_published": "此文件還未發佈到 Halo"
},
"import_markdown": {
"name": "從 Markdown 文件導入"
},
"manage_tags": {
"name": "管理標籤"
},
"manage_categories": {
"name": "管理分類"
}
},
"settings": {
@@ -53,7 +69,50 @@
},
"post_selection_modal": {
"title": "從 Halo 拉取文章",
"button_pull": "拉取"
"button_pull": "拉取",
"button_view": "查看",
"filter_all": "全部",
"filter_published": "已發布",
"filter_draft": "草稿",
"status_published": "已發布",
"status_draft": "草稿",
"total_items": "篇",
"button_prev": "上一頁",
"button_next": "下一頁",
"untitled": "無標題",
"empty": "沒有找到文章"
},
"post_list_modal": {
"title": "Halo 文章列表",
"button_pull": "拉取",
"button_view": "查看",
"button_edit": "編輯",
"button_delete": "刪除",
"filter_all": "全部",
"filter_published": "已發布",
"filter_draft": "草稿",
"status_published": "已發布",
"status_draft": "草稿",
"total_items": "篇",
"button_prev": "上一頁",
"button_next": "下一頁",
"untitled": "無標題",
"empty": "沒有找到文章",
"confirm_delete": "確認刪除",
"confirm_delete_message": "確定要刪除文章「{title}」嗎?此操作不可撤銷。"
},
"post_delete_modal": {
"title": "刪除文章",
"message": "確定要刪除文章「{title}」嗎?",
"message_halo_only": "僅刪除 Halo 上的文章,本地文件保留",
"message_local_only": "僅刪除本地文件,Halo 文章保留",
"message_both": "同時刪除 Halo 文章和本地文件",
"button_delete_halo": "僅刪除 Halo",
"button_delete_local": "僅刪除本地",
"button_delete_both": "全部刪除",
"success": "文章已刪除",
"error_failed": "刪除失敗",
"error_no_option_selected": "請至少選擇一個刪除選項"
},
"site_editing_modal": {
"title": "Halo 網站",
@@ -105,10 +164,72 @@
"error_post_not_found": "文章不存在",
"image_upload_success": "圖片上傳成功",
"image_upload_failed": "圖片上傳失敗",
"image_uploading": "正在上傳圖片..."
"image_uploading": "正在上傳圖片...",
"error_delete_failed": "刪除文章失敗",
"error_import_failed": "導入文章失敗",
"notice_import_success": "導入成功",
"notice_delete_success": "刪除成功"
},
"file_preview_modal": {
"title": "文件預覽",
"frontmatter": "Frontmatter 元數據",
"content_preview": "內容預覽",
"publish_immediately": "導入後立即發布",
"publish_immediately_desc": "勾選後,創建的文章將直接發布",
"button_import": "導入",
"error_file_not_in_vault": "文件不在 Vault 中"
},
"tag_manager_modal": {
"title": "標籤管理",
"button_create": "創建標籤",
"loading": "加載中...",
"empty": "暫無標籤",
"column_name": "名稱",
"column_slug": "別名",
"column_color": "顏色",
"column_actions": "操作",
"button_edit": "編輯",
"button_delete": "刪除",
"prompt_name": "請輸入標籤名稱:",
"prompt_slug": "請輸入標籤別名:",
"prompt_color": "請輸入標籤顏色(hex):",
"notice_create_success": "標籤創建成功",
"error_create_failed": "標籤創建失敗",
"notice_update_success": "標籤更新成功",
"error_update_failed": "標籤更新失敗",
"confirm_delete": "確定要刪除標籤「{name}」嗎?",
"notice_delete_success": "標籤刪除成功",
"error_delete_failed": "標籤刪除失敗"
},
"category_manager_modal": {
"title": "分類管理",
"button_create": "創建分類",
"loading": "加載中...",
"empty": "暫無分類",
"column_name": "名稱",
"column_slug": "別名",
"column_priority": "優先級",
"column_actions": "操作",
"button_edit": "編輯",
"button_delete": "刪除",
"prompt_name": "請輸入分類名稱:",
"prompt_slug": "請輸入分類別名:",
"prompt_priority": "請輸入分類優先級:",
"notice_create_success": "分類創建成功",
"error_create_failed": "分類創建失敗",
"notice_update_success": "分類更新成功",
"error_update_failed": "分類更新失敗",
"confirm_delete": "確定要刪除分類「{name}」嗎?",
"notice_delete_success": "分類刪除成功",
"error_delete_failed": "分類刪除失敗"
},
"common": {
"error_connection_failed": "連接失敗",
"button_close": "關閉"
"button_close": "關閉",
"button_cancel": "取消",
"button_confirm": "確認",
"button_delete": "刪除",
"button_import": "導入",
"error_no_active_editor": "沒有打開的文件"
}
}
}
+36 -1
View File
@@ -3,8 +3,11 @@ import { Notice, Plugin, moment } from "obsidian";
import { resources } from "./i18n";
import { addHaloIcon } from "./icons";
import { openPostSelectionModal } from "./post-selection-model";
import HaloService from "./service";
import { importFromMarkdownFile } from "./commands/import-markdown";
import { deletePost } from "./commands/delete-post";
import { manageTags, manageCategories } from "./commands/manage-taxonomy";
import { DEFAULT_SETTINGS, type HaloSetting, HaloSettingTab, type HaloSite } from "./settings";
import HaloService from "./service";
import { openSiteSelectionModal } from "./site-selection-modal";
export default class HaloPlugin extends Plugin {
@@ -106,6 +109,38 @@ export default class HaloPlugin extends Plugin {
},
});
this.addCommand({
id: "import-markdown",
name: i18next.t("command.import_markdown.name"),
callback: async () => {
await importFromMarkdownFile(this);
},
});
this.addCommand({
id: "delete-post",
name: i18next.t("command.delete_post.name"),
editorCallback: async () => {
await deletePost(this);
},
});
this.addCommand({
id: "manage-tags",
name: i18next.t("command.manage_tags.name"),
callback: async () => {
await manageTags(this);
},
});
this.addCommand({
id: "manage-categories",
name: i18next.t("command.manage_categories.name"),
callback: async () => {
await manageCategories(this);
},
});
this.addSettingTab(new HaloSettingTab(this));
}
@@ -0,0 +1,178 @@
import i18next from "i18next";
import { Modal, Notice, Setting } from "obsidian";
import type HaloPlugin from "../main";
import type { HaloSite } from "../settings";
import HaloService from "../service";
export function openCategoryManagerModal(plugin: HaloPlugin, site: HaloSite): void {
const modal = new CategoryManagerModal(plugin, site);
modal.open();
}
class CategoryManagerModal extends Modal {
private categories: any[] = [];
private loading = true;
constructor(
private readonly plugin: HaloPlugin,
private readonly site: HaloSite,
) {
super(app);
}
async onOpen() {
const { contentEl } = this;
contentEl.empty();
contentEl.createEl("h2", {
text: i18next.t("category_manager_modal.title"),
});
const actionsEl = contentEl.createDiv("category-manager-actions");
new Setting(actionsEl)
.addButton((button) => {
button
.setButtonText(i18next.t("category_manager_modal.button_create"))
.setCta()
.onClick(() => {
this.showCreateCategoryDialog();
});
})
.addButton((button) => {
button
.setButtonText(i18next.t("common.button_close"))
.onClick(() => this.close());
});
const listEl = contentEl.createDiv("category-list");
listEl.createEl("p", { text: i18next.t("category_manager_modal.loading") });
try {
const service = new HaloService(this.plugin.app, this.plugin.settings, this.site);
this.categories = await service.getCategories();
this.loading = false;
this.renderCategoryList();
} catch (error) {
new Notice(i18next.t("common.error_connection_failed"));
this.close();
}
}
private renderCategoryList() {
const { contentEl } = this;
const listEl = contentEl.querySelector(".category-list") as HTMLElement;
if (!listEl) return;
listEl.empty();
if (this.categories.length === 0) {
listEl.createEl("p", { text: i18next.t("category_manager_modal.empty") });
return;
}
const table = listEl.createEl("table", { cls: "category-table" });
const header = table.createEl("tr");
header.createEl("th", { text: i18next.t("category_manager_modal.column_name") });
header.createEl("th", { text: i18next.t("category_manager_modal.column_slug") });
header.createEl("th", { text: i18next.t("category_manager_modal.column_priority") });
header.createEl("th", { text: i18next.t("category_manager_modal.column_actions") });
for (const category of this.categories) {
const row = table.createEl("tr");
row.createEl("td", { text: category.spec.displayName });
row.createEl("td", { text: category.spec.slug });
row.createEl("td", { text: String(category.spec.priority || 0) });
const actionsCell = row.createEl("td");
actionsCell.createEl("button", {
text: i18next.t("category_manager_modal.button_edit"),
cls: "category-action-btn",
}).addEventListener("click", () => {
this.showEditCategoryDialog(category);
});
actionsCell.createEl("button", {
text: i18next.t("category_manager_modal.button_delete"),
cls: "category-action-btn danger",
}).addEventListener("click", () => {
this.showDeleteCategoryDialog(category);
});
}
}
private async showCreateCategoryDialog() {
const name = prompt(i18next.t("category_manager_modal.prompt_name"));
if (!name) return;
const slug = prompt(i18next.t("category_manager_modal.prompt_slug"), this.generateSlug(name));
if (!slug) return;
const priority = prompt(i18next.t("category_manager_modal.prompt_priority"), "0");
if (priority === null) return;
try {
const service = new HaloService(this.plugin.app, this.plugin.settings, this.site);
await service.createCategory(name, slug, parseInt(priority) || 0);
new Notice(i18next.t("category_manager_modal.notice_create_success"));
this.categories = await service.getCategories();
this.renderCategoryList();
} catch (error) {
new Notice(i18next.t("category_manager_modal.error_create_failed"));
}
}
private async showEditCategoryDialog(category: any) {
const name = prompt(i18next.t("category_manager_modal.prompt_name"), category.spec.displayName);
if (name === null) return;
const slug = prompt(i18next.t("category_manager_modal.prompt_slug"), category.spec.slug);
if (slug === null) return;
const priority = prompt(i18next.t("category_manager_modal.prompt_priority"), String(category.spec.priority || 0));
if (priority === null) return;
try {
const service = new HaloService(this.plugin.app, this.plugin.settings, this.site);
await service.updateCategory(category.metadata.name, name, slug, parseInt(priority) || 0);
new Notice(i18next.t("category_manager_modal.notice_update_success"));
this.categories = await service.getCategories();
this.renderCategoryList();
} catch (error) {
new Notice(i18next.t("category_manager_modal.error_update_failed"));
}
}
private async showDeleteCategoryDialog(category: any) {
if (!confirm(i18next.t("category_manager_modal.confirm_delete", { name: category.spec.displayName }))) {
return;
}
try {
const service = new HaloService(this.plugin.app, this.plugin.settings, this.site);
await service.deleteCategory(category.metadata.name);
new Notice(i18next.t("category_manager_modal.notice_delete_success"));
this.categories = await service.getCategories();
this.renderCategoryList();
} catch (error) {
new Notice(i18next.t("category_manager_modal.error_delete_failed"));
}
}
private generateSlug(name: string): string {
return name
.toLowerCase()
.replace(/[^a-z0-9\u4e00-\u9fa5]+/g, "-")
.replace(/^-|-$/g, "");
}
onClose() {
const { contentEl } = this;
contentEl.empty();
}
}
@@ -0,0 +1,131 @@
import i18next from "i18next";
import { Modal, Notice, Setting } from "obsidian";
import type HaloPlugin from "../main";
import type { HaloSite } from "../settings";
export interface DeleteResult {
success: boolean;
options: {
deleteHalo: boolean;
deleteLocal: boolean;
};
}
export function openDeleteConfirmModal(
plugin: HaloPlugin,
site: HaloSite,
title: string,
postName: string,
): Promise<DeleteResult> {
return new Promise((resolve) => {
const modal = new DeleteConfirmModal(plugin, site, title, postName, (result) => {
resolve(result);
});
modal.open();
});
}
class DeleteConfirmModal extends Modal {
private deleteHalo = true;
private deleteLocal = false;
constructor(
private readonly plugin: HaloPlugin,
private readonly site: HaloSite,
private readonly title: string,
private readonly postName: string,
private readonly onResult: (result: DeleteResult) => void,
) {
super(app);
}
async onOpen() {
const { contentEl } = this;
contentEl.empty();
contentEl.createEl("h2", {
text: i18next.t("post_delete_modal.title"),
});
contentEl.createEl("p", {
text: i18next.t("post_delete_modal.message", { title: this.title }),
cls: "delete-message",
});
const optionsEl = contentEl.createDiv("delete-options");
new Setting(optionsEl)
.setName(i18next.t("post_delete_modal.message_halo_only").split("")[0])
.setDesc(i18next.t("post_delete_modal.message_halo_only").split("").slice(1).join(""))
.addToggle((toggle) => {
toggle.setValue(this.deleteHalo).onChange((value) => {
this.deleteHalo = value;
});
});
new Setting(optionsEl)
.setName(i18next.t("post_delete_modal.message_local_only").split("")[0])
.setDesc(i18next.t("post_delete_modal.message_local_only").split("").slice(1).join(""))
.addToggle((toggle) => {
toggle.setValue(this.deleteLocal).onChange((value) => {
this.deleteLocal = value;
});
});
const hasLocalFile = this.plugin.app.metadataCache.getFileCache(
Array.from(this.plugin.app.vault.getFiles()).find(f => f.basename === this.title) || this.plugin.app.vault.getAbstractFileByPath(`/${this.title}.md`) as any
);
if (!hasLocalFile) {
const localFile = this.plugin.app.vault.getAbstractFileByPath(`/${this.title}.md`);
if (!localFile) {
const localToggle = optionsEl.querySelector('.setting-item:nth-child(2) input[type="checkbox"]') as HTMLInputElement;
if (localToggle) {
localToggle.disabled = true;
}
}
}
const actionsEl = contentEl.createDiv("delete-actions");
new Setting(actionsEl)
.addButton((button) => {
button
.setButtonText(i18next.t("post_delete_modal.button_delete_both"))
.setWarning()
.onClick(() => {
if (!this.deleteHalo && !this.deleteLocal) {
new Notice(i18next.t("post_delete_modal.error_no_option_selected"));
return;
}
this.onResult({
success: true,
options: {
deleteHalo: this.deleteHalo,
deleteLocal: this.deleteLocal,
},
});
this.close();
});
})
.addButton((button) => {
button
.setButtonText(i18next.t("common.button_cancel"))
.onClick(() => {
this.onResult({
success: false,
options: {
deleteHalo: false,
deleteLocal: false,
},
});
this.close();
});
});
}
onClose() {
const { contentEl } = this;
contentEl.empty();
}
}
@@ -0,0 +1,136 @@
import i18next from "i18next";
import { Modal, Notice, Setting } from "obsidian";
import type { TFile } from "obsidian";
import type HaloPlugin from "../main";
import type { HaloSite } from "../settings";
export interface ImportOptions {
publishImmediately: boolean;
}
export interface ImportResult {
file: TFile;
success: boolean;
}
export function openFilePreviewModal(
plugin: HaloPlugin,
site: HaloSite,
file: TFile,
options: ImportOptions,
): Promise<ImportResult> {
return new Promise((resolve) => {
const modal = new FilePreviewModal(plugin, site, file, options, (result) => {
resolve(result);
});
modal.open();
});
}
class FilePreviewModal extends Modal {
constructor(
private readonly plugin: HaloPlugin,
private readonly site: HaloSite,
private readonly file: TFile,
private readonly options: ImportOptions,
private readonly onResult: (result: ImportResult) => void,
) {
super(app);
}
async onOpen() {
const { contentEl } = this;
contentEl.empty();
const headerEl = contentEl.createDiv("file-preview-header");
headerEl.createEl("h2", {
text: i18next.t("file_preview_modal.title"),
});
headerEl.createEl("p", {
text: this.file.path,
cls: "file-path",
});
try {
const content = await this.app.vault.read(this.file);
const matterData = this.app.metadataCache.getFileCache(this.file)?.frontmatter;
const frontmatterPosition = this.app.metadataCache.getFileCache(this.file)?.frontmatterPosition;
const previewContent = contentEl.createDiv("file-preview-content");
if (matterData) {
const frontmatterEl = previewContent.createDiv("frontmatter-section");
frontmatterEl.createEl("h3", {
text: i18next.t("file_preview_modal.frontmatter"),
});
const frontmatterList = frontmatterEl.createEl("ul");
if (matterData.title) {
frontmatterList.createEl("li", { text: `Title: ${matterData.title}` });
}
if (matterData.slug) {
frontmatterList.createEl("li", { text: `Slug: ${matterData.slug}` });
}
if (matterData.tags) {
frontmatterList.createEl("li", { text: `Tags: ${Array.isArray(matterData.tags) ? matterData.tags.join(", ") : matterData.tags}` });
}
if (matterData.categories) {
frontmatterList.createEl("li", { text: `Categories: ${Array.isArray(matterData.categories) ? matterData.categories.join(", ") : matterData.categories}` });
}
}
const contentPreview = previewContent.createDiv("content-preview");
contentPreview.createEl("h3", {
text: i18next.t("file_preview_modal.content_preview"),
});
const contentText = frontmatterPosition
? content.slice(frontmatterPosition.end.offset, Math.min(content.length, 500))
: content.slice(0, 500);
contentPreview.createEl("pre", {
text: contentText + (content.length > 500 ? "..." : ""),
cls: "content-text",
});
const actionsEl = contentEl.createDiv("file-preview-actions");
const publishToggle = new Setting(actionsEl)
.setName(i18next.t("file_preview_modal.publish_immediately"))
.setDesc(i18next.t("file_preview_modal.publish_immediately_desc"))
.addToggle((toggle) => {
toggle.setValue(this.options.publishImmediately).onChange((value) => {
this.options.publishImmediately = value;
});
});
new Setting(actionsEl)
.addButton((button) => {
button
.setButtonText(i18next.t("file_preview_modal.button_import"))
.setCta()
.onClick(() => {
this.onResult({ file: this.file, success: true });
this.close();
});
})
.addButton((button) => {
button
.setButtonText(i18next.t("common.button_cancel"))
.onClick(() => {
this.onResult({ file: this.file, success: false });
this.close();
});
});
} catch (error) {
new Notice(i18next.t("common.error_connection_failed"));
this.close();
}
}
onClose() {
const { contentEl } = this;
contentEl.empty();
}
}
@@ -0,0 +1,184 @@
import i18next from "i18next";
import { Modal, Notice, Setting } from "obsidian";
import type HaloPlugin from "../main";
import type { HaloSite } from "../settings";
import HaloService from "../service";
export function openTagManagerModal(plugin: HaloPlugin, site: HaloSite): void {
const modal = new TagManagerModal(plugin, site);
modal.open();
}
class TagManagerModal extends Modal {
private tags: any[] = [];
private loading = true;
constructor(
private readonly plugin: HaloPlugin,
private readonly site: HaloSite,
) {
super(app);
}
async onOpen() {
const { contentEl } = this;
contentEl.empty();
contentEl.createEl("h2", {
text: i18next.t("tag_manager_modal.title"),
});
const actionsEl = contentEl.createDiv("tag-manager-actions");
new Setting(actionsEl)
.addButton((button) => {
button
.setButtonText(i18next.t("tag_manager_modal.button_create"))
.setCta()
.onClick(() => {
this.showCreateTagDialog();
});
})
.addButton((button) => {
button
.setButtonText(i18next.t("common.button_close"))
.onClick(() => this.close());
});
const listEl = contentEl.createDiv("tag-list");
listEl.createEl("p", { text: i18next.t("tag_manager_modal.loading") });
try {
const service = new HaloService(this.plugin.app, this.plugin.settings, this.site);
this.tags = await service.getTags();
this.loading = false;
this.renderTagList();
} catch (error) {
new Notice(i18next.t("common.error_connection_failed"));
this.close();
}
}
private renderTagList() {
const { contentEl } = this;
const listEl = contentEl.querySelector(".tag-list") as HTMLElement;
if (!listEl) return;
listEl.empty();
if (this.tags.length === 0) {
listEl.createEl("p", { text: i18next.t("tag_manager_modal.empty") });
return;
}
const table = listEl.createEl("table", { cls: "tag-table" });
const header = table.createEl("tr");
header.createEl("th", { text: i18next.t("tag_manager_modal.column_name") });
header.createEl("th", { text: i18next.t("tag_manager_modal.column_slug") });
header.createEl("th", { text: i18next.t("tag_manager_modal.column_color") });
header.createEl("th", { text: i18next.t("tag_manager_modal.column_actions") });
for (const tag of this.tags) {
const row = table.createEl("tr");
row.createEl("td", { text: tag.spec.displayName });
row.createEl("td", { text: tag.spec.slug });
const colorCell = row.createEl("td");
colorCell.createEl("span", {
text: tag.spec.color || "#ffffff",
cls: "tag-color-preview",
attr: { style: `background-color: ${tag.spec.color || "#ffffff"}` }
});
const actionsCell = row.createEl("td");
actionsCell.createEl("button", {
text: i18next.t("tag_manager_modal.button_edit"),
cls: "tag-action-btn",
}).addEventListener("click", () => {
this.showEditTagDialog(tag);
});
actionsCell.createEl("button", {
text: i18next.t("tag_manager_modal.button_delete"),
cls: "tag-action-btn danger",
}).addEventListener("click", () => {
this.showDeleteTagDialog(tag);
});
}
}
private async showCreateTagDialog() {
const name = prompt(i18next.t("tag_manager_modal.prompt_name"));
if (!name) return;
const slug = prompt(i18next.t("tag_manager_modal.prompt_slug"), this.generateSlug(name));
if (!slug) return;
const color = prompt(i18next.t("tag_manager_modal.prompt_color"), "#4A90E2");
if (!color) return;
try {
const service = new HaloService(this.plugin.app, this.plugin.settings, this.site);
await service.createTag(name, slug, color);
new Notice(i18next.t("tag_manager_modal.notice_create_success"));
this.tags = await service.getTags();
this.renderTagList();
} catch (error) {
new Notice(i18next.t("tag_manager_modal.error_create_failed"));
}
}
private async showEditTagDialog(tag: any) {
const name = prompt(i18next.t("tag_manager_modal.prompt_name"), tag.spec.displayName);
if (name === null) return;
const slug = prompt(i18next.t("tag_manager_modal.prompt_slug"), tag.spec.slug);
if (slug === null) return;
const color = prompt(i18next.t("tag_manager_modal.prompt_color"), tag.spec.color || "#4A90E2");
if (color === null) return;
try {
const service = new HaloService(this.plugin.app, this.plugin.settings, this.site);
await service.updateTag(tag.metadata.name, name, slug, color);
new Notice(i18next.t("tag_manager_modal.notice_update_success"));
this.tags = await service.getTags();
this.renderTagList();
} catch (error) {
new Notice(i18next.t("tag_manager_modal.error_update_failed"));
}
}
private async showDeleteTagDialog(tag: any) {
if (!confirm(i18next.t("tag_manager_modal.confirm_delete", { name: tag.spec.displayName }))) {
return;
}
try {
const service = new HaloService(this.plugin.app, this.plugin.settings, this.site);
await service.deleteTag(tag.metadata.name);
new Notice(i18next.t("tag_manager_modal.notice_delete_success"));
this.tags = await service.getTags();
this.renderTagList();
} catch (error) {
new Notice(i18next.t("tag_manager_modal.error_delete_failed"));
}
}
private generateSlug(name: string): string {
return name
.toLowerCase()
.replace(/[^a-z0-9\u4e00-\u9fa5]+/g, "-")
.replace(/^-|-$/g, "");
}
onClose() {
const { contentEl } = this;
contentEl.empty();
}
}
+160 -32
View File
@@ -14,6 +14,12 @@ export function openPostSelectionModal(plugin: HaloPlugin, site: HaloSite): Prom
}
class PostSelectionModal extends Modal {
private currentPage = 1;
private pageSize = 20;
private totalItems = 0;
private posts: ListedPost[] = [];
private filter: "all" | "published" | "draft" = "all";
constructor(
private readonly plugin: HaloPlugin,
private readonly site: HaloSite,
@@ -22,51 +28,173 @@ class PostSelectionModal extends Modal {
super(app);
}
onOpen() {
async onOpen() {
const { contentEl } = this;
contentEl.empty();
const renderPostList = (): void => {
contentEl.empty();
contentEl.createEl("h2", {
text: i18next.t("post_selection_modal.title"),
});
contentEl.createEl("h2", {
text: i18next.t("post_selection_modal.title"),
});
await this.loadPosts();
this.render();
}
requestUrl({
url: `${this.site.url}/apis/uc.api.content.halo.run/v1alpha1/posts?labelSelector=content.halo.run%2Fdeleted%3Dfalse`,
private async loadPosts() {
try {
let labelSelector = "content.halo.run/deleted=false";
if (this.filter === "published") {
labelSelector = "content.halo.run/deleted=false,content.halo.run/published=true";
} else if (this.filter === "draft") {
labelSelector = "content.halo.run/deleted=false,content.halo.run/published=false";
}
const response = await requestUrl({
url: `${this.site.url}/apis/uc.api.content.halo.run/v1alpha1/posts?labelSelector=${encodeURIComponent(labelSelector)}&page=${this.currentPage}&size=${this.pageSize}`,
headers: {
Authorization: `Bearer ${this.site.token}`,
},
})
.then((response) => {
const posts: ListedPost[] = response.json.items;
});
for (const post of posts) {
const setting = new Setting(contentEl).setName(post.post.spec.title).setDesc(post.post.spec.slug);
this.posts = response.json.items || [];
this.totalItems = response.json.total || this.posts.length;
} catch (error) {
new Notice(i18next.t("common.error_connection_failed"));
}
}
setting.addButton((button) =>
button.setButtonText(i18next.t("post_selection_modal.button_pull")).onClick(() => {
this.onSelect(post);
this.close();
}),
);
}
})
.catch(() => {
new Notice(i18next.t("common.error_connection_failed"));
})
.finally(() => {
new Setting(contentEl).addButton((button) =>
button.setButtonText(i18next.t("common.button_close")).onClick(() => this.close()),
);
});
};
private render() {
const { contentEl } = this;
contentEl.empty();
renderPostList();
contentEl.createEl("h2", {
text: i18next.t("post_selection_modal.title"),
});
this.renderFilter();
if (this.posts.length === 0) {
contentEl.createEl("p", {
text: i18next.t("post_selection_modal.empty"),
});
} else {
for (const post of this.posts) {
this.renderPostItem(post);
}
}
this.renderPagination();
}
private renderFilter() {
const filterEl = this.contentEl.createDiv("post-list-filter");
const allButton = filterEl.createEl("button", {
text: i18next.t("post_selection_modal.filter_all"),
cls: this.filter === "all" ? "active" : "",
});
allButton.addEventListener("click", async () => {
this.filter = "all";
this.currentPage = 1;
await this.loadPosts();
this.render();
});
const publishedButton = filterEl.createEl("button", {
text: i18next.t("post_selection_modal.filter_published"),
cls: this.filter === "published" ? "active" : "",
});
publishedButton.addEventListener("click", async () => {
this.filter = "published";
this.currentPage = 1;
await this.loadPosts();
this.render();
});
const draftButton = filterEl.createEl("button", {
text: i18next.t("post_selection_modal.filter_draft"),
cls: this.filter === "draft" ? "active" : "",
});
draftButton.addEventListener("click", async () => {
this.filter = "draft";
this.currentPage = 1;
await this.loadPosts();
this.render();
});
}
private renderPostItem(post: ListedPost) {
const postEl = this.contentEl.createDiv("post-item");
const headerEl = postEl.createDiv("post-header");
const titleEl = headerEl.createEl("h3", {
text: post.post.spec.title || i18next.t("post_selection_modal.untitled"),
});
const statusEl = headerEl.createSpan({
text: post.post.spec.publish ? i18next.t("post_selection_modal.status_published") : i18next.t("post_selection_modal.status_draft"),
cls: post.post.spec.publish ? "status-published" : "status-draft",
});
const metaEl = postEl.createDiv("post-meta");
metaEl.createEl("span", { text: `Slug: ${post.post.spec.slug}` });
metaEl.createEl("span", { text: ` | ${new Date(post.post.metadata.creationTimestamp).toLocaleDateString()}` });
const actionsEl = postEl.createDiv("post-actions");
actionsEl.createEl("button", {
text: i18next.t("post_selection_modal.button_pull"),
cls: "mod-cta",
}).addEventListener("click", () => {
this.onSelect(post);
this.close();
});
actionsEl.createEl("button", {
text: i18next.t("post_selection_modal.button_view"),
}).addEventListener("click", () => {
window.open(`${this.site.url}/archives/${post.post.spec.slug}`, "_blank");
});
}
private renderPagination() {
const paginationEl = this.contentEl.createDiv("pagination");
const totalPages = Math.ceil(this.totalItems / this.pageSize);
if (this.currentPage > 1) {
paginationEl.createEl("button", {
text: i18next.t("post_selection_modal.button_prev"),
}).addEventListener("click", async () => {
this.currentPage--;
await this.loadPosts();
this.render();
});
}
paginationEl.createEl("span", {
text: `${this.currentPage} / ${totalPages} (${this.totalItems} ${i18next.t("post_selection_modal.total_items")})`,
});
if (this.currentPage < totalPages) {
paginationEl.createEl("button", {
text: i18next.t("post_selection_modal.button_next"),
}).addEventListener("click", async () => {
this.currentPage++;
await this.loadPosts();
this.render();
});
}
new Setting(paginationEl).addButton((button) =>
button.setButtonText(i18next.t("common.button_close")).onClick(() => this.close()),
);
}
onClose() {
const { contentEl } = this;
contentEl.empty();
}
}
}
+240 -1
View File
@@ -1,6 +1,6 @@
import ImageUploader from "./image-uploader";
import i18next from "i18next";
import { type App, Notice, requestUrl } from "obsidian";
import { type App, Notice, requestUrl, type TFile } from "obsidian";
import { randomUUID } from "src/utils/id";
import markdownIt from "src/utils/markdown";
import { slugify } from "transliteration";
@@ -514,6 +514,245 @@ class HaloService {
})
.filter(Boolean) as string[];
}
public async importPost(file: TFile, publishImmediately: boolean = false): Promise<boolean> {
try {
const imageUploader = new ImageUploader(this.site.url, this.site.token);
const md = await this.app.vault.read(file);
const matterData = this.app.metadataCache.getFileCache(file)?.frontmatter;
const frontmatterPosition = this.app.metadataCache.getFileCache(file)?.frontmatterPosition;
let raw = frontmatterPosition ? md.slice(frontmatterPosition.end.offset) : md;
// 检测并上传本地图片
if (this.settings.imageUpload?.enabled) {
const imageReferences = extractImageReferences(raw);
if (imageReferences.length > 0) {
const localImages = imageReferences.filter((ref) =>
!ref.path.startsWith("http://") &&
!ref.path.startsWith("https://") &&
!ref.path.startsWith("data:")
);
if (localImages.length > 0) {
new Notice(`检测到 ${localImages.length} 个本地图片,正在上传...`);
const absolutePaths = localImages
.map((ref) => ({
original: ref.path,
absolute: getAbsolutePath(this.app.vault, ref.path, file.path),
}))
.filter((item) => item.absolute !== null) as { original: string; absolute: string }[];
if (absolutePaths.length > 0) {
const pathMapping = await imageUploader.uploadImages(absolutePaths.map((item) => item.absolute), this.app.vault);
if (pathMapping.size > 0) {
const mapping = new Map<string, string>();
absolutePaths.forEach((item) => {
const remoteUrl = pathMapping.get(item.absolute);
if (remoteUrl) {
mapping.set(item.original, remoteUrl);
}
});
raw = replaceImagePaths(raw, mapping);
const successCount = mapping.size;
const failCount = absolutePaths.length - successCount;
if (failCount === 0) {
new Notice(`✓ 图片上传成功 (${successCount}/${absolutePaths.length})`);
} else {
new Notice(`⚠ 部分图片上传失败 (成功: ${successCount}, 失败: ${failCount})`);
}
}
}
}
}
}
const content: Content = {
rawType: "markdown",
raw: raw,
content: markdownIt.render(raw),
};
let 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",
},
};
// 处理分类
if (matterData?.categories) {
const categoryNames = await this.getCategoryNames(matterData.categories);
params.spec.categories = categoryNames;
}
// 处理标签
if (matterData?.tags) {
const tagNames = await this.getTagNames(matterData.tags);
params.spec.tags = tagNames;
}
// 创建文章
const post = await requestUrl({
url: `${this.site.url}/apis/uc.api.content.halo.run/v1alpha1/posts`,
method: "POST",
contentType: "application/json",
headers: this.headers,
body: JSON.stringify(params),
}).json;
return !!post && !!post.metadata;
} catch (error) {
console.error("[HaloService] 导入文章失败:", error);
return false;
}
}
public async deletePost(name: string): Promise<boolean> {
try {
await requestUrl({
url: `${this.site.url}/apis/uc.api.content.halo.run/v1alpha1/posts/${name}`,
method: "DELETE",
headers: this.headers,
});
return true;
} catch (error) {
console.error("[HaloService] 删除文章失败:", error);
return false;
}
}
public async createTag(displayName: string, slug: string, color: string): Promise<void> {
await requestUrl({
url: `${this.site.url}/apis/content.halo.run/v1alpha1/tags`,
method: "POST",
contentType: "application/json",
headers: this.headers,
body: JSON.stringify({
spec: {
displayName,
slug,
color: color || "#ffffff",
cover: "",
},
apiVersion: "content.halo.run/v1alpha1",
kind: "Tag",
metadata: { name: "", generateName: "tag-" },
}),
});
}
public async updateTag(name: string, displayName: string, slug: string, color: string): Promise<void> {
const tag = (await requestUrl({
url: `${this.site.url}/apis/content.halo.run/v1alpha1/tags/${name}`,
headers: this.headers,
}).json) as any;
tag.spec.displayName = displayName;
tag.spec.slug = slug;
tag.spec.color = color || "#ffffff";
await requestUrl({
url: `${this.site.url}/apis/content.halo.run/v1alpha1/tags/${name}`,
method: "PUT",
contentType: "application/json",
headers: this.headers,
body: JSON.stringify(tag),
});
}
public async deleteTag(name: string): Promise<void> {
await requestUrl({
url: `${this.site.url}/apis/content.halo.run/v1alpha1/tags/${name}`,
method: "DELETE",
headers: this.headers,
});
}
public async createCategory(displayName: string, slug: string, priority: number): Promise<void> {
await requestUrl({
url: `${this.site.url}/apis/content.halo.run/v1alpha1/categories`,
method: "POST",
contentType: "application/json",
headers: this.headers,
body: JSON.stringify({
spec: {
displayName,
slug,
description: "",
cover: "",
template: "",
priority,
children: [],
},
apiVersion: "content.halo.run/v1alpha1",
kind: "Category",
metadata: { name: "", generateName: "category-" },
}),
});
}
public async updateCategory(name: string, displayName: string, slug: string, priority: number): Promise<void> {
const category = (await requestUrl({
url: `${this.site.url}/apis/content.halo.run/v1alpha1/categories/${name}`,
headers: this.headers,
}).json) as any;
category.spec.displayName = displayName;
category.spec.slug = slug;
category.spec.priority = priority;
await requestUrl({
url: `${this.site.url}/apis/content.halo.run/v1alpha1/categories/${name}`,
method: "PUT",
contentType: "application/json",
headers: this.headers,
body: JSON.stringify(category),
});
}
public async deleteCategory(name: string): Promise<void> {
await requestUrl({
url: `${this.site.url}/apis/content.halo.run/v1alpha1/categories/${name}`,
method: "DELETE",
headers: this.headers,
});
}
}
export default HaloService;
+13 -2
View File
@@ -1,6 +1,17 @@
---
title: Git团队协作指南(精简版)
slug: gittuan-dui-xie-zuo-zhi-nan-jing-jian-ban
cover: https://image.baidu.com/search/down?url=https%3A%2F%2Ftvax3.sinaimg.cn%2Flarge%2F0072Vf1pgy1foxlnu5w7cj31kw0w0hcd.jpg
categories: []
tags: []
halo:
site: http://101.133.128.193:8091
name: 2dd2a98c-0399-4ad8-8bfa-04326e302afb
publish: true
---
# Git团队协作指南:从入门到精通
> **作者**:刘航宇(河南工大学人工智能协会)
> **作者**:刘航宇(河南工大学人工智能协会)
> **面向读者**:有一定编程基础,需要团队协作参赛的同学
> **代码语言**Python为主
> **预计阅读时间**20分钟
@@ -652,5 +663,5 @@ Git是团队协作的利器,掌握以下核心要点:
---
*作者:刘航宇(河南工大学人工智能协会)*
*作者:刘航宇(河南工大学人工智能协会)*
*更新日期:2026年4月23日*
+4 -4
View File
@@ -3,13 +3,13 @@ title: Linear Regression 线性回归
id: 019d86e4-4749-708c-b0ba-240fc8a8dbf8
date: 2026-04-13 20:50:39
auther: openknow
cover:
cover:
excerpt: 线性回归通过建立自变量与因变量之间的线性关系来预测数值,通过最小化平方损失函数求解权重,可采用解析解或梯度下降法优化参数。
permalink: /archives/linear-regression-xian-xing-hui-gui
categories:
- cheng-chang-shi-yan-shi
tags:
- ji-shu-shen-qian
- cheng-chang-shi-yan-shi
tags:
- ji-shu-shen-qian
---
# Linear Regression 线性回归
+11
View File
@@ -1,3 +1,14 @@
---
title: 博客爬取报告
slug: bo-ke-pa-qu-bao-gao
cover: ""
categories: []
tags: []
halo:
site: https://blog.metarl.cc.cd
name: e826f389-79f7-451e-8deb-fcbf88786314
publish: true
---
# Serendipity 博客信息爬取报告
> 爬取时间:2026-04-23
+11
View File
@@ -1,3 +1,14 @@
---
title: 博客爬取报告
slug: bo-ke-pa-qu-bao-gao
cover: ""
categories: []
tags: []
halo:
site: https://blog.metarl.cc.cd
name: e826f389-79f7-451e-8deb-fcbf88786314
publish: true
---
# 大模型(LLM)赋能:基于多模型接入引擎的智能运维架构设计
在传统的储能电站监控系统中,主要依赖硬性的规则引擎(如温度阈值、温差报警)进行被动式的告警。然而,随着电站规模的扩大,海量告警信息往往导致运维人员“信息过载”,且初级运维人员往往缺乏快速、准确处理复杂故障的经验。